diff --git a/DRAGONUI_NEWERA.md b/DRAGONUI_NEWERA.md
new file mode 100644
index 0000000..807d265
--- /dev/null
+++ b/DRAGONUI_NEWERA.md
@@ -0,0 +1,82 @@
+# DragonUI_NewEra compatibility
+
+Compatibility was audited against `ghbset/DragonUI_NewEra` commit
+`37a29d640f97545ff1bb7d4b0ba7a5329cce1b8c` (2026-08-09).
+
+## Status
+
+DragonUI_NewEra is bundled in `patch-ruRU-5` as a required MoonWell system
+component. It is enabled by default and hidden from the AddOns list together
+with DragonUI and `!!!ClassicAPI`.
+
+The MoonWell DragonUI exposes the integration seams used by NewEra:
+
+- global `DragonUI` addon table;
+- `ModuleRegistry`;
+- `MoversSystem` and the editable-frame API;
+- `OptionsPanel` and `PanelControls` after `DragonUI_Options` is loaded;
+- `Fonts`, `SafeCall`, `CombatQueue`, and global `SetAtlasTexture`.
+
+NewEra remains a separate addon folder rather than being merged into DragonUI.
+Its TOC retains:
+
+```toc
+## Interface: 30300
+## Dependencies: DragonUI, !!!ClassicAPI
+## OptionalDeps: DragonUI_Options
+```
+
+`DragonUI_NewEra` is an extension, not a replacement for the HUD modules. It
+does not need `X-DragonUI-Overrides` unless a future NewEra module explicitly
+takes ownership of `mainbars`, `minimap`, `buffs`, or another registered HUD
+module.
+
+## Bundled sources
+
+The system package contains:
+
+```text
+Interface/AddOns/!!!ClassicAPI/ b3da3000bba23a2c31aa9c9b7c0232d5973789e6
+Interface/AddOns/DragonUI_NewEra/ 37a29d640f97545ff1bb7d4b0ba7a5329cce1b8c
+```
+
+Upstream Git metadata, screenshots, and top-level build tools are excluded.
+Runtime sources, assets, and the upstream QA harness are retained. The exact
+revisions are also recorded in addon TOC metadata as `X-Upstream-Commit`.
+
+Neither audited upstream contains an explicit repository-wide redistribution
+license. MoonWell maintainers must obtain and archive redistribution permission
+before a public release. The source revisions above preserve provenance but do
+not replace that permission.
+
+## Known ownership conflict
+
+Both MoonWellClient and NewEra provide an Encounter Journal / Adventure Guide
+and the `ToggleEncounterJournal` entry point. MoonWell's journal is currently
+authoritative because it receives server-backed data. NewEra's encounter
+journal files are intentionally omitted from its system TOC, and its dead
+options toggle is hidden.
+
+The NewEra combined-bag module is marked work in progress upstream. Treat it as
+opt-in and do not use it as the default replacement for DragonUI/Blizzard bags
+without a separate gameplay test pass.
+
+NewEra ships 94 power-of-two BLP atlases with one dimension above the project's
+conservative 512-pixel authoring limit; the largest are 2048x2048. Upstream
+marks their BLP2 encodings as 3.3.5a-loadable and they package successfully,
+but every affected panel still requires an in-client render pass on MoonWell's
+exact executable before release. If that client rejects them, split the atlases
+without changing their public atlas names instead of silently scaling UI
+geometry.
+
+## Release checklist
+
+1. Update only from reviewed, pinned DragonUI_NewEra and ClassicAPI revisions.
+2. Obtain and record licenses or permissions for code, fonts, sounds, and BLPs.
+3. Keep DragonUI_NewEra as its own folder and TOC; do not merge it into
+ DragonUI or FrameXML.
+4. Keep NewEra Encounter Journal disabled until ownership is deliberately
+ migrated from MoonWellClient.
+5. Test Character, Spellbook, Talents, Professions, Auction House, LFG, Social,
+ Collections, and the DragonUI options tab on build 12340.
+6. Test login with NewEra disabled: the default DragonUI must remain complete.
diff --git a/DRAGONUI_OVERRIDES.md b/DRAGONUI_OVERRIDES.md
new file mode 100644
index 0000000..4ad28b4
--- /dev/null
+++ b/DRAGONUI_OVERRIDES.md
@@ -0,0 +1,105 @@
+# DragonUI: replacement addons
+
+MoonWell ships DragonUI as the default interface. Players can replace an
+individual DragonUI module without copying or editing the bundled addon.
+
+## Replacement addon structure
+
+The replacement must use its own folder and addon name. Do not create another
+folder named `DragonUI`; files inside an MPQ and loose files with the same
+virtual path do not form a supported update mechanism.
+
+```text
+MyDragonUIMinimap/
+|-- MyDragonUIMinimap.toc
+`-- MyDragonUIMinimap.lua
+```
+
+`MyDragonUIMinimap.toc`:
+
+```toc
+## Interface: 30300
+## Title: My DragonUI Minimap
+## Dependencies: DragonUI
+## X-DragonUI-Overrides: minimap
+## X-DragonUI-Override-Priority: 100
+
+MyDragonUIMinimap.lua
+```
+
+The `X-DragonUI-Overrides` declaration is required. DragonUI reads it before
+dependent addons execute and does not apply the bundled implementation. A list
+may contain several comma-separated module identifiers.
+
+`MyDragonUIMinimap.lua`:
+
+```lua
+local provider = {}
+
+function provider:Activate(dragonUI, moduleName)
+ -- Create or show the replacement here.
+end
+
+function provider:Deactivate(dragonUI, moduleName)
+ -- Hide frames and unregister events created by this provider.
+end
+
+local ok, reason = DragonUI:RegisterModuleOverride(
+ "minimap",
+ "MyDragonUIMinimap",
+ provider,
+ { priority = 100 }
+)
+
+if not ok then
+ DEFAULT_CHAT_FRAME:AddMessage("MyDragonUIMinimap: " .. tostring(reason))
+end
+```
+
+The owner passed to `RegisterModuleOverride` must match the addon folder/TOC
+name. Supported provider activation names are `Activate`, `Apply`, `Enable`, or
+`OnEnable`; cleanup names are `Deactivate`, `Restore`, `Disable`, or
+`OnDisable`.
+
+## Public API (version 1)
+
+```lua
+DragonUI.PUBLIC_API_VERSION
+DragonUI:RegisterModuleOverride(moduleName, owner, provider[, options])
+DragonUI:UnregisterModuleOverride(moduleName, owner)
+DragonUI:IsModuleOverridden(moduleName)
+DragonUI:GetModuleOverride(moduleName) -- owner, provider
+```
+
+DragonUI also sends the Ace message `DRAGONUI_OVERRIDE_CHANGED` with
+`moduleName, owner`. `owner` is nil after the final provider is removed.
+
+Registering a replacement at runtime without TOC metadata is supported only
+for a module that can be restored safely. For a load-once module the call
+returns `true, "RELOAD_REQUIRED"`; add the metadata and reload the UI.
+
+## Bundled module identifiers
+
+The registry currently exposes these identifiers:
+
+```text
+bagsort, boss, buffs, buttons, castbar, chatmods, combuctor, cooldowns,
+darkmode, itemquality, keybinding, mainbars, micromenu, minimap, multicast,
+noop, petbar, player, questtracker, rage_indicator, stance, tooltip,
+unitframe_layers, vehicle
+```
+
+Target/focus/party frames still contain legacy startup code and will be added
+to the public registry before they are advertised as independently replaceable.
+
+## Conflict resolution
+
+When multiple enabled addons claim the same module, the highest numeric
+`X-DragonUI-Override-Priority` wins. Equal priorities are resolved by addon
+name to make the result deterministic. A declaration that never registers a
+provider is rejected at `PLAYER_LOGIN`, and DragonUI restores its bundled
+module so the player is not left without that UI element.
+
+Module suppression is session-only. DragonUI restores the player's saved
+enabled/disabled choice during `PLAYER_LOGOUT`, so removing a replacement addon
+automatically brings back the previous DragonUI configuration.
diff --git a/README.md b/README.md
index 62ce41d..6015579 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,18 @@ WarcraftXL не требуется и в объектное хранилище
- `patch-ruRU-5` — русская локализация и интерфейс;
- `patch-Z` — Mythic+ ресурсы и клиентские данные.
+## DragonUI и пользовательские замены
+
+DragonUI поставляется как включённый системный интерфейс. Игрок не заменяет
+файлы из MPQ: отдельный аддон может заранее объявить, какие модули он берёт под
+свой контроль, и зарегистрировать собственную реализацию после загрузки
+DragonUI. Формат TOC, Lua API, приоритеты и рабочий пример описаны в
+[`DRAGONUI_OVERRIDES.md`](DRAGONUI_OVERRIDES.md).
+
+DragonUI_NewEra и его `!!!ClassicAPI` входят в системный интерфейс MoonWell.
+Зафиксированные upstream-версии, состав поставки и решение конфликта двух
+Encounter Journal описаны в [`DRAGONUI_NEWERA.md`](DRAGONUI_NEWERA.md).
+
## Лицензирование
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc
new file mode 100644
index 0000000..b3b895c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc
@@ -0,0 +1,10 @@
+## Interface: 30300
+## Title: !!!ClassicAPI
+## Notes: Blizzard Classic API Emulation
+## Version: 1.1.64
+## Author: Tsoukie
+## DefaultState: Enabled
+## X-MoonWell-System: 1
+## X-Upstream-Commit: b3da3000bba23a2c31aa9c9b7c0232d5973789e6
+Lib\Load.xml
+Load.xml
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/CallbackHandler-1.0/CallbackHandler-1.0.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/CallbackHandler-1.0/CallbackHandler-1.0.lua
new file mode 100644
index 0000000..479a118
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/CallbackHandler-1.0/CallbackHandler-1.0.lua
@@ -0,0 +1,238 @@
+--[[ $Id: CallbackHandler-1.0.lua 18 2014-10-16 02:52:20Z mikk $ ]]
+local MAJOR, MINOR = "CallbackHandler-1.0", 7
+local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not CallbackHandler then return end -- No upgrade needed
+
+local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
+
+-- Lua APIs
+local tconcat = table.concat
+local assert, error, loadstring = assert, error, loadstring
+local setmetatable, rawset, rawget = setmetatable, rawset, rawget
+local next, select, pairs, type, tostring = next, select, pairs, type, tostring
+
+-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
+-- List them here for Mikk's FindGlobals script
+-- GLOBALS: geterrorhandler
+
+local xpcall = xpcall
+
+local function errorhandler(err)
+ return geterrorhandler()(err)
+end
+
+local function CreateDispatcher(argCount)
+ local code = [[
+ local next, xpcall, eh = ...
+
+ local method, ARGS
+ local function call() method(ARGS) end
+
+ local function dispatch(handlers, ...)
+ local index
+ index, method = next(handlers)
+ if not method then return end
+ local OLD_ARGS = ARGS
+ ARGS = ...
+ repeat
+ xpcall(call, eh)
+ index, method = next(handlers, index)
+ until not method
+ ARGS = OLD_ARGS
+ end
+
+ return dispatch
+ ]]
+
+ local ARGS, OLD_ARGS = {}, {}
+ for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
+ code = code:gsub("OLD_ARGS", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(ARGS, ", "))
+ return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
+end
+
+local Dispatchers = setmetatable({}, {__index=function(self, argCount)
+ local dispatcher = CreateDispatcher(argCount)
+ rawset(self, argCount, dispatcher)
+ return dispatcher
+end})
+
+--------------------------------------------------------------------------
+-- CallbackHandler:New
+--
+-- target - target object to embed public APIs in
+-- RegisterName - name of the callback registration API, default "RegisterCallback"
+-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
+-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
+
+function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName)
+
+ RegisterName = RegisterName or "RegisterCallback"
+ UnregisterName = UnregisterName or "UnregisterCallback"
+ if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
+ UnregisterAllName = "UnregisterAllCallbacks"
+ end
+
+ -- we declare all objects and exported APIs inside this closure to quickly gain access
+ -- to e.g. function names, the "target" parameter, etc
+
+
+ -- Create the registry object
+ local events = setmetatable({}, meta)
+ local registry = { recurse=0, events=events }
+
+ -- registry:Fire() - fires the given event/message into the registry
+ function registry:Fire(eventname, ...)
+ if not rawget(events, eventname) or not next(events[eventname]) then return end
+ local oldrecurse = registry.recurse
+ registry.recurse = oldrecurse + 1
+
+ Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
+
+ registry.recurse = oldrecurse
+
+ if registry.insertQueue and oldrecurse==0 then
+ -- Something in one of our callbacks wanted to register more callbacks; they got queued
+ for eventname,callbacks in pairs(registry.insertQueue) do
+ local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
+ for self,func in pairs(callbacks) do
+ events[eventname][self] = func
+ -- fire OnUsed callback?
+ if first and registry.OnUsed then
+ registry.OnUsed(registry, target, eventname)
+ first = nil
+ end
+ end
+ end
+ registry.insertQueue = nil
+ end
+ end
+
+ -- Registration of a callback, handles:
+ -- self["method"], leads to self["method"](self, ...)
+ -- self with function ref, leads to functionref(...)
+ -- "addonId" (instead of self) with function ref, leads to functionref(...)
+ -- all with an optional arg, which, if present, gets passed as first argument (after self if present)
+ target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
+ if type(eventname) ~= "string" then
+ error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
+ end
+
+ method = method or eventname
+
+ local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
+
+ if type(method) ~= "string" and type(method) ~= "function" then
+ error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
+ end
+
+ local regfunc
+
+ if type(method) == "string" then
+ -- self["method"] calling style
+ if type(self) ~= "table" then
+ error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
+ elseif self==target then
+ error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
+ elseif type(self[method]) ~= "function" then
+ error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
+ end
+
+ if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
+ local arg=select(1,...)
+ regfunc = function(...) self[method](self,arg,...) end
+ else
+ regfunc = function(...) self[method](self,...) end
+ end
+ else
+ -- function ref with self=object or self="addonId" or self=thread
+ if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
+ error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
+ end
+
+ if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
+ local arg=select(1,...)
+ regfunc = function(...) method(arg,...) end
+ else
+ regfunc = method
+ end
+ end
+
+
+ if events[eventname][self] or registry.recurse<1 then
+ -- if registry.recurse<1 then
+ -- we're overwriting an existing entry, or not currently recursing. just set it.
+ events[eventname][self] = regfunc
+ -- fire OnUsed callback?
+ if registry.OnUsed and first then
+ registry.OnUsed(registry, target, eventname)
+ end
+ else
+ -- we're currently processing a callback in this registry, so delay the registration of this new entry!
+ -- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
+ registry.insertQueue = registry.insertQueue or setmetatable({},meta)
+ registry.insertQueue[eventname][self] = regfunc
+ end
+ end
+
+ -- Unregister a callback
+ target[UnregisterName] = function(self, eventname)
+ if not self or self==target then
+ error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
+ end
+ if type(eventname) ~= "string" then
+ error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
+ end
+ if rawget(events, eventname) and events[eventname][self] then
+ events[eventname][self] = nil
+ -- Fire OnUnused callback?
+ if registry.OnUnused and not next(events[eventname]) then
+ registry.OnUnused(registry, target, eventname)
+ end
+ end
+ if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
+ registry.insertQueue[eventname][self] = nil
+ end
+ end
+
+ -- OPTIONAL: Unregister all callbacks for given selfs/addonIds
+ if UnregisterAllName then
+ target[UnregisterAllName] = function(...)
+ if select("#",...)<1 then
+ error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
+ end
+ if select("#",...)==1 and ...==target then
+ error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
+ end
+
+
+ for i=1,select("#",...) do
+ local self = select(i,...)
+ if registry.insertQueue then
+ for eventname, callbacks in pairs(registry.insertQueue) do
+ if callbacks[self] then
+ callbacks[self] = nil
+ end
+ end
+ end
+ for eventname, callbacks in pairs(events) do
+ if callbacks[self] then
+ callbacks[self] = nil
+ -- Fire OnUnused callback?
+ if registry.OnUnused and not next(callbacks) then
+ registry.OnUnused(registry, target, eventname)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return registry
+end
+
+
+-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
+-- try to upgrade old implicit embeds since the system is selfcontained and
+-- relies on closures to work.
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/CallbackHandler-1.0/CallbackHandler-1.0.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/CallbackHandler-1.0/CallbackHandler-1.0.xml
new file mode 100644
index 0000000..876df83
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/CallbackHandler-1.0/CallbackHandler-1.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/LibStub/LibStub.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/LibStub/LibStub.lua
new file mode 100644
index 0000000..ef702b8
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Ace3/LibStub/LibStub.lua
@@ -0,0 +1,51 @@
+-- $Id: LibStub.lua 103 2014-10-16 03:02:50Z mikk $
+-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/addons/libstub/ for more info
+-- LibStub is hereby placed in the Public Domain
+-- Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
+local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 3 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
+local LibStub = _G[LIBSTUB_MAJOR]
+
+-- Check to see is this version of the stub is obsolete
+if not LibStub or LibStub.minor < LIBSTUB_MINOR then
+ LibStub = LibStub or {libs = {}, minors = {} }
+ _G[LIBSTUB_MAJOR] = LibStub
+ LibStub.minor = LIBSTUB_MINOR
+
+ -- LibStub:NewLibrary(major, minor)
+ -- major (string) - the major version of the library
+ -- minor (string or number ) - the minor version of the library
+ --
+ -- returns nil if a newer or same version of the lib is already present
+ -- returns empty library object or old library object if upgrade is needed
+ function LibStub:NewLibrary(major, minor)
+ assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
+ minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
+
+ local oldminor = self.minors[major]
+ if oldminor and oldminor >= minor then return nil end
+ self.minors[major], self.libs[major] = minor, self.libs[major] or {}
+ return self.libs[major], oldminor
+ end
+
+ -- LibStub:GetLibrary(major, [silent])
+ -- major (string) - the major version of the library
+ -- silent (boolean) - if true, library is optional, silently return nil if its not found
+ --
+ -- throws an error if the library can not be found (except silent is set)
+ -- returns the library object if found
+ function LibStub:GetLibrary(major, silent)
+ if not self.libs[major] and not silent then
+ error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
+ end
+ return self.libs[major], self.minors[major]
+ end
+
+ -- LibStub:IterateLibraries()
+ --
+ -- Returns an iterator for the currently registered libraries
+ function LibStub:IterateLibraries()
+ return pairs(self.libs)
+ end
+
+ setmetatable(LibStub, { __call = LibStub.GetLibrary })
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/ChatThrottleLib.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/ChatThrottleLib.lua
new file mode 100644
index 0000000..bd7ea09
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/ChatThrottleLib.lua
@@ -0,0 +1,523 @@
+--
+-- ChatThrottleLib by Mikk
+--
+-- Manages AddOn chat output to keep player from getting kicked off.
+--
+-- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept
+-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
+--
+-- Priorities get an equal share of available bandwidth when fully loaded.
+-- Communication channels are separated on extension+chattype+destination and
+-- get round-robinned. (Destination only matters for whispers and channels,
+-- obviously)
+--
+-- Will install hooks for SendChatMessage and SendAddonMessage to measure
+-- bandwidth bypassing the library and use less bandwidth itself.
+--
+--
+-- Fully embeddable library. Just copy this file into your addon directory,
+-- add it to the .toc, and it's done.
+--
+-- Can run as a standalone addon also, but, really, just embed it! :-)
+--
+-- LICENSE: ChatThrottleLib is released into the Public Domain
+--
+
+local CTL_VERSION = 25
+
+local _G = _G
+
+if _G.ChatThrottleLib then
+ if _G.ChatThrottleLib.version >= CTL_VERSION then
+ -- There's already a newer (or same) version loaded. Buh-bye.
+ return
+ elseif not _G.ChatThrottleLib.securelyHooked then
+ print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, =v16) in it!")
+ -- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
+ -- ... and if someone has securehooked, they can kiss that goodbye too... >.<
+ _G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
+ if _G.ChatThrottleLib.ORIG_SendAddonMessage then
+ _G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
+ end
+ end
+ _G.ChatThrottleLib.ORIG_SendChatMessage = nil
+ _G.ChatThrottleLib.ORIG_SendAddonMessage = nil
+end
+
+if not _G.ChatThrottleLib then
+ _G.ChatThrottleLib = {}
+end
+
+ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
+local ChatThrottleLib = _G.ChatThrottleLib
+
+ChatThrottleLib.version = CTL_VERSION
+
+
+
+------------------ TWEAKABLES -----------------
+
+ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
+ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
+
+ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
+
+ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
+
+
+local setmetatable = setmetatable
+local table_remove = table.remove
+local tostring = tostring
+local GetTime = GetTime
+local math_min = math.min
+local math_max = math.max
+local next = next
+local strlen = string.len
+local GetFramerate = GetFramerate
+local strlower = string.lower
+local unpack,type,pairs,wipe = unpack,type,pairs,wipe
+local UnitInRaid,GetNumPartyMembers = UnitInRaid,GetNumPartyMembers
+
+
+-----------------------------------------------------------------------
+-- Double-linked ring implementation
+
+local Ring = {}
+local RingMeta = { __index = Ring }
+
+function Ring:New()
+ local ret = {}
+ setmetatable(ret, RingMeta)
+ return ret
+end
+
+function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
+ if self.pos then
+ obj.prev = self.pos.prev
+ obj.prev.next = obj
+ obj.next = self.pos
+ obj.next.prev = obj
+ else
+ obj.next = obj
+ obj.prev = obj
+ self.pos = obj
+ end
+end
+
+function Ring:Remove(obj)
+ obj.next.prev = obj.prev
+ obj.prev.next = obj.next
+ if self.pos == obj then
+ self.pos = obj.next
+ if self.pos == obj then
+ self.pos = nil
+ end
+ end
+end
+
+
+
+-----------------------------------------------------------------------
+-- Recycling bin for pipes
+-- A pipe is a plain integer-indexed queue of messages
+-- Pipes normally live in Rings of pipes (3 rings total, one per priority)
+
+ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
+local PipeBin = setmetatable({}, {__mode="k"})
+
+local function DelPipe(pipe)
+ for i = #pipe, 1, -1 do
+ pipe[i] = nil
+ end
+ pipe.prev = nil
+ pipe.next = nil
+
+ PipeBin[pipe] = true
+end
+
+local function NewPipe()
+ local pipe = next(PipeBin)
+ if pipe then
+ wipe(pipe)
+ PipeBin[pipe] = nil
+ return pipe
+ end
+ return {}
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Recycling bin for messages
+
+ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
+local MsgBin = setmetatable({}, {__mode="k"})
+
+local function DelMsg(msg)
+ msg[1] = nil
+ -- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them.
+ MsgBin[msg] = true
+end
+
+local function NewMsg()
+ local msg = next(MsgBin)
+ if msg then
+ MsgBin[msg] = nil
+ return msg
+ end
+ return {}
+end
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib:Init
+-- Initialize queues, set up frame for OnUpdate, etc
+
+
+function ChatThrottleLib:Init()
+
+ -- Set up queues
+ if not self.Prio then
+ self.Prio = {}
+ self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ end
+
+ -- v4: total send counters per priority
+ for _, Prio in pairs(self.Prio) do
+ Prio.nTotalSent = Prio.nTotalSent or 0
+ end
+
+ if not self.avail then
+ self.avail = 0 -- v5
+ end
+ if not self.nTotalSent then
+ self.nTotalSent = 0 -- v5
+ end
+
+
+ -- Set up a frame to get OnUpdate events
+ if not self.Frame then
+ self.Frame = CreateFrame("Frame")
+ self.Frame:Hide()
+ end
+ self.Frame:SetScript("OnUpdate", self.OnUpdate)
+ self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds
+ self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
+ self.OnUpdateDelay = 0
+ self.LastAvailUpdate = GetTime()
+ self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup
+
+ -- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
+ if not self.securelyHooked then
+ -- Use secure hooks as of v16. Old regular hook support yanked out in v21.
+ self.securelyHooked = true
+ --SendChatMessage
+ hooksecurefunc("SendChatMessage", function(...)
+ return ChatThrottleLib.Hook_SendChatMessage(...)
+ end)
+ --SendAddonMessage
+ hooksecurefunc("SendAddonMessage", function(...)
+ return ChatThrottleLib.Hook_SendAddonMessage(...)
+ end)
+ end
+ self.nBypass = 0
+end
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
+
+local bMyTraffic = false
+
+function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
+ if bMyTraffic then
+ return
+ end
+ local self = ChatThrottleLib
+ local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
+ self.avail = self.avail - size
+ self.nBypass = self.nBypass + size -- just a statistic
+end
+function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
+ if bMyTraffic then
+ return
+ end
+ local self = ChatThrottleLib
+ local size = tostring(text or ""):len() + tostring(prefix or ""):len();
+ size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
+ self.avail = self.avail - size
+ self.nBypass = self.nBypass + size -- just a statistic
+end
+
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib:UpdateAvail
+-- Update self.avail with how much bandwidth is currently available
+
+function ChatThrottleLib:UpdateAvail()
+ local now = GetTime()
+ local MAX_CPS = self.MAX_CPS;
+ local newavail = MAX_CPS * (now - self.LastAvailUpdate)
+ local avail = self.avail
+
+ if now - self.HardThrottlingBeginTime < 5 then
+ -- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
+ avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
+ self.bChoking = true
+ elseif GetFramerate() < self.MIN_FPS then -- GetFramerate call takes ~0.002 secs
+ avail = math_min(MAX_CPS, avail + newavail*0.5)
+ self.bChoking = true -- just a statistic
+ else
+ avail = math_min(self.BURST, avail + newavail)
+ self.bChoking = false
+ end
+
+ avail = math_max(avail, 0-(MAX_CPS*2)) -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
+
+ self.avail = avail
+ self.LastAvailUpdate = now
+
+ return avail
+end
+
+
+-----------------------------------------------------------------------
+-- Despooling logic
+-- Reminder:
+-- - We have 3 Priorities, each containing a "Ring" construct ...
+-- - ... made up of N "Pipe"s (1 for each destination/pipename)
+-- - and each pipe contains messages
+
+function ChatThrottleLib:Despool(Prio)
+ local ring = Prio.Ring
+ while ring.pos and Prio.avail > ring.pos[1].nSize do
+ local msg = table_remove(ring.pos, 1)
+ if not ring.pos[1] then -- did we remove last msg in this pipe?
+ local pipe = Prio.Ring.pos
+ Prio.Ring:Remove(pipe)
+ Prio.ByName[pipe.name] = nil
+ DelPipe(pipe)
+ else
+ Prio.Ring.pos = Prio.Ring.pos.next
+ end
+ local didSend=false
+ local lowerDest = strlower(msg[3] or "")
+ if lowerDest == "raid" and not UnitInRaid("player") then
+ -- do nothing
+ elseif lowerDest == "party" and GetNumPartyMembers() == 0 then
+ -- do nothing
+ else
+ Prio.avail = Prio.avail - msg.nSize
+ bMyTraffic = true
+ msg.f(unpack(msg, 1, msg.n))
+ bMyTraffic = false
+ Prio.nTotalSent = Prio.nTotalSent + msg.nSize
+ DelMsg(msg)
+ didSend = true
+ end
+ -- notify caller of delivery (even if we didn't send it)
+ if msg.callbackFn then
+ msg.callbackFn (msg.callbackArg, didSend)
+ end
+ -- USER CALLBACK MAY ERROR
+ end
+end
+
+
+function ChatThrottleLib.OnEvent(this,event)
+ -- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too.
+ local self = ChatThrottleLib
+ if event == "PLAYER_ENTERING_WORLD" then
+ self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning
+ self.avail = 0
+ end
+end
+
+
+function ChatThrottleLib.OnUpdate(this,delay)
+ local self = ChatThrottleLib
+
+ self.OnUpdateDelay = self.OnUpdateDelay + delay
+ if self.OnUpdateDelay < 0.08 then
+ return
+ end
+ self.OnUpdateDelay = 0
+
+ self:UpdateAvail()
+
+ if self.avail < 0 then
+ return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
+ end
+
+ -- See how many of our priorities have queued messages (we only have 3, don't worry about the loop)
+ local n = 0
+ for prioname,Prio in pairs(self.Prio) do
+ if Prio.Ring.pos or Prio.avail < 0 then
+ n = n + 1
+ end
+ end
+
+ -- Anything queued still?
+ if n<1 then
+ -- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
+ for prioname, Prio in pairs(self.Prio) do
+ self.avail = self.avail + Prio.avail
+ Prio.avail = 0
+ end
+ self.bQueueing = false
+ self.Frame:Hide()
+ return
+ end
+
+ -- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
+ local avail = self.avail/n
+ self.avail = 0
+
+ for prioname, Prio in pairs(self.Prio) do
+ if Prio.Ring.pos or Prio.avail < 0 then
+ Prio.avail = Prio.avail + avail
+ if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then
+ self:Despool(Prio)
+ -- Note: We might not get here if the user-supplied callback function errors out! Take care!
+ end
+ end
+ end
+
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Spooling logic
+
+function ChatThrottleLib:Enqueue(prioname, pipename, msg)
+ local Prio = self.Prio[prioname]
+ local pipe = Prio.ByName[pipename]
+ if not pipe then
+ self.Frame:Show()
+ pipe = NewPipe()
+ pipe.name = pipename
+ Prio.ByName[pipename] = pipe
+ Prio.Ring:Add(pipe)
+ end
+
+ pipe[#pipe + 1] = msg
+
+ self.bQueueing = true
+end
+
+function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
+ if not self or not prio or not prefix or not text or not self.Prio[prio] then
+ error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = text:len()
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
+ end
+
+ nSize = nSize + self.MSG_OVERHEAD
+
+ -- Check if there's room in the global available bandwidth gauge to send directly
+ if not self.bQueueing and nSize < self:UpdateAvail() then
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendChatMessage(text, chattype, language, destination)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg, true)
+ end
+ -- USER CALLBACK MAY ERROR
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendChatMessage
+ msg[1] = text
+ msg[2] = chattype or "SAY"
+ msg[3] = language
+ msg[4] = destination
+ msg.n = 4
+ msg.nSize = nSize
+ msg.callbackFn = callbackFn
+ msg.callbackArg = callbackArg
+
+ self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg)
+end
+
+
+function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
+ if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
+ error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = prefix:len() + 1 + text:len();
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2)
+ end
+
+ nSize = nSize + self.MSG_OVERHEAD;
+
+ -- Check if there's room in the global available bandwidth gauge to send directly
+ if not self.bQueueing and nSize < self:UpdateAvail() then
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendAddonMessage(prefix, text, chattype, target)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg, true)
+ end
+ -- USER CALLBACK MAY ERROR
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendAddonMessage
+ msg[1] = prefix
+ msg[2] = text
+ msg[3] = chattype
+ msg[4] = target
+ msg.n = (target~=nil) and 4 or 3;
+ msg.nSize = nSize
+ msg.callbackFn = callbackFn
+ msg.callbackArg = callbackArg
+
+ self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg)
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Get the ball rolling!
+
+ChatThrottleLib:Init()
+
+--[[ WoWBench debugging snippet
+if(WOWB_VER) then
+ local function SayTimer()
+ print("SAY: "..GetTime().." "..arg1)
+ end
+ ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
+ ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
+end
+]]
+
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/ChatThrottleLib.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/ChatThrottleLib.lua
new file mode 100644
index 0000000..bd7ea09
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/ChatThrottleLib.lua
@@ -0,0 +1,523 @@
+--
+-- ChatThrottleLib by Mikk
+--
+-- Manages AddOn chat output to keep player from getting kicked off.
+--
+-- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept
+-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
+--
+-- Priorities get an equal share of available bandwidth when fully loaded.
+-- Communication channels are separated on extension+chattype+destination and
+-- get round-robinned. (Destination only matters for whispers and channels,
+-- obviously)
+--
+-- Will install hooks for SendChatMessage and SendAddonMessage to measure
+-- bandwidth bypassing the library and use less bandwidth itself.
+--
+--
+-- Fully embeddable library. Just copy this file into your addon directory,
+-- add it to the .toc, and it's done.
+--
+-- Can run as a standalone addon also, but, really, just embed it! :-)
+--
+-- LICENSE: ChatThrottleLib is released into the Public Domain
+--
+
+local CTL_VERSION = 25
+
+local _G = _G
+
+if _G.ChatThrottleLib then
+ if _G.ChatThrottleLib.version >= CTL_VERSION then
+ -- There's already a newer (or same) version loaded. Buh-bye.
+ return
+ elseif not _G.ChatThrottleLib.securelyHooked then
+ print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, =v16) in it!")
+ -- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
+ -- ... and if someone has securehooked, they can kiss that goodbye too... >.<
+ _G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
+ if _G.ChatThrottleLib.ORIG_SendAddonMessage then
+ _G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
+ end
+ end
+ _G.ChatThrottleLib.ORIG_SendChatMessage = nil
+ _G.ChatThrottleLib.ORIG_SendAddonMessage = nil
+end
+
+if not _G.ChatThrottleLib then
+ _G.ChatThrottleLib = {}
+end
+
+ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
+local ChatThrottleLib = _G.ChatThrottleLib
+
+ChatThrottleLib.version = CTL_VERSION
+
+
+
+------------------ TWEAKABLES -----------------
+
+ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
+ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
+
+ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
+
+ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
+
+
+local setmetatable = setmetatable
+local table_remove = table.remove
+local tostring = tostring
+local GetTime = GetTime
+local math_min = math.min
+local math_max = math.max
+local next = next
+local strlen = string.len
+local GetFramerate = GetFramerate
+local strlower = string.lower
+local unpack,type,pairs,wipe = unpack,type,pairs,wipe
+local UnitInRaid,GetNumPartyMembers = UnitInRaid,GetNumPartyMembers
+
+
+-----------------------------------------------------------------------
+-- Double-linked ring implementation
+
+local Ring = {}
+local RingMeta = { __index = Ring }
+
+function Ring:New()
+ local ret = {}
+ setmetatable(ret, RingMeta)
+ return ret
+end
+
+function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
+ if self.pos then
+ obj.prev = self.pos.prev
+ obj.prev.next = obj
+ obj.next = self.pos
+ obj.next.prev = obj
+ else
+ obj.next = obj
+ obj.prev = obj
+ self.pos = obj
+ end
+end
+
+function Ring:Remove(obj)
+ obj.next.prev = obj.prev
+ obj.prev.next = obj.next
+ if self.pos == obj then
+ self.pos = obj.next
+ if self.pos == obj then
+ self.pos = nil
+ end
+ end
+end
+
+
+
+-----------------------------------------------------------------------
+-- Recycling bin for pipes
+-- A pipe is a plain integer-indexed queue of messages
+-- Pipes normally live in Rings of pipes (3 rings total, one per priority)
+
+ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
+local PipeBin = setmetatable({}, {__mode="k"})
+
+local function DelPipe(pipe)
+ for i = #pipe, 1, -1 do
+ pipe[i] = nil
+ end
+ pipe.prev = nil
+ pipe.next = nil
+
+ PipeBin[pipe] = true
+end
+
+local function NewPipe()
+ local pipe = next(PipeBin)
+ if pipe then
+ wipe(pipe)
+ PipeBin[pipe] = nil
+ return pipe
+ end
+ return {}
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Recycling bin for messages
+
+ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
+local MsgBin = setmetatable({}, {__mode="k"})
+
+local function DelMsg(msg)
+ msg[1] = nil
+ -- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them.
+ MsgBin[msg] = true
+end
+
+local function NewMsg()
+ local msg = next(MsgBin)
+ if msg then
+ MsgBin[msg] = nil
+ return msg
+ end
+ return {}
+end
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib:Init
+-- Initialize queues, set up frame for OnUpdate, etc
+
+
+function ChatThrottleLib:Init()
+
+ -- Set up queues
+ if not self.Prio then
+ self.Prio = {}
+ self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ end
+
+ -- v4: total send counters per priority
+ for _, Prio in pairs(self.Prio) do
+ Prio.nTotalSent = Prio.nTotalSent or 0
+ end
+
+ if not self.avail then
+ self.avail = 0 -- v5
+ end
+ if not self.nTotalSent then
+ self.nTotalSent = 0 -- v5
+ end
+
+
+ -- Set up a frame to get OnUpdate events
+ if not self.Frame then
+ self.Frame = CreateFrame("Frame")
+ self.Frame:Hide()
+ end
+ self.Frame:SetScript("OnUpdate", self.OnUpdate)
+ self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds
+ self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
+ self.OnUpdateDelay = 0
+ self.LastAvailUpdate = GetTime()
+ self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup
+
+ -- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
+ if not self.securelyHooked then
+ -- Use secure hooks as of v16. Old regular hook support yanked out in v21.
+ self.securelyHooked = true
+ --SendChatMessage
+ hooksecurefunc("SendChatMessage", function(...)
+ return ChatThrottleLib.Hook_SendChatMessage(...)
+ end)
+ --SendAddonMessage
+ hooksecurefunc("SendAddonMessage", function(...)
+ return ChatThrottleLib.Hook_SendAddonMessage(...)
+ end)
+ end
+ self.nBypass = 0
+end
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
+
+local bMyTraffic = false
+
+function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
+ if bMyTraffic then
+ return
+ end
+ local self = ChatThrottleLib
+ local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
+ self.avail = self.avail - size
+ self.nBypass = self.nBypass + size -- just a statistic
+end
+function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
+ if bMyTraffic then
+ return
+ end
+ local self = ChatThrottleLib
+ local size = tostring(text or ""):len() + tostring(prefix or ""):len();
+ size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
+ self.avail = self.avail - size
+ self.nBypass = self.nBypass + size -- just a statistic
+end
+
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib:UpdateAvail
+-- Update self.avail with how much bandwidth is currently available
+
+function ChatThrottleLib:UpdateAvail()
+ local now = GetTime()
+ local MAX_CPS = self.MAX_CPS;
+ local newavail = MAX_CPS * (now - self.LastAvailUpdate)
+ local avail = self.avail
+
+ if now - self.HardThrottlingBeginTime < 5 then
+ -- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
+ avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
+ self.bChoking = true
+ elseif GetFramerate() < self.MIN_FPS then -- GetFramerate call takes ~0.002 secs
+ avail = math_min(MAX_CPS, avail + newavail*0.5)
+ self.bChoking = true -- just a statistic
+ else
+ avail = math_min(self.BURST, avail + newavail)
+ self.bChoking = false
+ end
+
+ avail = math_max(avail, 0-(MAX_CPS*2)) -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
+
+ self.avail = avail
+ self.LastAvailUpdate = now
+
+ return avail
+end
+
+
+-----------------------------------------------------------------------
+-- Despooling logic
+-- Reminder:
+-- - We have 3 Priorities, each containing a "Ring" construct ...
+-- - ... made up of N "Pipe"s (1 for each destination/pipename)
+-- - and each pipe contains messages
+
+function ChatThrottleLib:Despool(Prio)
+ local ring = Prio.Ring
+ while ring.pos and Prio.avail > ring.pos[1].nSize do
+ local msg = table_remove(ring.pos, 1)
+ if not ring.pos[1] then -- did we remove last msg in this pipe?
+ local pipe = Prio.Ring.pos
+ Prio.Ring:Remove(pipe)
+ Prio.ByName[pipe.name] = nil
+ DelPipe(pipe)
+ else
+ Prio.Ring.pos = Prio.Ring.pos.next
+ end
+ local didSend=false
+ local lowerDest = strlower(msg[3] or "")
+ if lowerDest == "raid" and not UnitInRaid("player") then
+ -- do nothing
+ elseif lowerDest == "party" and GetNumPartyMembers() == 0 then
+ -- do nothing
+ else
+ Prio.avail = Prio.avail - msg.nSize
+ bMyTraffic = true
+ msg.f(unpack(msg, 1, msg.n))
+ bMyTraffic = false
+ Prio.nTotalSent = Prio.nTotalSent + msg.nSize
+ DelMsg(msg)
+ didSend = true
+ end
+ -- notify caller of delivery (even if we didn't send it)
+ if msg.callbackFn then
+ msg.callbackFn (msg.callbackArg, didSend)
+ end
+ -- USER CALLBACK MAY ERROR
+ end
+end
+
+
+function ChatThrottleLib.OnEvent(this,event)
+ -- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too.
+ local self = ChatThrottleLib
+ if event == "PLAYER_ENTERING_WORLD" then
+ self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning
+ self.avail = 0
+ end
+end
+
+
+function ChatThrottleLib.OnUpdate(this,delay)
+ local self = ChatThrottleLib
+
+ self.OnUpdateDelay = self.OnUpdateDelay + delay
+ if self.OnUpdateDelay < 0.08 then
+ return
+ end
+ self.OnUpdateDelay = 0
+
+ self:UpdateAvail()
+
+ if self.avail < 0 then
+ return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
+ end
+
+ -- See how many of our priorities have queued messages (we only have 3, don't worry about the loop)
+ local n = 0
+ for prioname,Prio in pairs(self.Prio) do
+ if Prio.Ring.pos or Prio.avail < 0 then
+ n = n + 1
+ end
+ end
+
+ -- Anything queued still?
+ if n<1 then
+ -- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
+ for prioname, Prio in pairs(self.Prio) do
+ self.avail = self.avail + Prio.avail
+ Prio.avail = 0
+ end
+ self.bQueueing = false
+ self.Frame:Hide()
+ return
+ end
+
+ -- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
+ local avail = self.avail/n
+ self.avail = 0
+
+ for prioname, Prio in pairs(self.Prio) do
+ if Prio.Ring.pos or Prio.avail < 0 then
+ Prio.avail = Prio.avail + avail
+ if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then
+ self:Despool(Prio)
+ -- Note: We might not get here if the user-supplied callback function errors out! Take care!
+ end
+ end
+ end
+
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Spooling logic
+
+function ChatThrottleLib:Enqueue(prioname, pipename, msg)
+ local Prio = self.Prio[prioname]
+ local pipe = Prio.ByName[pipename]
+ if not pipe then
+ self.Frame:Show()
+ pipe = NewPipe()
+ pipe.name = pipename
+ Prio.ByName[pipename] = pipe
+ Prio.Ring:Add(pipe)
+ end
+
+ pipe[#pipe + 1] = msg
+
+ self.bQueueing = true
+end
+
+function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
+ if not self or not prio or not prefix or not text or not self.Prio[prio] then
+ error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = text:len()
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
+ end
+
+ nSize = nSize + self.MSG_OVERHEAD
+
+ -- Check if there's room in the global available bandwidth gauge to send directly
+ if not self.bQueueing and nSize < self:UpdateAvail() then
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendChatMessage(text, chattype, language, destination)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg, true)
+ end
+ -- USER CALLBACK MAY ERROR
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendChatMessage
+ msg[1] = text
+ msg[2] = chattype or "SAY"
+ msg[3] = language
+ msg[4] = destination
+ msg.n = 4
+ msg.nSize = nSize
+ msg.callbackFn = callbackFn
+ msg.callbackArg = callbackArg
+
+ self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg)
+end
+
+
+function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
+ if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
+ error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = prefix:len() + 1 + text:len();
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2)
+ end
+
+ nSize = nSize + self.MSG_OVERHEAD;
+
+ -- Check if there's room in the global available bandwidth gauge to send directly
+ if not self.bQueueing and nSize < self:UpdateAvail() then
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendAddonMessage(prefix, text, chattype, target)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg, true)
+ end
+ -- USER CALLBACK MAY ERROR
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendAddonMessage
+ msg[1] = prefix
+ msg[2] = text
+ msg[3] = chattype
+ msg[4] = target
+ msg.n = (target~=nil) and 4 or 3;
+ msg.nSize = nSize
+ msg.callbackFn = callbackFn
+ msg.callbackArg = callbackArg
+
+ self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg)
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Get the ball rolling!
+
+ChatThrottleLib:Init()
+
+--[[ WoWBench debugging snippet
+if(WOWB_VER) then
+ local function SayTimer()
+ print("SAY: "..GetTime().." "..arg1)
+ end
+ ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
+ ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
+end
+]]
+
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/LibHealComm-4.0.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/LibHealComm-4.0.lua
new file mode 100644
index 0000000..c51ef97
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/LibHealComm-4.0.lua
@@ -0,0 +1,2965 @@
+local major = "LibHealComm-4.0"
+local minor = 67
+assert(LibStub, string.format("%s requires LibStub.", major))
+
+local HealComm = LibStub:NewLibrary(major, minor)
+if( not HealComm ) then return end
+
+local pairs = pairs
+local rawget, rawset = rawget, rawset
+local select = select
+local setmetatable = setmetatable
+local tonumber = tonumber
+local type = type
+local unpack = unpack
+local band, bor = bit.band, bit.bor
+local ceil, floor, max, min = math.ceil, math.floor, math.max, math.min
+local byte, char, format, gsub, len, match, split = string.byte, string.char, string.format, string.gsub, string.len, string.match, string.split
+local tconcat, tinsert, tremove, twipe = table.concat, table.insert, table.remove, table.wipe
+
+local GetGlyphSocketInfo = GetGlyphSocketInfo
+local GetInventoryItemLink = GetInventoryItemLink
+local GetNumGlyphSockets = GetNumGlyphSockets
+local GetNumPartyMembers = GetNumPartyMembers
+local GetNumRaidMembers = GetNumRaidMembers
+local GetNumSpellTabs = GetNumSpellTabs
+local GetNumTalentTabs = GetNumTalentTabs
+local GetNumTalents = GetNumTalents
+local GetRaidRosterInfo = GetRaidRosterInfo
+local GetSpellBonusHealing = GetSpellBonusHealing
+local GetSpellCritChance = GetSpellCritChance
+local GetSpellInfo = GetSpellInfo
+local GetSpellName = GetSpellName
+local GetSpellTabInfo = GetSpellTabInfo
+local GetTalentInfo = GetTalentInfo
+local GetTime = GetTime
+local GetZonePVPInfo = GetZonePVPInfo
+local InCombatLockdown = InCombatLockdown
+local IsEquippedItem = IsEquippedItem
+local IsInInstance = IsInInstance
+local IsSpellInRange = IsSpellInRange
+local SpellIsTargeting = SpellIsTargeting
+local UnitAura = UnitAura
+local UnitBuff = UnitBuff
+local UnitCanAssist = UnitCanAssist
+local UnitCastingInfo = UnitCastingInfo
+local UnitChannelInfo = UnitChannelInfo
+local UnitExists = UnitExists
+local UnitGUID = UnitGUID
+local UnitHasVehicleUI = UnitHasVehicleUI
+local UnitIsCharmed = UnitIsCharmed
+local UnitIsVisible = UnitIsVisible
+local UnitLevel = UnitLevel
+local UnitName = UnitName
+local UnitPlayerControlled = UnitPlayerControlled
+
+local BOOKTYPE_SPELL = BOOKTYPE_SPELL
+local MAX_PARTY_MEMBERS = MAX_PARTY_MEMBERS
+local MAX_PLAYER_LEVEL = MAX_PLAYER_LEVEL
+local MAX_RAID_MEMBERS = MAX_RAID_MEMBERS
+
+-- API CONSTANTS
+local ALL_DATA = 0x0f
+local DIRECT_HEALS = 0x01
+local CHANNEL_HEALS = 0x02
+local HOT_HEALS = 0x04
+local ABSORB_SHIELDS = 0x08
+local BOMB_HEALS = 0x10
+local ALL_HEALS = bor(DIRECT_HEALS, CHANNEL_HEALS, HOT_HEALS, BOMB_HEALS)
+local CASTED_HEALS = bor(DIRECT_HEALS, CHANNEL_HEALS)
+local OVERTIME_HEALS = bor(HOT_HEALS, CHANNEL_HEALS)
+
+HealComm.ALL_HEALS, HealComm.CHANNEL_HEALS, HealComm.DIRECT_HEALS, HealComm.HOT_HEALS, HealComm.CASTED_HEALS, HealComm.ABSORB_SHIELDS, HealComm.ALL_DATA, HealComm.BOMB_HEALS = ALL_HEALS, CHANNEL_HEALS, DIRECT_HEALS, HOT_HEALS, CASTED_HEALS, ABSORB_SHIELDS, ALL_DATA, BOMB_HEALS
+
+local COMM_PREFIX = "LHC40"
+local playerGUID, playerName, playerLevel
+local playerHealModifier = 1
+local IS_BUILD30300 = tonumber((select(4, GetBuildInfo()))) >= 30300
+
+HealComm.callbacks = HealComm.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(HealComm)
+HealComm.spellData = HealComm.spellData or {}
+HealComm.hotData = HealComm.hotData or {}
+HealComm.talentData = HealComm.talentData or {}
+HealComm.itemSetsData = HealComm.itemSetsData or {}
+HealComm.glyphCache = HealComm.glyphCache or {}
+HealComm.equippedSetCache = HealComm.equippedSetCache or {}
+HealComm.guidToGroup = HealComm.guidToGroup or {}
+HealComm.guidToUnit = HealComm.guidToUnit or {}
+HealComm.pendingHeals = HealComm.pendingHeals or {}
+HealComm.tempPlayerList = HealComm.tempPlayerList or {}
+HealComm.activePets = HealComm.activePets or {}
+HealComm.activeHots = HealComm.activeHots or {}
+
+if( not HealComm.unitToPet ) then
+ HealComm.unitToPet = {["player"] = "pet"}
+ for i=1, MAX_PARTY_MEMBERS do HealComm.unitToPet["party" .. i] = "partypet" .. i end
+ for i=1, MAX_RAID_MEMBERS do HealComm.unitToPet["raid" .. i] = "raidpet" .. i end
+end
+
+local spellData, hotData, tempPlayerList, pendingHeals = HealComm.spellData, HealComm.hotData, HealComm.tempPlayerList, HealComm.pendingHeals
+local equippedSetCache, itemSetsData, talentData = HealComm.equippedSetCache, HealComm.itemSetsData, HealComm.talentData
+local activeHots, activePets = HealComm.activeHots, HealComm.activePets
+
+-- Figure out what they are now since a few things change based off of this
+local playerClass = select(2, UnitClass("player"))
+local isHealerClass = playerClass == "DRUID" or playerClass == "PRIEST" or playerClass == "SHAMAN" or playerClass == "PALADIN"
+
+-- Stolen from Threat-2.0, compresses GUIDs from 18 characters to around 8 - 9, 50%/55% savings
+-- 44 = , / 58 = : / 255 = \255 / 0 = line break / 64 = @ / 254 = FE, used for escape code so has to be escaped
+if( not HealComm.compressGUID or not HealComm.fixedCompress ) then
+ local map = {[58] = "\254\250", [64] = "\254\251", [44] = "\254\252", [255] = "\254\253", [0] = "\255", [254] = "\254\249"}
+ local function guidCompressHelper(x)
+ local a = tonumber(x, 16)
+ return map[a] or char(a)
+ end
+
+ local dfmt = "0x%02X%02X%02X%02X%02X%02X%02X%02X"
+ local function unescape(str)
+ str = gsub(str, "\255", "\000")
+ str = gsub(str, "\254\250", "\058")
+ str = gsub(str, "\254\251", "\064")
+ str = gsub(str, "\254\252", "\044")
+ str = gsub(str, "\254\253", "\255")
+ return gsub(str, "\254\249", "\254")
+ end
+
+ HealComm.fixedCompress = true
+ HealComm.compressGUID = setmetatable({}, {
+ __index = function(tbl, guid)
+ local cguid = match(guid, "0x(.*)")
+ local str = gsub(cguid, "(%x%x)", guidCompressHelper)
+
+ rawset(tbl, guid, str)
+ return str
+ end})
+
+ HealComm.decompressGUID = setmetatable({}, {
+ __index = function(tbl, str)
+ if( not str ) then return nil end
+ local usc = unescape(str)
+ local a, b, c, d, e, f, g, h = byte(usc, 1, 8)
+
+ -- Failed to decompress, silently exit
+ if( not a or not b or not c or not d or not e or not f or not g or not h ) then
+ return ""
+ end
+
+ local guid = format(dfmt, a, b, c, d, e, f, g, h)
+
+ rawset(tbl, str, guid)
+ return guid
+ end})
+end
+
+local compressGUID, decompressGUID = HealComm.compressGUID, HealComm.decompressGUID
+
+-- Handles caching of tables for variable tick spells, like Wild Growth
+if( not HealComm.tableCache ) then
+ HealComm.tableCache = setmetatable({}, {__mode = "k"})
+ function HealComm:RetrieveTable()
+ return tremove(HealComm.tableCache, 1) or {}
+ end
+
+ function HealComm:DeleteTable(tbl)
+ twipe(tbl)
+ tinsert(HealComm.tableCache, tbl)
+ end
+end
+
+-- Validation for passed arguments
+if( not HealComm.tooltip ) then
+ local tooltip = CreateFrame("GameTooltip")
+ tooltip:SetOwner(UIParent, "ANCHOR_NONE")
+ tooltip.TextLeft1 = tooltip:CreateFontString()
+ tooltip.TextRight1 = tooltip:CreateFontString()
+ tooltip:AddFontStrings(tooltip.TextLeft1, tooltip.TextRight1)
+
+ HealComm.tooltip = tooltip
+end
+
+-- So I don't have to keep matching the same numbers every time or create a local copy of every rank -> # map for locals
+if( not HealComm.rankNumbers ) then
+ HealComm.rankNumbers = setmetatable({}, {
+ __index = function(tbl, index)
+ local number = tonumber(match(index, "(%d+)")) or 1
+
+ rawset(tbl, index, number)
+ return number
+ end,
+ })
+end
+
+-- Find the spellID by the name/rank combination
+-- Need to swap this to a double table metatable something like [spellName][spellRank] so I can reduce the garbage created
+if( not HealComm.spellToID ) then
+ HealComm.spellToID = setmetatable({}, {
+ __index = function(tbl, index)
+ -- Find the spell from the spell book and cache the results!
+ local offset, numSpells = select(3, GetSpellTabInfo(GetNumSpellTabs()))
+ for id=1, (offset + numSpells) do
+ -- Match, yay!
+ local spellName, spellRank = GetSpellName(id, BOOKTYPE_SPELL)
+ local name = spellName .. spellRank
+ if( index == name ) then
+ HealComm.tooltip:SetSpell(id, BOOKTYPE_SPELL)
+ local spellID = select(3, HealComm.tooltip:GetSpell())
+ if( spellID ) then
+ rawset(tbl, index, spellID)
+ return spellID
+ end
+ end
+ end
+
+ rawset(tbl, index, false)
+ return false
+ end,
+ })
+end
+
+-- This gets filled out after data has been loaded, this is only for casted heals. Hots just directly pull from the averages as they do not increase in power with level, Cataclysm will change this though.
+if( HealComm.averageHealMT and not HealComm.fixedAverage ) then
+ HealComm.averageHealMT = nil
+end
+
+HealComm.fixedAverage = true
+HealComm.averageHeal = HealComm.averageHeal or {}
+HealComm.averageHealMT = HealComm.averageHealMT or {
+ __index = function(tbl, index)
+ local rank = HealComm.rankNumbers[index]
+ local spellData = HealComm.spellData[rawget(tbl, "spell")]
+ local spellLevel = spellData.levels[rank]
+
+ -- No increase, it doesn't scale with levely
+ if( not spellData.increase or UnitLevel("player") <= spellLevel ) then
+ rawset(tbl, index, spellData.averages[rank])
+ return spellData.averages[rank]
+ end
+
+ local average = spellData.averages[rank]
+ if( UnitLevel("level") >= MAX_PLAYER_LEVEL ) then
+ average = average + spellData.increase[rank]
+ -- Here's how this works: If a spell increases 1,000 between 70 and 80, the player is level 75 the spell is 70
+ -- it's 1000 / (80 - 70) so 100, the player learned the spell 5 levels ago which means that the spell average increases by 500
+ -- This figures out how much it increases per level and how ahead of the spells level they are to figure out how much to add
+ else
+ average = average + (UnitLevel("player") - spellLevel) * (spellData.increase[rank] / (MAX_PLAYER_LEVEL - spellLevel))
+ end
+
+ rawset(tbl, index, average)
+ return average
+ end}
+
+-- Record management, because this is getting more complicted to deal with
+local function updateRecord(pending, guid, amount, stack, endTime, ticksLeft)
+ if( pending[guid] ) then
+ local id = pending[guid]
+
+ pending[id] = guid
+ pending[id + 1] = amount
+ pending[id + 2] = stack
+ pending[id + 3] = endTime or 0
+ pending[id + 4] = ticksLeft or 0
+ else
+ pending[guid] = #(pending) + 1
+ tinsert(pending, guid)
+ tinsert(pending, amount)
+ tinsert(pending, stack)
+ tinsert(pending, endTime or 0)
+ tinsert(pending, ticksLeft or 0)
+
+ if( pending.bitType == HOT_HEALS ) then
+ activeHots[guid] = (activeHots[guid] or 0) + 1
+ HealComm.hotMonitor:Show()
+ end
+ end
+end
+
+local function getRecord(pending, guid)
+ local id = pending[guid]
+ if( not id ) then return nil end
+
+ -- amount, stack, endTime, ticksLeft
+ return pending[id + 1], pending[id + 2], pending[id + 3], pending[id + 4]
+end
+
+local function removeRecord(pending, guid)
+ local id = pending[guid]
+ if( not id ) then return nil end
+
+ -- ticksLeft, endTime, stack, amount, guid
+ tremove(pending, id + 4)
+ tremove(pending, id + 3)
+ tremove(pending, id + 2)
+ local amount = tremove(pending, id + 1)
+ tremove(pending, id)
+ pending[guid] = nil
+
+ -- Release the table
+ if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
+
+ if( pending.bitType == HOT_HEALS and activeHots[guid] ) then
+ activeHots[guid] = activeHots[guid] - 1
+ activeHots[guid] = activeHots[guid] > 0 and activeHots[guid] or nil
+ end
+
+ -- Shift any records after this ones index down 5 to account for the removal
+ for i=1, #(pending), 5 do
+ local guid = pending[i]
+ if( pending[guid] > id ) then
+ pending[guid] = pending[guid] - 5
+ end
+ end
+end
+
+local function removeRecordList(pending, inc, comp, ...)
+ for i=1, select("#", ...), inc do
+ local guid = select(i, ...)
+ guid = comp and decompressGUID[guid] or guid
+
+ local id = pending[guid]
+ -- ticksLeft, endTime, stack, amount, guid
+ tremove(pending, id + 4)
+ tremove(pending, id + 3)
+ tremove(pending, id + 2)
+ local amount = tremove(pending, id + 1)
+ tremove(pending, id)
+ pending[guid] = nil
+
+ -- Release the table
+ if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
+ end
+
+ -- Redo all the id maps
+ for i=1, #(pending), 5 do
+ pending[pending[i]] = i
+ end
+end
+
+-- Removes every mention to the given GUID
+local function removeAllRecords(guid)
+ local changed
+ for _, spells in pairs(pendingHeals) do
+ for _, pending in pairs(spells) do
+ if( pending.bitType and pending[guid] ) then
+ local id = pending[guid]
+
+ -- ticksLeft, endTime, stack, amount, guid
+ tremove(pending, id + 4)
+ tremove(pending, id + 3)
+ tremove(pending, id + 2)
+ local amount = tremove(pending, id + 1)
+ tremove(pending, id)
+ pending[guid] = nil
+
+ -- Release the table
+ if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
+
+ -- Shift everything back
+ if( #(pending) > 0 ) then
+ for i=1, #(pending), 5 do
+ local guid = pending[i]
+ if( pending[guid] > id ) then
+ pending[guid] = pending[guid] - 5
+ end
+ end
+ else
+ twipe(pending)
+ end
+
+ changed = true
+ end
+ end
+ end
+
+ activeHots[guid] = nil
+
+ if( changed ) then
+ HealComm.callbacks:Fire("HealComm_GUIDDisappeared", guid)
+ end
+end
+
+-- These are not public APIs and are purely for the wrapper to use
+HealComm.removeRecordList = removeRecordList
+HealComm.removeRecord = removeRecord
+HealComm.getRecord = getRecord
+HealComm.updateRecord = updateRecord
+
+-- Removes all pending heals, if it's a group that is causing the clear then we won't remove the players heals on themselves
+local function clearPendingHeals()
+ for casterGUID, spells in pairs(pendingHeals) do
+ for _, pending in pairs(spells) do
+ if( pending.bitType ) then
+ twipe(tempPlayerList)
+ for i=#(pending), 1, -5 do tinsert(tempPlayerList, pending[i - 4]) end
+
+ if( #(tempPlayerList) > 0 ) then
+ local spellID, bitType = pending.spellID, pending.bitType
+ twipe(pending)
+
+ HealComm.callbacks:Fire("HealComm_HealStopped", casterGUID, spellID, bitType, true, unpack(tempPlayerList))
+ end
+ end
+ end
+ end
+end
+
+-- APIs
+-- Returns the players current heaing modifier
+function HealComm:GetPlayerHealingMod()
+ return playerHealModifier or 1
+end
+
+-- Returns the current healing modifier for the GUID
+function HealComm:GetHealModifier(guid)
+ return HealComm.currentModifiers[guid] or 1
+end
+
+-- Returns whether or not the GUID has casted a heal
+function HealComm:GUIDHasHealed(guid)
+ return pendingHeals[guid] and true or nil
+end
+
+-- Returns the guid to unit table
+function HealComm:GetGUIDUnitMapTable()
+ if( not HealComm.protectedMap ) then
+ HealComm.protectedMap = setmetatable({}, {
+ __index = function(tbl, key) return HealComm.guidToUnit[key] end,
+ __newindex = function() error("This is a read only table and cannot be modified.", 2) end,
+ __metatable = false
+ })
+ end
+
+ return HealComm.protectedMap
+end
+
+-- Gets the next heal landing on someone using the passed filters
+function HealComm:GetNextHealAmount(guid, bitFlag, time, ignoreGUID)
+ local healTime, healAmount, healFrom
+ local currentTime = GetTime()
+
+ for casterGUID, spells in pairs(pendingHeals) do
+ if( not ignoreGUID or ignoreGUID ~= casterGUID ) then
+ for _, pending in pairs(spells) do
+ if( pending.bitType and band(pending.bitType, bitFlag) > 0 ) then
+ for i=1, #(pending), 5 do
+ local guid = pending[i]
+ local amount = pending[i + 1]
+ local stack = pending[i + 2]
+ local endTime = pending[i + 3]
+ endTime = endTime > 0 and endTime or pending.endTime
+
+ -- Direct heals are easy, if they match the filter then return them
+ if( ( pending.bitType == DIRECT_HEALS or pending.bitType == BOMB_HEALS ) and ( not time or endTime <= time ) ) then
+ if( not healTime or endTime < healTime ) then
+ healTime = endTime
+ healAmount = amount * stack
+ healFrom = casterGUID
+ end
+
+ -- Channeled heals and hots, have to figure out how many times it'll tick within the given time band
+ elseif( ( pending.bitType == CHANNEL_HEALS or pending.bitType == HOT_HEALS ) and ( not pending.hasVariableTicks or pending.hasVariableTicks and amount[1] ) ) then
+ local secondsLeft = time and time - currentTime or endTime - currentTime
+ local nextTick = currentTime + (secondsLeft % pending.tickInterval)
+ if( not healTime or nextTick < healTime ) then
+ healTime = nextTick
+ healAmount = not pending.hasVariableTicks and amount * stack or amount[1] * stack
+ healFrom = casterGUID
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return healTime, healFrom, healAmount
+end
+
+-- Get the healing amount that matches the passed filters
+local function filterData(spells, filterGUID, bitFlag, time, ignoreGUID)
+ local healAmount = 0
+ local currentTime = GetTime()
+
+ for _, pending in pairs(spells) do
+ if( pending.bitType and band(pending.bitType, bitFlag) > 0 ) then
+ for i=1, #(pending), 5 do
+ local guid = pending[i]
+ if( guid == filterGUID or ignoreGUID ) then
+ local amount = pending[i + 1]
+ local stack = pending[i + 2]
+ local endTime = pending[i + 3]
+ endTime = endTime > 0 and endTime or pending.endTime
+
+ -- Direct heals are easy, if they match the filter then return them
+ if( ( pending.bitType == DIRECT_HEALS or pending.bitType == BOMB_HEALS ) and ( not time or endTime <= time ) ) then
+ healAmount = healAmount + amount * stack
+ -- Channeled heals and hots, have to figure out how many times it'll tick within the given time band
+ elseif( ( pending.bitType == CHANNEL_HEALS or pending.bitType == HOT_HEALS ) and endTime > currentTime ) then
+ local ticksLeft = pending[i + 4]
+ if( not time or time >= endTime ) then
+ if( not pending.hasVariableTicks ) then
+ healAmount = healAmount + (amount * stack) * ticksLeft
+ else
+ for _, heal in pairs(amount) do
+ healAmount = healAmount + (heal * stack)
+ end
+ end
+ else
+ local secondsLeft = endTime - currentTime
+ local bandSeconds = time - currentTime
+ local ticks = floor(min(bandSeconds, secondsLeft) / pending.tickInterval)
+ local nextTickIn = secondsLeft % pending.tickInterval
+ local fractionalBand = bandSeconds % pending.tickInterval
+ if( nextTickIn > 0 and nextTickIn < fractionalBand ) then
+ ticks = ticks + 1
+ end
+
+ if( not pending.hasVariableTicks ) then
+ healAmount = healAmount + (amount * stack) * min(ticks, ticksLeft)
+ else
+ for j=1, min(ticks, #(amount)) do
+ healAmount = healAmount + (amount[j] * stack)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return healAmount
+end
+
+-- Gets healing amount using the passed filters
+function HealComm:GetHealAmount(guid, bitFlag, time, casterGUID)
+ local amount = 0
+ if( casterGUID and pendingHeals[casterGUID] ) then
+ amount = filterData(pendingHeals[casterGUID], guid, bitFlag, time)
+ elseif( not casterGUID ) then
+ for _, spells in pairs(pendingHeals) do
+ amount = amount + filterData(spells, guid, bitFlag, time)
+ end
+ end
+
+ return amount > 0 and amount or nil
+end
+
+-- Gets healing amounts for everyone except the player using the passed filters
+function HealComm:GetOthersHealAmount(guid, bitFlag, time)
+ local amount = 0
+ for casterGUID, spells in pairs(pendingHeals) do
+ if( casterGUID ~= playerGUID ) then
+ amount = amount + filterData(spells, guid, bitFlag, time)
+ end
+ end
+
+ return amount > 0 and amount or nil
+end
+
+function HealComm:GetCasterHealAmount(guid, bitFlag, time)
+ local amount = pendingHeals[guid] and filterData(pendingHeals[guid], nil, bitFlag, time, true) or 0
+ return amount > 0 and amount or nil
+end
+
+-- Healing class data
+-- Thanks to Gagorian (DrDamage) for letting me steal his formulas and such
+local playerCurrentRelic
+local averageHeal, rankNumbers = HealComm.averageHeal, HealComm.rankNumbers
+local guidToUnit, guidToGroup, glyphCache = HealComm.guidToUnit, HealComm.guidToGroup, HealComm.glyphCache
+
+-- UnitBuff priortizes our buffs over everyone elses when there is a name conflict, so yay for that
+local function unitHasAura(unit, name)
+ return select(8, UnitBuff(unit, name)) == "player"
+end
+
+-- Note because I always forget on the order:
+-- Talents that effective the coeffiency of spell power to healing are first and are tacked directly onto the coeffiency (Empowered Rejuvenation)
+-- Penalty modifiers (downranking/spell level too low) are applied directly to the spell power
+-- Spell power modifiers are then applied to the spell power
+-- Heal modifiers are applied after all of that
+-- Crit modifiers are applied after
+-- Any other modifiers such as Mortal Strike or Avenging Wrath are applied after everything else
+local function calculateGeneralAmount(level, amount, spellPower, spModifier, healModifier)
+ -- Apply downranking penalities for spells below 20
+ local penalty = level > 20 and 1 or (1 - ((20 - level) * 0.0375))
+
+ -- Apply further downranking penalities
+ spellPower = spellPower * (penalty * min(1, max(0, 1 - (playerLevel - level - 11) * 0.05)))
+
+ -- Apply zone modifier
+ healModifier = healModifier * HealComm.zoneHealModifier
+
+ -- Do the general factoring
+ return healModifier * (amount + (spellPower * spModifier))
+end
+
+-- For spells like Wild Growth, it's a waste to do the calculations for each tick, easier to calculate spell power now and then manually calculate it all after
+local function calculateSpellPower(level, spellPower)
+ -- Apply downranking penalities for spells below 20
+ local penalty = level > 20 and 1 or (1 - ((20 - level) * 0.0375))
+
+ -- Apply further downranking penalities
+ return spellPower * (penalty * min(1, max(0, 1 - (playerLevel - level - 11) * 0.05)))
+end
+
+-- Yes silly function, just cleaner to look at
+local function avg(a, b)
+ return (a + b) / 2
+end
+
+--[[
+ What the different callbacks do:
+
+ AuraHandler: Specific aura tracking needed for this class, who has Beacon up on them and such
+
+ ResetChargeData: Due to spell "queuing" you can't always rely on aura data for buffs that last one or two casts, for example Divine Favor (+100% crit, one spell)
+ if you cast Holy Light and queue Flash of Light the library would still see they have Divine Favor and give them crits on both spells. The reset means that the flag that indicates
+ they have the aura can be killed and if they interrupt the cast then it will call this and let you reset the flags.
+
+ What happens in terms of what the client thinks and what actually is, is something like this:
+
+ UNIT_SPELLCAST_START, Holy Light -> Divine Favor up
+ UNIT_SPELLCAST_SUCCEEDED, Holy Light -> Divine Favor up (But it was really used)
+ UNIT_SPELLCAST_START, Flash of Light -> Divine Favor up (It's not actually up but auras didn't update)
+ UNIT_AURA -> Divine Favor up (Split second where it still thinks it's up)
+ UNIT_AURA -> Divine Favor faded (Client catches up and realizes it's down)
+
+ CalculateHealing: Calculates the healing value, does all the formula calculations talent modifiers and such
+
+ CalculateHotHealing: Used specifically for calculating the heals of hots
+
+ GetHealTargets: Who the heal is going to hit, used for setting extra targets for Beacon of Light + Paladin heal or Prayer of Healing.
+ The returns should either be:
+
+ "compressedGUID1,compressedGUID2,compressedGUID3,compressedGUID4", healthAmount
+ Or if you need to set specific healing values for one GUID it should be
+ "compressedGUID1,healthAmount1,compressedGUID2,healAmount2,compressedGUID3,healAmount3", -1
+
+ The latter is for cases like Glyph of Healing Wave where you need a heal for 1,000 on A and a heal for 200 on the player for B without sending 2 events.
+ The -1 tells the library to look in the GUId list for the heal amounts
+
+ **NOTE** Any GUID returned from GetHealTargets must be compressed through a call to compressGUID[guid]
+]]
+
+local CalculateHealing, GetHealTargets, AuraHandler, CalculateHotHealing, ResetChargeData, LoadClassData, LoadRaceData
+
+-- DRUIDS
+-- All data is accurate as of 3.2.2 (build 10392)
+if( playerClass == "DRUID" ) then
+ LoadClassData = function()
+ -- Rejuvenation
+ local Rejuvenation = GetSpellInfo(774)
+ hotData[Rejuvenation] = {interval = 3,
+ levels = {4, 10, 16, 22, 28, 34, 40, 46, 52, 58, 60, 63, 69, 75, 80}, averages = {32, 56, 116, 180, 244, 304, 388, 488, 608, 756, 888, 932, 1060, 1192, 1690}}
+ -- Regrowth
+ local Regrowth = GetSpellInfo(8936)
+ hotData[Regrowth] = {interval = 3, ticks = 7, coeff = 1.316,
+ levels = {12, 18, 24, 30, 36, 42, 48, 54, 60, 65, 71, 77}, averages = {98, 175, 259, 343, 427, 546, 686, 861, 1064, 1274, 1792, 2345}}
+ -- Lifebloom
+ local Lifebloom = GetSpellInfo(33763)
+ hotData[Lifebloom] = {interval = 1, ticks = 7, coeff = 0.66626, dhCoeff = 0.34324 * 0.8, levels = {64, 72, 80}, averages = {224, 287, 371}, bomb = {480, 616, 776}}
+ -- Wild Growth
+ local WildGrowth = GetSpellInfo(48438)
+ hotData[WildGrowth] = {interval = 1, ticks = 7, coeff = 0.8056, levels = {60, 70, 75, 80}, averages = {686, 861, 1239, 1442}}
+
+ -- Regrowth
+ spellData[Regrowth] = {coeff = 0.2867,
+ levels = hotData[Regrowth].levels,
+ averages = {avg(84, 98), avg(164, 188), avg(240, 274), avg(318, 360), avg(405, 457), avg(511, 575), avg(646, 724), avg(809, 905), avg(1003, 1119), avg(1215, 1355), avg(1710, 1908), avg(2234, 2494)},
+ increase = {122, 155, 173, 180, 180, 178, 169, 156, 136, 115, 97, 23}}
+ -- Healing Touch
+ local HealingTouch = GetSpellInfo(5185)
+ spellData[HealingTouch] = {
+ levels = {1, 8, 14, 20, 26, 32, 38, 44, 50, 56, 60, 62, 69, 74, 79},
+ averages = {avg(37, 51), avg(88, 112), avg(195, 243), avg(363, 445), avg(490, 594), avg(636, 766), avg(802, 960), avg(1199, 1427), avg(1299, 1539), avg(1620, 1912), avg(1944, 2294), avg(2026, 2392), avg(2321, 2739), avg(3223, 3805), avg(3750, 4428)}}
+ -- Nourish
+ local Nourish = GetSpellInfo(50464)
+ spellData[Nourish] = {coeff = 0.358005, levels = {80}, averages = {avg(1883, 2187)}}
+ -- Tranquility
+ local Tranquility = GetSpellInfo(740)
+ spellData[Tranquility] = {coeff = 1.144681, ticks = 4, levels = {30, 40, 50, 60, 70, 75, 80}, averages = {351, 515, 765, 1097, 1518, 2598, 3035}}
+
+ -- Talent data, these are filled in later and modified on talent changes
+ -- Master Shapeshifter (Multi)
+ local MasterShapeshifter = GetSpellInfo(48411)
+ talentData[MasterShapeshifter] = {mod = 0.02, current = 0}
+ -- Gift of Nature (Add)
+ local GiftofNature = GetSpellInfo(17104)
+ talentData[GiftofNature] = {mod = 0.02, current = 0}
+ -- Empowered Touch (Add, increases spell power HT/Nourish gains)
+ local EmpoweredTouch = GetSpellInfo(33879)
+ talentData[EmpoweredTouch] = {mod = 0.2, current = 0}
+ -- Empowered Rejuvenation (Multi, this ups both the direct heal and the hot)
+ local EmpoweredRejuv = GetSpellInfo(33886)
+ talentData[EmpoweredRejuv] = {mod = 0.04, current = 0}
+ -- Genesis (Add)
+ local Genesis = GetSpellInfo(57810)
+ talentData[Genesis] = {mod = 0.01, current = 0}
+ -- Improved Rejuvenation (Add)
+ local ImprovedRejuv = GetSpellInfo(17111)
+ talentData[ImprovedRejuv] = {mod = 0.05, current = 0}
+ -- Nature's Splendor (+3s Rejuv/+6s Regrowth/+2s Lifebloom)
+ local NaturesSplendor = GetSpellInfo(57865)
+ talentData[NaturesSplendor] = {mod = 1, current = 0}
+
+ local TreeofLife = GetSpellInfo(33891)
+ local Innervate = GetSpellInfo(29166)
+
+ -- Set data
+ -- 2 piece, +6 seconds to Regrowth
+ itemSetsData["T5 Resto"] = {30216, 30217, 30219, 30220, 30221}
+ -- +5% more healing to Nourish per hot
+ itemSetsData["T7 Resto"] = {40460, 40461, 40462, 40463, 40465, 39531, 39538, 39539, 39542, 39543}
+ --itemSetsData["T8 Resto"] = {46183, 46184, 46185, 46186, 46187, 45345, 45346, 45347, 45348, 45349}
+ --itemSetsData["T9 Resto"] = {48102, 48129, 48130, 48131, 48132, 48153, 48154, 48155, 48156, 48157, 48133, 48134, 48135, 48136, 48137, 48142, 48141, 48140, 48139, 48138, 48152, 48151, 48150, 48149, 48148, 48143, 48144, 48145, 48146, 48147}
+ -- 2 piece, 30% less healing lost on WG
+ itemSetsData["T10 Resto"] = {50106, 50107, 50108, 50109, 50113, 51139, 51138, 51137, 51136, 51135, 51300, 51301, 51302, 51303, 51304}
+
+ local bloomBombIdols = {[28355] = 87, [33076] = 105, [33841] = 116, [35021] = 131, [42576] = 188, [42577] = 217, [42578] = 246, [42579] = 294, [42580] = 376, [51423] = 448}
+
+ local hotTotals, hasRegrowth = {}, {}
+ AuraHandler = function(unit, guid)
+ hotTotals[guid] = 0
+ if( unitHasAura(unit, Rejuvenation) ) then hotTotals[guid] = hotTotals[guid] + 1 end
+ if( unitHasAura(unit, Lifebloom) ) then hotTotals[guid] = hotTotals[guid] + 1 end
+ if( unitHasAura(unit, WildGrowth) ) then hotTotals[guid] = hotTotals[guid] + 1 end
+ if( unitHasAura(unit, Regrowth) ) then
+ hasRegrowth[guid] = true
+ hotTotals[guid] = hotTotals[guid] + 1
+ else
+ hasRegrowth[guid] = nil
+ end
+ end
+
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ -- Tranquility pulses on everyone within 30 yards, if they are in range of Innervate they'll get Tranquility
+ if( spellName == Tranquility ) then
+ local targets = compressGUID[playerGUID]
+ local playerGroup = guidToGroup[playerGUID]
+
+ for groupGUID, groupID in pairs(guidToGroup) do
+ if( groupID == playerGroup and playerGUID ~= groupGUID and not UnitHasVehicleUI(guidToUnit[groupID]) and IsSpellInRange(Innervate, guidToUnit[groupGUID]) == 1 ) then
+ targets = targets .. "," .. compressGUID[groupGUID]
+ end
+ end
+
+ return targets, healAmount
+ elseif( hasVariableTicks ) then
+ healAmount = tconcat(healAmount, "@")
+ end
+
+ return compressGUID[guid], healAmount
+ end
+
+ -- Calculate hot heals
+ local wgTicks = {}
+ CalculateHotHealing = function(guid, spellID)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ local rank = HealComm.rankNumbers[spellRank]
+ local healAmount = hotData[spellName].averages[rank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local bombAmount, totalTicks
+ healModifier = healModifier + talentData[GiftofNature].current
+ healModifier = healModifier + talentData[Genesis].current
+
+ -- Master Shapeshifter does not apply directly when using Lifebloom
+ if( unitHasAura("player", TreeofLife) ) then
+ healModifier = healModifier * (1 + talentData[MasterShapeshifter].current)
+
+ -- 32387 - Idol of the Raven Godess, +44 SP while in TOL
+ if( playerCurrentRelic == 32387 ) then
+ spellPower = spellPower + 44
+ end
+ end
+
+ -- Rejuvenation
+ if( spellName == Rejuvenation ) then
+ healModifier = healModifier + talentData[ImprovedRejuv].current
+
+ -- 25643 - Harold's Rejuvenation Broach, +86 Rejuv SP
+ if( playerCurrentRelic == 25643 ) then
+ spellPower = spellPower + 86
+ -- 22398 - Idol of Rejuvenation, +50 SP to Rejuv
+ elseif( playerCurrentRelic == 22398 ) then
+ spellPower = spellPower + 50
+ end
+
+ local duration, ticks
+ if( IS_BUILD30300 ) then
+ duration = 15
+ ticks = 5
+ totalTicks = 5
+ else
+ duration = rank > 14 and 15 or 12
+ ticks = duration / hotData[spellName].interval
+ totalTicks = ticks
+ end
+
+ spellPower = spellPower * (((duration / 15) * 1.88) * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / ticks
+ healAmount = healAmount / ticks
+
+ --38366 - Idol of Pure Thoughts, +33 SP base per tick
+ if( playerCurrentRelic == 38366 ) then
+ spellPower = spellPower + 33
+ end
+
+ -- Nature's Splendor, +6 seconds
+ if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 1 end
+
+ -- Regrowth
+ elseif( spellName == Regrowth ) then
+ spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+
+ totalTicks = 7
+ -- Nature's Splendor, +6 seconds
+ if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 2 end
+ -- T5 Resto, +6 seconds
+ if( equippedSetCache["T5 Resto"] >= 2 ) then totalTicks = totalTicks + 2 end
+
+ -- Lifebloom
+ elseif( spellName == Lifebloom ) then
+ -- Figure out the bomb heal, apparently Gift of Nature double dips and will heal 10% for the HOT + 10% again for the direct heal
+ local bombSpellPower = spellPower
+ if( playerCurrentRelic and bloomBombIdols[playerCurrentRelic] ) then
+ bombSpellPower = bombSpellPower + bloomBombIdols[playerCurrentRelic]
+ end
+
+ local bombSpell = bombSpellPower * (hotData[spellName].dhCoeff * 1.88)
+ bombAmount = ceil(calculateGeneralAmount(hotData[spellName].levels[rank], hotData[spellName].bomb[rank], bombSpell, spModifier, healModifier + talentData[GiftofNature].current))
+
+ -- Figure out the hot tick healing
+ spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+ -- Figure out total ticks
+ totalTicks = 7
+
+ -- Idol of Lush Moss, +125 SP per tick
+ if( playerCurrentRelic == 40711 ) then
+ spellPower = spellPower + 125
+ -- Idol of the Emerald Queen, +47 SP per tick
+ elseif( playerCurrentRelic == 27886 ) then
+ spellPower = spellPower + 47
+ end
+
+ -- Glyph of Lifebloom, +1 second
+ if( glyphCache[54826] ) then totalTicks = totalTicks + 1 end
+ -- Nature's Splendor, +2 seconds
+ if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 1 end
+ -- Wild Growth
+ elseif( spellName == WildGrowth ) then
+ spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / hotData[spellName].ticks
+ spellPower = calculateSpellPower(hotData[spellName].levels[rank], spellPower)
+ healAmount = healAmount / hotData[spellName].ticks
+ healModifier = healModifier * HealComm.zoneHealModifier
+
+ twipe(wgTicks)
+ local tickModifier = equippedSetCache["T10 Resto"] >= 2 and 0.70 or 1
+ local tickAmount = healAmount / hotData[spellName].ticks
+ for i=1, hotData[spellName].ticks do
+ tinsert(wgTicks, ceil(healModifier * ((healAmount + tickAmount * (3 - (i - 1) * tickModifier)) + (spellPower * spModifier))))
+ end
+
+ return HOT_HEALS, wgTicks, hotData[spellName].ticks, hotData[spellName].interval, nil, true
+ end
+
+ healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+ return HOT_HEALS, ceil(healAmount), totalTicks, hotData[spellName].interval, bombAmount
+ end
+
+ -- Calcualte direct and channeled heals
+ CalculateHealing = function(guid, spellName, spellRank)
+ local healAmount = averageHeal[spellName][spellRank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local rank = HealComm.rankNumbers[spellRank]
+
+ -- Gift of Nature
+ healModifier = healModifier + talentData[GiftofNature].current
+
+ -- Master Shapeshifter does not apply directly when using Lifebloom
+ if( unitHasAura("player", TreeofLife) ) then
+ healModifier = healModifier * (1 + talentData[MasterShapeshifter].current)
+
+ -- 32387 - Idol of the Raven Godess, +44 SP while in TOL
+ if( playerCurrentRelic == 32387 ) then
+ spellPower = spellPower + 44
+ end
+ end
+
+ -- Regrowth
+ if( spellName == Regrowth ) then
+ -- Glyph of Regrowth - +20% if target has Regrowth
+ if( glyphCache[54743] and hasRegrowth[guid] ) then
+ healModifier = healModifier * 1.20
+ end
+
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredRejuv].current))
+ -- Nourish
+ elseif( spellName == Nourish ) then
+ -- 46138 - Idol of Flourishing Life, +187 Nourish SP
+ if( playerCurrentRelic == 46138 ) then
+ spellPower = spellPower + 187
+ end
+
+ -- Apply any hot specific bonuses
+ local hots = hotTotals[guid]
+ if( hots and hots > 0 ) then
+ local bonus = 1.20
+
+ -- T7 Resto, +5% healing per each of the players hot on their target
+ if( equippedSetCache["T7 Resto"] >= 2 ) then
+ bonus = bonus + 0.05 * hots
+ end
+
+ -- Glyph of Nourish - 6% per HoT
+ if( glyphCache[62971] ) then
+ bonus = bonus + 0.06 * hots
+ end
+
+ healModifier = healModifier * bonus
+ end
+
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) + talentData[EmpoweredTouch].spent * 0.10)
+ -- Healing Touch
+ elseif( spellName == HealingTouch ) then
+ -- Glyph of Healing Touch, -50% healing
+ if( glyphCache[54825] ) then
+ healModifier = healModifier - 0.50
+ end
+
+ -- Idol of the Avian Heart, +136 baseh ealing
+ if( playerCurrentRelic == 28568 ) then
+ healAmount = healAmount + 136
+ -- Idol of Health, +100 base healing
+ elseif( playerCurrentRelic == 22399 ) then
+ healAmount = healAmount + 100
+ end
+
+ -- Rank 1 - 3: 1.5/2/2.5 cast time, Rank 4+: 3 cast time
+ local castTime = rank > 3 and 3 or rank == 3 and 2.5 or rank == 2 and 2 or 1.5
+ spellPower = spellPower * (((castTime / 3.5) * 1.88) + talentData[EmpoweredTouch].current)
+
+ -- Tranquility
+ elseif( spellName == Tranquility ) then
+ healModifier = healModifier + talentData[Genesis].current
+
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / spellData[spellName].ticks
+ end
+
+ healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+
+ -- 100% chance to crit with Nature, this mostly just covers fights like Loatheb where you will basically have 100% crit
+ if( GetSpellCritChance(4) >= 100 ) then
+ healAmount = healAmount * 1.50
+ end
+
+ if( spellData[spellName].ticks ) then
+ return CHANNEL_HEALS, ceil(healAmount), spellData[spellName].ticks, spellData[spellName].ticks
+ end
+
+ return DIRECT_HEALS, ceil(healAmount)
+ end
+ end
+end
+
+-- PALADINS
+-- All data is accurate as of 3.2.2 (build 10392)
+if( playerClass == "PALADIN" ) then
+ LoadClassData = function()
+ -- Hot data, this is just so it realizes that FoL can be a hot so it will call the calculator
+ --local FlashofLight = GetSpellInfo(19750)
+ --hotData[FlashofLight] = true
+
+ -- Spell data
+ -- Holy Light
+ local HolyLight = GetSpellInfo(635)
+ spellData[HolyLight] = {coeff = 2.5 / 3.5 * 1.25,
+ levels = {1, 6, 14, 22, 30, 38, 46, 54, 60, 62, 70, 75, 80},
+ averages = {avg(50, 60), avg(96, 116), avg(203, 239), avg(397, 455), avg(628, 708), avg(894, 998), avg(1209, 1349), avg(1595, 1777), avg(2034, 2266), avg(2232, 2486), avg(2818, 3138), avg(4199, 4677), avg(4888, 5444)},
+ increase = {63, 81, 112, 139, 155, 159, 156, 135, 116, 115, 70, 52, 0}}
+ -- Flash of Light
+ local FlashofLight = GetSpellInfo(19750)
+ spellData[FlashofLight] = {coeff = 1.5 / 3.5 * 1.25,
+ levels = {20, 26, 34, 42, 50, 58, 66, 74, 79},
+ averages = {avg(81, 93), avg(124, 144), avg(189, 211), avg(256, 288), avg(346, 390), avg(445, 499), avg(588, 658), avg(682, 764), avg(785, 879)},
+ increase = {60, 70, 73, 72, 66, 57, 42, 20, 3}}
+
+ -- Talent data
+ -- Need to figure out a way of supporting +6% healing from imp devo aura, might not be able to
+ -- Healing Light (Add)
+ local HealingLight = GetSpellInfo(20237)
+ talentData[HealingLight] = {mod = 0.04, current = 0}
+ -- Divinity (Add)
+ local Divinity = GetSpellInfo(63646)
+ talentData[Divinity] = {mod = 0.01, current = 0}
+ -- Touched by the Light (Add?)
+ local TouchedbytheLight = GetSpellInfo(53592)
+ talentData[TouchedbytheLight] = {mod = 0.10, current = 0}
+ -- 100% of your heal on someone within range of your beacon heals the beacon target too
+ local BeaconofLight = GetSpellInfo(53563)
+ -- 100% chance to crit
+ local DivineFavor = GetSpellInfo(20216)
+ -- Seal of Light + Glyph = 5% healing
+ local SealofLight = GetSpellInfo(20165)
+ -- Divine Illumination, used in T10 holy
+ local DivineIllumination = GetSpellInfo(31842)
+
+ local flashLibrams = {[42616] = 436, [42615] = 375, [42614] = 331, [42613] = 293, [42612] = 204, [28592] = 89, [25644] = 79, [23006] = 43, [23201] = 28}
+ local holyLibrams = {[45436] = 160, [40268] = 141, [28296] = 47}
+ local flashSPLibrams = {[51472] = 510}
+
+ -- Holy Shock crits put a hot that heals for 15% of the HS over 9s
+ --itemSetsData["T8 Holy"] = { 45370, 45371, 45372, 45373, 45374, 46178, 46179, 46180, 46181, 46182 }
+ -- +100% to the hot when using Flash of Light + Sacred Shield
+ --itemSetsData["T9 Holy"] = { 48595, 48596, 48597, 48598, 48599, 48564, 48566, 48568, 48572, 48574, 48593, 48591, 48592, 48590, 48594, 48588, 48586, 48587, 48585, 48589, 48576, 48578, 48577, 48579, 48575, 48583, 48581, 48582, 48580, 48584}
+ itemSetsData["T10 Holy"] = {50865, 50866, 50867, 50868, 50869, 51270, 51271, 51272, 51273, 51274, 51165, 51166, 51167, 51168, 51169}
+
+ -- Need the GUID of whoever has beacon on them so we can make sure they are visible to us and so we can check the mapping
+ local activeBeaconGUID, hasDivineFavor
+ AuraHandler = function(unit, guid)
+ if( unitHasAura(unit, BeaconofLight) ) then
+ activeBeaconGUID = guid
+ elseif( activeBeaconGUID == guid ) then
+ activeBeaconGUID = nil
+ end
+
+ -- Check Divine Favor
+ if( unit == "player" ) then
+ hasDivineFavor = unitHasAura("player", DivineFavor)
+ end
+ end
+
+ ResetChargeData = function(guid)
+ hasDivineFavor = unitHasAura("player", DivineFavor)
+ end
+
+ -- Check for beacon when figuring out who to heal
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ if( activeBeaconGUID and activeBeaconGUID ~= guid and guidToUnit[activeBeaconGUID] and UnitIsVisible(guidToUnit[activeBeaconGUID]) ) then
+ return format("%s,%s", compressGUID[guid], compressGUID[activeBeaconGUID]), healAmount
+ elseif( hasVariableTicks ) then
+ healAmount = tconcat(healAmount, "@")
+ end
+
+ return compressGUID[guid], healAmount
+ end
+
+ -- If only every other class was as easy as Paladins
+ CalculateHealing = function(guid, spellName, spellRank)
+ local healAmount = averageHeal[spellName][spellRank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local rank = HealComm.rankNumbers[spellRank]
+
+ -- Glyph of Seal of Light, +5% healing if the player has Seal of Light up
+ if( glyphCache[54943] and unitHasAura("player", SealofLight) ) then
+ healModifier = healModifier + 0.05
+ end
+
+ healModifier = healModifier + talentData[HealingLight].current
+ healModifier = healModifier * (1 + talentData[Divinity].current)
+
+ -- Apply extra spell power based on libram
+ if( playerCurrentRelic ) then
+ if( spellName == HolyLight and holyLibrams[playerCurrentRelic] ) then
+ healAmount = healAmount + (holyLibrams[playerCurrentRelic] * 0.805)
+ elseif( spellName == FlashofLight and flashLibrams[playerCurrentRelic] ) then
+ healAmount = healAmount + (flashLibrams[playerCurrentRelic] * 0.805)
+ elseif( spellName == FlashofLight and flashSPLibrams[playerCurrentRelic] ) then
+ spellPower = spellPower + flashSPLibrams[playerCurrentRelic]
+ end
+ end
+
+ -- +35% healing while Divine Illumination is active
+ if( equippedSetCache["T10 Holy"] >= 2 and unitHasAura("player", DivineIllumination) ) then
+ healModifier = healModifier * 1.35
+ end
+
+ -- Normal calculations
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+
+ -- Divine Favor, 100% chance to crit
+ -- ... or the player has over a 100% chance to crit with Holy spells
+ if( hasDivineFavor or GetSpellCritChance(2) >= 100 ) then
+ hasDivineFavor = nil
+ healAmount = healAmount * (1.50 * (1 + talentData[TouchedbytheLight].current))
+ end
+
+ return DIRECT_HEALS, ceil(healAmount)
+ end
+ end
+end
+
+-- PRIESTS
+-- Accurate as of 3.2.2 (build 10392)
+if( playerClass == "PRIEST" ) then
+ LoadClassData = function()
+ -- Hot data
+ local Renew = GetSpellInfo(139)
+ hotData[Renew] = {coeff = 1, interval = 3, ticks = 5, levels = {8, 14, 20, 26, 32, 38, 44, 50, 56, 60, 65, 70, 75, 80}, averages = {45, 100, 175, 245, 315, 400, 510, 650, 810, 970, 1010, 1110, 1235, 1400}}
+ --local GlyphofPoH = GetSpellInfo(56161)
+ --hotData[GlyphofPoH] = {isMulti = true, interval = 3}
+
+ -- Spell data
+ -- Greater Heal
+ local GreaterHeal = GetSpellInfo(2060)
+ spellData[GreaterHeal] = {coeff = 3 / 3.5, levels = {40, 46, 52, 58, 60, 63, 68, 73, 78}, increase = {204, 197, 184, 165, 162, 142, 111, 92, 30},
+ averages = {avg(899, 1013), avg(1149, 1289), avg(1437, 1609), avg(1798, 2006), avg(1966, 2194), avg(2074, 2410), avg(2394, 2784), avg(3395, 3945), avg(3950, 4590)}}
+ -- Prayer of Healing
+ local PrayerofHealing = GetSpellInfo(596)
+ spellData[PrayerofHealing] = {coeff = 0.2798, levels = {30, 40, 50, 60, 60, 68, 76}, increase = {65, 64, 60, 48, 50, 33, 18},
+ averages = {avg(301, 321), avg(444, 472), avg(657, 695), avg(939, 991), avg(997, 1053), avg(1246, 1316), avg(2091, 2209)}}
+ -- Flash Heal
+ local FlashHeal = GetSpellInfo(2061)
+ spellData[FlashHeal] = {coeff = 1.5 / 3.5, levels = {20, 26, 32, 38, 44, 52, 58, 61, 67, 73, 79}, increase = {114, 118, 120, 117, 118, 111, 100, 89, 67, 56, 9},
+ averages = {avg(193, 237), avg(258, 314), avg(327, 393), avg(400, 478), avg(518, 616), avg(644, 764), avg(812, 958), avg(913, 1059), avg(1101, 1279), avg(1578, 1832), avg(1887, 2198)}}
+ -- Binding Heal
+ local BindingHeal = GetSpellInfo(32546)
+ spellData[BindingHeal] = {coeff = 1.5 / 3.5, levels = {64, 72, 78}, averages = {avg(1042, 1338), avg(1619, 2081), avg(1952, 2508)}, increase = {30, 24, 7}}
+ -- Penance
+ local Penance = GetSpellInfo(53007)
+ spellData[Penance] = {coeff = 0.857, ticks = 3, levels = {60, 70, 75, 80}, averages = {avg(670, 756), avg(805, 909), avg(1278, 1442), avg(1484, 1676)}}
+ -- Heal
+ local Heal = GetSpellInfo(2054)
+ spellData[Heal] = {coeff = 3 / 3.5, levels = {16, 22, 28, 34}, averages = {avg(295, 341), avg(429, 491), avg(566, 642), avg(712, 804)}, increase = {153, 185, 208, 207}}
+ -- Lesser Heal
+ local LesserHeal = GetSpellInfo(2050)
+ spellData[LesserHeal] = {levels = {1, 4, 10}, averages = {avg(46, 56), avg(71, 85), avg(135, 157)}, increase = {71, 83, 112}}
+
+ -- Talent data
+ local Grace = GetSpellInfo(47517)
+ -- Spiritual Healing (Add)
+ local SpiritualHealing = GetSpellInfo(14898)
+ talentData[SpiritualHealing] = {mod = 0.02, current = 0}
+ -- Empowered Healing (Add, also 0.04 for FH/BH)
+ local EmpoweredHealing = GetSpellInfo(33158)
+ talentData[EmpoweredHealing] = {mod = 0.08, current = 0}
+ -- Blessed Resilience (Add)
+ local BlessedResilience = GetSpellInfo(33142)
+ talentData[BlessedResilience] = {mod = 0.01, current = 0}
+ -- Focused Power (Add)
+ local FocusedPower = GetSpellInfo(33190)
+ talentData[FocusedPower] = {mod = 0.02, current = 0}
+ -- Divine Providence (Add)
+ local DivineProvidence = GetSpellInfo(47567)
+ talentData[DivineProvidence] = {mod = 0.02, current = 0}
+ -- Improved Renew (Add)
+ local ImprovedRenew = GetSpellInfo(14908)
+ talentData[ImprovedRenew] = {mod = 0.05, current = 0}
+ -- Empowered Renew (Multi, spell power)
+ local EmpoweredRenew = GetSpellInfo(63534)
+ talentData[EmpoweredRenew] = {mod = 0.05, current = 0}
+ -- Twin Disciplines (Add)
+ local TwinDisciplines = GetSpellInfo(47586)
+ talentData[TwinDisciplines] = {mod = 0.01, current = 0}
+
+ -- Keep track of who has grace on them
+ local activeGraceGUID, activeGraceModifier
+ AuraHandler = function(unit, guid)
+ local stack, _, _, _, caster = select(4, UnitBuff(unit, Grace))
+ if( caster == "player" ) then
+ activeGraceModifier = stack * 0.03
+ activeGraceGUID = guid
+ elseif( activeGraceGUID == guid ) then
+ activeGraceGUID = nil
+ end
+ end
+
+ -- Check for beacon when figuring out who to heal
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ if( spellName == BindingHeal ) then
+ return format("%s,%s", compressGUID[guid], compressGUID[playerGUID]), healAmount
+ elseif( spellName == PrayerofHealing ) then
+ local targets = compressGUID[guid]
+ local group = guidToGroup[guid]
+
+ for groupGUID, id in pairs(guidToGroup) do
+ local unit = guidToUnit[groupGUID]
+ if( id == group and guid ~= groupGUID and UnitIsVisible(unit) and not UnitHasVehicleUI(unit) ) then
+ targets = targets .. "," .. compressGUID[groupGUID]
+ end
+ end
+
+ return targets, healAmount
+ elseif( hasVariableTicks ) then
+ healAmount = tconcat(healAmount, "@")
+ end
+
+ return compressGUID[guid], healAmount
+ end
+
+ CalculateHotHealing = function(guid, spellID)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ local rank = HealComm.rankNumbers[spellRank]
+ local healAmount = hotData[spellName].averages[rank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local totalTicks
+
+ -- Add grace if it's active on them
+ if( activeGraceGUID == guid and activeGraceModifier ) then
+ healModifier = healModifier + activeGraceModifier
+ end
+
+ healModifier = healModifier + talentData[FocusedPower].current
+ healModifier = healModifier + talentData[BlessedResilience].current
+ healModifier = healModifier + talentData[SpiritualHealing].current
+
+ if( spellName == Renew ) then
+ healModifier = healModifier + talentData[ImprovedRenew].current
+ healModifier = healModifier + talentData[TwinDisciplines].current
+
+ -- Glyph of Renew, one less tick for +25% healing per tick. As this heals the same just faster, it has to be a flat 25% modifier
+ if( glyphCache[55674] ) then
+ healModifier = healModifier + 0.25
+ totalTicks = 4
+ else
+ totalTicks = 5
+ end
+
+ spellPower = spellPower * ((hotData[spellName].coeff * 1.88) * (1 + (talentData[EmpoweredRenew].current)))
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+
+ end
+
+ healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+ return HOT_HEALS, ceil(healAmount), totalTicks, hotData[spellName].interval
+ end
+
+ -- If only every other class was as easy as Paladins
+ CalculateHealing = function(guid, spellName, spellRank)
+ local healAmount = averageHeal[spellName][spellRank]
+ local rank = HealComm.rankNumbers[spellRank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+
+ -- Add grace if it's active on them
+ if( activeGraceGUID == guid ) then
+ healModifier = healModifier + activeGraceModifier
+ end
+
+ healModifier = healModifier + talentData[FocusedPower].current
+ healModifier = healModifier + talentData[BlessedResilience].current
+ healModifier = healModifier + talentData[SpiritualHealing].current
+
+ -- Greater Heal
+ if( spellName == GreaterHeal ) then
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].current))
+ -- Flash Heal
+ elseif( spellName == FlashHeal ) then
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].spent * 0.04))
+ -- Binding Heal
+ elseif( spellName == BindingHeal ) then
+ healModifier = healModifier + talentData[DivineProvidence].current
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].spent * 0.04))
+ -- Penance
+ elseif( spellName == Penance ) then
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ spellPower = spellPower / spellData[spellName].ticks
+ -- Prayer of Heaing
+ elseif( spellName == PrayerofHealing ) then
+ healModifier = healModifier + talentData[DivineProvidence].current
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ -- Heal
+ elseif( spellName == Heal ) then
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ -- Lesser Heal
+ elseif( spellName == LesserHeal ) then
+ local castTime = rank > 3 and 2.5 or rank == 2 and 2 or 1.5
+ spellPower = spellPower * ((castTime / 3.5) * 1.88)
+ end
+
+ healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+
+ -- Player has over a 100% chance to crit with Holy spells
+ if( GetSpellCritChance(2) >= 100 ) then
+ healAmount = healAmount * 1.50
+ end
+
+ -- Penance ticks 3 times, the player will see all 3 ticks, everyone else should only see the last 2
+ if( spellName == Penance ) then
+ return CHANNEL_HEALS, ceil(healAmount), 2, 3
+ end
+
+ return DIRECT_HEALS, ceil(healAmount)
+ end
+ end
+end
+
+-- SHAMANS
+-- All spells accurate as of 3.2.2 (build 10392)
+if( playerClass == "SHAMAN" ) then
+ LoadClassData = function()
+ -- Hot data
+ -- Riptide
+ local Riptide = GetSpellInfo(61295)
+ hotData[Riptide] = {interval = 3, ticks = 5, coeff = 0.50, levels = {60, 70, 75, 80}, averages = {665, 885, 1435, 1670}}
+ -- Earthliving Weapon proc
+ local Earthliving = GetSpellInfo(52000)
+ hotData[Earthliving] = {interval = 3, ticks = 4, coeff = 0.80, levels = {30, 40, 50, 60, 70, 80}, averages = {116, 160, 220, 348, 456, 652}}
+
+ -- Spell data
+ -- Chain Heal
+ local ChainHeal = GetSpellInfo(1064)
+ spellData[ChainHeal] = {coeff = 2.5 / 3.5, levels = {40, 46, 54, 61, 68, 74, 80}, increase = {100, 95, 85, 72, 45, 22, 0},
+ averages = {avg(320, 368), avg(405, 465), avg(551, 629), avg(605, 691), avg(826, 942), avg(906, 1034), avg(1055, 1205)}}
+ -- Healing Wave
+ local HealingWave = GetSpellInfo(331)
+ spellData[HealingWave] = {levels = {1, 6, 12, 18, 24, 32, 40, 48, 56, 60, 63, 70, 75, 80},
+ averages = {avg(34, 44), avg(64, 78), avg(129, 155), avg(268, 316), avg(376, 440), avg(536, 622), avg(740, 854), avg(1017, 1167), avg(1367, 1561), avg(1620, 1850), avg(1725, 1969), avg(2134, 2438), avg(2624, 2996), avg(3034, 3466)},
+ increase = {55, 74, 102, 142, 151, 158, 156, 150, 132, 110, 107, 71, 40, 0}}
+ -- Lesser Healing Wave
+ local LesserHealingWave = GetSpellInfo(8004)
+ spellData[LesserHealingWave] = {coeff = 1.5 / 3.5, levels = {20, 28, 36, 44, 52, 60, 66, 72, 77}, increase = {102, 109, 110, 108, 100, 84, 58, 40, 18},
+ averages = {avg(162, 186), avg(247, 281), avg(337, 381), avg(458, 514), avg(631, 705), avg(832, 928), avg(1039, 1185), avg(1382, 1578), avg(1606, 1834)}}
+
+ -- Talent data
+ local EarthShield = GetSpellInfo(49284)
+ -- Improved Chain Heal (Multi)
+ local ImpChainHeal = GetSpellInfo(30872)
+ talentData[ImpChainHeal] = {mod = 0.10, current = 0}
+ -- Tidal Waves (Add, this is a buff)
+ local TidalWaves = GetSpellInfo(51566)
+ talentData[TidalWaves] = {mod = 0.04, current = 0}
+ -- Healing Way (Multi, this goes from 8 -> 16 -> 25 so have to manually do the conversion)
+ local HealingWay = GetSpellInfo(29206)
+ talentData[HealingWay] = {mod = 0, current = 0}
+ -- Purification (Add)
+ local Purification = GetSpellInfo(16178)
+ talentData[Purification] = {mod = 0.02, current = 0}
+
+ -- Set bonuses
+ -- T7 Resto 4 piece, +5% healing on Chain Heal and Healing Wave
+ itemSetsData["T7 Resto"] = {40508, 40509, 40510, 40512, 40513, 39583, 39588, 39589, 39590, 39591}
+ -- T9 Resto 2 piece, +20% healing to Riptide
+ itemSetsData["T9 Resto"] = {48280, 48281, 48282, 48283, 48284, 48295, 48296, 48297, 48298, 48299, 48301, 48302, 48303, 48304, 48300, 48306, 48307, 48308, 48309, 48305, 48286, 48287, 48288, 48289, 48285, 48293, 48292, 48291, 48290, 48294}
+
+ -- Totems
+ local lhwTotems = {[42598] = 320, [42597] = 267, [42596] = 236, [42595] = 204, [25645] = 79, [22396] = 80, [23200] = 53}
+ local chTotems = {[45114] = 243, [38368] = 102, [28523] = 87}
+
+ -- Keep track of who has riptide on them
+ local riptideData, earthshieldList = {}, {}
+ AuraHandler = function(unit, guid)
+ riptideData[guid] = unitHasAura(unit, Riptide) and true or nil
+
+ -- Currently, Glyph of Lesser Healing Wave + Any Earth Shield increase the healing not just the players own
+ if( UnitBuff(unit, EarthShield) ) then
+ earthshieldList[guid] = true
+ elseif( earthshieldList[guid] ) then
+ earthshieldList[guid] = nil
+ end
+ end
+
+ -- Cast was interrupted, recheck if we still have the auras up
+ ResetChargeData = function(guid)
+ riptideData[guid] = guidToUnit[guid] and unitHasAura(guidToUnit[guid], Riptide) and true or nil
+ end
+
+ -- Lets a specific override on how many people this will hit
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ -- Glyph of Healing Wave, heals you for 20% of your heal when you heal someone else
+ if( glyphCache[55440] and guid ~= playerGUID and spellName == HealingWave ) then
+ return format("%s,%d,%s,%d", compressGUID[guid], healAmount, compressGUID[playerGUID], healAmount * 0.20), -1
+ elseif( hasVariableTicks ) then
+ healAmount = tconcat(healAmount, "@")
+ end
+
+ return compressGUID[guid], healAmount
+ end
+
+ CalculateHotHealing = function(guid, spellID)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ local rank = HealComm.rankNumbers[spellRank]
+ local healAmount = hotData[spellName].averages[rank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local totalTicks
+
+ healModifier = healModifier + talentData[Purification].current
+
+ -- Riptide
+ if( spellName == Riptide ) then
+ if( equippedSetCache["T9 Resto"] >= 2 ) then
+ spModifier = spModifier * 1.20
+ end
+
+ spellPower = spellPower * (hotData[spellName].coeff * 1.88)
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+
+ totalTicks = hotData[spellName].ticks
+ -- Glyph of Riptide, +6 seconds
+ if( glyphCache[63273] ) then totalTicks = totalTicks + 2 end
+
+ -- Earthliving Weapon
+ elseif( spellName == Earthliving ) then
+ spellPower = (spellPower * (hotData[spellName].coeff * 1.88) * 0.45)
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+
+ totalTicks = hotData[spellName].ticks
+ end
+
+ healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+ return HOT_HEALS, healAmount, totalTicks, hotData[spellName].interval
+ end
+
+
+ -- If only every other class was as easy as Paladins
+ CalculateHealing = function(guid, spellName, spellRank)
+ local healAmount = averageHeal[spellName][spellRank]
+ local rank = HealComm.rankNumbers[spellRank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+
+ healModifier = healModifier + talentData[Purification].current
+
+ -- Chain Heal
+ if( spellName == ChainHeal ) then
+ healModifier = healModifier * (1 + talentData[ImpChainHeal].current)
+ healAmount = healAmount + (playerCurrentRelic and chTotems[playerCurrentRelic] or 0)
+
+ if( equippedSetCache["T7 Resto"] >= 4 ) then
+ healModifier = healModifier * 1.05
+ end
+
+ -- Add +25% from Riptide being up and reset the flag
+ if( riptideData[guid] ) then
+ healModifier = healModifier * 1.25
+ riptideData[guid] = nil
+ end
+
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ -- Heaing Wave
+ elseif( spellName == HealingWave ) then
+ healModifier = healModifier * (talentData[HealingWay].spent == 3 and 1.25 or talentData[HealingWay].spent == 2 and 1.16 or talentData[HealingWay].spent == 1 and 1.08 or 1)
+
+ if( equippedSetCache["T7 Resto"] >= 4 ) then
+ healModifier = healModifier * 1.05
+ end
+
+ -- Totem of Spontaneous Regrowth, +88 Spell Power to Healing Wave
+ if( playerCurrentRelic == 27544 ) then
+ spellPower = spellPower + 88
+ end
+
+ local castTime = rank > 3 and 3 or rank == 3 and 2.5 or rank == 2 and 2 or 1.5
+ spellPower = spellPower * (((castTime / 3.5) * 1.88) + talentData[TidalWaves].current)
+
+ -- Lesser Healing Wave
+ elseif( spellName == LesserHealingWave ) then
+ -- Glyph of Lesser Healing Wave, +20% healing on LHW if target has ES up
+ if( glyphCache[55438] and earthshieldList[guid] ) then
+ healModifier = healModifier * 1.20
+ end
+
+ spellPower = spellPower + (playerCurrentRelic and lhwTotems[playerCurrentRelic] or 0)
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) + talentData[TidalWaves].spent * 0.02)
+ end
+
+ healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+
+ -- Player has over a 100% chance to crit with Nature spells
+ if( GetSpellCritChance(4) >= 100 ) then
+ healAmount = healAmount * 1.50
+ end
+
+ -- Apply the final modifier of any MS or self heal increasing effects
+ return DIRECT_HEALS, ceil(healAmount)
+ end
+ end
+end
+
+if( select(2, UnitRace("player")) == "Draenei") or true then
+ local GetSpellBonusDamage = GetSpellBonusDamage
+ local UnitAttackPower = UnitAttackPower
+ local UnitRangedAttackPower = UnitRangedAttackPower
+
+ LoadRaceData = function()
+ local naaruClassSpells = {
+ [28880] = "WARRIOR",
+ [59542] = "PALADIN",
+ [59543] = "HUNTER",
+ [59544] = "PRIEST",
+ [59545] = "DEATHKNIGHT",
+ [59547] = "SHAMAN",
+ [59548] = "MAGE",
+ }
+ local naaruSpellID, naaruSpellName
+
+ for spellID, class in pairs(naaruClassSpells) do
+ if playerClass == class then
+ naaruSpellID = spellID
+ naaruSpellName = GetSpellInfo(spellID)
+ end
+ end
+
+ if not (naaruSpellID and naaruSpellName) then return end
+
+ hotData[naaruSpellName] = {interval = 3, ticks = 5}
+
+ local FocusedPower, BlessedResilience
+ local Divinity, DivineIllumination
+
+ if playerClass == "PRIEST" then
+ FocusedPower = GetSpellInfo(33190)
+ BlessedResilience = GetSpellInfo(33142)
+ elseif playerClass == "PALADIN" then
+ Divinity = GetSpellInfo(63646)
+ DivineIllumination = GetSpellInfo(31842)
+ end
+
+ local CalculateHotHealing_Naaru = function(guid, spellID)
+ local healAmount = playerLevel * 15 + 35
+ local spellPower, spellModifier
+
+ if playerClass == "MAGE" or playerClass == "PRIEST" then
+ spellPower = GetSpellBonusDamage(2)
+ spellModifier = 1.885
+ elseif playerClass == "PALADIN" or playerClass == "SHAMAN" then
+ local apBase, apPosBuff, apNegBuff = UnitAttackPower("player")
+ local meleeAttackPower = apBase + apPosBuff + apNegBuff
+ local holySpellPower = GetSpellBonusDamage(2)
+
+ if (meleeAttackPower * 1.1) > (holySpellPower * 1.885) then
+ spellPower = meleeAttackPower
+ spellModifier = 1.1
+ else
+ spellPower = holySpellPower
+ spellModifier = 1.885
+ end
+ else
+ local apBase, apPosBuff, apNegBuff = UnitAttackPower("player")
+ local meleeAttackPower = apBase + apPosBuff + apNegBuff
+
+ local rapBase, rapPosBuff, rapNegBuff = UnitRangedAttackPower("player")
+ local rangedAttackPower = rapBase + rapPosBuff + rapNegBuff
+
+ spellModifier = 1.1
+
+ if (meleeAttackPower * spellModifier) > (rangedAttackPower * spellModifier) then
+ spellPower = meleeAttackPower
+ else
+ spellPower = rangedAttackPower
+ end
+ end
+
+ local healModifier = playerHealModifier
+
+ if playerClass == "PRIEST" then
+ healAmount = healAmount * (1 + talentData[FocusedPower].current)
+ healAmount = healAmount * (1 + talentData[BlessedResilience].current)
+ elseif playerClass == "PALADIN" then
+ if talentData[Divinity].current > 0 then
+ healModifier = healModifier + talentData[Divinity].current
+ healAmount = healAmount * (1 + talentData[Divinity].current)
+ end
+
+ if equippedSetCache["T10 Holy"] >= 2 and unitHasAura("player", DivineIllumination) then
+ healAmount = healAmount * 1.35
+ end
+ end
+
+ healAmount = calculateGeneralAmount(playerLevel, healAmount, spellPower, spellModifier, healModifier)
+ healAmount = floor(healAmount / hotData[naaruSpellName].ticks)
+
+ return HOT_HEALS, healAmount, hotData[naaruSpellName].ticks, hotData[naaruSpellName].interval
+ end
+
+ if CalculateHotHealing then
+ local CalculateHotHealing_Orig = CalculateHotHealing
+ function CalculateHotHealing(guid, spellID)
+ if naaruClassSpells[spellID] then
+ return CalculateHotHealing_Naaru(guid, spellID)
+ else
+ return CalculateHotHealing_Orig(guid, spellID)
+ end
+ end
+ else
+ CalculateHotHealing = CalculateHotHealing_Naaru
+ end
+
+ if not GetHealTargets then
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ return compressGUID[guid], healAmount
+ end
+ end
+ end
+end
+
+local function getName(spellID)
+ local name = GetSpellInfo(spellID)
+ --[===[@debug@
+ if( not name ) then
+ print(format("%s-r%s: Failed to find spellID %d", major, minor, spellID))
+ end
+ --@end-debug@]===]
+ return name or ""
+end
+
+-- Healing modifiers
+if( not HealComm.aurasUpdated ) then
+ HealComm.aurasUpdated = true
+ HealComm.selfModifiers = nil
+ HealComm.healingModifiers = nil
+end
+
+HealComm.currentModifiers = HealComm.currentModifiers or {}
+
+HealComm.selfModifiers = HealComm.selfModifiers or {
+ [64850] = 0.50, -- Unrelenting Assault
+ [65925] = 0.50, -- Unrelenting Assault
+ [54428] = 0.50, -- Divine Plea
+ [32346] = 0.50, -- Stolen Soul
+ [64849] = 0.75, -- Unrelenting Assault
+ [72221] = 1.05, -- Luck of the Draw
+ [70873] = 1.10, -- Emerald Vigor (Valithria Dreamwalker)
+ [31884] = 1.20, -- Avenging Wrath
+ [72393] = 0.25, -- Hopelessness
+ [72397] = 0.40, -- Hopelessness
+ [72391] = 0.50, -- Hopelessness
+ [72396] = 0.60, -- Hopelessness
+ [72390] = 0.75, -- Hopelessness
+ [72395] = 0.80, -- Hopelessness
+}
+
+-- The only spell in the game with a name conflict is Ray of Pain from the Nagrand Void Walkers
+HealComm.healingModifiers = HealComm.healingModifiers or {
+ [getName(30843)] = 0.00, -- Enfeeble
+ [getName(41292)] = 0.00, -- Aura of Suffering
+ [59513] = 0.00, -- Embrace of the Vampyr
+ [getName(55593)] = 0.00, -- Necrotic Aura
+ [28776] = 0.10, -- Necrotic Poison
+ [getName(34625)] = 0.25, -- Demolish
+ [getName(19716)] = 0.25, -- Gehennas' Curse
+ [getName(24674)] = 0.25, -- Veil of Shadow
+ [69633] = 0.25, -- Veil of Shadow, in German this is translated differently from the one above
+ [46296] = 0.25, -- Necrotic Poison
+ [54121] = 0.25, -- Necrotic Poison
+ -- Wound Poison still uses a unique spellID/spellName despite the fact that it's a static 50% reduction.
+ [getName(13218)] = 0.50, -- 1
+ [getName(13222)] = 0.50, -- 2
+ [getName(13223)] = 0.50, -- 3
+ [getName(13224)] = 0.50, -- 4
+ [getName(27189)] = 0.50, -- 5
+ [getName(57974)] = 0.50, -- 6
+ [getName(57975)] = 0.50, -- 7
+ [getName(20900)] = 0.50, -- Aimed Shot
+ [getName(44534)] = 0.50, -- Wretched Strike
+ [getName(21551)] = 0.50, -- Mortal Strike
+ [getName(40599)] = 0.50, -- Arcing Smash
+ [getName(36917)] = 0.50, -- Magma-Throwser's Curse
+ [getName(23169)] = 0.50, -- Brood Affliction: Green
+ [getName(22859)] = 0.50, -- Mortal Cleave
+ [getName(36023)] = 0.50, -- Deathblow
+ [getName(13583)] = 0.50, -- Curse of the Deadwood
+ [getName(32378)] = 0.50, -- Filet
+ [getName(35189)] = 0.50, -- Solar Strike
+ [getName(32315)] = 0.50, -- Soul Strike
+ [getName(60084)] = 0.50, -- The Veil of Shadow
+ [getName(45885)] = 0.50, -- Shadow Spike
+ [getName(69674)] = 0.50, -- Mutated Infection (Rotface)
+ [36693] = 0.55, -- Necrotic Poison
+ [getName(63038)] = 0.75, -- Dark Volley
+ [getName(52771)] = 0.75, -- Wounding Strike
+ [getName(48291)] = 0.75, -- Fetid Healing
+ [getName(34366)] = 0.75, -- Ebon Poison
+ [getName(54525)] = 0.80, -- Shroud of Darkness (This might be wrong)
+ [getName(48301)] = 0.80, -- Mind Trauma (Improved Mind Blast)
+ [getName(68391)] = 0.80, -- Permafrost, the debuff is generic no way of seeing 7/13/20, go with 20
+ [getName(52645)] = 0.80, -- Hex of Weakness
+ [getName(34073)] = 0.85, -- Curse of the Bleeding Hollow
+ [getName(43410)] = 0.90, -- Chop
+ [getName(70588)] = 0.90, -- Suppression (Valithria Dreamwalker NPCs?)
+ [getName(34123)] = 1.06, -- Tree of Life
+ [getName(64844)] = 1.10, -- Divine Hymn
+ [getName(47788)] = 1.40, -- Guardian Spirit
+ [getName(38387)] = 1.50, -- Bane of Infinity
+ [getName(31977)] = 1.50, -- Curse of Infinity
+ [getName(41350)] = 2.00, -- Aura of Desire
+ [73762] = 1.05, -- Strength of Wrynn (5%)
+ [73816] = 1.05, -- Hellscream's Warsong (5%)
+ [73824] = 1.10, -- Strength of Wrynn (10%)
+ [73818] = 1.10, -- Hellscream's Warsong (10%)
+ [73825] = 1.15, -- Strength of Wrynn (15%)
+ [73819] = 1.15, -- Hellscream's Warsong (15%)
+ [73826] = 1.20, -- Strength of Wrynn (20%)
+ [73820] = 1.20, -- Hellscream's Warsong (20%)
+ [73827] = 1.25, -- Strength of Wrynn (25%)
+ [73821] = 1.25, -- Hellscream's Warsong (25%)
+ [73828] = 1.30, -- Strength of Wrynn (30%)
+ [73822] = 1.30, -- Hellscream's Warsong (30%)
+}
+
+HealComm.healingStackMods = HealComm.healingStackMods or {
+ -- Enervating Band
+ [getName(74502)] = function(name, rank, icon, stacks) return 1 - stacks * 0.02 end,
+ -- Tenacity
+ [getName(58549)] = function(name, rank, icon, stacks) return icon == "Interface\\Icons\\Ability_Warrior_StrengthOfArms" and stacks ^ 1.18 or 1 end,
+ -- Focused Will
+ [getName(45242)] = function(name, rank, icon, stacks) return 1 + (stacks * (0.02 + rankNumbers[rank] / 100)) end,
+ -- Nether Portal - Dominance
+ [getName(30423)] = function(name, rank, icon, stacks) return 1 + stacks * 0.01 end,
+ -- Dark Touched
+ [getName(45347)] = function(name, rank, icon, stacks) return 1 - stacks * 0.04 end,
+ -- Necrotic Strike
+ [getName(60626)] = function(name, rank, icon, stacks) return 1 - stacks * 0.10 end,
+ -- Mortal Wound
+ [getName(28467)] = function(name, rank, icon, stacks) return 1 - stacks * 0.10 end,
+ -- Furious Strikes
+ [getName(56112)] = function(name, rank, icon, stacks) return 1 - stacks * 0.25 end,
+}
+
+local healingStackMods, selfModifiers = HealComm.healingStackMods, HealComm.selfModifiers
+local healingModifiers, currentModifiers = HealComm.healingModifiers, HealComm.currentModifiers
+
+local distribution
+local CTL = ChatThrottleLib
+local function sendMessage(msg)
+ if( distribution and len(msg) <= 240 ) then
+ CTL:SendAddonMessage("BULK", COMM_PREFIX, msg, distribution)
+ end
+end
+
+-- Keep track of where all the data should be going
+local instanceType
+local function updateDistributionChannel()
+ local lastChannel = distribution
+ if( instanceType == "pvp" or instanceType == "arena" ) then
+ distribution = "BATTLEGROUND"
+ elseif( GetNumRaidMembers() > 0 ) then
+ distribution = "RAID"
+ elseif( GetNumPartyMembers() > 0 ) then
+ distribution = "PARTY"
+ else
+ distribution = nil
+ end
+
+ if( distribution == lastChannel ) then return end
+
+ -- If the player is not a healer, some events can be disabled until the players grouped.
+ if( distribution ) then
+ HealComm.eventFrame:RegisterEvent("CHAT_MSG_ADDON")
+ if( not isHealerClass ) then
+ HealComm.eventFrame:RegisterEvent("UNIT_AURA")
+ HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_DELAYED")
+ HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
+ HealComm.eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ end
+ else
+ HealComm.eventFrame:UnregisterEvent("CHAT_MSG_ADDON")
+ if( not isHealerClass ) then
+ HealComm.eventFrame:UnregisterEvent("UNIT_AURA")
+ HealComm.eventFrame:UnregisterEvent("UNIT_SPELLCAST_DELAYED")
+ HealComm.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
+ HealComm.eventFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ end
+ end
+end
+
+-- Figure out where we should be sending messages and wipe some caches
+function HealComm:PLAYER_ENTERING_WORLD()
+ HealComm.eventFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
+ HealComm:ZONE_CHANGED_NEW_AREA()
+end
+
+function HealComm:ZONE_CHANGED_NEW_AREA()
+ local pvpType = GetZonePVPInfo()
+ local type = select(2, IsInInstance())
+
+ HealComm.zoneHealModifier = 1
+ if( pvpType == "combat" or type == "arena" or type == "pvp" ) then
+ HealComm.zoneHealModifier = 0.90
+ end
+
+ if( type ~= instanceType ) then
+ instanceType = type
+
+ guidToUnit[playerGUID] = "player" -- Tsoukie: Fix warmane dungeon finder.
+
+ updateDistributionChannel()
+ clearPendingHeals()
+ twipe(activeHots)
+ end
+
+ instanceType = type
+end
+
+local alreadyAdded = {}
+function HealComm:UNIT_AURA(unit)
+ if not unit then return end
+
+ local guid = UnitGUID(unit)
+ if( not guidToUnit[guid] ) then return end
+ local increase, decrease, playerIncrease, playerDecrease = 1, 1, 1, 1
+
+ -- Scan buffs
+ local id = 1
+ while( true ) do
+ local name, rank, icon, stack, _, _, _, _, _, _, spellID = UnitAura(unit, id, "HELPFUL")
+ if( not name ) then break end
+ -- Prevent buffs like Tree of Life that have the same name for the shapeshift/healing increase from being calculated twice
+ if( not alreadyAdded[name] ) then
+ alreadyAdded[name] = true
+
+ if( healingModifiers[spellID] ) then
+ increase = increase * healingModifiers[spellID]
+ elseif( healingModifiers[name] ) then
+ increase = increase * healingModifiers[name]
+ elseif( healingStackMods[name] ) then
+ increase = increase * healingStackMods[name](name, rank, icon, stack)
+ end
+
+ if( unit == "player" and selfModifiers[spellID] ) then
+ playerIncrease = playerIncrease * selfModifiers[spellID]
+ end
+ end
+
+ id = id + 1
+ end
+
+ -- Scan debuffs
+ id = 1
+ while( true ) do
+ local name, rank, icon, stack, _, _, _, _, _, _, spellID = UnitAura(unit, id, "HARMFUL")
+ if( not name ) then break end
+
+ if( healingModifiers[spellID] ) then
+ decrease = min(decrease, healingModifiers[spellID])
+ elseif( healingModifiers[name] ) then
+ decrease = min(decrease, healingModifiers[name])
+ elseif( healingStackMods[name] ) then
+ decrease = min(decrease, healingStackMods[name](name, rank, icon, stack))
+ end
+
+ if( unit == "player" and selfModifiers[spellID] ) then
+ playerDecrease = min(playerDecrease, selfModifiers[spellID])
+ end
+
+ id = id + 1
+ end
+
+ -- Check if modifier changed
+ local modifier = increase * decrease
+ if( modifier ~= currentModifiers[guid] ) then
+ if( currentModifiers[guid] or modifier ~= 1 ) then
+ currentModifiers[guid] = modifier
+ self.callbacks:Fire("HealComm_ModifierChanged", guid, modifier)
+ else
+ currentModifiers[guid] = modifier
+ end
+ end
+
+ twipe(alreadyAdded)
+
+ if( unit == "player" ) then
+ playerHealModifier = playerIncrease * playerDecrease
+ end
+
+ -- Class has a specific monitor it needs for auras
+ if( AuraHandler ) then
+ AuraHandler(unit, guid)
+ end
+end
+
+-- Monitor glyph changes
+function HealComm:GlyphsUpdated(id)
+ local spellID = glyphCache[id]
+
+ -- Invalidate the old cache value
+ if( spellID ) then
+ glyphCache[spellID] = nil
+ glyphCache[id] = nil
+ end
+
+ -- Cache the new one if any
+ local enabled, _, glyphID = GetGlyphSocketInfo(id)
+ if( enabled and glyphID ) then
+ glyphCache[glyphID] = true
+ glyphCache[id] = glyphID
+ end
+end
+
+HealComm.GLYPH_ADDED = HealComm.GlyphsUpdated
+HealComm.GLYPH_REMOVED = HealComm.GlyphsUpdated
+HealComm.GLYPH_UPDATED = HealComm.GlyphsUpdated
+
+-- Invalidate he average cache to recalculate for spells that increase in power due to leveling up (but not training new ranks)
+function HealComm:PLAYER_LEVEL_UP(level)
+ for spell, average in pairs(averageHeal) do
+ twipe(average)
+
+ average.spell = spell
+ end
+
+ -- WoWProgramming says this is a string, why this is a string I do not know.
+ playerLevel = tonumber(level) or UnitLevel("player")
+end
+
+-- Cache player talent data for spells we need
+function HealComm:PLAYER_TALENT_UPDATE()
+ for tabIndex=1, GetNumTalentTabs() do
+ for i=1, GetNumTalents(tabIndex) do
+ local name, _, _, _, spent = GetTalentInfo(tabIndex, i)
+ if( name and talentData[name] ) then
+ talentData[name].current = talentData[name].mod * spent
+ talentData[name].spent = spent
+ end
+ end
+ end
+end
+
+-- Save the currently equipped range weapon
+local RANGED_SLOT = GetInventorySlotInfo("RangedSlot")
+function HealComm:PLAYER_EQUIPMENT_CHANGED()
+ -- Caches set bonus info, as you can't reequip set bonus gear in combat no sense in checking it
+ if( not InCombatLockdown() ) then
+ for name, items in pairs(itemSetsData) do
+ equippedSetCache[name] = 0
+ for _, itemID in pairs(items) do
+ if( IsEquippedItem(itemID) ) then
+ equippedSetCache[name] = equippedSetCache[name] + 1
+ end
+ end
+ end
+ end
+
+ -- Check relic
+ local relic = GetInventoryItemLink("player", RANGED_SLOT)
+ playerCurrentRelic = relic and tonumber(match(relic, "item:(%d+):")) or nil
+end
+
+-- COMM CODE
+local function loadHealAmount(...)
+ local tbl = HealComm:RetrieveTable()
+ for i=1, select("#", ...) do
+ tbl[i] = tonumber((select(i, ...)))
+ end
+
+ return tbl
+end
+
+-- Direct heal started
+local function loadHealList(pending, amount, stack, endTime, ticksLeft, ...)
+ twipe(tempPlayerList)
+
+ -- For the sake of consistency, even a heal doesn't have multiple end times like a hot, it'll be treated as such in the DB
+ if( amount ~= -1 and amount ~= "-1" ) then
+ amount = not pending.hasVariableTicks and amount or loadHealAmount(split("@", amount))
+
+ for i=1, select("#", ...) do
+ local guid = select(i, ...)
+ if( guid ) then
+ updateRecord(pending, decompressGUID[guid], amount, stack, endTime, ticksLeft)
+ tinsert(tempPlayerList, decompressGUID[guid])
+ end
+ end
+ else
+ for i=1, select("#", ...), 2 do
+ local guid = select(i, ...)
+ local amount = not pending.hasVariableTicks and tonumber((select(i + 1, ...))) or loadHealAmount(split("@", amount))
+ if( guid and amount ) then
+ updateRecord(pending, decompressGUID[guid], amount, stack, endTime, ticksLeft)
+ tinsert(tempPlayerList, decompressGUID[guid])
+ end
+ end
+ end
+end
+
+local function parseDirectHeal(casterGUID, spellID, amount, ...)
+ local spellName = GetSpellInfo(spellID)
+ local unit = guidToUnit[casterGUID]
+ if( not unit or not spellName or not amount or select("#", ...) == 0 ) then return end
+
+ local endTime = select(6, UnitCastingInfo(unit))
+ if( not endTime ) then return end
+
+ pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
+ pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellName] or {}
+
+ local pending = pendingHeals[casterGUID][spellName]
+ twipe(pending)
+ pending.endTime = endTime / 1000
+ pending.spellID = spellID
+ pending.bitType = DIRECT_HEALS
+
+ loadHealList(pending, amount, 1, 0, nil, ...)
+
+ HealComm.callbacks:Fire("HealComm_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+end
+
+HealComm.parseDirectHeal = parseDirectHeal
+
+-- Channeled heal started
+local function parseChannelHeal(casterGUID, spellID, amount, totalTicks, ...)
+ local spellName = GetSpellInfo(spellID)
+ local unit = guidToUnit[casterGUID]
+ if( not unit or not spellName or not totalTicks or not amount or select("#", ...) == 0 ) then return end
+
+ local startTime, endTime = select(5, UnitChannelInfo(unit))
+ if( not startTime or not endTime ) then return end
+
+ pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
+ pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellName] or {}
+
+ local inc = amount == -1 and 2 or 1
+ local pending = pendingHeals[casterGUID][spellName]
+ twipe(pending)
+ pending.startTime = startTime / 1000
+ pending.endTime = endTime / 1000
+ pending.duration = max(pending.duration or 0, pending.endTime - pending.startTime)
+ pending.totalTicks = totalTicks
+ pending.tickInterval = (pending.endTime - pending.startTime) / totalTicks
+ pending.spellID = spellID
+ pending.isMultiTarget = (select("#", ...) / inc) > 1
+ pending.bitType = CHANNEL_HEALS
+
+ loadHealList(pending, amount, 1, 0, ceil(pending.duration / pending.tickInterval), ...)
+
+ HealComm.callbacks:Fire("HealComm_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+end
+
+-- Hot heal started
+-- When the person is within visible range of us, the aura is available by the time the message reaches the target
+-- as such, we can rely that at least one person is going to have the aura data on them (and that it won't be different, at least for this cast)
+local function findAura(casterGUID, spellName, spellRank, inc, ...)
+ for i=1, select("#", ...), inc do
+ local guid = decompressGUID[select(i, ...)]
+ local unit = guid and guidToUnit[guid]
+ if( unit and UnitIsVisible(unit) ) then
+ local id = 1
+ while( true ) do
+ local name, rank, _, stack, _, duration, endTime, caster = UnitBuff(unit, id)
+ if( not name ) then break end
+
+ if( name == spellName and spellRank == rank and caster and UnitGUID(caster) == casterGUID ) then
+ return (stack and stack > 0 and stack or 1), duration, endTime
+ end
+
+ id = id + 1
+ end
+ end
+ end
+end
+
+local function parseHotHeal(casterGUID, wasUpdated, spellID, tickAmount, totalTicks, tickInterval, ...)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ -- If the user is on 3.3, then anything without a total ticks attached to it is rejected
+ if( ( IS_BUILD30300 and not totalTicks ) or not tickAmount or not spellName or select("#", ...) == 0 ) then return end
+
+ -- Retrieve the hot information
+ local inc = ( tickAmount == -1 or tickAmount == "-1" ) and 2 or 1
+ local stack, duration, endTime = findAura(casterGUID, spellName, spellRank, inc, ...)
+ if( not stack or not duration or not endTime ) then return end
+
+ pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
+ pendingHeals[casterGUID][spellID] = pendingHeals[casterGUID][spellID] or {}
+
+ local pending = pendingHeals[casterGUID][spellID]
+ pending.duration = duration
+ pending.endTime = endTime
+ pending.stack = stack
+ pending.totalTicks = totalTicks or duration / tickInterval
+ pending.tickInterval = totalTicks and duration / totalTicks or tickInterval
+ pending.spellID = spellID
+ pending.hasVariableTicks = type(tickAmount) == "string"
+ pending.isMutliTarget = (select("#", ...) / inc) > 1
+ pending.bitType = HOT_HEALS
+
+ -- As you can't rely on a hot being the absolutely only one up, have to apply the total amount now :<
+ local ticksLeft = ceil((endTime - GetTime()) / pending.tickInterval)
+ loadHealList(pending, tickAmount, stack, endTime, ticksLeft, ...)
+
+ if( not wasUpdated ) then
+ HealComm.callbacks:Fire("HealComm_HealStarted", casterGUID, spellID, pending.bitType, endTime, unpack(tempPlayerList))
+ else
+ HealComm.callbacks:Fire("HealComm_HealUpdated", casterGUID, spellID, pending.bitType, endTime, unpack(tempPlayerList))
+ end
+end
+
+local function parseHotBomb(casterGUID, wasUpdated, spellID, amount, ...)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ if( not amount or not spellName or select("#", ...) == 0 ) then return end
+
+ -- If we don't have a pending hot then there is no bomb as far as were concerned
+ local hotPending = pendingHeals[casterGUID] and pendingHeals[casterGUID][spellID]
+ if( not hotPending or not hotPending.bitType ) then return end
+ hotPending.hasBomb = true
+
+ pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellName] or {}
+
+ local pending = pendingHeals[casterGUID][spellName]
+ pending.endTime = hotPending.endTime
+ pending.spellID = spellID
+ pending.bitType = BOMB_HEALS
+ pending.stack = hotPending.stack
+
+ loadHealList(pending, amount, pending.stack, pending.endTime, nil, ...)
+
+ if( not wasUpdated ) then
+ HealComm.callbacks:Fire("HealComm_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+ else
+ HealComm.callbacks:Fire("HealComm_HealUpdated", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+ end
+end
+
+-- Heal finished
+local function parseHealEnd(casterGUID, pending, checkField, spellID, interrupted, ...)
+ local spellName = GetSpellInfo(spellID)
+ if( not spellName or not pendingHeals[casterGUID] ) then return end
+
+ -- Hots use spell IDs while everything else uses spell names. Avoids naming conflicts for multi-purpose spells such as Lifebloom or Regrowth
+ if( not pending ) then
+ pending = checkField == "id" and pendingHeals[casterGUID][spellID] or pendingHeals[casterGUID][spellName]
+ end
+ if( not pending or not pending.bitType ) then return end
+
+ twipe(tempPlayerList)
+
+ if( select("#", ...) == 0 ) then
+ for i=#(pending), 1, -5 do
+ tinsert(tempPlayerList, pending[i - 4])
+ removeRecord(pending, pending[i - 4])
+ end
+ else
+ for i=1, select("#", ...) do
+ local guid = decompressGUID[select(i, ...)]
+
+ tinsert(tempPlayerList, guid)
+ removeRecord(pending, guid)
+ end
+ end
+
+ -- Double check and make sure we actually removed at least one person
+ if( #(tempPlayerList) == 0 ) then return end
+
+ -- Heals that also have a bomb associated to them have to end at this point, they will fire there own callback too
+ local bombPending = pending.hasBomb and pendingHeals[casterGUID][spellName]
+ if( bombPending and bombPending.bitType ) then
+ parseHealEnd(casterGUID, bombPending, "name", spellID, interrupted, ...)
+ end
+
+ local bitType = pending.bitType
+ -- Clear data if we're done
+ if( #(pending) == 0 ) then twipe(pending) end
+
+ HealComm.callbacks:Fire("HealComm_HealStopped", casterGUID, spellID, bitType, interrupted, unpack(tempPlayerList))
+end
+
+HealComm.parseHealEnd = parseHealEnd
+
+-- Heal delayed
+local function parseHealDelayed(casterGUID, startTime, endTime, spellName)
+ local pending = pendingHeals[casterGUID][spellName]
+ -- It's possible to get duplicate interrupted due to raid1 = party1, player = raid# etc etc, just block it here
+ if( pending.endTime == endTime and pending.startTime == startTime ) then return end
+
+ -- Casted heal
+ if( pending.bitType == DIRECT_HEALS ) then
+ pending.startTime = startTime
+ pending.endTime = endTime
+ -- Channel heal
+ elseif( pending.bitType == CHANNEL_HEALS ) then
+ pending.startTime = startTime
+ pending.endTime = endTime
+ pending.tickInterval = (pending.endTime - pending.startTime)
+ else
+ return
+ end
+
+ twipe(tempPlayerList)
+ for i=1, #(pending), 5 do
+ tinsert(tempPlayerList, pending[i])
+ end
+
+ HealComm.callbacks:Fire("HealComm_HealDelayed", casterGUID, pending.spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+end
+
+-- After checking around 150-200 messages in battlegrounds, server seems to always be passed (if they are from another server)
+-- Channels use tick total because the tick interval varies by haste
+-- Hots use tick interval because the total duration varies but the tick interval stays the same
+function HealComm:CHAT_MSG_ADDON(prefix, message, channel, sender)
+ if( prefix ~= COMM_PREFIX or channel ~= distribution or sender == playerName ) then return end
+
+ local commType, extraArg, spellID, arg1, arg2, arg3, arg4, arg5, arg6 = split(":", message)
+ local casterGUID = UnitGUID(sender)
+ spellID = tonumber(spellID)
+
+ if( not commType or not spellID or not casterGUID ) then return end
+
+ -- New direct heal - D::::target1,target2...
+ if( commType == "D" and arg1 and arg2 ) then
+ parseDirectHeal(casterGUID, spellID, tonumber(arg1), split(",", arg2))
+ -- New channel heal - C:::::target1,target2...
+ elseif( commType == "C" and arg1 and arg3 ) then
+ parseChannelHeal(casterGUID, spellID, tonumber(arg1), tonumber(arg2), split(",", arg3))
+ -- New hot with a "bomb" component - B::::target1,target2::::target1,target2...
+ elseif( commType == "B" and arg1 and arg6 ) then
+ parseHotHeal(casterGUID, false, spellID, tonumber(arg3), tonumber(extraArg), tonumber(arg5), split(",", arg6))
+ parseHotBomb(casterGUID, false, spellID, tonumber(arg1), split(",", arg2))
+ -- New hot - H::::::target1,target2...
+ elseif( commType == "H" and arg1 and arg4 ) then
+ parseHotHeal(casterGUID, false, spellID, tonumber(arg1), tonumber(extraArg), tonumber(arg3), split(",", arg4))
+ -- New updated heal somehow before ending - U:::::target1,target2...
+ elseif( commType == "U" and arg1 and arg3 ) then
+ parseHotHeal(casterGUID, true, spellID, tonumber(arg1), tonumber(extraArg), tonumber(arg2), split(",", arg3))
+ -- New variable tick hot - VH::::::target1,target2...
+ elseif( commType == "VH" and arg1 and arg4 ) then
+ parseHotHeal(casterGUID, false, spellID, arg1, tonumber(arg3), nil, split(",", arg4))
+ -- New updated variable tick hot - U:::amount1@amount2@amount3::target1,target2...
+ elseif( commType == "VU" and arg1 and arg3 ) then
+ parseHotHeal(casterGUID, true, spellID, arg1, tonumber(arg2), nil, split(",", arg3))
+ -- New updated bomb hot - UB::::target1,target2:::target1,target2...
+ elseif( commType == "UB" and arg1 and arg5 ) then
+ parseHotHeal(casterGUID, true, spellID, tonumber(arg3), tonumber(extraArg), tonumber(arg4), split(",", arg5))
+ parseHotBomb(casterGUID, true, spellID, tonumber(arg1), split(",", arg2))
+ -- Heal stopped - S::::target1,target2...
+ elseif( commType == "S" or commType == "HS" ) then
+ local interrupted = arg1 == "1" and true or false
+ local type = commType == "HS" and "id" or "name"
+
+ if( arg2 and arg2 ~= "" ) then
+ parseHealEnd(casterGUID, nil, type, spellID, interrupted, split(",", arg2))
+ else
+ parseHealEnd(casterGUID, nil, type, spellID, interrupted)
+ end
+ end
+end
+
+-- Bucketing reduces the number of events triggered for heals such as Tranquility that hit multiple targets
+-- instead of firing 5 events * ticks it will fire 1 (maybe 2 depending on lag) events
+HealComm.bucketHeals = HealComm.bucketHeals or {}
+local bucketHeals = HealComm.bucketHeals
+local BUCKET_FILLED = 0.30
+
+HealComm.bucketFrame = HealComm.bucketFrame or CreateFrame("Frame")
+HealComm.bucketFrame:Hide()
+
+HealComm.bucketFrame:SetScript("OnUpdate", function(self, elapsed)
+ local totalLeft = 0
+ for casterGUID, spells in pairs(bucketHeals) do
+ for id, data in pairs(spells) do
+ if( data.timeout ) then
+ data.timeout = data.timeout - elapsed
+
+ if( data.timeout <= 0 ) then
+ -- This shouldn't happen, on the offhand chance it does then don't bother sending an event
+ if( #(data) == 0 or not data.spellID or not data.spellName ) then
+ twipe(data)
+ -- We're doing a bucket for a tick heal like Tranquility or Wild Growth
+ elseif( data.type == "tick" ) then
+ local pending = pendingHeals[casterGUID] and ( pendingHeals[casterGUID][data.spellID] or pendingHeals[casterGUID][data.spellName] )
+ if( pending and pending.bitType ) then
+ local endTime = select(3, getRecord(pending, data[1]))
+ HealComm.callbacks:Fire("HealComm_HealUpdated", casterGUID, pending.spellID, pending.bitType, endTime, unpack(data))
+ end
+
+ twipe(data)
+ -- We're doing a bucket for a cast thats a multi-target heal like Wild Growth or Prayer of Healing
+ elseif( data.type == "heal" ) then
+ local type, amount, totalTicks, tickInterval, _, hasVariableTicks = CalculateHotHealing(data[1], data.spellID)
+ if( type ) then
+ local targets, amount = GetHealTargets(type, data[1], hasVariableTicks and amount or max(amount, 0), data.spellName, data, hasVariableTicks)
+ parseHotHeal(playerGUID, false, data.spellID, amount, totalTicks, tickInterval, split(",", targets))
+
+ if( not hasVariableTicks ) then
+ sendMessage(format("H:%d:%d:%d::%d:%s", totalTicks, data.spellID, amount, tickInterval, targets))
+ else
+ sendMessage(format("VH::%d:%s::%d:%s", data.spellID, amount, totalTicks, targets))
+ end
+ end
+
+ twipe(data)
+ end
+ else
+ totalLeft = totalLeft + 1
+ end
+ end
+ end
+ end
+
+ if( totalLeft <= 0 ) then
+ self:Hide()
+ end
+end)
+
+-- Monitor aura changes as well as new hots being cast
+local eventRegistered = {["SPELL_HEAL"] = true, ["SPELL_PERIODIC_HEAL"] = true}
+if( isHealerClass ) then
+ eventRegistered["SPELL_AURA_REMOVED"] = true
+ eventRegistered["SPELL_AURA_APPLIED"] = true
+ eventRegistered["SPELL_AURA_REFRESH"] = true
+ eventRegistered["SPELL_AURA_APPLIED_DOSE"] = true
+ eventRegistered["SPELL_AURA_REMOVED_DOSE"] = true
+end
+
+local COMBATLOG_OBJECT_AFFILIATION_MINE = COMBATLOG_OBJECT_AFFILIATION_MINE
+local CAN_HEAL = bor(COMBATLOG_OBJECT_REACTION_FRIENDLY, COMBATLOG_OBJECT_REACTION_NEUTRAL)
+function HealComm:COMBAT_LOG_EVENT_UNFILTERED(timestamp, eventType, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
+ if( not eventRegistered[eventType] ) then return end
+
+ -- Heal or hot ticked that the library is tracking
+ -- It's more efficient/accurate to have the library keep track of this locally, spamming the comm channel would not be a very good thing especially when a single player can have 4 - 8 hots/channels going on them.
+ if( eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL" ) then
+ local spellID, spellName, spellSchool = ...
+ local pending = sourceGUID and pendingHeals[sourceGUID] and (pendingHeals[sourceGUID][spellID] or pendingHeals[sourceGUID][spellName])
+ if( pending and pending[destGUID] and pending.bitType and band(pending.bitType, OVERTIME_HEALS) > 0 ) then
+ local amount, stack, endTime, ticksLeft = getRecord(pending, destGUID)
+ ticksLeft = ticksLeft - 1
+ endTime = GetTime() + pending.tickInterval * ticksLeft
+ if( pending.hasVariableTicks ) then tremove(amount, 1) end
+
+ updateRecord(pending, destGUID, amount, stack, endTime, ticksLeft)
+
+ if( pending.isMultiTarget ) then
+ bucketHeals[sourceGUID] = bucketHeals[sourceGUID] or {}
+ bucketHeals[sourceGUID][spellID] = bucketHeals[sourceGUID][spellID] or {}
+
+ local spellBucket = bucketHeals[sourceGUID][spellID]
+ if( not spellBucket[destGUID] ) then
+ spellBucket.timeout = BUCKET_FILLED
+ spellBucket.type = "tick"
+ spellBucket.spellName = spellName
+ spellBucket.spellID = spellID
+ spellBucket[destGUID] = true
+ tinsert(spellBucket, destGUID)
+
+ self.bucketFrame:Show()
+ end
+ else
+ HealComm.callbacks:Fire("HealComm_HealUpdated", sourceGUID, spellID, pending.bitType, endTime, destGUID)
+ end
+ end
+
+ -- New hot was applied
+ elseif( ( eventType == "SPELL_AURA_APPLIED" or eventType == "SPELL_AURA_REFRESH" or eventType == "SPELL_AURA_APPLIED_DOSE" ) and band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
+ local spellID, spellName, spellSchool, auraType = ...
+ if( hotData[spellName] ) then
+ -- Multi target heal so put it in the bucket
+ if( hotData[spellName].isMulti ) then
+ bucketHeals[sourceGUID] = bucketHeals[sourceGUID] or {}
+ bucketHeals[sourceGUID][spellName] = bucketHeals[sourceGUID][spellName] or {}
+
+ -- For some reason, Glyph of Prayer of Healing fires a SPELL_AURA_APPLIED then a SPELL_AURA_REFRESH right after
+ local spellBucket = bucketHeals[sourceGUID][spellName]
+ if( not spellBucket[destGUID] ) then
+ spellBucket.timeout = BUCKET_FILLED
+ spellBucket.type = "heal"
+ spellBucket.spellName = spellName
+ spellBucket.spellID = spellID
+ spellBucket[destGUID] = true
+ tinsert(spellBucket, destGUID)
+
+ self.bucketFrame:Show()
+ end
+ return
+ end
+
+ -- Single target so we can just send it off now thankfully
+ local type, amount, totalTicks, tickInterval, bombAmount, hasVariableTicks = CalculateHotHealing(destGUID, spellID)
+ if( type ) then
+ local targets, amount = GetHealTargets(type, destGUID, hasVariableTicks and amount or max(amount, 0), spellName, hasVariableTicks)
+ parseHotHeal(sourceGUID, false, spellID, amount, totalTicks, tickInterval, split(",", targets))
+
+ -- Hot with a bomb!
+ if( bombAmount ) then
+ local bombTargets, bombAmount = GetHealTargets(BOMB_HEALS, destGUID, max(bombAmount, 0), spellName)
+ parseHotBomb(sourceGUID, false, spellID, bombAmount, split(",", bombTargets))
+ sendMessage(format("B:%d:%d:%d:%s:%d::%d:%s", totalTicks, spellID, bombAmount, bombTargets, amount, tickInterval, targets))
+ elseif( hasVariableTicks ) then
+ sendMessage(format("VH::%d:%s::%d:%s", spellID, amount, totalTicks, targets))
+ -- Normal hot, nothing fancy
+ else
+ sendMessage(format("H:%d:%d:%d::%d:%s", totalTicks, spellID, amount, tickInterval, targets))
+ end
+ end
+ end
+ -- Single stack of a hot was removed, this only applies when going from 2 -> 1, when it goes from 1 -> 0 it fires SPELL_AURA_REMOVED
+ elseif( eventType == "SPELL_AURA_REMOVED_DOSE" and band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
+ local spellID, spellName, spellSchool, auraType, stacks = ...
+ local pending = sourceGUID and pendingHeals[sourceGUID] and pendingHeals[sourceGUID][spellID]
+ if( pending and pending.bitType ) then
+ local amount = getRecord(pending, destGUID)
+ if( amount ) then
+ parseHotHeal(sourceGUID, true, spellID, amount, pending.totalTicks, pending.tickInterval, compressGUID[destGUID])
+
+ -- Plant the bomb
+ local bombPending = pending.hasBomb and pendingHeals[sourceGUID][spellName]
+ if( bombPending and bombPending.bitType ) then
+ local bombAmount = getRecord(bombPending, destGUID)
+ if( bombAmount ) then
+ parseHotBomb(sourceGUID, true, spellID, bombAmount, compressGUID[destGUID])
+
+ sendMessage(format("UB:%s:%d:%d:%s:%d:%d:%s", pending.totalTicks, spellID, bombAmount, compressGUID[destGUID], amount, pending.tickInterval, compressGUID[destGUID]))
+ return
+ end
+ end
+
+ -- Failed to find any sort of bomb-y info we needed or it doesn't have a bomb anyway
+ if( pending.hasVariableTicks ) then
+ sendMessage(format("VU::%d:%s:%d:%s", spellID, amount, pending.totalTicks, compressGUID[destGUID]))
+ else
+ sendMessage(format("U:%s:%d:%d:%d:%s", spellID, amount, pending.totalTicks, pending.tickInterval, compressGUID[destGUID]))
+ end
+ end
+ end
+
+ -- Aura faded
+ elseif( eventType == "SPELL_AURA_REMOVED" ) then
+ local spellID, spellName, spellSchool, auraType = ...
+
+ -- Hot faded that we cast
+ if( hotData[spellName] and band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
+ parseHealEnd(sourceGUID, nil, "id", spellID, false, compressGUID[destGUID])
+ sendMessage(format("HS::%d::%s", spellID, compressGUID[destGUID]))
+ end
+ end
+end
+
+-- Spell cast magic
+-- When auto self cast is on, the UNIT_SPELLCAST_SENT event will always come first followed by the funciton calls
+-- Otherwise either SENT comes first then function calls, or some function calls then SENT then more function calls
+local castTarget, castID, mouseoverGUID, mouseoverName, hadTargetingCursor, lastSentID, lastTargetGUID, lastTargetName
+local lastFriendlyGUID, lastFriendlyName, lastGUID, lastName, lastIsFriend
+local castGUIDs, guidPriorities = {}, {}
+
+-- Deals with the fact that functions are called differently
+-- Why a table when you can only cast one spell at a time you ask? When you factor in lag and mash clicking it's possible to:
+-- cast A, interrupt it, cast B and have A fire SUCEEDED before B does, the tables keeps it from bugging out
+local function setCastData(priority, name, guid)
+ if( not guid or not lastSentID ) then return end
+ if( guidPriorities[lastSentID] and guidPriorities[lastSentID] >= priority ) then return end
+
+ -- This is meant as a way of locking a cast in because which function has accurate data can be called into question at times, one of them always does though
+ -- this means that as soon as it finds a name match it locks the GUID in until another SENT is fired. Technically it's possible to get a bad GUID but it first requires
+ -- the functions to return different data and it requires the messed up call to be for another name conflict.
+ if( castTarget and castTarget == name ) then priority = 99 end
+
+ castGUIDs[lastSentID] = guid
+ guidPriorities[lastSentID] = priority
+end
+
+-- When the game tries to figure out the UnitID from the name it will prioritize players over non-players
+-- if there are conflicts in names it will pull the one with the least amount of current health
+function HealComm:UNIT_SPELLCAST_SENT(unit, spellName, spellRank, castOn)
+ if( unit ~= "player" or not spellData[spellName] or not averageHeal[spellName][spellRank] ) then return end
+
+ castTarget = gsub(castOn, "(.-)%-(.*)$", "%1")
+ lastSentID = spellName .. spellRank
+
+ -- Self cast is off which means it's possible to have a spell waiting for a target.
+ -- It's possible that it's the mouseover unit, but if a Target, TargetLast or AssistUnit call comes right after it means it's casting on that instead instead.
+ if( hadTargetingCursor ) then
+ hadTargetingCursor = nil
+ self.resetFrame:Show()
+
+ guidPriorities[lastSentID] = nil
+ setCastData(5, mouseoverName, mouseoverGUID)
+ else
+ -- If the player is ungrouped and healing, you can't take advantage of the name -> "unit" map, look in the UnitIDs that would most likely contain the information that's needed.
+ local guid = UnitGUID(castOn)
+ if( not guid ) then
+ guid = UnitName("target") == castTarget and UnitGUID("target") or UnitName("focus") == castTarget and UnitGUID("focus") or UnitName("mouseover") == castTarget and UnitGUID("mouseover") or UnitName("targettarget") == castTarget and UnitGUID("target") or UnitName("focustarget") == castTarget and UnitGUID("focustarget")
+ end
+
+ guidPriorities[lastSentID] = nil
+ setCastData(0, nil, guid)
+ end
+end
+
+function HealComm:UNIT_SPELLCAST_START(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or not averageHeal[spellName][spellRank] or UnitIsCharmed("player") or not UnitPlayerControlled("player") ) then return end
+ local nameID = spellName .. spellRank
+ local castGUID = castGUIDs[nameID]
+ if( not castGUID ) then
+ return
+ end
+
+ castID = id
+
+ -- Figure out who we are healing and for how much
+ local healType, amount, ticks, localTicks = CalculateHealing(castGUID, spellName, spellRank)
+ local targets, amount = GetHealTargets(healType, castGUID, max(amount, 0), spellName)
+
+ if( healType == DIRECT_HEALS ) then
+ parseDirectHeal(playerGUID, self.spellToID[nameID], amount, split(",", targets))
+ sendMessage(format("D::%d:%d:%s", self.spellToID[nameID] or 0, amount or "", targets))
+ elseif( healType == CHANNEL_HEALS ) then
+ parseChannelHeal(playerGUID, self.spellToID[nameID], amount, localTicks, split(",", targets))
+ sendMessage(format("C::%d:%d:%s:%s", self.spellToID[nameID] or 0, amount, ticks, targets))
+ end
+end
+
+HealComm.UNIT_SPELLCAST_CHANNEL_START = HealComm.UNIT_SPELLCAST_START
+
+function HealComm:UNIT_SPELLCAST_SUCCEEDED(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or id ~= castID or id == 0 ) then return end
+ local nameID = spellName .. spellRank
+
+ castID = nil
+ parseHealEnd(playerGUID, nil, "name", self.spellToID[nameID], false)
+ sendMessage(format("S::%d:0", self.spellToID[nameID] or 0))
+end
+
+function HealComm:UNIT_SPELLCAST_STOP(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or id ~= castID ) then return end
+ local nameID = spellName .. spellRank
+
+ castID = nil
+ parseHealEnd(playerGUID, nil, "name", self.spellToID[nameID], true)
+ sendMessage(format("S::%d:1", self.spellToID[nameID] or 0))
+end
+
+function HealComm:UNIT_SPELLCAST_CHANNEL_STOP(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or id ~= castID ) then return end
+ local nameID = spellName .. spellRank
+
+ castID = nil
+ parseHealEnd(playerGUID, nil, "name", self.spellToID[nameID], false)
+ sendMessage(format("S::%d:0", self.spellToID[nameID] or 0))
+end
+
+-- Cast didn't go through, recheck any charge data if necessary
+function HealComm:UNIT_SPELLCAST_INTERRUPTED(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or castID ~= id ) then return end
+
+ local guid = castGUIDs[spellName .. spellRank]
+ if( guid ) then
+ ResetChargeData(guid, spellName, spellRank)
+ end
+end
+
+-- It's faster to do heal delays locally rather than through syncing, as it only has to go from WoW -> Player instead of Caster -> WoW -> Player
+function HealComm:UNIT_SPELLCAST_DELAYED(unit, spellName, spellRank, id)
+ local casterGUID = UnitGUID(unit)
+ if( unit == "focus" or unit == "target" or not pendingHeals[casterGUID] or not pendingHeals[casterGUID][spellName] ) then return end
+
+ -- Direct heal delayed
+ if( pendingHeals[casterGUID][spellName].bitType == DIRECT_HEALS ) then
+ local startTime, endTime = select(5, UnitCastingInfo(unit))
+ if( startTime and endTime ) then
+ parseHealDelayed(casterGUID, startTime / 1000, endTime / 1000, spellName)
+ end
+ -- Channel heal delayed
+ elseif( pendingHeals[casterGUID][spellName].bitType == CHANNEL_HEALS ) then
+ local startTime, endTime = select(5, UnitChannelInfo(unit))
+ if( startTime and endTime ) then
+ parseHealDelayed(casterGUID, startTime / 1000, endTime / 1000, spellName)
+ end
+ end
+end
+
+HealComm.UNIT_SPELLCAST_CHANNEL_UPDATE = HealComm.UNIT_SPELLCAST_DELAYED
+
+-- Need to keep track of mouseover as it can change in the split second after/before casts
+function HealComm:UPDATE_MOUSEOVER_UNIT()
+ mouseoverGUID = UnitCanAssist("player", "mouseover") and UnitGUID("mouseover")
+ mouseoverName = UnitCanAssist("player", "mouseover") and UnitName("mouseover")
+end
+
+-- Keep track of our last target/friendly target for the sake of /targetlast and /targetlastfriend
+function HealComm:PLAYER_TARGET_CHANGED()
+ if( lastGUID and lastName ) then
+ if( lastIsFriend ) then
+ lastFriendlyGUID, lastFriendlyName = lastGUID, lastName
+ end
+
+ lastTargetGUID, lastTargetName = lastGUID, lastName
+ end
+
+ -- Despite the fact that it's called target last friend, UnitIsFriend won't actually work
+ lastGUID = UnitGUID("target")
+ lastName = UnitName("target")
+ lastIsFriend = UnitCanAssist("player", "target")
+end
+
+-- Unit was targeted through a function
+function HealComm:Target(unit)
+ if( self.resetFrame:IsShown() and UnitCanAssist("player", unit) ) then
+ setCastData(6, UnitName(unit), UnitGUID(unit))
+ end
+
+ self.resetFrame:Hide()
+ hadTargetingCursor = nil
+end
+
+-- This is only needed when auto self cast is off, in which case this is called right after UNIT_SPELLCAST_SENT
+-- because the player got a waiting-for-cast icon up and they pressed a key binding to target someone
+HealComm.TargetUnit = HealComm.Target
+
+-- Works the same as the above except it's called when you have a cursor icon and you click on a secure frame with a target attribute set
+HealComm.SpellTargetUnit = HealComm.Target
+
+-- Used in /assist macros
+function HealComm:AssistUnit(unit)
+ if( self.resetFrame:IsShown() and UnitCanAssist("player", unit .. "target") ) then
+ setCastData(6, UnitName(unit .. "target"), UnitGUID(unit .. "target"))
+ end
+
+ self.resetFrame:Hide()
+ hadTargetingCursor = nil
+end
+
+-- Target last was used, the only reason this is called with reset frame being shown is we're casting on a valid unit
+-- don't have to worry about the GUID no longer being invalid etc
+function HealComm:TargetLast(guid, name)
+ if( name and guid and self.resetFrame:IsShown() ) then
+ setCastData(6, name, guid)
+ end
+
+ self.resetFrame:Hide()
+ hadTargetingCursor = nil
+end
+
+function HealComm:TargetLastFriend()
+ self:TargetLast(lastFriendlyGUID, lastFriendlyName)
+end
+
+function HealComm:TargetLastTarget()
+ self:TargetLast(lastTargetGUID, lastTargetName)
+end
+
+-- Spell was cast somehow
+function HealComm:CastSpell(arg, unit)
+ -- If the spell is waiting for a target and it's a spell action button then we know that the GUID has to be mouseover or a key binding cast.
+ if( unit and UnitCanAssist("player", unit) ) then
+ setCastData(4, UnitName(unit), UnitGUID(unit))
+ -- No unit, or it's a unit we can't assist
+ elseif( not SpellIsTargeting() ) then
+ if( UnitCanAssist("player", "target") ) then
+ setCastData(4, UnitName("target"), UnitGUID("target"))
+ else
+ setCastData(4, playerName, playerGUID)
+ end
+
+ hadTargetingCursor = nil
+ else
+ hadTargetingCursor = true
+ end
+end
+
+HealComm.CastSpellByName = HealComm.CastSpell
+HealComm.CastSpellByID = HealComm.CastSpell
+HealComm.UseAction = HealComm.CastSpell
+
+-- Make sure we don't have invalid units in this
+local function sanityCheckMapping()
+ for guid, unit in pairs(guidToUnit) do
+ -- Unit no longer exists, remove all healing for them
+ if( not UnitExists(unit) ) then
+ -- Check for (and remove) any active heals
+ if( pendingHeals[guid] ) then
+ for id, pending in pairs(pendingHeals[guid]) do
+ if( pending.bitType ) then
+ parseHealEnd(guid, pending, nil, pending.spellID, true)
+ end
+ end
+
+ pendingHeals[guid] = nil
+ end
+
+ -- Remove any heals that are on them
+ removeAllRecords(guid)
+
+ guidToUnit[guid] = nil
+ guidToGroup[guid] = nil
+ end
+ end
+end
+
+-- 5s poll that tries to solve the problem of X running out of range while a HoT is ticking
+-- this is not really perfect far from it in fact. If I can find a better solution I will switch to that.
+if( not HealComm.hotMonitor ) then
+ HealComm.hotMonitor = CreateFrame("Frame")
+ HealComm.hotMonitor:Hide()
+ HealComm.hotMonitor.timeElapsed = 0
+ HealComm.hotMonitor:SetScript("OnUpdate", function(self, elapsed)
+ self.timeElapsed = self.timeElapsed + elapsed
+ if( self.timeElapsed < 5 ) then return end
+ self.timeElapsed = self.timeElapsed - 5
+
+ -- For the time being, it will only remove them if they don't exist and it found a valid unit
+ -- units that leave the raid are automatically removed
+ local found
+ for guid in pairs(activeHots) do
+ if( guidToUnit[guid] and not UnitIsVisible(guidToUnit[guid]) ) then
+ removeAllRecords(guid)
+ else
+ found = true
+ end
+ end
+
+ if( not found ) then
+ self:Hide()
+ end
+ end)
+end
+
+-- After the player leaves a group, tables are wiped out or released for GC
+local wasInParty, wasInRaid
+local function clearGUIDData()
+ clearPendingHeals()
+
+ twipe(compressGUID)
+ twipe(decompressGUID)
+ twipe(activePets)
+
+ playerGUID = playerGUID or UnitGUID("player")
+ HealComm.guidToUnit = {[playerGUID] = "player"}
+ guidToUnit = HealComm.guidToUnit
+
+ HealComm.guidToGroup = {}
+ guidToGroup = HealComm.guidToGroup
+
+ HealComm.activeHots = {}
+ activeHots = HealComm.activeHots
+
+ HealComm.pendingHeals = {}
+ pendingHeals = HealComm.pendingHeals
+
+ HealComm.bucketHeals = {}
+ bucketHeals = HealComm.bucketHeals
+
+ wasInParty, wasInRaid = nil, nil
+end
+
+-- Keeps track of pet GUIDs, as pets are considered vehicles this will also map vehicle GUIDs to unit
+function HealComm:UNIT_PET(unit)
+ local pet = self.unitToPet[unit]
+ local guid = pet and UnitGUID(pet)
+
+ -- We have an active pet guid from this user and it's different, kill it
+ local activeGUID = activePets[unit]
+ if( activeGUID and activeGUID ~= guid ) then
+ removeAllRecords(activeGUID)
+
+ guidToUnit[activeGUID] = nil
+ guidToGroup[activeGUID] = nil
+ activePets[unit] = nil
+ end
+
+ -- Add the new record
+ if( guid ) then
+ guidToUnit[guid] = pet
+ guidToGroup[guid] = guidToGroup[UnitGUID(unit)]
+ activePets[unit] = guid
+ end
+end
+
+-- Keep track of party GUIDs, ignored in raids as RRU will handle that mapping
+function HealComm:PARTY_MEMBERS_CHANGED()
+ if( GetNumRaidMembers() > 0 ) then return end
+ updateDistributionChannel()
+
+ if( GetNumPartyMembers() == 0 ) then
+ if( wasInParty ) then
+ clearGUIDData()
+ end
+ return
+ end
+
+ -- Parties are not considered groups in terms of API, so fake it and pretend they are all in group 0
+ guidToGroup[playerGUID or UnitGUID("player")] = 0
+ if( not wasInParty ) then self:UNIT_PET("player") end
+
+ for i=1, MAX_PARTY_MEMBERS do
+ local unit = "party" .. i
+ if( UnitExists(unit) ) then
+ local guid = UnitGUID(unit)
+ local lastGroup = guidToGroup[guid]
+ guidToUnit[guid] = unit
+ guidToGroup[guid] = 0
+
+ if( not wasInParty or lastGroup ~= guidToGroup[guid] ) then
+ self:UNIT_PET(unit)
+ end
+ end
+ end
+
+ sanityCheckMapping()
+ wasInParty = true
+end
+
+-- Keep track of raid GUIDs
+function HealComm:RAID_ROSTER_UPDATE()
+ updateDistributionChannel()
+
+ -- Left raid, clear any cache we had
+ if( GetNumRaidMembers() == 0 ) then
+ if( wasInRaid ) then
+ clearGUIDData()
+ end
+ return
+ end
+
+ -- Add new members
+ for i=1, MAX_RAID_MEMBERS do
+ local unit = "raid" .. i
+ if( UnitExists(unit) ) then
+ local guid = UnitGUID(unit)
+ local lastGroup = guidToGroup[guid]
+ guidToUnit[guid] = unit
+ guidToGroup[guid] = select(3, GetRaidRosterInfo(i))
+
+ -- If the pets owners group changed then the pets group should be updated too
+ if( not wasInRaid or guidToGroup[guid] ~= lastGroup ) then
+ self:UNIT_PET(unit)
+ end
+ end
+ end
+
+ sanityCheckMapping()
+ wasInRaid = true
+end
+
+-- PLAYER_ALIVE = got talent data
+function HealComm:PLAYER_ALIVE()
+ self:PLAYER_TALENT_UPDATE()
+ self.eventFrame:UnregisterEvent("PLAYER_ALIVE")
+end
+
+-- Initialize the library
+function HealComm:OnInitialize()
+ -- If another instance already loaded then the tables should be wiped to prevent old data from persisting
+ -- in case of a spell being removed later on, only can happen if a newer LoD version is loaded
+ twipe(spellData)
+ twipe(hotData)
+ twipe(itemSetsData)
+ twipe(talentData)
+ twipe(averageHeal)
+
+ -- Load all of the classes formulas and such
+ LoadClassData()
+
+ -- Setup the metatables for average healing
+ for spell in pairs(spellData) do
+ averageHeal[spell] = setmetatable({spell = spell}, self.averageHealMT)
+ end
+
+ -- Cache glyphs initially
+ for id=1, GetNumGlyphSockets() do
+ local enabled, _, glyphID = GetGlyphSocketInfo(id)
+ if( enabled and glyphID ) then
+ glyphCache[glyphID] = true
+ glyphCache[id] = glyphID
+ end
+ end
+
+ self:PLAYER_EQUIPMENT_CHANGED()
+
+ -- When first logging in talent data isn't available until at least PLAYER_ALIVE, so if we don't have data
+ -- will wait for that event otherwise will just cache it right now
+ if( GetNumTalentTabs() == 0 ) then
+ self.eventFrame:RegisterEvent("PLAYER_ALIVE")
+ else
+ self:PLAYER_TALENT_UPDATE()
+ end
+
+ if( ResetChargeData ) then
+ HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
+ end
+
+ -- Finally, register it all
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_SENT")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_START")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_STOP")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_DELAYED")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
+ self.eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ self.eventFrame:RegisterEvent("PLAYER_TALENT_UPDATE")
+ self.eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+ self.eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
+ self.eventFrame:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
+ self.eventFrame:RegisterEvent("PLAYER_LEVEL_UP")
+ self.eventFrame:RegisterEvent("GLYPH_ADDED")
+ self.eventFrame:RegisterEvent("GLYPH_REMOVED")
+ self.eventFrame:RegisterEvent("GLYPH_UPDATED")
+ self.eventFrame:RegisterEvent("UNIT_AURA")
+
+ if( self.initialized ) then return end
+ self.initialized = true
+
+ self.resetFrame = CreateFrame("Frame")
+ self.resetFrame:Hide()
+ self.resetFrame:SetScript("OnUpdate", function(self) self:Hide() end)
+
+ -- You can't unhook secure hooks after they are done, so will hook once and the HealComm table will update with the latest functions
+ -- automagically. If a new function is ever used it'll need a specific variable to indicate those set of hooks.
+ -- By default most of these are mapped to a more generic function, but I call separate ones so I don't have to rehook
+ -- if it turns out I need to know something specific
+ hooksecurefunc("TargetUnit", function(...) HealComm:TargetUnit(...) end)
+ hooksecurefunc("SpellTargetUnit", function(...) HealComm:SpellTargetUnit(...) end)
+ hooksecurefunc("AssistUnit", function(...) HealComm:AssistUnit(...) end)
+ hooksecurefunc("UseAction", function(...) HealComm:UseAction(...) end)
+ hooksecurefunc("TargetLastFriend", function(...) HealComm:TargetLastFriend(...) end)
+ hooksecurefunc("TargetLastTarget", function(...) HealComm:TargetLastTarget(...) end)
+ hooksecurefunc("CastSpellByName", function(...) HealComm:CastSpellByName(...) end)
+
+ -- Fixes hook error for people who are not on 3.2 yet
+ if( CastSpellByID ) then
+ hooksecurefunc("CastSpellByID", function(...) HealComm:CastSpellByID(...) end)
+ end
+end
+
+-- General event handler
+local function OnEvent(self, event, ...)
+ HealComm[event](HealComm, ...)
+end
+
+-- Event handler
+HealComm.eventFrame = HealComm.frame or HealComm.eventFrame or CreateFrame("Frame")
+HealComm.eventFrame:UnregisterAllEvents()
+HealComm.eventFrame:RegisterEvent("UNIT_PET")
+HealComm.eventFrame:SetScript("OnEvent", OnEvent)
+HealComm.frame = nil
+
+-- At PLAYER_LEAVING_WORLD (Actually more like MIRROR_TIMER_STOP but anyway) UnitGUID("player") returns nil, delay registering
+-- events and set a playerGUID/playerName combo for all players on PLAYER_LOGIN not just the healers.
+function HealComm:PLAYER_LOGIN()
+ playerGUID = UnitGUID("player")
+ playerName = UnitName("player")
+ playerLevel = UnitLevel("player")
+
+ -- Oddly enough player GUID is not available on file load, so keep the map of player GUID to themselves too
+ guidToUnit[playerGUID] = "player"
+
+ if( isHealerClass ) then
+ self:OnInitialize()
+ LoadRaceData()
+ elseif LoadRaceData then
+ twipe(hotData)
+ eventRegistered["SPELL_AURA_APPLIED"] = true
+ self.eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ LoadRaceData()
+ end
+
+ self.eventFrame:UnregisterEvent("PLAYER_LOGIN")
+ self.eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
+ self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
+ self.eventFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
+ self.eventFrame:RegisterEvent("RAID_ROSTER_UPDATE")
+
+ self:ZONE_CHANGED_NEW_AREA()
+ self:RAID_ROSTER_UPDATE()
+ self:PARTY_MEMBERS_CHANGED()
+end
+
+if( not IsLoggedIn() ) then
+ HealComm.eventFrame:RegisterEvent("PLAYER_LOGIN")
+else
+ HealComm:PLAYER_LOGIN()
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/LibHealComm-4.0.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/LibHealComm-4.0.xml
new file mode 100644
index 0000000..db6cfd2
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/LibHealComm-4.0/LibHealComm-4.0.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Load.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Load.xml
new file mode 100644
index 0000000..fb16f62
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Lib/Load.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Load.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Load.xml
new file mode 100644
index 0000000..30f73e4
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Load.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Backdrop.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Backdrop.lua
new file mode 100644
index 0000000..1a68169
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Backdrop.lua
@@ -0,0 +1,455 @@
+BackdropTemplateMixin = BackdropTemplateMixin or {}
+
+BACKDROP_TOOLTIP_8_8_1111 = {
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 8,
+ edgeSize = 8,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 },
+};
+BACKDROP_TOOLTIP_8_12_1111 = {
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 8,
+ edgeSize = 12,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 },
+};
+BACKDROP_TOOLTIP_16_16_5555 = {
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 16,
+ edgeSize = 16,
+ insets = { left = 5, right = 5, top = 5, bottom = 5 },
+};
+BACKDROP_TOOLTIP_12_12_4444 = {
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 12,
+ edgeSize = 12,
+ insets = { left = 4, right = 4, top = 4, bottom = 4 },
+};
+BACKDROP_TOOLTIP_0_16 = {
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ edgeSize = 16,
+ tileEdge = true,
+};
+BACKDROP_TOOLTIP_0_12_0055 = {
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ edgeSize = 12,
+ tileEdge = true,
+ insets = { left = 0, right = 0, top = 5, bottom = 5 },
+};
+BACKDROP_TOOLTIP_0_16_5555 = {
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ edgeSize = 16,
+ tileEdge = true,
+ insets = { left = 5, right = 5, top = 5, bottom = 5 },
+};
+BACKDROP_ACHIEVEMENTS_0_64 = {
+ edgeFile = "Interface\\AchievementFrame\\UI-Achievement-WoodBorder",
+ edgeSize = 64,
+ tileEdge = true,
+};
+BACKDROP_ARENA_32_32 = {
+ bgFile = "Interface\\CharacterFrame\\UI-Party-Background",
+ edgeFile = "Interface\\ArenaEnemyFrame\\UI-Arena-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 32,
+ edgeSize = 32,
+ insets = { left = 32, right = 32, top = 32, bottom = 32 },
+};
+BACKDROP_DIALOG_32_32 = {
+ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
+ edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 32,
+ edgeSize = 32,
+ insets = { left = 11, right = 12, top = 12, bottom = 11 },
+};
+BACKDROP_GOLD_DIALOG_32_32 = {
+ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
+ edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 32,
+ edgeSize = 32,
+ insets = { left = 11, right = 12, top = 12, bottom = 11 },
+};
+BACKDROP_WATERMARK_DIALOG_0_16 = {
+ edgeFile = "Interface\\DialogFrame\\UI-DialogBox-TestWatermark-Border",
+ tileEdge = true,
+ edgeSize = 16,
+};
+BACKDROP_SLIDER_8_8 = {
+ bgFile = "Interface\\Buttons\\UI-SliderBar-Background",
+ edgeFile = "Interface\\Buttons\\UI-SliderBar-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 8,
+ edgeSize = 8,
+ insets = { left = 3, right = 3, top = 6, bottom = 6 },
+};
+BACKDROP_PARTY_32_32 = {
+ bgFile = "Interface\\CharacterFrame\\UI-Party-Background",
+ edgeFile = "Interface\\CharacterFrame\\UI-Party-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 32,
+ edgeSize = 32,
+ insets = { left = 32, right = 32, top = 32, bottom = 32 },
+};
+BACKDROP_TOAST_12_12 = {
+ bgFile = "Interface\\FriendsFrame\\UI-Toast-Background",
+ edgeFile = "Interface\\FriendsFrame\\UI-Toast-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 12,
+ edgeSize = 12,
+ insets = { left = 5, right = 5, top = 5, bottom = 5 },
+};
+BACKDROP_CALLOUT_GLOW_0_16 = {
+ edgeFile = "Interface\\TutorialFrame\\UI-TutorialFrame-CalloutGlow",
+ edgeSize = 16,
+ tileEdge = true,
+};
+BACKDROP_CALLOUT_GLOW_0_20 = {
+ edgeFile = "Interface\\TutorialFrame\\UI-TutorialFrame-CalloutGlow",
+ edgeSize = 20,
+ tileEdge = true,
+};
+BACKDROP_GLUE_TOOLTIP_16_16 = {
+ bgFile = "Interface\\Glues\\Common\\Glue-Tooltip-Background",
+ edgeFile = "Interface\\Glues\\Common\\Glue-Tooltip-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 16,
+ edgeSize = 16,
+ insets = { left = 10, right = 5, top = 4, bottom = 9 },
+};
+BACKDROP_GLUE_TOOLTIP_0_16 = {
+ edgeFile = "Interface\\Glues\\Common\\Glue-Tooltip-Border",
+ tileEdge = true,
+ edgeSize = 16,
+};
+BACKDROP_MIXED_TOOLTIP_16_16 = {
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
+ edgeFile = "Interface\\Glues\\Common\\Glue-Tooltip-Border",
+ tile = true,
+ tileEdge = true,
+ tileSize = 16,
+ edgeSize = 16,
+ insets = { left = 10, right = 5, top = 4, bottom = 9 },
+};
+BACKDROP_TEXT_PANEL_0_16 = {
+ edgeFile = "Interface\\Glues\\Common\\TextPanel-Border",
+ tileEdge = true,
+ edgeSize = 16,
+};
+
+local coordStart = 0.0625;
+local coordEnd = 1 - coordStart;
+
+local textureUVs = { -- keys have to match pieceNames in nineSliceSetup table
+ TopLeftCorner = { setWidth = true, setHeight = true, ULx = 0.5078125, ULy = coordStart, LLx = 0.5078125, LLy = coordEnd, URx = 0.6171875, URy = coordStart, LRx = 0.6171875, LRy = coordEnd },
+ TopRightCorner = { setWidth = true, setHeight = true, ULx = 0.6328125, ULy = coordStart, LLx = 0.6328125, LLy = coordEnd, URx = 0.7421875, URy = coordStart, LRx = 0.7421875, LRy = coordEnd },
+ BottomLeftCorner = { setWidth = true, setHeight = true, ULx = 0.7578125, ULy = coordStart, LLx = 0.7578125, LLy = coordEnd, URx = 0.8671875, URy = coordStart, LRx = 0.8671875, LRy = coordEnd },
+ BottomRightCorner = { setWidth = true, setHeight = true, ULx = 0.8828125, ULy = coordStart, LLx = 0.8828125, LLy = coordEnd, URx = 0.9921875, URy = coordStart, LRx = 0.9921875, LRy = coordEnd },
+ TopEdge = { setHeight = true, ULx = 0.2578125, ULy = "repeatX", LLx = 0.3671875, LLy = "repeatX", URx = 0.2578125, URy = coordStart, LRx = 0.3671875, LRy = coordStart },
+ BottomEdge = { setHeight = true, ULx = 0.3828125, ULy = "repeatX", LLx = 0.4921875, LLy = "repeatX", URx = 0.3828125, URy = coordStart, LRx = 0.4921875, LRy = coordStart },
+ LeftEdge = { setWidth = true, ULx = 0.0078125, ULy = coordStart, LLx = 0.0078125, LLy = "repeatY", URx = 0.1171875, URy = coordStart, LRx = 0.1171875, LRy = "repeatY" },
+ RightEdge = { setWidth = true, ULx = 0.1328125, ULy = coordStart, LLx = 0.1328125, LLy = "repeatY", URx = 0.2421875, URy = coordStart, LRx = 0.2421875, LRy = "repeatY" },
+ Center = { ULx = 0, ULy = 0, LLx = 0, LLy = "repeatY", URx = "repeatX", URy = 0, LRx = "repeatX", LRy = "repeatY" },
+};
+
+local defaultEdgeSize = 39; -- the old default
+
+function BackdropTemplateMixin:OnBackdropLoaded()
+ if self.backdropInfo then
+ -- check for invalid info
+ if not self.backdropInfo.edgeFile and not self.backdropInfo.bgFile then
+ self.backdropInfo = nil;
+ return;
+ end
+ self:ApplyBackdrop();
+ do
+ if self.backdropColor then
+ local r, g, b = 1, 1, 1;
+ r, g, b = self.backdropColor:GetRGB();
+ local a = self.backdropColorAlpha or 1;
+ self:SetBackdropColor(r, g, b, a);
+ end
+ end
+ do
+ if self.backdropBorderColor then
+ local r, g, b = 1, 1, 1;
+ r, g, b = self.backdropBorderColor:GetRGB();
+ local a = self.backdropBorderColorAlpha or 1;
+ self:SetBackdropBorderColor(r, g, b, a);
+ end
+ end
+ if self.backdropBorderBlendMode then
+ self:SetBorderBlendMode(self.backdropBorderBlendMode);
+ end
+ end
+end
+
+function BackdropTemplateMixin:OnBackdropSizeChanged()
+ if self.backdropInfo then
+ self:SetupTextureCoordinates();
+ end
+end
+
+function BackdropTemplateMixin:GetEdgeSize()
+ if self.backdropInfo.edgeSize and self.backdropInfo.edgeSize > 0 then
+ return self.backdropInfo.edgeSize;
+ else
+ return defaultEdgeSize;
+ end
+end
+
+local function GetBackdropCoordValue(coord, pieceSetup, repeatX, repeatY)
+ local value = pieceSetup[coord];
+ if value == "repeatX" then
+ return repeatX;
+ elseif value == "repeatY" then
+ return repeatY;
+ else
+ return value;
+ end
+end
+
+local function SetupBackdropTextureCoordinates(region, pieceSetup, repeatX, repeatY)
+ region:SetTexCoord( GetBackdropCoordValue("ULx", pieceSetup, repeatX, repeatY), GetBackdropCoordValue("ULy", pieceSetup, repeatX, repeatY),
+ GetBackdropCoordValue("LLx", pieceSetup, repeatX, repeatY), GetBackdropCoordValue("LLy", pieceSetup, repeatX, repeatY),
+ GetBackdropCoordValue("URx", pieceSetup, repeatX, repeatY), GetBackdropCoordValue("URy", pieceSetup, repeatX, repeatY),
+ GetBackdropCoordValue("LRx", pieceSetup, repeatX, repeatY), GetBackdropCoordValue("LRy", pieceSetup, repeatX, repeatY));
+end
+
+function BackdropTemplateMixin:SetupTextureCoordinates()
+ local width = self:GetWidth();
+ local height = self:GetHeight();
+ local effectiveScale = self:GetEffectiveScale();
+ local edgeSize = self:GetEdgeSize();
+ local edgeRepeatX = max(0, (width / edgeSize) * effectiveScale - 2 - coordStart);
+ local edgeRepeatY = max(0, (height / edgeSize) * effectiveScale - 2 - coordStart);
+ for pieceName, pieceSetup in pairs(textureUVs) do
+ local region = self[pieceName];
+ if region then
+ if pieceName == "Center" then
+ local repeatX = 1;
+ local repeatY = 1;
+ if self.backdropInfo.tile then
+ local divisor = self.backdropInfo.tileSize;
+ if not divisor or divisor == 0 then
+ divisor = edgeSize;
+ end
+ if divisor ~= 0 then
+ repeatX = (width / divisor) * effectiveScale;
+ repeatY = (height / divisor) * effectiveScale;
+ end
+ end
+ SetupBackdropTextureCoordinates(region, pieceSetup, repeatX, repeatY);
+ else
+ SetupBackdropTextureCoordinates(region, pieceSetup, edgeRepeatX, edgeRepeatY);
+ end
+ end
+ end
+end
+
+function BackdropTemplateMixin:SetupPieceVisuals(piece, setupInfo, pieceLayout)
+ local textureInfo = textureUVs[setupInfo.pieceName];
+ local tileVerts = false;
+ local file;
+ if setupInfo.pieceName == "Center" then
+ file = self.backdropInfo.bgFile;
+ tileVerts = self.backdropInfo.tile;
+ else
+ if self.backdropInfo.tileEdge ~= false then
+ tileVerts = true;
+ end
+ file = self.backdropInfo.edgeFile;
+ end
+ piece:SetTexture(file, tileVerts, tileVerts);
+ local cornerWidth = textureInfo.setWidth and self:GetEdgeSize() or 0;
+ local cornerHeight = textureInfo.setHeight and self:GetEdgeSize() or 0;
+ piece:SetSize(cornerWidth, cornerHeight);
+end
+
+function BackdropTemplateMixin:SetBorderBlendMode(blendMode)
+ if not self.backdropInfo then
+ return;
+ end
+ for pieceName in pairs(textureUVs) do
+ local region = self[pieceName];
+ if region and pieceName ~= "Center" then
+ region:SetBlendMode(blendMode);
+ end
+ end
+end
+
+function BackdropTemplateMixin:HasBackdropInfo(backdropInfo)
+ return self.backdropInfo == backdropInfo;
+end
+
+function BackdropTemplateMixin:ClearBackdrop()
+ if self.backdropInfo then
+ for pieceName in pairs(textureUVs) do
+ local region = self[pieceName];
+ if region then
+ region:SetTexture(nil);
+ end
+ end
+ self.backdropInfo = nil;
+ end
+end
+
+function BackdropTemplateMixin:ApplyBackdrop()
+ local x, y, x1, y1 = 0, 0, 0, 0;
+ if self.backdropInfo.bgFile then
+ local edgeSize = self:GetEdgeSize();
+ x = -edgeSize;
+ y = edgeSize;
+ x1 = edgeSize;
+ y1 = -edgeSize;
+ local insets = self.backdropInfo.insets;
+ if insets then
+ x = x + (insets.left or 0);
+ y = y - (insets.top or 0);
+ x1 = x1 - (insets.right or 0);
+ y1 = y1 + (insets.bottom or 0);
+ end
+ end
+
+ local layout = {
+ TopLeftCorner = { },
+ TopRightCorner = { },
+ BottomLeftCorner = { },
+ BottomRightCorner = { },
+ TopEdge = { },
+ BottomEdge = { },
+ LeftEdge = { },
+ RightEdge = { },
+ Center = { layer = "BACKGROUND", x = x, y = y, x1 = x1, y1 = y1 },
+ setupPieceVisualsFunction = BackdropTemplateMixin.SetupPieceVisuals,
+ };
+
+ NineSliceUtil.ApplyLayout(self, layout);
+ --self:SetBackdropColor(1, 1, 1, 1);
+ --self:SetBackdropBorderColor(1, 1, 1, 1);
+ self:SetupTextureCoordinates();
+end
+
+-- backwards compatibility API starts here
+function BackdropTemplateMixin:SetBackdrop(backdropInfo)
+ if backdropInfo then
+ if self:HasBackdropInfo(backdropInfo) then
+ return;
+ end
+ if not backdropInfo.edgeFile and not backdropInfo.bgFile then
+ self:ClearBackdrop();
+ return;
+ end
+ self.backdropInfo = backdropInfo;
+ self:ApplyBackdrop();
+ else
+ self:ClearBackdrop();
+ end
+end
+
+function BackdropTemplateMixin:GetBackdrop()
+ if self.backdropInfo then
+ -- make a copy because it will be altered to match old API output
+ local backdropInfo = CopyTable(self.backdropInfo);
+ -- fill in defaults
+ if not backdropInfo.bgFile then
+ backdropInfo.bgFile = "";
+ end
+ if not backdropInfo.edgeFile then
+ backdropInfo.edgeFile = "";
+ end
+ if backdropInfo.tile == nil then
+ backdropInfo.tile = false;
+ end
+ if backdropInfo.tileSize == nil then
+ backdropInfo.tileSize = 0;
+ end
+ if backdropInfo.tileEdge == nil then
+ backdropInfo.tileEdge = true;
+ end
+ if not backdropInfo.edgeSize then
+ backdropInfo.edgeSize = self:GetEdgeSize();
+ end
+ if not backdropInfo.insets then
+ backdropInfo.insets = { };
+ end
+ if not backdropInfo.insets.left then
+ backdropInfo.insets.left = 0;
+ end
+ if not backdropInfo.insets.right then
+ backdropInfo.insets.right = 0;
+ end
+ if not backdropInfo.insets.top then
+ backdropInfo.insets.top = 0;
+ end
+ if not backdropInfo.insets.bottom then
+ backdropInfo.insets.bottom = 0;
+ end
+ return backdropInfo;
+ end
+ return nil;
+end
+
+function BackdropTemplateMixin:GetBackdropColor()
+ if not self.backdropInfo then
+ return;
+ end
+ if self.Center then
+ return self.Center:GetVertexColor();
+ end
+end
+
+function BackdropTemplateMixin:SetBackdropColor(r, g, b, a)
+ if not self.backdropInfo then
+ -- Ideally this would throw an error here but the old API just failed silently
+ return;
+ end
+ if self.Center then
+ self.Center:SetVertexColor(r, g, b, a or 1);
+ end
+end
+
+function BackdropTemplateMixin:GetBackdropBorderColor()
+ if not self.backdropInfo then
+ return
+ end
+ -- return the vertex color of any valid region
+ for pieceName in pairs(textureUVs) do
+ local region = self[pieceName];
+ if region and pieceName ~= "Center" then
+ return region:GetVertexColor();
+ end
+ end
+end
+
+function BackdropTemplateMixin:SetBackdropBorderColor(r, g, b, a)
+ if not self.backdropInfo then
+ -- Ideally this would throw an error here but the old API just failed silently
+ return;
+ end
+ for pieceName in pairs(textureUVs) do
+ local region = self[pieceName];
+ if region and pieceName ~= "Center" then
+ region:SetVertexColor(r, g, b, a or 1);
+ end
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Backdrop.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Backdrop.xml
new file mode 100644
index 0000000..f90fcf0
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Backdrop.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Mixin(self, BackdropTemplateMixin)
+
+
+ self:OnBackdropLoaded()
+ self:OnBackdropSizeChanged()
+
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_EasyMenu.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_EasyMenu.lua
new file mode 100644
index 0000000..e1efb97
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_EasyMenu.lua
@@ -0,0 +1,33 @@
+
+-- Simplified Menu Display System
+-- This is a basic system for displaying a menu from a structure table.
+--
+-- See UIDropDownMenu.lua for the menuList details.
+--
+-- Args:
+-- menuList - menu table
+-- menuFrame - the UI frame to populate
+-- anchor - where to anchor the frame (e.g. CURSOR)
+-- x - x offset
+-- y - y offset
+-- displayMode - border type
+-- autoHideDelay - how long until the menu disappears
+--
+--
+function C_EasyMenu(menuList, menuFrame, anchor, x, y, displayMode, autoHideDelay )
+ if ( displayMode == "MENU" ) then
+ menuFrame.displayMode = displayMode;
+ end
+ C_UIDropDownMenu_Initialize(menuFrame, EasyMenu_Initialize, displayMode, nil, menuList);
+ C_ToggleDropDownMenu(1, nil, menuFrame, anchor, x, y, menuList);
+end
+
+function C_EasyMenu_Initialize( frame, level, menuList )
+ for index = 1, #menuList do
+ local value = menuList[index]
+ if (value.text) then
+ value.index = index;
+ C_UIDropDownMenu_AddButton( value, level );
+ end
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_UIDropDownMenu.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_UIDropDownMenu.lua
new file mode 100644
index 0000000..d21dac4
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_UIDropDownMenu.lua
@@ -0,0 +1,1031 @@
+C_UIDROPDOWNMENU_MINBUTTONS = 8;
+C_UIDROPDOWNMENU_MAXBUTTONS = 8;
+C_UIDROPDOWNMENU_MAXLEVELS = 2;
+C_UIDROPDOWNMENU_BUTTON_HEIGHT = 16;
+C_UIDROPDOWNMENU_BORDER_HEIGHT = 15;
+-- The current open menu
+C_UIDROPDOWNMENU_OPEN_MENU = nil;
+-- The current menu being initialized
+C_UIDROPDOWNMENU_INIT_MENU = nil;
+-- Current level shown of the open menu
+C_UIDROPDOWNMENU_MENU_LEVEL = 1;
+-- Current value of the open menu
+C_UIDROPDOWNMENU_MENU_VALUE = nil;
+-- Time to wait to hide the menu
+C_UIDROPDOWNMENU_SHOW_TIME = 2;
+-- Default dropdown text height
+C_UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT = nil;
+-- List of open menus
+C_OPEN_DROPDOWNMENUS = {};
+
+local C_UIDropDownMenuDelegate = CreateFrame("FRAME");
+
+-- UIDropDownMenu.xml -> Lua
+ -- DropDownList1
+ local f = CreateFrame("Button", "C_DropDownList1", nil, "C_UIDropDownListTemplate");
+ f:SetID(1);
+ f:SetSize(180, 10);
+ f:SetToplevel(true);
+ f:SetFrameStrata("FULLSCREEN_DIALOG");
+ f:Hide();
+ local fontName, fontHeight, fontFlags = _G["C_DropDownList1Button1NormalText"]:GetFont();
+ C_UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT = fontHeight;
+
+ -- DropDownList2
+ local f = CreateFrame("Button", "C_DropDownList2", nil, "C_UIDropDownListTemplate");
+ f:SetID(2);
+ f:SetSize(180, 10);
+ f:SetToplevel(true);
+ f:SetFrameStrata("FULLSCREEN_DIALOG");
+ f:Hide();
+
+ UIMenus[#UIMenus+1] = "C_DropDownList1"
+ UIMenus[#UIMenus+1] = "C_DropDownList2"
+
+function C_UIDropDownMenuDelegate_OnAttributeChanged (self, attribute, value)
+ if ( attribute == "createframes" and value == true ) then
+ C_UIDropDownMenu_CreateFrames(self:GetAttribute("createframes-level"), self:GetAttribute("createframes-index"));
+ elseif ( attribute == "initmenu" ) then
+ C_UIDROPDOWNMENU_INIT_MENU = value;
+ elseif ( attribute == "openmenu" ) then
+ C_UIDROPDOWNMENU_OPEN_MENU = value;
+ end
+end
+
+C_UIDropDownMenuDelegate:SetScript("OnAttributeChanged", C_UIDropDownMenuDelegate_OnAttributeChanged);
+
+function C_UIDropDownMenu_InitializeHelper (frame)
+ -- This deals with the potentially tainted stuff!
+ if ( frame ~= C_UIDROPDOWNMENU_OPEN_MENU ) then
+ C_UIDROPDOWNMENU_MENU_LEVEL = 1;
+ end
+
+ -- Set the frame that's being intialized
+ C_UIDropDownMenuDelegate:SetAttribute("initmenu", frame);
+
+ -- Hide all the buttons
+ local button, dropDownList;
+ for i = 1, C_UIDROPDOWNMENU_MAXLEVELS, 1 do
+ dropDownList = _G["C_DropDownList"..i];
+ if ( i >= C_UIDROPDOWNMENU_MENU_LEVEL or frame ~= C_UIDROPDOWNMENU_OPEN_MENU ) then
+ dropDownList.numButtons = 0;
+ dropDownList.maxWidth = 0;
+ for j=1, C_UIDROPDOWNMENU_MAXBUTTONS, 1 do
+ button = _G["C_DropDownList"..i.."Button"..j];
+ button:Hide();
+ end
+ dropDownList:Hide();
+ end
+ end
+ frame:SetHeight(C_UIDROPDOWNMENU_BUTTON_HEIGHT * 2);
+end
+
+function C_UIDropDownMenu_Initialize(frame, initFunction, displayMode, level, menuList)
+ frame.menuList = menuList;
+
+ securecall("C_UIDropDownMenu_InitializeHelper", frame);
+
+ -- Set the initialize function and call it. The initFunction populates the dropdown list.
+ if ( initFunction ) then
+ frame.initialize = initFunction;
+ initFunction(frame, level, frame.menuList);
+ end
+
+ -- Change appearance based on the displayMode
+ if ( displayMode == "MENU" ) then
+ local name = frame:GetName();
+ _G[name.."Left"]:Hide();
+ _G[name.."Middle"]:Hide();
+ _G[name.."Right"]:Hide();
+ _G[name.."ButtonNormalTexture"]:SetTexture("");
+ _G[name.."ButtonDisabledTexture"]:SetTexture("");
+ _G[name.."ButtonPushedTexture"]:SetTexture("");
+ _G[name.."ButtonHighlightTexture"]:SetTexture("");
+ _G[name.."Button"]:ClearAllPoints();
+ _G[name.."Button"]:SetPoint("LEFT", name.."Text", "LEFT", -9, 0);
+ _G[name.."Button"]:SetPoint("RIGHT", name.."Text", "RIGHT", 6, 0);
+ frame.displayMode = "MENU";
+ end
+
+end
+
+-- If dropdown is visible then see if its timer has expired, if so hide the frame
+function C_UIDropDownMenu_OnUpdate(self, elapsed)
+ if ( not self.showTimer or not self.isCounting ) then
+ return;
+ elseif ( self.showTimer < 0 ) then
+ self:Hide();
+ self.showTimer = nil;
+ self.isCounting = nil;
+ else
+ self.showTimer = self.showTimer - elapsed;
+ end
+end
+
+-- Start the countdown on a frame
+function C_UIDropDownMenu_StartCounting(frame)
+ if ( frame.parent ) then
+ C_UIDropDownMenu_StartCounting(frame.parent);
+ else
+ frame.showTimer = C_UIDROPDOWNMENU_SHOW_TIME;
+ frame.isCounting = 1;
+ end
+end
+
+-- Stop the countdown on a frame
+function C_UIDropDownMenu_StopCounting(frame)
+ if ( frame.parent ) then
+ C_UIDropDownMenu_StopCounting(frame.parent);
+ else
+ frame.isCounting = nil;
+ end
+end
+
+--[[
+List of button attributes
+======================================================
+info.text = [STRING] -- The text of the button
+info.value = [ANYTHING] -- The value that C_UIDROPDOWNMENU_MENU_VALUE is set to when the button is clicked
+info.func = [function()] -- The function that is called when you click the button
+info.checked = [nil, true, function] -- Check the button if true or function returns true
+info.isTitle = [nil, true] -- If it's a title the button is disabled and the font color is set to yellow
+info.disabled = [nil, true] -- Disable the button and show an invisible button that still traps the mouseover event so menu doesn't time out
+info.tooltipWhileDisabled = [nil, 1] -- Show the tooltip, even when the button is disabled.
+info.hasArrow = [nil, true] -- Show the expand arrow for multilevel menus
+info.hasColorSwatch = [nil, true] -- Show color swatch or not, for color selection
+info.r = [1 - 255] -- Red color value of the color swatch
+info.g = [1 - 255] -- Green color value of the color swatch
+info.b = [1 - 255] -- Blue color value of the color swatch
+info.colorCode = [STRING] -- "|cAARRGGBB" embedded hex value of the button text color. Only used when button is enabled
+info.swatchFunc = [function()] -- Function called by the color picker on color change
+info.hasOpacity = [nil, 1] -- Show the opacity slider on the colorpicker frame
+info.opacity = [0.0 - 1.0] -- Percentatge of the opacity, 1.0 is fully shown, 0 is transparent
+info.opacityFunc = [function()] -- Function called by the opacity slider when you change its value
+info.cancelFunc = [function(previousValues)] -- Function called by the colorpicker when you click the cancel button (it takes the previous values as its argument)
+info.notClickable = [nil, 1] -- Disable the button and color the font white
+info.notCheckable = [nil, 1] -- Shrink the size of the buttons and don't display a check box
+info.owner = [Frame] -- Dropdown frame that "owns" the current dropdownlist
+info.keepShownOnClick = [nil, 1] -- Don't hide the dropdownlist after a button is clicked
+info.tooltipTitle = [nil, STRING] -- Title of the tooltip shown on mouseover
+info.tooltipText = [nil, STRING] -- Text of the tooltip shown on mouseover
+info.tooltipOnButton = [nil, 1] -- Show the tooltip attached to the button instead of as a Newbie tooltip.
+info.justifyH = [nil, "CENTER"] -- Justify button text
+info.arg1 = [ANYTHING] -- This is the first argument used by info.func
+info.arg2 = [ANYTHING] -- This is the second argument used by info.func
+info.fontObject = [FONT] -- font object replacement for Normal and Highlight
+info.menuTable = [TABLE] -- This contains an array of info tables to be displayed as a child menu
+info.noClickSound = [nil, 1] -- Set to 1 to suppress the sound when clicking the button. The sound only plays if .func is set.
+info.padding = [nil, NUMBER] -- Number of pixels to pad the text on the right side
+]]
+
+local UIDropDownMenu_ButtonInfo = {};
+
+--Until we get around to making this betterz...
+local UIDropDownMenu_SecureInfo = {};
+
+function C_UIDropDownMenu_CreateInfo()
+ -- Reuse the same table to prevent memory churn
+ local info;
+ local secure = issecure();
+
+ if ( secure ) then
+ info = UIDropDownMenu_SecureInfo;
+ else
+ info = UIDropDownMenu_ButtonInfo;
+ end
+
+ for k,v in pairs(info) do
+ info[k] = nil;
+ end
+
+ return info;
+end
+
+function C_UIDropDownMenu_CreateFrames(level, index)
+ while ( level > C_UIDROPDOWNMENU_MAXLEVELS ) do
+ C_UIDROPDOWNMENU_MAXLEVELS = C_UIDROPDOWNMENU_MAXLEVELS + 1;
+ local newList = CreateFrame("Button", "C_DropDownList"..C_UIDROPDOWNMENU_MAXLEVELS, nil, "C_UIDropDownListTemplate");
+ newList:SetFrameStrata("FULLSCREEN_DIALOG");
+ newList:SetToplevel(1);
+ newList:Hide();
+ newList:SetID(C_UIDROPDOWNMENU_MAXLEVELS);
+ newList:SetWidth(180)
+ newList:SetHeight(10)
+ for i=C_UIDROPDOWNMENU_MINBUTTONS+1, C_UIDROPDOWNMENU_MAXBUTTONS do
+ local newButton = CreateFrame("Button", "C_DropDownList"..C_UIDROPDOWNMENU_MAXLEVELS.."Button"..i, newList, "C_UIDropDownMenuButtonTemplate");
+ newButton:SetID(i);
+ end
+ end
+
+ while ( index > C_UIDROPDOWNMENU_MAXBUTTONS ) do
+ C_UIDROPDOWNMENU_MAXBUTTONS = C_UIDROPDOWNMENU_MAXBUTTONS + 1;
+ for i=1, C_UIDROPDOWNMENU_MAXLEVELS do
+ local newButton = CreateFrame("Button", "C_DropDownList"..i.."Button"..C_UIDROPDOWNMENU_MAXBUTTONS, _G["C_DropDownList"..i], "C_UIDropDownMenuButtonTemplate");
+ newButton:SetID(C_UIDROPDOWNMENU_MAXBUTTONS);
+ end
+ end
+end
+
+function C_UIDropDownMenu_AddButton(info, level)
+ --[[ Might to uncomment this if there are performance issues
+ if ( not C_UIDROPDOWNMENU_OPEN_MENU ) then
+ return;
+ end]]
+
+ if ( not level ) then
+ level = 1;
+ end
+
+ local listFrame = _G["C_DropDownList"..level];
+ local index = listFrame and (listFrame.numButtons + 1) or 1;
+ local width;
+
+ C_UIDropDownMenuDelegate:SetAttribute("createframes-level", level);
+ C_UIDropDownMenuDelegate:SetAttribute("createframes-index", index);
+ C_UIDropDownMenuDelegate:SetAttribute("createframes", true);
+
+ listFrame = listFrame or _G["C_DropDownList"..level];
+ local listFrameName = listFrame:GetName();
+
+ -- Set the number of buttons in the listframe
+ listFrame.numButtons = index;
+
+ local button = _G[listFrameName.."Button"..index];
+ local normalText = _G[button:GetName().."NormalText"];
+ local icon = _G[button:GetName().."Icon"];
+ -- This button is used to capture the mouse OnEnter/OnLeave events if the dropdown button is disabled, since a disabled button doesn't receive any events
+ -- This is used specifically for drop down menu time outs
+ local invisibleButton = _G[button:GetName().."InvisibleButton"];
+
+ -- Default settings
+ button:SetDisabledFontObject(GameFontDisableSmallLeft);
+ invisibleButton:Hide();
+ button:Enable();
+
+ -- If not clickable then disable the button and set it white
+ if ( info.notClickable ) then
+ info.disabled = 1;
+ button:SetDisabledFontObject(GameFontHighlightSmallLeft);
+ end
+
+ -- Set the text color and disable it if its a title
+ if ( info.isTitle ) then
+ info.disabled = 1;
+ button:SetDisabledFontObject(GameFontNormalSmallLeft);
+ end
+
+ -- Disable the button if disabled and turn off the color code
+ if ( info.disabled ) then
+ button:Disable();
+ invisibleButton:Show();
+ info.colorCode = nil;
+ end
+
+ -- Configure button
+ if ( info.text ) then
+ -- look for inline color code this is only if the button is enabled
+ if ( info.colorCode ) then
+ button:SetText(info.colorCode..info.text.."|r");
+ else
+ button:SetText(info.text);
+ end
+ -- Determine the width of the button
+ width = normalText:GetWidth() + 40;
+ -- Add padding if has and expand arrow or color swatch
+ if ( info.hasArrow or info.hasColorSwatch ) then
+ width = width + 10;
+ end
+ if ( info.notCheckable ) then
+ width = width - 30;
+ end
+ -- Set icon
+ if ( info.icon ) then
+ icon:SetTexture(info.icon);
+ if ( info.tCoordLeft ) then
+ icon:SetTexCoord(info.tCoordLeft, info.tCoordRight, info.tCoordTop, info.tCoordBottom);
+ else
+ icon:SetTexCoord(0, 1, 0, 1);
+ end
+ icon:Show();
+ -- Add padding for the icon
+ width = width + 10;
+ else
+ icon:Hide();
+ end
+ if ( info.padding ) then
+ width = width + info.padding;
+ end
+ -- Set maximum button width
+ if ( width > listFrame.maxWidth ) then
+ listFrame.maxWidth = width;
+ end
+ -- Check to see if there is a replacement font
+ if ( info.fontObject ) then
+ button:SetNormalFontObject(info.fontObject);
+ button:SetHighlightFontObject(info.fontObject);
+ else
+ button:SetNormalFontObject(GameFontHighlightSmallLeft);
+ button:SetHighlightFontObject(GameFontHighlightSmallLeft);
+ end
+ else
+ button:SetText("");
+ icon:Hide();
+ end
+
+ -- Pass through attributes
+ button.func = info.func;
+ button.owner = info.owner;
+ button.hasOpacity = info.hasOpacity;
+ button.opacity = info.opacity;
+ button.opacityFunc = info.opacityFunc;
+ button.cancelFunc = info.cancelFunc;
+ button.swatchFunc = info.swatchFunc;
+ button.keepShownOnClick = info.keepShownOnClick;
+ button.tooltipTitle = info.tooltipTitle;
+ button.tooltipText = info.tooltipText;
+ button.arg1 = info.arg1;
+ button.arg2 = info.arg2;
+ button.hasArrow = info.hasArrow;
+ button.hasColorSwatch = info.hasColorSwatch;
+ button.notCheckable = info.notCheckable;
+ button.menuList = info.menuList;
+ button.tooltipWhileDisabled = info.tooltipWhileDisabled;
+ button.tooltipOnButton = info.tooltipOnButton;
+ button.noClickSound = info.noClickSound;
+ button.padding = info.padding;
+
+ if ( info.value ) then
+ button.value = info.value;
+ elseif ( info.text ) then
+ button.value = info.text;
+ else
+ button.value = nil;
+ end
+
+ -- Show the expand arrow if it has one
+ if ( info.hasArrow ) then
+ _G[listFrameName.."Button"..index.."ExpandArrow"]:Show();
+ else
+ _G[listFrameName.."Button"..index.."ExpandArrow"]:Hide();
+ end
+ button.hasArrow = info.hasArrow;
+
+ -- If not checkable move everything over to the left to fill in the gap where the check would be
+ local xPos = 5;
+ local yPos = -((button:GetID() - 1) * C_UIDROPDOWNMENU_BUTTON_HEIGHT) - C_UIDROPDOWNMENU_BORDER_HEIGHT;
+ normalText:ClearAllPoints();
+ if ( info.notCheckable ) then
+ if ( info.justifyH and info.justifyH == "CENTER" ) then
+ normalText:SetPoint("CENTER", button, "CENTER", -7, 0);
+ else
+ normalText:SetPoint("LEFT", button, "LEFT", 0, 0);
+ end
+ xPos = xPos + 10;
+
+ else
+ xPos = xPos + 12;
+ normalText:SetPoint("LEFT", button, "LEFT", 20, 0);
+ end
+
+ -- Adjust offset if displayMode is menu
+ local frame = C_UIDROPDOWNMENU_OPEN_MENU;
+ if ( frame and frame.displayMode == "MENU" ) then
+ if ( not info.notCheckable ) then
+ xPos = xPos - 6;
+ end
+ end
+
+ -- If no open frame then set the frame to the currently initialized frame
+ frame = frame or C_UIDROPDOWNMENU_INIT_MENU;
+
+ button:SetPoint("TOPLEFT", button:GetParent(), "TOPLEFT", xPos, yPos);
+
+ -- See if button is selected by id or name
+ if ( frame ) then
+ if ( C_UIDropDownMenu_GetSelectedName(frame) ) then
+ if ( button:GetText() == C_UIDropDownMenu_GetSelectedName(frame) ) then
+ info.checked = 1;
+ end
+ elseif ( C_UIDropDownMenu_GetSelectedID(frame) ) then
+ if ( button:GetID() == C_UIDropDownMenu_GetSelectedID(frame) ) then
+ info.checked = 1;
+ end
+ elseif ( C_UIDropDownMenu_GetSelectedValue(frame) ) then
+ if ( button.value == C_UIDropDownMenu_GetSelectedValue(frame) ) then
+ info.checked = 1;
+ end
+ end
+ end
+
+ -- Checked can be a function now
+ local checked = info.checked;
+ if ( type(checked) == "function" ) then
+ checked = checked();
+ end
+
+ local check = _G[listFrameName.."Button"..index.."Check"]
+
+ -- Show the check if checked
+ if ( checked ) then
+ button:LockHighlight();
+ check:Show();
+ else
+ button:UnlockHighlight();
+ check:Hide();
+ end
+ button.checked = info.checked;
+
+ -- Allow radio buttons within UIDropDownMenu.
+ if ( C_UIDROPDOWNMENU_OPEN_MENU and not button.notCheckable ) then
+ if ( info.disabled or (info.isRadio and not info.checked) ) then
+ check:SetDesaturated(true)
+ check:SetAlpha(.25)
+ else
+ check:SetDesaturated(false)
+ check:SetAlpha(1)
+ end
+
+ if ( info.isRadio ) then
+ check:SetTexture("Interface\\Buttons\\UI-RadioButton")
+ check:Show()
+
+ if ( info.checked ) then
+ check:SetTexCoord(.25, .5, 0, 1)
+ else
+ check:SetTexCoord(0, .25, 0, 1)
+ end
+ else
+ check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
+ check:SetTexCoord(0, 1, 0, 1)
+ end
+ end
+
+ -- If has a colorswatch, show it and vertex color it
+ local colorSwatch = _G[listFrameName.."Button"..index.."ColorSwatch"];
+ if ( info.hasColorSwatch ) then
+ _G["C_DropDownList"..level.."Button"..index.."ColorSwatch".."NormalTexture"]:SetVertexColor(info.r, info.g, info.b);
+ button.r = info.r;
+ button.g = info.g;
+ button.b = info.b;
+ colorSwatch:Show();
+ else
+ colorSwatch:Hide();
+ end
+
+ -- Set the height of the listframe
+ listFrame:SetHeight((index * C_UIDROPDOWNMENU_BUTTON_HEIGHT) + (C_UIDROPDOWNMENU_BORDER_HEIGHT * 2));
+
+ button:Show();
+end
+
+function C_UIDropDownMenu_Refresh(frame, useValue, dropdownLevel)
+ local button, checked, checkImage, normalText, width;
+ local maxWidth = 0;
+ if ( not dropdownLevel ) then
+ dropdownLevel = C_UIDROPDOWNMENU_MENU_LEVEL;
+ end
+
+ -- Just redraws the existing menu
+ for i=1, C_UIDROPDOWNMENU_MAXBUTTONS do
+ button = _G["C_DropDownList"..dropdownLevel.."Button"..i];
+ checked = nil;
+ -- See if checked or not
+ if ( C_UIDropDownMenu_GetSelectedName(frame) ) then
+ if ( button:GetText() == C_UIDropDownMenu_GetSelectedName(frame) ) then
+ checked = 1;
+ end
+ elseif ( C_UIDropDownMenu_GetSelectedID(frame) ) then
+ if ( button:GetID() == C_UIDropDownMenu_GetSelectedID(frame) ) then
+ checked = 1;
+ end
+ elseif ( C_UIDropDownMenu_GetSelectedValue(frame) ) then
+ if ( button.value == C_UIDropDownMenu_GetSelectedValue(frame) ) then
+ checked = 1;
+ end
+ end
+
+ -- If checked show check image
+ checkImage = _G["C_DropDownList"..dropdownLevel.."Button"..i.."Check"];
+ if ( checked ) then
+ if ( useValue ) then
+ C_UIDropDownMenu_SetText(frame, button.value);
+ else
+ C_UIDropDownMenu_SetText(frame, button:GetText());
+ end
+ button:LockHighlight();
+ checkImage:Show();
+ else
+ button:UnlockHighlight();
+ checkImage:Hide();
+ end
+
+ if ( button:IsShown() ) then
+ normalText = _G[button:GetName().."NormalText"];
+ -- Determine the maximum width of a button
+ width = normalText:GetWidth() + 40;
+ -- Add padding if has and expand arrow or color swatch
+ if ( button.hasArrow or button.hasColorSwatch ) then
+ width = width + 10;
+ end
+ if ( button.notCheckable ) then
+ width = width - 30;
+ end
+ if ( button.padding ) then
+ width = width + button.padding;
+ end
+ if ( width > maxWidth ) then
+ maxWidth = width;
+ end
+ end
+ end
+ for i=1, C_UIDROPDOWNMENU_MAXBUTTONS do
+ button = _G["C_DropDownList"..dropdownLevel.."Button"..i];
+ button:SetWidth(maxWidth);
+ end
+ _G["C_DropDownList"..dropdownLevel]:SetWidth(maxWidth+25);
+end
+
+function C_UIDropDownMenu_SetSelectedName(frame, name, useValue)
+ frame.selectedName = name;
+ frame.selectedID = nil;
+ frame.selectedValue = nil;
+ C_UIDropDownMenu_Refresh(frame, useValue);
+end
+
+function C_UIDropDownMenu_SetSelectedValue(frame, value, useValue)
+ -- useValue will set the value as the text, not the name
+ frame.selectedName = nil;
+ frame.selectedID = nil;
+ frame.selectedValue = value;
+ C_UIDropDownMenu_Refresh(frame, useValue);
+end
+
+function C_UIDropDownMenu_SetSelectedID(frame, id, useValue)
+ frame.selectedID = id;
+ frame.selectedName = nil;
+ frame.selectedValue = nil;
+ C_UIDropDownMenu_Refresh(frame, useValue);
+end
+
+function C_UIDropDownMenu_GetSelectedName(frame)
+ return frame.selectedName;
+end
+
+function C_UIDropDownMenu_GetSelectedID(frame)
+ if ( frame.selectedID ) then
+ return frame.selectedID;
+ else
+ -- If no explicit selectedID then try to send the id of a selected value or name
+ local button;
+ for i=1, C_UIDROPDOWNMENU_MAXBUTTONS do
+ button = _G["C_DropDownList"..C_UIDROPDOWNMENU_MENU_LEVEL.."Button"..i];
+ -- See if checked or not
+ if ( C_UIDropDownMenu_GetSelectedName(frame) ) then
+ if ( button:GetText() == C_UIDropDownMenu_GetSelectedName(frame) ) then
+ return i;
+ end
+ elseif ( C_UIDropDownMenu_GetSelectedValue(frame) ) then
+ if ( button.value == C_UIDropDownMenu_GetSelectedValue(frame) ) then
+ return i;
+ end
+ end
+ end
+ end
+end
+
+function C_UIDropDownMenu_GetSelectedValue(frame)
+ return frame.selectedValue;
+end
+
+function C_UIDropDownMenuButton_OnClick(self)
+ local checked = self.checked;
+ if ( type (checked) == "function" ) then
+ checked = checked();
+ end
+
+ if ( self.keepShownOnClick ) then
+ if ( checked ) then
+ _G[self:GetName().."Check"]:Hide();
+ checked = false;
+ else
+ _G[self:GetName().."Check"]:Show();
+ checked = true;
+ end
+ else
+ self:GetParent():Hide();
+ end
+
+ if ( type (self.checked) ~= "function" ) then
+ self.checked = checked;
+ end
+
+ -- saving this here because func might use a dropdown, changing this self's attributes
+ local playSound = true;
+ if ( self.noClickSound ) then
+ playSound = false;
+ end
+
+ local func = self.func;
+ if ( func ) then
+ func(self, self.arg1, self.arg2, checked);
+ else
+ return;
+ end
+
+ if ( playSound ) then
+ PlaySound("UChatScrollButton");
+ end
+end
+
+function C_HideDropDownMenu(level)
+ local listFrame = _G["C_DropDownList"..level];
+ listFrame:Hide();
+end
+
+function C_ToggleDropDownMenu(level, value, dropDownFrame, anchorName, xOffset, yOffset, menuList, button)
+ if ( not level ) then
+ level = 1;
+ end
+ C_UIDropDownMenuDelegate:SetAttribute("createframes-level", level);
+ C_UIDropDownMenuDelegate:SetAttribute("createframes-index", 0);
+ C_UIDropDownMenuDelegate:SetAttribute("createframes", true);
+ C_UIDROPDOWNMENU_MENU_LEVEL = level;
+ C_UIDROPDOWNMENU_MENU_VALUE = value;
+ local listFrame = _G["C_DropDownList"..level];
+ local listFrameName = "C_DropDownList"..level;
+ local tempFrame;
+ local point, relativePoint, relativeTo;
+ if ( not dropDownFrame ) then
+ tempFrame = button:GetParent();
+ else
+ tempFrame = dropDownFrame;
+ end
+ if ( listFrame:IsShown() and (C_UIDROPDOWNMENU_OPEN_MENU == tempFrame) ) then
+ listFrame:Hide();
+ else
+ -- Set the dropdownframe scale
+ local uiScale;
+ local uiParentScale = UIParent:GetScale();
+ if ( tempFrame ~= WorldMapContinentDropDown and tempFrame ~= WorldMapZoneDropDown ) then
+ if ( GetCVar("useUIScale") == "1" ) then
+ uiScale = tonumber(GetCVar("uiscale"));
+ if ( uiParentScale < uiScale ) then
+ uiScale = uiParentScale;
+ end
+ else
+ uiScale = uiParentScale;
+ end
+ else
+ uiScale = 1;
+ end
+ listFrame:SetScale(uiScale);
+
+ -- Hide the listframe anyways since it is redrawn OnShow()
+ listFrame:Hide();
+
+ -- Frame to anchor the dropdown menu to
+ local anchorFrame;
+
+ -- Display stuff
+ -- Level specific stuff
+ if ( level == 1 ) then
+ C_UIDropDownMenuDelegate:SetAttribute("openmenu", dropDownFrame);
+ listFrame:ClearAllPoints();
+ -- If there's no specified anchorName then use left side of the dropdown menu
+ if ( not anchorName ) then
+ -- See if the anchor was set manually using setanchor
+ if ( dropDownFrame.xOffset ) then
+ xOffset = dropDownFrame.xOffset;
+ end
+ if ( dropDownFrame.yOffset ) then
+ yOffset = dropDownFrame.yOffset;
+ end
+ if ( dropDownFrame.point ) then
+ point = dropDownFrame.point;
+ end
+ if ( dropDownFrame.relativeTo ) then
+ relativeTo = dropDownFrame.relativeTo;
+ else
+ relativeTo = C_UIDROPDOWNMENU_OPEN_MENU:GetName().."Left";
+ end
+ if ( dropDownFrame.relativePoint ) then
+ relativePoint = dropDownFrame.relativePoint;
+ end
+ elseif ( anchorName == "cursor" ) then
+ relativeTo = nil;
+ local cursorX, cursorY = GetCursorPosition();
+ cursorX = cursorX/uiScale;
+ cursorY = cursorY/uiScale;
+
+ if ( not xOffset ) then
+ xOffset = 0;
+ end
+ if ( not yOffset ) then
+ yOffset = 0;
+ end
+ xOffset = cursorX + xOffset;
+ yOffset = cursorY + yOffset;
+ else
+ -- See if the anchor was set manually using setanchor
+ if ( dropDownFrame.xOffset ) then
+ xOffset = dropDownFrame.xOffset;
+ end
+ if ( dropDownFrame.yOffset ) then
+ yOffset = dropDownFrame.yOffset;
+ end
+ if ( dropDownFrame.point ) then
+ point = dropDownFrame.point;
+ end
+ if ( dropDownFrame.relativeTo ) then
+ relativeTo = dropDownFrame.relativeTo;
+ else
+ relativeTo = anchorName;
+ end
+ if ( dropDownFrame.relativePoint ) then
+ relativePoint = dropDownFrame.relativePoint;
+ end
+ end
+ if ( not xOffset or not yOffset ) then
+ xOffset = 8;
+ yOffset = 22;
+ end
+ if ( not point ) then
+ point = "TOPLEFT";
+ end
+ if ( not relativePoint ) then
+ relativePoint = "BOTTOMLEFT";
+ end
+ listFrame:SetPoint(point, relativeTo, relativePoint, xOffset, yOffset);
+ else
+ if ( not dropDownFrame ) then
+ dropDownFrame = C_UIDROPDOWNMENU_OPEN_MENU;
+ end
+ listFrame:ClearAllPoints();
+ -- If this is a dropdown button, not the arrow anchor it to itself
+ if ( strsub(button:GetParent():GetName(), 0,12) == "C_DropDownList" and strlen(button:GetParent():GetName()) == 13 ) then
+ anchorFrame = button;
+ else
+ anchorFrame = button:GetParent();
+ end
+ point = "TOPLEFT";
+ relativePoint = "TOPRIGHT";
+ listFrame:SetPoint(point, anchorFrame, relativePoint, 0, 0);
+ end
+
+ -- Change list box appearance depending on display mode
+ if ( dropDownFrame and dropDownFrame.displayMode == "MENU" ) then
+ _G[listFrameName.."Backdrop"]:Hide();
+ _G[listFrameName.."MenuBackdrop"]:Show();
+ else
+ _G[listFrameName.."Backdrop"]:Show();
+ _G[listFrameName.."MenuBackdrop"]:Hide();
+ end
+ dropDownFrame.menuList = menuList;
+ C_UIDropDownMenu_Initialize(dropDownFrame, dropDownFrame.initialize, nil, level, menuList);
+ -- If no items in the drop down don't show it
+ if ( listFrame.numButtons == 0 ) then
+ return;
+ end
+
+ -- Check to see if the dropdownlist is off the screen, if it is anchor it to the top of the dropdown button
+ listFrame:Show();
+ -- Hack since GetCenter() is returning coords relative to 1024x768
+ local x, y = listFrame:GetCenter();
+ -- Hack will fix this in next revision of dropdowns
+ if ( not x or not y ) then
+ listFrame:Hide();
+ return;
+ end
+
+ listFrame.onHide = dropDownFrame.onHide;
+
+ -- Determine whether the menu is off the screen or not
+ local offscreenY, offscreenX;
+ if ( (y - listFrame:GetHeight()/2) < 0 ) then
+ offscreenY = 1;
+ end
+ if ( listFrame:GetRight() > GetScreenWidth() ) then
+ offscreenX = 1;
+ end
+
+ -- If level 1 can only go off the bottom of the screen
+ if ( level == 1 ) then
+ if ( offscreenY and offscreenX ) then
+ point = gsub(point, "TOP(.*)", "BOTTOM%1");
+ point = gsub(point, "(.*)LEFT", "%1RIGHT");
+ relativePoint = gsub(relativePoint, "TOP(.*)", "BOTTOM%1");
+ relativePoint = gsub(relativePoint, "(.*)RIGHT", "%1LEFT");
+ elseif ( offscreenY ) then
+ point = gsub(point, "TOP(.*)", "BOTTOM%1");
+ relativePoint = gsub(relativePoint, "(.*)RIGHT", "%1LEFT");
+ elseif ( offscreenX ) then
+ point = gsub(point, "(.*)LEFT", "%1RIGHT");
+ relativePoint = gsub(relativePoint, "(.*)RIGHT", "%1LEFT");
+ end
+
+ listFrame:ClearAllPoints();
+ if ( anchorName == "cursor" ) then
+ listFrame:SetPoint(point, relativeTo, "BOTTOMLEFT", xOffset, yOffset);
+ else
+ listFrame:SetPoint(point, relativeTo, relativePoint, xOffset, yOffset);
+ end
+ else
+ if ( offscreenY and offscreenX ) then
+ point = gsub(point, "TOP(.*)", "BOTTOM%1");
+ point = gsub(point, "(.*)LEFT", "%1RIGHT");
+ relativePoint = gsub(relativePoint, "TOP(.*)", "BOTTOM%1");
+ relativePoint = gsub(relativePoint, "(.*)RIGHT", "%1LEFT");
+ xOffset = -11;
+ yOffset = -14;
+ elseif ( offscreenY ) then
+ point = gsub(point, "TOP(.*)", "BOTTOM%1");
+ relativePoint = gsub(relativePoint, "TOP(.*)", "BOTTOM%1");
+ xOffset = 0;
+ yOffset = -14;
+ elseif ( offscreenX ) then
+ point = gsub(point, "(.*)LEFT", "%1RIGHT");
+ relativePoint = gsub(relativePoint, "(.*)RIGHT", "%1LEFT");
+ xOffset = -11;
+ yOffset = 14;
+ else
+ xOffset = 0;
+ yOffset = 14;
+ end
+
+ listFrame:ClearAllPoints();
+ listFrame.parentLevel = tonumber(strmatch(anchorFrame:GetName(), "C_DropDownList(%d+)"));
+ listFrame.parentID = anchorFrame:GetID();
+ listFrame:SetPoint(point, anchorFrame, relativePoint, xOffset, yOffset);
+ end
+ end
+end
+
+function C_CloseDropDownMenus(level)
+ if ( not level ) then
+ level = 1;
+ end
+ for i=level, C_UIDROPDOWNMENU_MAXLEVELS do
+ _G["C_DropDownList"..i]:Hide();
+ end
+end
+
+function C_UIDropDownMenu_OnHide(self)
+ local id = self:GetID()
+ if ( self.onHide ) then
+ self.onHide(id+1);
+ self.onHide = nil;
+ end
+ C_CloseDropDownMenus(id+1);
+ C_OPEN_DROPDOWNMENUS[id] = nil;
+end
+
+function C_UIDropDownMenu_SetWidth(frame, width, padding)
+ _G[frame:GetName().."Middle"]:SetWidth(width);
+ local defaultPadding = 25;
+ if ( padding ) then
+ frame:SetWidth(width + padding);
+ else
+ frame:SetWidth(width + defaultPadding + defaultPadding);
+ end
+ if ( padding ) then
+ _G[frame:GetName().."Text"]:SetWidth(width);
+ else
+ _G[frame:GetName().."Text"]:SetWidth(width - defaultPadding);
+ end
+ frame.noResize = 1;
+end
+
+function C_UIDropDownMenu_SetButtonWidth(frame, width)
+ if ( width == "TEXT" ) then
+ width = _G[frame:GetName().."Text"]:GetWidth();
+ end
+
+ _G[frame:GetName().."Button"]:SetWidth(width);
+ frame.noResize = 1;
+end
+
+function C_UIDropDownMenu_SetText(frame, text)
+ local filterText = _G[frame:GetName().."Text"];
+ filterText:SetText(text);
+end
+
+function C_UIDropDownMenu_GetText(frame)
+ local filterText = _G[frame:GetName().."Text"];
+ return filterText:GetText();
+end
+
+function C_UIDropDownMenu_ClearAll(frame)
+ -- Previous code refreshed the menu quite often and was a performance bottleneck
+ frame.selectedID = nil;
+ frame.selectedName = nil;
+ frame.selectedValue = nil;
+ C_UIDropDownMenu_SetText(frame, "");
+
+ local button, checkImage;
+ for i=1, C_UIDROPDOWNMENU_MAXBUTTONS do
+ button = _G["C_DropDownList"..C_UIDROPDOWNMENU_MENU_LEVEL.."Button"..i];
+ button:UnlockHighlight();
+
+ checkImage = _G["C_DropDownList"..C_UIDROPDOWNMENU_MENU_LEVEL.."Button"..i.."Check"];
+ checkImage:Hide();
+ end
+end
+
+function C_UIDropDownMenu_JustifyText(frame, justification)
+ local text = _G[frame:GetName().."Text"];
+ text:ClearAllPoints();
+ if ( justification == "LEFT" ) then
+ text:SetPoint("LEFT", frame:GetName().."Left", "LEFT", 27, 2);
+ text:SetJustifyH("LEFT");
+ elseif ( justification == "RIGHT" ) then
+ text:SetPoint("RIGHT", frame:GetName().."Right", "RIGHT", -43, 2);
+ text:SetJustifyH("RIGHT");
+ elseif ( justification == "CENTER" ) then
+ text:SetPoint("CENTER", frame:GetName().."Middle", "CENTER", -5, 2);
+ text:SetJustifyH("CENTER");
+ end
+end
+
+function C_UIDropDownMenu_SetAnchor(dropdown, xOffset, yOffset, point, relativeTo, relativePoint)
+ dropdown.xOffset = xOffset;
+ dropdown.yOffset = yOffset;
+ dropdown.point = point;
+ dropdown.relativeTo = relativeTo;
+ dropdown.relativePoint = relativePoint;
+end
+
+function C_UIDropDownMenu_GetCurrentDropDown()
+ if ( C_UIDROPDOWNMENU_OPEN_MENU ) then
+ return C_UIDROPDOWNMENU_OPEN_MENU;
+ elseif ( C_UIDROPDOWNMENU_INIT_MENU ) then
+ return C_UIDROPDOWNMENU_INIT_MENU;
+ end
+end
+
+function C_UIDropDownMenuButton_GetChecked(self)
+ return _G[self:GetName().."Check"]:IsShown();
+end
+
+function C_UIDropDownMenuButton_GetName(self)
+ return _G[self:GetName().."NormalText"]:GetText();
+end
+
+function C_UIDropDownMenuButton_C_OpenColorPicker(self, button)
+ CloseMenus();
+ if ( not button ) then
+ button = self;
+ end
+ C_UIDROPDOWNMENU_MENU_VALUE = button.value;
+ C_OpenColorPicker(button);
+end
+
+function C_UIDropDownMenu_DisableButton(level, id)
+ _G["C_DropDownList"..level.."Button"..id]:Disable();
+end
+
+function C_UIDropDownMenu_EnableButton(level, id)
+ _G["C_DropDownList"..level.."Button"..id]:Enable();
+end
+
+function C_UIDropDownMenu_SetButtonText(level, id, text, colorCode)
+ local button = _G["C_DropDownList"..level.."Button"..id];
+ if ( colorCode) then
+ button:SetText(colorCode..text.."|r");
+ else
+ button:SetText(text);
+ end
+end
+
+function C_UIDropDownMenu_DisableDropDown(dropDown)
+ local label = _G[dropDown:GetName().."Label"];
+ if ( label ) then
+ label:SetVertexColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b);
+ end
+ _G[dropDown:GetName().."Text"]:SetVertexColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b);
+ _G[dropDown:GetName().."Button"]:Disable();
+ dropDown.isDisabled = 1;
+end
+
+function C_UIDropDownMenu_EnableDropDown(dropDown)
+ local label = _G[dropDown:GetName().."Label"];
+ if ( label ) then
+ label:SetVertexColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b);
+ end
+ _G[dropDown:GetName().."Text"]:SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b);
+ _G[dropDown:GetName().."Button"]:Enable();
+ dropDown.isDisabled = nil;
+end
+
+function C_UIDropDownMenu_IsEnabled(dropDown)
+ return not dropDown.isDisabled;
+end
+
+function C_UIDropDownMenu_GetValue(id)
+ --Only works if the dropdown has just been initialized, lame, I know =(
+ local button = _G["C_DropDownList1Button"..id];
+ if ( button ) then
+ return _G["C_DropDownList1Button"..id].value;
+ else
+ return nil;
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_UIDropDownMenu.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_UIDropDownMenu.xml
new file mode 100644
index 0000000..970d488
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/C_UIDropDownMenu.xml
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b);
+ self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self:Hide();
+
+
+ C_UIDropDownMenu_StopCounting(self, motion);
+
+
+ C_UIDropDownMenu_StartCounting(self, motion);
+
+
+ C_UIDropDownMenu_OnUpdate(self, elapsed);
+
+
+ for i=1, C_UIDROPDOWNMENU_MAXBUTTONS do
+ if (not self.noResize) then
+ _G[self:GetName().."Button"..i]:SetWidth(self.maxWidth);
+ end
+ end
+ if (not self.noResize) then
+ self:SetWidth(self.maxWidth+25);
+ end
+ self.showTime = nil;
+ if ( self:GetID() > 1 ) then
+ self.parent = _G["C_DropDownList"..(self:GetID() - 1)];
+ end
+
+
+ C_UIDropDownMenu_OnHide(self);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ C_ToggleDropDownMenu(nil, nil, self:GetParent());
+ PlaySound("igMainMenuOptionCheckBoxOn");
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ C_CloseDropDownMenus();
+
+
+
+
+
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/InterfaceOptions.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/InterfaceOptions.lua
new file mode 100644
index 0000000..c2141c1
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/InterfaceOptions.lua
@@ -0,0 +1,13 @@
+local WIDTH, HEIGHT = 858, 660
+local IOF = InterfaceOptionsFrame
+
+IOF:SetPoint("CENTER", UIParent, "CENTER")
+
+InterfaceOptionsFrameCategories:SetPoint("BOTTOMLEFT", IOF, "BOTTOMLEFT", 22, 50)
+InterfaceOptionsFrameAddOns:SetPoint("BOTTOMLEFT", IOF, "BOTTOMLEFT", 22, 50)
+
+IOF:SetSize(WIDTH, HEIGHT)
+IOF:SetToplevel(true)
+IOF:HookScript("OnShow", function()
+ IOF:SetSize(WIDTH, HEIGHT)
+end)
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Load.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Load.xml
new file mode 100644
index 0000000..fdbf99c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/Load.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SecureUIPanelTemplates.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SecureUIPanelTemplates.xml
new file mode 100644
index 0000000..a79586a
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SecureUIPanelTemplates.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SharedUIPanelTemplates.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SharedUIPanelTemplates.lua
new file mode 100644
index 0000000..c9daeaf
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SharedUIPanelTemplates.lua
@@ -0,0 +1,62 @@
+--[[
+
+Search Box Template
+
+]]
+
+function InputBoxInstructions_OnTextChanged(self)
+ self.Instructions:SetShown(self:GetText() == "")
+end
+
+function InputBoxInstructions_UpdateColorForEnabledState(self, color)
+ if color then
+ self:SetTextColor(color:GetRGBA());
+ end
+end
+
+function InputBoxInstructions_OnDisable(self)
+ InputBoxInstructions_UpdateColorForEnabledState(self, self.disabledColor);
+end
+
+function InputBoxInstructions_OnEnable(self)
+ InputBoxInstructions_UpdateColorForEnabledState(self, self.enabledColor);
+end
+
+function SearchBoxTemplate_OnLoad(self)
+ self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
+ self:SetTextInsets(16, 20, 0, 0);
+ self.Instructions:SetText(SEARCH);
+ self.Instructions:ClearAllPoints();
+ self.Instructions:SetPoint("TOPLEFT", self, "TOPLEFT", 16, 0);
+ self.Instructions:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -20, 0);
+end
+
+function SearchBoxTemplate_OnEditFocusLost(self)
+ if ( self:GetText() == "" ) then
+ self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
+ self.clearButton:Hide();
+ end
+end
+
+function SearchBoxTemplate_OnEditFocusGained(self)
+ self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
+ self.clearButton:Show();
+end
+
+function SearchBoxTemplate_OnTextChanged(self)
+ if ( not self:HasFocus() and self:GetText() == "" ) then
+ self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
+ self.clearButton:Hide();
+ else
+ self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
+ self.clearButton:Show();
+ end
+ InputBoxInstructions_OnTextChanged(self);
+end
+
+function SearchBoxTemplateClearButton_OnClick(self)
+ --PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
+ local editBox = self:GetParent();
+ editBox:SetText("");
+ editBox:ClearFocus();
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SharedUIPanelTemplates.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SharedUIPanelTemplates.xml
new file mode 100644
index 0000000..4499d71
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/SharedUIPanelTemplates.xml
@@ -0,0 +1,312 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self.texture:SetAlpha(1.0);
+
+
+ self.texture:SetAlpha(0.5);
+
+
+ if self:IsEnabled() then
+ self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 4, -4);
+ end
+
+
+ self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 3, -3);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self.backdropInfo = _G["BACKDROP_SLIDER_8_8"]
+
+
+
+
+
+
+
+ self.layoutType = "Dialog"
+ Mixin(self, NineSlicePanelMixin)
+ self:OnLoad()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self.texture:SetPoint("TOPLEFT", 1, -1);
+
+
+ self.texture:SetPoint("TOPLEFT", 0, 0);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if self:IsEnabled() ~= 0 then
+ self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ if ( self.Icon ) then
+ if ( not self.Icon.oldPoint ) then
+ local point, relativeTo, relativePoint, x, y = self.Icon:GetPoint(1)
+ self.Icon.oldPoint = point
+ self.Icon.oldX = x
+ self.Icon.oldY = y
+ end
+ self.Icon:SetPoint(self.Icon.oldPoint, self.Icon.oldX + 1, self.Icon.oldY - 1)
+ end
+ end
+
+
+ if self:IsEnabled() ~= 0 then
+ self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ if ( self.Icon ) then
+ self.Icon:SetPoint(self.Icon.oldPoint, self.Icon.oldX, self.Icon.oldY)
+ end
+ end
+
+
+ if self:IsEnabled() ~= 0 then
+ -- we need to reset our textures just in case we were hidden before a mouse up fired
+ self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ end
+
+
+ self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+
+
+ if(self.tooltipText ~= nil) then
+ GameTooltip_AddNewbieTip(self, self.tooltipText, 1.0, 1.0, 1.0, self.newbieText)
+ end
+
+
+ if(self.tooltipText ~= nil) then
+ GameTooltip:Hide()
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIDropDownMenu.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIDropDownMenu.lua
new file mode 100644
index 0000000..49c48b0
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIDropDownMenu.lua
@@ -0,0 +1,79 @@
+local function UIDropDownMenu_CreateUnChecked(object)
+ local uncheck = _G[object:GetName().."UnCheck"];
+ if ( uncheck ) then
+ return;
+ end
+
+ uncheck = object:CreateTexture("$parentUnCheck", "BORDER");
+ _G[object:GetName().."UnCheck"] = uncheck;
+ uncheck:SetSize(16, 16);
+ uncheck:SetPoint("Left");
+end
+
+hooksecurefunc("UIDropDownMenu_AddButton", function(info, level)
+ if ( not level ) then
+ level = 1;
+ end
+
+ local listFrame = _G["DropDownList"..level];
+ local index = listFrame and listFrame.numButtons or 1;
+
+ listFrame = listFrame or _G["DropDownList"..level];
+ local listFrameName = listFrame:GetName();
+
+ local button = _G[listFrameName.."Button"..index];
+ UIDropDownMenu_CreateUnChecked(button);
+
+ if ( not info.notCheckable ) then
+ local check = _G[listFrameName.."Button"..index.."Check"];
+ local uncheck = _G[listFrameName.."Button"..index.."UnCheck"];
+ if ( info.disabled ) then
+ check:SetDesaturated(true);
+ check:SetAlpha(0.5);
+ uncheck:SetDesaturated(true);
+ uncheck:SetAlpha(0.5);
+ else
+ check:SetDesaturated(false);
+ check:SetAlpha(1);
+ uncheck:SetDesaturated(false);
+ uncheck:SetAlpha(1);
+ end
+
+ if ( info.isRadio ) then
+ --check:SetTexCoord(0.0, 0.5, 0.5, 1.0);
+ check:SetTexCoord(0.25, 0.5, 0, 1);
+ check:SetTexture("Interface\\Buttons\\UI-RadioButton");
+ --uncheck:SetTexCoord(0.5, 1.0, 0.5, 1.0);
+ uncheck:SetTexCoord(0.75, 1, 0, 1);
+ uncheck:SetTexture("Interface\\Buttons\\UI-RadioButton");
+ else
+ check:SetTexCoord(0, 1, 0, 1);
+ check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check");
+ end
+
+ -- Checked can be a function now
+ local checked = info.checked;
+ if ( type(checked) == "function" ) then
+ checked = checked(button);
+ end
+ -- Show the check if checked
+ if ( checked ) then
+ button:LockHighlight();
+ check:Show();
+ uncheck:Hide();
+ else
+ button:UnlockHighlight();
+ check:Hide();
+ uncheck:Show();
+ end
+ else
+ _G[listFrameName.."Button"..index.."Check"]:Hide();
+ _G[listFrameName.."Button"..index.."UnCheck"]:Hide();
+ end
+
+ if ( not info.isRadio ) then
+ _G[listFrameName.."Button"..index.."UnCheck"]:Hide();
+ end
+
+ button.checked = info.checked;
+end)
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIPanelTemplates.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIPanelTemplates.lua
new file mode 100644
index 0000000..fba1fe8
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIPanelTemplates.lua
@@ -0,0 +1,398 @@
+function SearchBoxTemplate_OnLoad(self)
+ self:SetText(SEARCH);
+ self:SetFontObject("GameFontDisable");
+ self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
+ self:SetTextInsets(16, 20, 0, 0);
+end
+
+function SearchBoxTemplate_OnEditFocusLost(self)
+ self:HighlightText(0, 0);
+ self:SetFontObject("GameFontDisable");
+ self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
+ if ( self:GetText() == "" or self:GetText() == SEARCH ) then
+ self:SetText(SEARCH);
+ self.clearButton:Hide();
+ end
+end
+
+function SerachBoxTemplate_OnEditFocusGained(self)
+ self:HighlightText();
+ self:SetFontObject("ChatFontSmall");
+ self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
+ if ( self:GetText() == SEARCH ) then
+ self:SetText("")
+ end
+ self.clearButton:Show();
+end
+
+ITEM_SEARCHBAR_LIST = {
+ "BagItemSearchBox",
+ "GuildItemSearchBox",
+ "VoidItemSearchBox",
+ "BankItemSearchBox",
+};
+
+function BagSearch_OnHide(self)
+ local allClosed = true;
+ for _,barName in pairs(ITEM_SEARCHBAR_LIST) do
+ local bar = _G[barName];
+ if bar and bar ~= self and bar:IsVisible() then
+ allClosed = false;
+ end
+ end
+ if ( allClosed ) then
+ self.clearButton:Click();
+ BagSearch_OnTextChanged(self);
+ end
+end
+
+function BagSearch_OnTextChanged(self, userChanged)
+ local text = self:GetText();
+ if ( text == SEARCH ) then
+ text = "";
+ end
+ SetItemSearch(text);
+ if (text ~= "") then
+ self.clearButton:Show();
+ else
+ self.clearButton:Hide();
+ end
+end
+
+function BagSearch_OnChar(self, text)
+ -- clear focus if the player is repeating keys (ie - trying to move)
+ -- TODO: move into base editbox code?
+ local MIN_REPEAT_CHARACTERS = 3
+ local searchString = self:GetText();
+ if (string.len(searchString) > MIN_REPEAT_CHARACTERS) then
+ local repeatChar = true;
+ for i=1, MIN_REPEAT_CHARACTERS, 1 do
+ if ( string.sub(searchString,(0-i), (0-i)) ~= string.sub(searchString,(-1-i),(-1-i)) ) then
+ repeatChar = false;
+ break;
+ end
+ end
+ if ( repeatChar ) then
+ self:ClearFocus();
+ end
+ end
+end
+
+function BagSearch_OnEditFocusGained(self)
+ SerachBoxTemplate_OnEditFocusGained(self);
+
+ for _,barName in pairs(ITEM_SEARCHBAR_LIST) do
+ local bar = _G[barName];
+ if bar and bar ~= self then
+ bar:SetText(SEARCH);
+ end
+ end
+end
+
+function BagSearch_OnEditFocusLost(self)
+ SerachBoxTemplate_OnEditFocusGained(self);
+
+ local search = self:GetText();
+ for _,barName in pairs(ITEM_SEARCHBAR_LIST) do
+ local bar = _G[barName];
+ if bar and bar ~= self then
+ bar:SetText(search);
+ end
+ end
+end
+
+-- Scrollframe functions
+function ScrollBar_AdjustAnchors(scrollBar, topAdj, bottomAdj, xAdj)
+ -- assumes default anchoring of topleft-topright, bottomleft-bottomright
+ local topY = 0;
+ local bottomY = 0;
+ local point, parent, refPoint, x, y;
+ for i = 1, 2 do
+ point, parent, refPoint, x, y = scrollBar:GetPoint(i);
+ if ( point == "TOPLEFT" ) then
+ topY = y;
+ elseif ( point == "BOTTOMLEFT" ) then
+ bottomY = y;
+ end
+ end
+ xAdj = xAdj or 0;
+ topAdj = topAdj or 0;
+ bottomAdj = bottomAdj or 0;
+ scrollBar:SetPoint("TOPLEFT", parent, "TOPRIGHT", x + xAdj, topY + topAdj);
+ scrollBar:SetPoint("BOTTOMLEFT", parent, "BOTTOMRIGHT", x + xAdj, bottomY + bottomAdj);
+end
+
+-- Truncated Button code
+
+function TruncatedButton_OnEnter(self)
+ local text = _G[self:GetName().."Text"];
+ if ( text:IsTruncated() ) then
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT");
+ GameTooltip:SetText(text:GetText());
+ GameTooltip:Show();
+ end
+end
+
+function TruncatedButton_OnLeave(self)
+ if ( GameTooltip:GetOwner() == self ) then
+ GameTooltip:Hide();
+ end
+end
+
+-- Magic Button code
+function MagicButton_OnLoad(self)
+ local leftHandled = false;
+ local rightHandled = false;
+
+ -- Find out where this button is anchored and adjust positions/separators as necessary
+ for i=1, self:GetNumPoints() do
+ local point, relativeTo, relativePoint, offsetX, offsetY = self:GetPoint(i);
+
+ if (relativeTo:GetObjectType() == "Button" and (point == "TOPLEFT" or point == "LEFT")) then
+
+ if (offsetX == 0 and offsetY == 0) then
+ self:SetPoint(point, relativeTo, relativePoint, 1, 0);
+ end
+
+ if (relativeTo.RightSeparator) then
+ -- Modify separator to make it a Middle
+ self.LeftSeparator = relativeTo.RightSeparator;
+ else
+ -- Add a Middle separator
+ self.LeftSeparator = self:CreateTexture(self:GetName().."_LeftSeparator", "BORDER");
+ relativeTo.RightSeparator = self.LeftSeparator;
+ end
+
+ self.LeftSeparator:SetTexture("Interface\\AddOns\\!!!ClassicAPI\\Texture\\FrameGeneral\\UI-Frame");
+ self.LeftSeparator:SetTexCoord("0.00781250", "0.10937500", "0.75781250", "0.95312500");
+ self.LeftSeparator:SetWidth(13);
+ self.LeftSeparator:SetHeight(25);
+ self.LeftSeparator:SetPoint("TOPRIGHT", self, "TOPLEFT", 5, 1);
+
+ leftHandled = true;
+
+ elseif (relativeTo:GetObjectType() == "Button" and (point == "TOPRIGHT" or point == "RIGHT")) then
+
+ if (offsetX == 0 and offsetY == 0) then
+ self:SetPoint(point, relativeTo, relativePoint, -1, 0);
+ end
+
+ if (relativeTo.LeftSeparator) then
+ -- Modify separator to make it a Middle
+ self.RightSeparator = relativeTo.LeftSeparator;
+ else
+ -- Add a Middle separator
+ self.RightSeparator = self:CreateTexture(self:GetName().."_RightSeparator", "BORDER");
+ relativeTo.LeftSeparator = self.RightSeparator;
+ end
+
+ self.RightSeparator:SetTexture("Interface\\AddOns\\!!!ClassicAPI\\Texture\\FrameGeneral\\UI-Frame");
+ self.RightSeparator:SetTexCoord("0.00781250", "0.10937500", "0.75781250", "0.95312500");
+ self.RightSeparator:SetWidth(13);
+ self.RightSeparator:SetHeight(25);
+ self.RightSeparator:SetPoint("TOPLEFT", self, "TOPRIGHT", -5, 1);
+
+ rightHandled = true;
+
+ elseif (point == "BOTTOMLEFT") then
+ if (offsetX == 0 and offsetY == 0) then
+ self:SetPoint(point, relativeTo, relativePoint, 4, 4);
+ end
+ leftHandled = true;
+ elseif (point == "BOTTOMRIGHT") then
+ if (offsetX == 0 and offsetY == 0) then
+ self:SetPoint(point, relativeTo, relativePoint, -6, 4);
+ end
+ rightHandled = true;
+ elseif (point == "BOTTOM") then
+ if (offsetY == 0) then
+ self:SetPoint(point, relativeTo, relativePoint, 0, 4);
+ end
+ end
+ end
+
+ -- If this button didn't have a left anchor, add the left border texture
+ if (not leftHandled) then
+ if (not self.LeftSeparator) then
+ -- Add a Left border
+ self.LeftSeparator = self:CreateTexture(self:GetName().."_LeftSeparator", "BORDER");
+ self.LeftSeparator:SetTexture("Interface\\AddOns\\!!!ClassicAPI\\Texture\\FrameGeneral\\UI-Frame");
+ self.LeftSeparator:SetTexCoord("0.24218750", "0.32812500", "0.63281250", "0.82812500");
+ self.LeftSeparator:SetWidth(11);
+ self.LeftSeparator:SetHeight(25);
+ self.LeftSeparator:SetPoint("TOPRIGHT", self, "TOPLEFT", 6, 1);
+ end
+ end
+
+ -- If this button didn't have a right anchor, add the right border texture
+ if (not rightHandled) then
+ if (not self.RightSeparator) then
+ -- Add a Right border
+ self.RightSeparator = self:CreateTexture(self:GetName().."_RightSeparator", "BORDER");
+ self.RightSeparator:SetTexture("Interface\\AddOns\\!!!ClassicAPI\\Texture\\FrameGeneral\\UI-Frame");
+ self.RightSeparator:SetTexCoord("0.90625000", "0.99218750", "0.00781250", "0.20312500");
+ self.RightSeparator:SetWidth(11);
+ self.RightSeparator:SetHeight(25);
+ self.RightSeparator:SetPoint("TOPLEFT", self, "TOPRIGHT", -6, 1);
+ end
+ end
+end
+
+-- ButtonFrameTemplate code
+function ButtonFrameTemplate_HideButtonBar(self)
+ if self.bottomInset then
+ self.bottomInset:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", PANEL_INSET_RIGHT_OFFSET, PANEL_INSET_BOTTOM_OFFSET);
+ else
+ _G[self:GetName() .. "Inset"]:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", PANEL_INSET_RIGHT_OFFSET, PANEL_INSET_BOTTOM_OFFSET);
+ end
+ _G[self:GetName() .. "BtnCornerLeft"]:Hide();
+ _G[self:GetName() .. "BtnCornerRight"]:Hide();
+ _G[self:GetName() .. "ButtonBottomBorder"]:Hide();
+end
+
+function ButtonFrameTemplate_ShowButtonBar(self)
+ if self.topInset then
+ self.topInset:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", PANEL_INSET_RIGHT_OFFSET, PANEL_INSET_BOTTOM_BUTTON_OFFSET);
+ else
+ _G[self:GetName() .. "Inset"]:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", PANEL_INSET_RIGHT_OFFSET, PANEL_INSET_BOTTOM_BUTTON_OFFSET);
+ end
+ _G[self:GetName() .. "BtnCornerLeft"]:Show();
+ _G[self:GetName() .. "BtnCornerRight"]:Show();
+ _G[self:GetName() .. "ButtonBottomBorder"]:Show();
+end
+
+function ButtonFrameTemplate_HideAttic(self)
+ if self.topInset then
+ self.topInset:SetPoint("TOPLEFT", self, "TOPLEFT", PANEL_INSET_LEFT_OFFSET, PANEL_INSET_TOP_OFFSET);
+ else
+ self.Inset:SetPoint("TOPLEFT", self, "TOPLEFT", PANEL_INSET_LEFT_OFFSET, PANEL_INSET_TOP_OFFSET);
+ end
+ self.TopTileStreaks:Hide();
+end
+
+function ButtonFrameTemplate_ShowAttic(self)
+ if self.topInset then
+ self.topInset:SetPoint("TOPLEFT", self, "TOPLEFT", PANEL_INSET_LEFT_OFFSET, PANEL_INSET_ATTIC_OFFSET);
+ else
+ self.Inset:SetPoint("TOPLEFT", self, "TOPLEFT", PANEL_INSET_LEFT_OFFSET, PANEL_INSET_ATTIC_OFFSET);
+ end
+ self.TopTileStreaks:Show();
+end
+
+
+function ButtonFrameTemplate_HidePortrait(self)
+ self.portrait:Hide();
+ self.portraitFrame:Hide();
+ self.topLeftCorner:Show();
+ self.topBorderBar:SetPoint("TOPLEFT", self.topLeftCorner, "TOPRIGHT", 0, 0);
+ self.leftBorderBar:SetPoint("TOPLEFT", self.topLeftCorner, "BOTTOMLEFT", 0, 0);
+end
+
+
+function ButtonFrameTemplate_ShowPortrait(self)
+ self.portrait:Show();
+ self.portraitFrame:Show();
+ self.topLeftCorner:Hide();
+ self.topBorderBar:SetPoint("TOPLEFT", self.portraitFrame, "TOPRIGHT", 0, -10);
+ self.leftBorderBar:SetPoint("TOPLEFT", self.portraitFrame, "BOTTOMLEFT", 8, 0);
+end
+
+
+-- SquareButton template code
+SQUARE_BUTTON_TEXCOORDS = {
+ ["UP"] = { 0.45312500, 0.64062500, 0.01562500, 0.20312500};
+ ["DOWN"] = { 0.45312500, 0.64062500, 0.20312500, 0.01562500};
+ ["LEFT"] = { 0.23437500, 0.42187500, 0.01562500, 0.20312500};
+ ["RIGHT"] = { 0.42187500, 0.23437500, 0.01562500, 0.20312500};
+ ["DELETE"] = { 0.01562500, 0.20312500, 0.01562500, 0.20312500};
+};
+
+function SquareButton_SetIcon(self, name)
+ local coords = SQUARE_BUTTON_TEXCOORDS[strupper(name)];
+ if (coords) then
+ self.icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]);
+ end
+end
+
+
+-- Cap progress bar
+function CapProgressBar_SetNotches(capBar, count)
+ local barWidth = capBar:GetWidth();
+ local barName = capBar:GetName();
+
+ if ( capBar.notchCount and capBar.notchCount > count ) then
+ for i = count + 1, capBar.notchCount do
+ _G[barName.."Divider"..i]:Hide();
+ end
+ end
+
+ local notchWidth = barWidth / count;
+
+ for i=1, count - 1 do
+ local notch = _G[barName.."Divider"..i];
+ if ( not notch ) then
+ notch = capBar:CreateTexture(barName.."Divider"..i, "BORDER", "CapProgressBarDividerTemplate", -1);
+ end
+ notch:ClearAllPoints();
+ notch:SetPoint("LEFT", capBar, "LEFT", notchWidth * i - 2, 0);
+ end
+ capBar.notchCount = count;
+end
+
+function CapProgressBar_Update(capBar, cap1Quantity, cap1Limit, cap2Quantity, cap2Limit, totalQuantity, totalLimit, hasNoSharedStats)
+ if ( totalLimit == 0) then
+ return;
+ end
+
+ local barWidth = capBar:GetWidth();
+ local sizePerPoint = barWidth / totalLimit;
+ local progressWidth = totalQuantity * sizePerPoint;
+
+ local cap1Width, cap2Width;
+ if ( cap2Quantity and cap2Limit ) then
+ cap1Width = min(cap1Limit - cap1Quantity, cap2Limit - cap2Quantity) * sizePerPoint; --cap1 can't go past the cap2 LFG limit either.
+ cap2Width = (cap2Limit - cap2Quantity) * sizePerPoint - cap1Width;
+ else
+ cap1Width = (cap1Limit - cap1Quantity) * sizePerPoint;
+ cap2Width = 0;
+ end
+
+ --Don't let it go past the end.
+ progressWidth = min(progressWidth, barWidth);
+ cap1Width = min(cap1Width, barWidth - progressWidth);
+ cap2Width = min(cap2Width, barWidth - progressWidth - cap1Width);
+ capBar.progress:SetWidth(progressWidth);
+
+ capBar.cap1:SetWidth(cap1Width);
+ capBar.cap2:SetWidth(cap2Width);
+
+ local lastFrame, lastRelativePoint = capBar, "LEFT";
+
+ if ( progressWidth > 0 ) then
+ capBar.progress:Show();
+ capBar.progress:SetPoint("LEFT", lastFrame, lastRelativePoint, 0, 0);
+ lastFrame, lastRelativePoint = capBar.progress, "RIGHT";
+ else
+ capBar.progress:Hide();
+ end
+
+ if ( cap1Width > 0 and not hasNoSharedStats) then
+ capBar.cap1:Show();
+ capBar.cap1Marker:Show();
+ capBar.cap1:SetPoint("LEFT", lastFrame, lastRelativePoint, 0, 0);
+ lastFrame, lastRelativePoint = capBar.cap1, "RIGHT";
+ else
+ capBar.cap1:Hide();
+ capBar.cap1Marker:Hide();
+ end
+
+ if ( cap2Width > 0 and not hasNoSharedStats) then
+ capBar.cap2:Show();
+ capBar.cap2Marker:Show();
+ capBar.cap2:SetPoint("LEFT", lastFrame, lastRelativePoint, 0, 0);
+ lastFrame, lastRelativePoint = capBar.cap2, "RIGHT";
+ else
+ capBar.cap2:Hide();
+ capBar.cap2Marker:Hide();
+ end
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIPanelTemplates.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIPanelTemplates.xml
new file mode 100644
index 0000000..b0ad77d
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/UIPanelTemplates.xml
@@ -0,0 +1,2381 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if self:IsEnabled() ~= 0 then
+ self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
+ if ( self.Icon ) then
+ if ( not self.Icon.oldPoint ) then
+ local point, relativeTo, relativePoint, x, y = self.Icon:GetPoint(1)
+ self.Icon.oldPoint = point
+ self.Icon.oldX = x
+ self.Icon.oldY = y
+ end
+ self.Icon:SetPoint(self.Icon.oldPoint, self.Icon.oldX + 1, self.Icon.oldY - 1)
+ end
+ end
+
+
+ if self:IsEnabled() ~= 0 then
+ self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ if ( self.Icon ) then
+ self.Icon:SetPoint(self.Icon.oldPoint, self.Icon.oldX, self.Icon.oldY)
+ end
+ end
+
+
+ if self:IsEnabled() ~= 0 then
+ -- we need to reset our textures just in case we were hidden before a mouse up fired
+ self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ end
+
+
+ self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+ self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
+
+
+ if(self.tooltipText ~= nil) then
+ GameTooltip_AddNewbieTip(self, self.tooltipText, 1.0, 1.0, 1.0, self.newbieText)
+ end
+
+
+ if(self.tooltipText ~= nil) then
+ GameTooltip:Hide()
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = self:GetParent();
+ local scrollStep = self:GetParent().scrollStep or (parent:GetHeight() / 2);
+ parent:SetValue(parent:GetValue() - scrollStep);
+ PlaySound("UChatScrollButton");
+
+
+
+
+
+
+
+
+
+ local parent = self:GetParent();
+ local scrollStep = self:GetParent().scrollStep or (parent:GetHeight() / 2);
+ parent:SetValue(parent:GetValue() + scrollStep);
+ PlaySound("UChatScrollButton");
+
+
+
+
+
+
+ self:GetParent():SetVerticalScroll(value);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local parent = self:GetParent();
+ local scrollStep = self:GetParent().scrollStep or (parent:GetHeight() / 2);
+ parent:SetValue(parent:GetValue() - scrollStep);
+ PlaySound("UChatScrollButton");
+
+
+
+
+
+
+
+
+
+ local parent = self:GetParent();
+ local scrollStep = self:GetParent().scrollStep or (parent:GetHeight() / 2);
+ parent:SetValue(parent:GetValue() + scrollStep);
+ PlaySound("UChatScrollButton");
+
+
+
+
+
+
+ self:GetParent():SetVerticalScroll(value);
+
+
+
+
+
+
+
+
+
+
+
+
+ ScrollFrame_OnLoad(self);
+
+
+ ScrollFrame_OnScrollRangeChanged(self, xrange, yrange);
+
+
+
+ local scrollbar = _G[self:GetName().."ScrollBar"];
+ scrollbar:SetValue(offset);
+ local min;
+ local max;
+ min, max = scrollbar:GetMinMaxValues();
+ if ( offset == 0 ) then
+ _G[scrollbar:GetName().."ScrollUpButton"]:Disable();
+ else
+ _G[scrollbar:GetName().."ScrollUpButton"]:Enable();
+ end
+ if ((scrollbar:GetValue() - max) == 0) then
+ _G[scrollbar:GetName().."ScrollDownButton"]:Disable();
+ else
+ _G[scrollbar:GetName().."ScrollDownButton"]:Enable();
+ end
+
+
+ ScrollFrameTemplate_OnMouseWheel(self, delta);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self.texture:SetAlpha(1.0);
+
+
+ self.texture:SetAlpha(0.5);
+
+
+ if self:IsEnabled() then
+ self.texture:SetPoint("TOPLEFT", 1, -1);
+ end
+
+
+ self.texture:SetPoint("TOPLEFT", 0, 0);
+
+
+ PlaySound("igMainMenuOptionCheckBoxOn");
+ local editBox = self:GetParent();
+ if editBox.clearFunc then
+ editBox.clearFunc(editBox);
+ end
+
+ editBox:SetText("");
+ if not editBox:HasFocus() then
+ editBox:GetScript("OnEditFocusLost")(editBox);
+ end
+ editBox:ClearFocus();
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SmallMoneyFrame_OnLoad(self);
+ MoneyFrame_SetType(self, "STATIC");
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ _G[self:GetName().."Texture"]:SetPoint("TOPLEFT", 1, -1);
+
+
+ _G[self:GetName().."Texture"]:SetPoint("TOPLEFT", 0, 0);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if self:IsEnabled() then
+ self.icon:SetPoint("CENTER", -1, -1);
+ end
+
+
+ self.icon:SetPoint("CENTER", 0, 0);
+
+
+ SetDesaturation(self.icon, true);
+ self.icon:SetAlpha(0.5);
+
+
+ SetDesaturation(self.icon, false);
+ self.icon:SetAlpha(1.0);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if ( self:IsEnabled() ) then
+ local name = self:GetName();
+ _G[name.."Left"]:SetTexture("Interface\\Buttons\\UI-SilverButtonLG-Left-Down");
+ _G[name.."Middle"]:SetTexture("Interface\\Buttons\\UI-SilverButtonLG-Mid-Down");
+ _G[name.."Right"]:SetTexture("Interface\\Buttons\\UI-SilverButtonLG-Right-Down");
+ local contentsFrame = _G[name.."ContentsFrame"];
+ if ( contentsFrame ) then
+ contentsFrame:SetPoint("TOPLEFT", -2, -1);
+ end
+ end
+
+
+ if ( self:IsEnabled() ) then
+ local name = self:GetName();
+ _G[name.."Left"]:SetTexture("Interface\\Buttons\\UI-SilverButtonLG-Left-Up");
+ _G[name.."Middle"]:SetTexture("Interface\\Buttons\\UI-SilverButtonLG-Mid-Up");
+ _G[name.."Right"]:SetTexture("Interface\\Buttons\\UI-SilverButtonLG-Right-Up");
+ local contentsFrame = _G[name.."ContentsFrame"];
+ if ( contentsFrame ) then
+ contentsFrame:SetPoint("TOPLEFT", 0, 0);
+ end
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ local text = _G[self:GetName().."Text"];
+ text:SetWidth(self:GetWidth() - 5);
+ text:SetHeight(self:GetHeight());
+
+
+ TruncatedButton_OnEnter(self);
+
+
+ TruncatedButton_OnLeave(self);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self.TopRight:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0);
+ self.BottomRight:SetTexCoord(1, 1, 1, 0, 0, 1, 0, 0);
+ self.BottomLeft:SetTexCoord(1, 0, 0, 0, 1, 1, 0, 1);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ExampleButtonFramePortrait:SetTexture("Interface\\FriendsFrame\\FriendsFrameScrollIcon");
+ ExampleButtonFrameTitleText:SetText("Example Button Frame");
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self.backdropInfo = _G["BACKDROP_SLIDER_8_8"]
+
+
+
+
+
+
+
+
+
+
+
+
+ XML_UseParentLevel(self)
+ XML_NineSlice(self, "Dialog")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/XML_Helper.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/XML_Helper.lua
new file mode 100644
index 0000000..49004a0
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Templates/XML_Helper.lua
@@ -0,0 +1,9 @@
+function XML_NineSlice(Self, Layout)
+ Self.layoutType = Layout
+ Mixin(Self, NineSlicePanelMixin)
+ Self:OnLoad()
+end
+
+function XML_UseParentLevel(Self)
+ Self:SetFrameLevel(Self:GetParent():GetFrameLevel())
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/BUTTONS/UI-Panel-Button-YellowHighlight.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/BUTTONS/UI-Panel-Button-YellowHighlight.blp
new file mode 100644
index 0000000..783309a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/BUTTONS/UI-Panel-Button-YellowHighlight.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/ContainerFrame/Bags.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/ContainerFrame/Bags.blp
new file mode 100644
index 0000000..d36fd2c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/ContainerFrame/Bags.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/!UI-Frame.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/!UI-Frame.blp
new file mode 100644
index 0000000..5ef38ff
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/!UI-Frame.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Background-Marble.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Background-Marble.blp
new file mode 100644
index 0000000..9610174
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Background-Marble.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Background-Rock.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Background-Rock.blp
new file mode 100644
index 0000000..4a83ae0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Background-Rock.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Frame.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Frame.blp
new file mode 100644
index 0000000..ee1959d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UI-Frame.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UIFrameDiamondMetal.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UIFrameDiamondMetal.blp
new file mode 100644
index 0000000..de1cba8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UIFrameDiamondMetal.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UIFrameDiamondMetalVertical.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UIFrameDiamondMetalVertical.blp
new file mode 100644
index 0000000..93679ed
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/UIFrameDiamondMetalVertical.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/_UI-Frame.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/_UI-Frame.blp
new file mode 100644
index 0000000..9be9170
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/FrameGeneral/_UI-Frame.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Edge.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Edge.blp
new file mode 100644
index 0000000..0596975
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Edge.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Fill.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Fill.blp
new file mode 100644
index 0000000..557089c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Fill.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Overabsorb.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Overabsorb.blp
new file mode 100644
index 0000000..c5d2f65
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Absorb-Overabsorb.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Hp-Bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Hp-Bg.blp
new file mode 100644
index 0000000..ed54d6d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Hp-Bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Hp-Fill.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Hp-Fill.blp
new file mode 100644
index 0000000..3dc7325
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Hp-Fill.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Resource-Background.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Resource-Background.blp
new file mode 100644
index 0000000..0da9558
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Resource-Background.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Resource-Fill.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Resource-Fill.blp
new file mode 100644
index 0000000..cf38418
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Bar-Resource-Fill.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-FrameHighlights.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-FrameHighlights.blp
new file mode 100644
index 0000000..1bc20d8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-FrameHighlights.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-HSeparator.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-HSeparator.blp
new file mode 100644
index 0000000..db903e6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-HSeparator.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffCurse.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffCurse.blp
new file mode 100644
index 0000000..fd89e33
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffCurse.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffDisease.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffDisease.blp
new file mode 100644
index 0000000..fb738b8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffDisease.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffMagic.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffMagic.blp
new file mode 100644
index 0000000..4777e55
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffMagic.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffPoison.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffPoison.blp
new file mode 100644
index 0000000..823e6d4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-DebuffPoison.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-Rez.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-Rez.blp
new file mode 100644
index 0000000..17b160f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Icon-Rez.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-BottomFade.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-BottomFade.blp
new file mode 100644
index 0000000..09b646e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-BottomFade.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-Down.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-Down.blp
new file mode 100644
index 0000000..433c0d9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-Down.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-TopFade.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-TopFade.blp
new file mode 100644
index 0000000..aa33d22
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-TopFade.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-Up.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-Up.blp
new file mode 100644
index 0000000..b7f2ef8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-Move-Up.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-VSeparator.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-VSeparator.blp
new file mode 100644
index 0000000..82bacfe
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-VSeparator.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-WorldPing.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-WorldPing.blp
new file mode 100644
index 0000000..ad9be98
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Raid-WorldPing.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomLeft.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomLeft.blp
new file mode 100644
index 0000000..93906b4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomLeft.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomMiddle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomMiddle.blp
new file mode 100644
index 0000000..740f105
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomMiddle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomRight.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomRight.blp
new file mode 100644
index 0000000..702879e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-BottomRight.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-Left.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-Left.blp
new file mode 100644
index 0000000..fa5274c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-Left.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-Right.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-Right.blp
new file mode 100644
index 0000000..6d57a22
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-Right.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperLeft.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperLeft.blp
new file mode 100644
index 0000000..b54a55c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperLeft.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperMiddle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperMiddle.blp
new file mode 100644
index 0000000..9331823
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperMiddle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperRight.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperRight.blp
new file mode 100644
index 0000000..73eef09
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidBorder-UpperRight.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidFrameSummon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidFrameSummon.blp
new file mode 100644
index 0000000..14b460e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidFrameSummon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomLeft.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomLeft.blp
new file mode 100644
index 0000000..b20def4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomLeft.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomMiddle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomMiddle.blp
new file mode 100644
index 0000000..8ba4ed8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomMiddle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomRight.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomRight.blp
new file mode 100644
index 0000000..dfcaa5e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-BottomRight.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Left.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Left.blp
new file mode 100644
index 0000000..6bad137
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Left.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Right.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Right.blp
new file mode 100644
index 0000000..935c9bc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Right.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Toggle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Toggle.blp
new file mode 100644
index 0000000..4e7ac23
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-Toggle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperLeft.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperLeft.blp
new file mode 100644
index 0000000..788e8f4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperLeft.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperMiddle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperMiddle.blp
new file mode 100644
index 0000000..63b7d5d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperMiddle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperRight.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperRight.blp
new file mode 100644
index 0000000..16daff2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/RaidPanel-UpperRight.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Fill.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Fill.blp
new file mode 100644
index 0000000..204cec6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Fill.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Overlay.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Overlay.blp
new file mode 100644
index 0000000..55c89be
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Overlay.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Overshield.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Overshield.blp
new file mode 100644
index 0000000..9b0d756
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Texture/RAIDFRAME/Shield-Overshield.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/API.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/API.lua
new file mode 100644
index 0000000..ec0f627
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/API.lua
@@ -0,0 +1,166 @@
+local _G = _G
+local Mod = mod
+local Next = next
+local PCall = pcall
+local Error = error
+local Ceil = math.ceil
+local GetTime = GetTime
+local GetCVar = GetCVar
+local Sub = string.gsub
+local String = tostring
+local Floor = math.floor
+local SecureCall = securecall
+local GetAddOnInfo = GetAddOnInfo
+local IsAddOnLoaded = IsAddOnLoaded
+local IsAddOnLoadOnDemand = IsAddOnLoadOnDemand
+
+function InGlue()
+ return false
+end
+
+function AnimateTexCoords(Self, Width, Height, FrameW, FrameH, NumFrames, Elapsed, Throt)
+ local Throt = Throt or Self.throttle or 0.1
+
+ if ( not Self.frame ) then
+ Self.frame = 1
+ Self.Throt = Throt
+ Self.numColumns = Floor(Width / FrameW)
+ Self.numRows = Floor(Height / FrameH)
+ Self.columnWidth = FrameW / Width
+ Self.rowHeight = FrameH / Height
+ end
+
+ if ( not Self.Throt or Self.Throt > Throt ) then
+ local Frame = Self.frame
+ local FramesToAdvance = Floor(Self.Throt / Throt)
+ while Frame + FramesToAdvance > NumFrames do
+ Frame = Frame - NumFrames
+ end
+
+ Frame = Frame + FramesToAdvance
+ Self.Throt = 0
+ local SetLeft = Mod(Frame - 1, Self.numColumns) * Self.columnWidth
+ local SetRight = SetLeft + Self.columnWidth
+ local SetBottom = Ceil(Frame / Self.numColumns) * Self.rowHeight
+ local SetTop = SetBottom - Self.rowHeight
+ Self:SetTexCoord(SetLeft, SetRight, SetTop, SetBottom)
+ Self.frame = Frame
+ else
+ Self.Throt = Self.Throt + Elapsed
+ end
+end
+
+function GetTexCoordsForRoleSmallCircle(Role)
+ if ( Role == "TANK" ) then
+ return 0, 19/64, 22/64, 41/64
+ elseif ( Role == "HEALER" ) then
+ return 20/64, 39/64, 1/64, 20/64
+ elseif ( Role == "DAMAGER" ) then
+ return 20/64, 39/64, 22/64, 41/64
+ else
+ Error("Unknown role: "..String(Role))
+ end
+end
+
+function PassClickToParent(Self, ...)
+ -- This is def not working on secure frames.
+ local Parent = Self:GetParent()
+ local OnClick = Parent:GetScript("OnClick")
+ if ( OnClick ) then
+ OnClick(Parent, ...)
+ end
+end
+
+local function secureexecutenext(Table, Prev, Func, ...)
+ local Key, Value = Next(Table, Prev)
+ if ( Key ~= nil ) then
+ PCall(Func, Key, Value, ...) -- Errors are silently discarded!
+ end
+ return Key
+end
+
+function secureexecuterange(Table, Func, ...)
+ local Key = nil
+ repeat
+ Key = SecureCall(secureexecutenext, Table, Key, Func, ...)
+ until Key == nil
+end
+
+function securecallfunction(Func, ...)
+ return SecureCall(Func, ...)
+end
+
+function GetAddOnEnableState(Character, Index)
+ -- Can't get per-char addons, doing what we can.
+ local _, _, _, Enabled, Loadable = GetAddOnInfo(Index)
+ return (Enabled) and 2 or 0
+end
+
+function IsAddonVersionCheckEnabled()
+ return GetCVar("checkAddonVersion") == "1"
+end
+
+function C_GetAddOnInfo(Index)
+ local Name, Title, Notes, Enabled, Loadable, Reason, Security = GetAddOnInfo(Index)
+ local NewVersion = nil
+
+ -- Missing "Reason" values: BANNED", "CORRUPT", "DEMAND_LOADED", "INCOMPATIBLE"
+ if ( Loadable and not IsAddOnLoaded(Index) and IsAddOnLoadOnDemand(Index) ) then
+ Reason = "DEMAND_LOADED"
+ Loadable = nil
+ end
+
+ return Name, Title, Notes, Loadable, Reason, Security, NewVersion
+end
+
+function HasOverrideActionBar()
+ return _G.BonusActionBarFrame:IsShown()
+end
+
+function HasVehicleActionBar()
+ return _G.VehicleMenuBar:IsShown()
+end
+
+--[[function GetSchoolString(arg)
+ -- TODO
+ -- Return name of spell school?
+ return "_UNKNOWN_"
+ --print(arg)
+ --SchoolStringTable[school]
+end]]
+
+function GetDifficultyInfo(ID)
+ --return "0 Player (Heroic)", "party", false, false, false, false, nil
+end
+
+function GetServerTime()
+ -- This will cause problems. We could use game time somehow.
+ return GetTime()
+end
+
+function GetNormalizedRealmName()
+ return Sub(GetRealmName(), "[-%s]", "")
+end
+
+function Ambiguate(FullName, Context)
+ -- TODO: Make diff context work properly.
+ return FullName
+end
+
+function CombatLogGetCurrentEventInfo(Timestamp, SubEvent, SrcGUID, SrcName, SrcFlag, DstGUID, DstName, DstFlag, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12)
+ if ( Timestamp ) then
+ -- Modern payload (Missing)
+ local HideCaster, SrcRaidFlag, DstRaidFlag = false, nil, nil
+
+ -- Note: Blizzard could have changed order of payload from 9th onwards.
+ return Timestamp, SubEvent, HideCaster, SrcGUID, SrcName, SrcFlag, SrcRaidFlag, DstGUID, DstName, DstFlag, DstRaidFlag, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12
+ end
+end
+
+function ActionButton_HideOverlayGlow()
+ -- TODO
+end
+
+function ActionButton_ShowOverlayGlow()
+ -- TODO
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AddOnCompat.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AddOnCompat.lua
new file mode 100644
index 0000000..caa2b1b
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AddOnCompat.lua
@@ -0,0 +1,122 @@
+local _G = _G
+local Select = select
+local Sort = table.sort
+local Upper = string.upper
+local GetAddOnInfo = GetAddOnInfo
+
+local Processor, DB, DBAddOn
+
+local function Handler(Self, Event, AddOn)
+ if ( Event == "ADDON_LOADED" ) then
+ local Compat = DB[AddOn]
+ if ( Compat ) then
+ for Name, Value in pairs(Compat) do
+ _G[Name] = (Value ~= "nil") and Value or nil
+ end
+ end
+ else
+ DB = nil
+ DBAddOn = nil
+ Self:UnregisterEvent(Event)
+ Self:UnregisterEvent("ADDON_LOADED")
+ Self:SetScript("OnEvent", nil)
+ end
+end
+
+local function FindPreviousAddOn(AddOn)
+ if ( not DBAddOn ) then
+ local IsAddOnLoadOnDemand = IsAddOnLoadOnDemand
+ local Count = 1
+
+ DBAddOn = {}
+
+ for i=1,GetNumAddOns() do
+ local Name, _, _, Enabled = GetAddOnInfo(i)
+ if ( Enabled and not IsAddOnLoadOnDemand(i) ) then
+ DBAddOn[Count] = Name
+ Count = Count + 1
+ end
+ end
+
+ Sort(DBAddOn, function (a, b) return Upper(a) < Upper(b) end)
+ end
+
+ for i=1,#DBAddOn do
+ if ( DBAddOn[i] == AddOn and i > 1 ) then
+ return DBAddOn[i-1]
+ end
+ end
+end
+
+local function Process(AddOn, Mode, ...)
+ local _, _, _, Enabled = GetAddOnInfo(AddOn)
+
+ if ( Enabled ) then
+ if ( Mode == "ALERT" ) then
+ local Meta, Flag = ...
+
+ if ( GetAddOnMetadata(AddOn, Meta) == Flag ) then
+ local PopupID = "CAPI_ADDONCOMPAT_"..AddOn
+ local URL = "https://gitlab.com/users/Tsoukie/projects"
+ local Width, Height = 540, 200
+
+ StaticPopupDialogs[PopupID] = {
+ text = "ClassicAPI - Incompatible:\n\n|cffFFA500%s (%s)|r\n\nTo avoid issues please use version at:",
+ button1 = CLOSE,
+ timeout = 0,
+ whileDead = true,
+ hideOnEscape = false,
+ showAlert = true,
+ hasEditBox = 1,
+ hasWideEditBox = 1,
+ OnShow = function (Self)
+ Self:SetSize(Width, Height)
+ Self.maxHeightSoFar = Height
+ Self.maxWidthSoFar = Width
+
+ local Edit = Self.wideEditBox
+ Edit:SetWidth(300)
+ Edit:SetText(URL)
+ Edit:SetScript("OnTextChanged", function(S) S:SetText(URL) end)
+ end,
+ }
+
+ StaticPopup_Show(PopupID, AddOn, Flag)
+ end
+ else
+ if ( not Processor ) then
+ DB = {}
+ Processor = CreateFrame("Frame")
+ Processor:SetScript("OnEvent", Handler)
+ Processor:RegisterEvent("ADDON_LOADED")
+ Processor:RegisterEvent("PLAYER_LOGIN")
+ end
+
+ if ( Mode == "NULL" ) then
+ Process(FindPreviousAddOn(AddOn), "NULL_PROCESSED", ...)
+ end
+
+ local Compat = DB[AddOn]
+ if ( not Compat ) then
+ if ( Mode and DB[Mode] ) then
+ DB[AddOn] = DB[Mode]
+ return
+ end
+
+ Compat = {}
+ DB[AddOn] = Compat
+ end
+
+ for i=1, Select("#", ...) do
+ local Obj = Select(i, ...)
+ Compat[Obj] = (Mode == "NULL_PROCESSED") and "nil" or _G[Obj]
+ end
+ end
+ end
+end
+
+Process("WeakAuras", nil, "IsInGroup", "IsInRaid", "GetNumGroupMembers")
+Process("Details", "WeakAuras", "IsInGroup", "IsInRaid", "GetNumGroupMembers")
+Process("AI_VoiceOver", "NULL", "WOW_PROJECT_ID")
+Process("Outfitter", "NULL", "UnitGetIncomingHeals")
+Process("CompactRaidFrame", "ALERT", "Author", "RomanSpector & Blizzard")
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AnchorUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AnchorUtil.lua
new file mode 100644
index 0000000..525d217
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AnchorUtil.lua
@@ -0,0 +1,335 @@
+
+AnchorMixin = {};
+
+function AnchorMixin:Init(point, relativeTo, relativePoint, x, y)
+ self:Set(point, relativeTo, relativePoint, x, y);
+end
+
+function AnchorMixin:Set(point, relativeTo, relativePoint, x, y)
+ self.point = point;
+ self.relativeTo = relativeTo;
+ self.relativePoint = relativePoint;
+ self.x = x;
+ self.y = y;
+end
+
+function AnchorMixin:SetFromPoint(region, pointIndex)
+ -- TODO: Need to check if this has no point set...probably don't want default behavior in some cases, probably
+ -- want to signal something or return an invalid anchor.
+ self:Set(region:GetPoint(pointIndex));
+end
+
+function AnchorMixin:Get()
+ local point = self.point or "TOPLEFT";
+ local relativePoint = self.relativePoint or "TOPLEFT";
+ local x = self.x or 0;
+ local y = self.y or 0;
+ return point, self.relativeTo, relativePoint, x, y;
+end
+
+function AnchorMixin:SetPoint(region, clearAllPoints)
+ if clearAllPoints then
+ region:ClearAllPoints();
+ end
+ region:SetPoint(self:Get());
+end
+
+function AnchorMixin:SetPointWithExtraOffset(region, clearAllPoints, extraOffsetX, extraOffsetY)
+ if clearAllPoints then
+ region:ClearAllPoints();
+ end
+ local point, relativeTo, relativePoint, x, y = self:Get();
+ region:SetPoint(point, relativeTo, relativePoint, x + extraOffsetX, y + extraOffsetY);
+end
+
+function CreateAnchor(point, relativeTo, relativePoint, x, y)
+ return CreateAndInitFromMixin(AnchorMixin, point, relativeTo, relativePoint or point, x or 0, y or 0);
+end
+
+GridLayoutMixin = {};
+
+-- If isVertical is true lay out columns first then rows, otherwise we lay out rows then columns
+-- So for example with a stride of 2 and 6 frames this is how they would look using TopLeftToBottomRight and TopLeftToBottomRightVertical:
+--
+-- TopLeftToBottomRight TopLeftToBottomRightVertical
+-- 1 2 1 3 5
+-- 3 4 2 4 6
+-- 5 6
+GridLayoutMixin.Direction = {
+ TopLeftToBottomRight = { x = 1, y = -1 },
+ TopRightToBottomLeft = { x = -1, y = -1 },
+ BottomRightToTopLeft = { x = -1, y = 1 },
+ TopLeftToBottomRightVertical = { x = 1, y = -1, isVertical = true },
+ TopRightToBottomLeftVertical = { x = -1, y = -1, isVertical = true },
+};
+
+function GridLayoutMixin:Init(direction, stride, paddingX, paddingY, horizontalSpacing, verticalSpacing)
+ self.direction = direction or GridLayoutMixin.Direction.TopLeftToBottomRight;
+ self.stride = stride or 1;
+ self.paddingX = paddingX or 0;
+ self.paddingY = paddingY or 0;
+ self.horizontalSpacing = horizontalSpacing;
+ self.verticalSpacing = verticalSpacing;
+end
+
+function GridLayoutMixin:SetCustomOffsetFunction(func)
+ self.customOffsetFunction = func;
+end
+
+function GridLayoutMixin:GetCustomOffset(row, col)
+ if self.customOffsetFunction then
+ return self.customOffsetFunction(row, col);
+ end
+
+ return 0, 0;
+end
+
+
+AnchorUtil = {};
+
+AnchorUtil.CreateAnchor = GenerateClosure(CreateAndInitFromMixin, AnchorMixin);
+AnchorUtil.CreateGridLayout = GenerateClosure(CreateAndInitFromMixin, GridLayoutMixin);
+
+
+function AnchorUtil.CreateAnchorFromPoint(region, pointIndex)
+ local anchor = AnchorUtil.CreateAnchor();
+ anchor:SetFromPoint(region, pointIndex);
+ return anchor;
+end
+
+-- For initialAnchor and layout, use AnchorUtil.CreateAnchor(...) and AnchorUtil.CreateGridLayout(...)
+function AnchorUtil.GridLayout(frames, initialAnchor, layout)
+ if #frames <= 0 then
+ return;
+ end
+
+ local width = layout.horizontalSpacing or frames[1]:GetWidth();
+ local height = layout.verticalSpacing or frames[1]:GetHeight();
+ local stride = layout.stride;
+ local paddingX = layout.paddingX;
+ local paddingY = layout.paddingY;
+ local direction = layout.direction;
+ for i, frame in ipairs(frames) do
+ local row = math.floor((i - 1) / stride) + 1;
+ local col = (i - 1) % stride + 1;
+ if direction.isVertical then
+ local tempRow = row;
+ row = col;
+ col = tempRow;
+ end
+ local clearAllPoints = true;
+ local customOffsetX, customOffsetY = layout:GetCustomOffset(row, col);
+ local extraOffsetX = (col - 1) * (width + paddingX) * direction.x + customOffsetX;
+ local extraOffsetY = (row - 1) * (height + paddingY) * direction.y + customOffsetY;
+ initialAnchor:SetPointWithExtraOffset(frame, clearAllPoints, extraOffsetX, extraOffsetY);
+ end
+end
+
+local function GetFrameSpacing(totalSize, numElements, elementSize)
+ if numElements <= 1 then
+ return 0;
+ end
+
+ return (totalSize - (numElements * elementSize)) / (numElements - 1);
+end
+
+local function SanitizeTotalSize(size)
+ if not size or size == 0 then
+ return math.huge;
+ else
+ return Round(size);
+ end
+end
+
+-- For initialAnchor and layout, use AnchorUtil.CreateAnchor(...) and AnchorUtil.CreateGridLayout(...)
+function AnchorUtil.GridLayoutFactoryByCount(factoryFunction, count, initialAnchor, layout)
+ if count <= 0 then
+ return;
+ end
+
+ local frames = { };
+ while #frames < count do
+ local frame = factoryFunction(#frames + 1);
+ if not frame then
+ break;
+ end
+
+ table.insert(frames, frame);
+ end
+
+ AnchorUtil.GridLayout(frames, initialAnchor, layout);
+
+ return frames;
+end
+
+-- For initialAnchor, use AnchorUtil.CreateAnchor(...)
+function AnchorUtil.GridLayoutFactory(factoryFunction, initialAnchor, totalWidth, totalHeight, overrideDirection, overridePaddingX, overridePaddingY)
+ local frame = factoryFunction(1);
+ if not frame then
+ return;
+ end
+
+ totalWidth = SanitizeTotalSize(totalWidth);
+ totalHeight = SanitizeTotalSize(totalHeight);
+
+ -- If we have an override padding, count it in the frame width. We add a padding to totalWidth/totalHeight to account for the
+ -- extra space we save for the last element which doesn't need padding.
+ local width = Round(frame:GetWidth()) + (overridePaddingX or 0);
+ local height = Round(frame:GetHeight()) + (overridePaddingY or 0);
+ local rowSize = math.floor((totalWidth + (overridePaddingX or 0)) / width);
+ local colSize = math.floor((totalHeight + (overridePaddingY or 0)) / height);
+
+ local spacingX = overridePaddingX or GetFrameSpacing(totalWidth, rowSize, width);
+ local spacingY = overridePaddingY or GetFrameSpacing(totalHeight, colSize, height);
+
+ local frames = { frame };
+ while #frames < rowSize * colSize do
+ local frame = factoryFunction(#frames + 1);
+ if not frame then
+ break;
+ end
+
+ table.insert(frames, frame);
+ end
+
+ local direction = overrideDirection or GridLayoutMixin.Direction.TopLeftToBottomRight;
+
+ AnchorUtil.GridLayout(frames, initialAnchor, AnchorUtil.CreateGridLayout(direction, rowSize, spacingX, spacingY));
+end
+
+-- Mirrors an array of regions along the specified axis. For example, if horizontal, a region
+-- anchored LEFT TOPLEFT 20 20 will become anchored RIGHT TOPRIGHT -20 20.
+-- Mirror description format: {region = region, mirrorUV = [true, false]}
+local function MirrorRegionsAlongAxis(mirrorDescriptions, exchangeables, setPointWrapper, setTexCoordsWrapper)
+ for _, description in ipairs(mirrorDescriptions) do
+ local exchanged = {};
+
+ local region = description.region;
+ local mirrorUV = description.mirrorUV;
+ for p in pairs(exchangeables) do
+ if not exchanged[p] then
+ local point1, relative1, relativePoint1, x1, y1 = region:GetPointByName(p);
+ if point1 then
+ -- Retrieve point information for what we're replacing, if any.
+ local mirrorPoint1 = exchangeables[point1];
+ local point2, relative2, relativePoint2, x2, y2 = region:GetPointByName(mirrorPoint1);
+ setPointWrapper(region, point1, relative1, relativePoint1, x1, y1);
+
+ -- If we replaced a point, mirror the information to the original point.
+ if point2 then
+ setPointWrapper(region, point2, relative2, relativePoint2, x2, y2);
+ else
+ -- Otherwise, clear the original point.
+ region:ClearPoint(point1);
+ end
+
+ exchanged[point1] = true;
+ exchanged[mirrorPoint1] = true;
+ end
+ end
+ end
+
+ if mirrorUV then
+ setTexCoordsWrapper(region);
+ end
+ end
+end
+
+local SetPointAlongAxis = function(points, region, point, relative, relativePoint, x, y)
+ local mirrorPoint = points[point];
+ local mirrorRelativePoint = points[relativePoint] or relativePoint;
+ region:SetPoint(mirrorPoint, relative, mirrorRelativePoint, x, y);
+end
+
+local VERTICAL_MIRROR_POINTS =
+{
+ ["TOPLEFT"] = "BOTTOMLEFT",
+ ["TOP"] = "BOTTOM",
+ ["TOPRIGHT"] = "BOTTOMRIGHT",
+ ["BOTTOMLEFT"] = "TOPLEFT",
+ ["BOTTOM"] = "TOP",
+ ["BOTTOMRIGHT"] = "TOPRIGHT",
+ ["CENTER"] = "CENTER", -- Mirrored only along x and y offsets.
+ ["LEFT"] = "LEFT", -- Mirrored only along x and y offsets.
+ ["RIGHT"] = "RIGHT", -- Mirrored only along x and y offsets.
+};
+
+local SetPointVertical = function(region, point, relative, relativePoint, x, y)
+ SetPointAlongAxis(VERTICAL_MIRROR_POINTS, region, point, relative, relativePoint, x, -y);
+end;
+
+local SetTexCoordVertical = function(region)
+ local x1, y1, x2, y2, x3, y3, x4, y4 = region:GetTexCoord();
+ region:SetTexCoord(x2, y2, x1, y1, x4, y4, x3, y3);
+end
+
+function AnchorUtil.MirrorRegionsAlongVerticalAxis(mirrorDescriptions)
+ MirrorRegionsAlongAxis(mirrorDescriptions, VERTICAL_MIRROR_POINTS, SetPointVertical, SetTexCoordVertical);
+end
+
+local HORIZONTAL_MIRROR_POINTS =
+{
+ ["TOPLEFT"] = "TOPRIGHT",
+ ["LEFT"] = "RIGHT",
+ ["BOTTOMLEFT"] = "BOTTOMRIGHT",
+ ["TOPRIGHT"] = "TOPLEFT",
+ ["RIGHT"] = "LEFT",
+ ["BOTTOMRIGHT"] = "BOTTOMLEFT",
+ ["CENTER"] = "CENTER", -- Mirrored only along x and y offsets.
+ ["TOP"] = "TOP", -- Mirrored only along x and y offsets.
+ ["BOTTOM"] = "BOTTOM", -- Mirrored only along x and y offsets.
+};
+
+local SetPointHorizontal = function(region, point, relative, relativePoint, x, y)
+ SetPointAlongAxis(HORIZONTAL_MIRROR_POINTS, region, point, relative, relativePoint, -x, y);
+end
+
+local SetTexCoordHorizontal = function(region)
+ local x1, y1, x2, y2, x3, y3, x4, y4 = region:GetTexCoord();
+ region:SetTexCoord(x3, y3, x4, y4, x1, y1, x2, y2);
+end
+
+function AnchorUtil.MirrorRegionsAlongHorizontalAxis(mirrorDescriptions)
+ MirrorRegionsAlongAxis(mirrorDescriptions, HORIZONTAL_MIRROR_POINTS, SetPointHorizontal, SetTexCoordHorizontal);
+end
+
+function AnchorUtil.DebugAnchorGraph(frame, indent, visited, output)
+ local indentString = " ";
+ indent = indent or indentString;
+ output = output or {};
+ visited = visited or {};
+
+ if visited[frame] then
+ return output;
+ end
+
+ visited[frame] = true;
+
+ local function FormatFrame(frame)
+ local color = frame:IsRectValid() and GREEN_FONT_COLOR or RED_FONT_COLOR;
+ local x, y = frame:GetSize();
+ local x2, y2 = frame:GetSize(true);
+ return color:WrapTextInColorCode(frame:GetDebugName() .. (" calculated size <%.2f, %.2f> explicit size <%.2f, %.2f> points <%d>"):format(
+ x, y, x2, y2, frame:GetNumPoints()));
+ end
+
+ table.insert(output, indent .. FormatFrame(frame));
+
+ for i = 1, frame:GetNumPoints() do
+ local point, relativeTo, relativePoint, x, y = frame:GetPoint(i);
+ local anchorString = ("Anchor%d %s to %s at %s offset <%.2f, %.2f>"):format(i, point, relativeTo and relativeTo:GetDebugName() or "?", relativePoint, x, y);
+ table.insert(output, indent .. LIGHTGRAY_FONT_COLOR:WrapTextInColorCode(anchorString));
+ if relativeTo then
+ AnchorUtil.DebugAnchorGraph(relativeTo, indent .. indentString, visited, output);
+ end
+ end
+
+ return output;
+end
+
+function AnchorUtil.PrintAnchorGraph(frame)
+ -- Printing to multiple places in case the chat frame isn't visible.
+ local str = table.concat(AnchorUtil.DebugAnchorGraph(frame), "\n");
+ ConsolePrint(str);
+ print(str);
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Atlas.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Atlas.lua
new file mode 100644
index 0000000..ad5e1e1
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Atlas.lua
@@ -0,0 +1,66 @@
+ATLAS_INFO_STORAGE = ATLAS_INFO_STORAGE or {}
+
+local _ = ATLAS_INFO_STORAGE
+local CAPI_TEXTURE_PATH = "Interface\\AddOns\\!!!ClassicAPI\\Texture\\" -- Make this global in restructure.
+
+--[[
+ INFORMATION
+ =================================
+
+ Build: 3.4.2 (50664):
+ https://www.townlong-yak.com/framexml/9.2.7/Helix/AtlasInfo.lua
+ https://github.com/Ketho/BlizzardInterfaceResources/blob/wrath/Resources/AtlasInfo.lua
+ https://github.com/Gethe/wow-ui-textures
+
+ Build (Fallback): 10.1.5 (50622):
+ https://github.com/Ketho/BlizzardInterfaceResources/blob/mainline/Resources/AtlasInfo.lua
+ https://wow.tools
+
+ Manual Calc: X/WIDTH || Y/HEIGHT
+]]
+
+-- _["atlasName"]={width, height, left, right, top, bottom, tilesHoriz, tilesVert, texturePath}
+
+-- Interface/FrameGeneral/UIFrameDiamondMetal
+local FrameGeneral_UIFrameDiamondMetal = CAPI_TEXTURE_PATH.."FrameGeneral\\UIFrameDiamondMetal"
+_["UI-Frame-DiamondMetal-CornerBottomLeft"]={32, 32, 0.015625, 0.515625, 0.269531, 0.394531, false, false, FrameGeneral_UIFrameDiamondMetal}
+_["UI-Frame-DiamondMetal-CornerBottomRight"]={32, 32, 0.015625, 0.515625, 0.402344, 0.527344, false, false, FrameGeneral_UIFrameDiamondMetal}
+_["UI-Frame-DiamondMetal-CornerTopLeft"]={32, 32, 0.015625, 0.515625, 0.535156, 0.660156, false, false, FrameGeneral_UIFrameDiamondMetal}
+_["UI-Frame-DiamondMetal-CornerTopRight"]={32, 32, 0.015625, 0.515625, 0.667969, 0.792969, false, false, FrameGeneral_UIFrameDiamondMetal}
+_["_UI-Frame-DiamondMetal-EdgeBottom"]={32, 32, 0, 0.5, 0.00390625, 0.128906, false, false, FrameGeneral_UIFrameDiamondMetal}
+_["_UI-Frame-DiamondMetal-EdgeTop"]={32, 32, 0, 0.5, 0.136719, 0.261719, false, false, FrameGeneral_UIFrameDiamondMetal}
+
+-- Interface/FrameGeneral/UIFrameDiamondMetalVertical
+local FrameGeneral_UIFrameDiamondMetalVertical = CAPI_TEXTURE_PATH.."FrameGeneral\\UIFrameDiamondMetalVertical"
+_["!UI-Frame-DiamondMetal-EdgeLeft"]={32, 32, 0.0078125, 0.257812, 0, 1, false, false, FrameGeneral_UIFrameDiamondMetalVertical}
+_["!UI-Frame-DiamondMetal-EdgeRight"]={32, 32, 0.273438, 0.523438, 0, 1, false, false, FrameGeneral_UIFrameDiamondMetalVertical}
+
+-- Interface/RaidFrame/RaidFrameSummon
+local RaidFrame_RaidFrameSummon = CAPI_TEXTURE_PATH.."RaidFrame\\RaidFrameSummon"
+_["Raid-Icon-SummonAccepted"]={32, 32, 0.0078125, 0.257812, 0.015625, 0.515625, false, false, RaidFrame_RaidFrameSummon}
+_["Raid-Icon-SummonDeclined"]={32, 32, 0.273438, 0.523438, 0.015625, 0.515625, false, false, RaidFrame_RaidFrameSummon}
+_["Raid-Icon-SummonPending"]={32, 32, 0.539062, 0.789062, 0.015625, 0.515625, false, false, RaidFrame_RaidFrameSummon}
+
+-- Interface/ContainerFrame/Bags
+local ContainerFrame_Bags = CAPI_TEXTURE_PATH.."ContainerFrame\\Bags"
+_["bags-newitem"]={44, 44, 0.363281, 0.535156, 0.00390625, 0.175781, false, false, ContainerFrame_Bags}
+_["bags-junkcoin"]={20, 18, 0.863281, 0.941406, 0.28125, 0.351562, false, false, ContainerFrame_Bags}
+_["bags-innerglow"]={36, 36, 0.164062, 0.304688, 0.539062, 0.679688, false, false, ContainerFrame_Bags}
+_["bags-glow-purple"]={39, 39, 0.00390625, 0.15625, 0.539062, 0.691406, false, false, ContainerFrame_Bags}
+_["bags-glow-blue"]={39, 39, 0.542969, 0.695312, 0.164062, 0.316406, false, false, ContainerFrame_Bags}
+_["bags-glow-orange"]={39, 39, 0.707031, 0.859375, 0.363281, 0.515625, false, false, ContainerFrame_Bags}
+_["bags-glow-green"]={39, 39, 0.703125, 0.855469, 0.00390625, 0.15625, false, false, ContainerFrame_Bags}
+_["bags-glow-heirloom"]={39, 39, 0.703125, 0.855469, 0.164062, 0.316406, false, false, ContainerFrame_Bags}
+_["bags-glow-white"]={39, 39, 0.00390625, 0.15625, 0.699219, 0.851562, false, false, ContainerFrame_Bags}
+_["bags-glow-flash"]={90, 90, 0.00390625, 0.355469, 0.00390625, 0.355469, false, false, ContainerFrame_Bags}
+_["bags-button-autosort-down"]={28, 26, 0.164062, 0.273438, 0.835938, 0.9375, false, false, ContainerFrame_Bags}
+_["bags-button-autosort-up"]={28, 26, 0.3125, 0.421875, 0.539062, 0.640625, false, false, ContainerFrame_Bags}
+_["bags-roundhighlight"]={36, 36, 0.164062, 0.304688, 0.6875, 0.828125, false, false, ContainerFrame_Bags}
+_["bags-icon-consumables"]={28, 28, 0.863281, 0.972656, 0.00390625, 0.113281, false, false, ContainerFrame_Bags}
+_["bags-icon-equipment"]={28, 28, 0.863281, 0.972656, 0.164062, 0.273438, false, false, ContainerFrame_Bags}
+_["bags-icon-tradegoods"]={28, 28, 0.867188, 0.976562, 0.363281, 0.472656, false, false, ContainerFrame_Bags}
+_["bags-glow-artifact"]={39, 39, 0.542969, 0.695312, 0.00390625, 0.15625, false, false, ContainerFrame_Bags}
+_["bags-greenarrow"]={20, 22, 0.3125, 0.390625, 0.648438, 0.734375, false, false, ContainerFrame_Bags}
+_["bags-icon-addslots"]={42, 42, 0.363281, 0.527344, 0.183594, 0.347656, false, false, ContainerFrame_Bags}
+_["bags-static"]={178, 43, 0.00390625, 0.699219, 0.363281, 0.53125, false, false, ContainerFrame_Bags}
+_["bags-icon-scrappable"]={36, 32, 0.00390625, 0.144531, 0.859375, 0.984375, false, false, ContainerFrame_Bags}
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Aura.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Aura.lua
new file mode 100644
index 0000000..89df3ac
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Aura.lua
@@ -0,0 +1,324 @@
+local UnitAura = UnitAura
+local UnitBuff = UnitBuff
+local UnitDebuff = UnitDebuff
+local GetSpellInfo = GetSpellInfo
+local UnitAffectingCombat = UnitAffectingCombat
+
+local _, CLASS_PLAYER = UnitClass("player")
+
+local AURA
+
+function SpellIsPriorityAura(SpellID)
+ return false
+end
+
+function SpellCanApplyAura(SpellName)
+ local Info = (SpellName) and AURA[SpellName]
+ return (Info and Info.CanApply)
+end
+
+function SpellAppliesOnlyYourself(SpellName)
+ local Info = (SpellName) and AURA[SpellName]
+ return (Info and Info.SelfOnly)
+end
+
+function SpellIsSelfBuff(SpellID)
+ if ( SpellID ) then
+ local SpellName = GetSpellInfo(SpellID)
+ local Info = (SpellName) and AURA[SpellName]
+
+ if ( Info ) then
+ return Info.SelfOnly, Info.CanApply
+ end
+ end
+end
+
+function SpellGetVisibilityInfo(SpellID, Type)
+ if ( SpellID and Type ) then
+ local SpellName = GetSpellInfo(SpellID)
+ local Info = (SpellName) and AURA[SpellName]
+
+ if ( Info ) then
+ local ShowMine, ShowSpec
+ if ( Type == "RAID_INCOMBAT" ) then
+ ShowMine = Info.ShowMine_INCOMBAT
+ ShowSpec = Info.ShowSpec_INCOMBAT
+ elseif ( Type == "RAID_OUTOFCOMBAT" ) then
+ ShowMine = Info.ShowMine_OUTCOMBAT
+ ShowSpec = Info.ShowSpec_OUTCOMBAT
+ end
+
+ return (ShowMine ~= nil or ShowSpec ~= nil), ShowMine, ShowSpec
+ end
+ end
+end
+
+--[[
+
+ Aura Information
+
+]]
+
+local function Aura(SpellID, CanApply, SelfOnly, ShowMine_INCOMBAT, ShowMine_OUTCOMBAT, ShowSpec_INCOMBAT, ShowSpec_OUTCOMBAT)
+ AURA = AURA or {}
+
+ AURA[GetSpellInfo(SpellID)] = {
+ CanApply = CanApply, -- Player can cast it.
+ SelfOnly = SelfOnly, -- Spell is not usable on others.
+
+ --[[
+ Combat State Information: RAID_INCOMBAT, RAID_OUTOFCOMBAT
+ ShowMine: whether to show the spell if cast by the player/player's pet/vehicle (e.g. the Paladin Forbearance debuff)
+ ShowSpec: whether to show the spell for the current specialization of the player, it will override all others if set.
+ ]]
+ ShowMine_INCOMBAT = ShowMine_INCOMBAT,
+ ShowMine_OUTCOMBAT = ShowMine_OUTCOMBAT,
+
+ ShowSpec_INCOMBAT = ShowSpec_INCOMBAT,
+ ShowSpec_OUTCOMBAT = ShowSpec_OUTCOMBAT,
+ }
+end
+
+if ( CLASS_PLAYER == "DEATHKNIGHT" ) then
+ Aura(48265, true, true) -- Unholy Presence
+ Aura(48263, true, true) -- Frost Presence
+ Aura(48266, true, true) -- Blood Presence
+ Aura(45529, true, true) -- Blood Tap
+ Aura(49016, true, false) -- Hysteria
+ Aura(57330, true, false) -- Horn of Winter
+ Aura(49028, true, false) -- Dancing Rune Weapon
+
+ Aura(3714, true, false, false, true) -- Path of Frost
+elseif ( CLASS_PLAYER == "DRUID" ) then
+ Aura(29166, true, false) -- Innervate
+ Aura(9634, true, true) -- Dire Bear Form
+ Aura(768, true, true) -- Cat Form
+ Aura(48451, true, false) -- Lifebloom
+ Aura(48441, true, false) -- Rejuvenation
+ Aura(48443, true, false) -- Regrowth
+ Aura(53251, true, false) -- Wild Growth
+ Aura(61336, true, true) -- Survival Instincts
+ Aura(50334, true, true) -- Berserk
+ Aura(5229, true, true) -- Enrage
+ Aura(22812, true, true) -- Barkskin
+ Aura(53312, true, true) -- Nature's Grasp
+ Aura(22842, true, true) -- Frenzied Regeneration
+ Aura(2893, true, false) -- Abolish Poison
+
+ Aura(467, true, false, false, true) -- Thorns
+ Aura(1126, true, false, false, true) -- Mark of the Wild
+ Aura(21849, true, false, false, true) -- Gift of the Wild
+elseif ( CLASS_PLAYER == "HUNTER" ) then
+ Aura(5384, true, true) -- Feign Death
+ Aura(3045, true, true) -- Rapid Fire
+ Aura(53480, true, false) -- Roar of Sacrifice
+ Aura(53271, true, false) -- Master's Call
+ Aura(53476, true, false) -- Intervene
+
+ Aura(19506, true, false, false, true) -- Trueshot Aura
+elseif ( CLASS_PLAYER == "MAGE" ) then
+ Aura(43039, true, true) -- Ice Barrier
+ Aura(45438, true, true) -- Ice Block
+ Aura(43012, true, true) -- Frost Ward
+ Aura(43008, true, true) -- Ice Armor
+ Aura(7301, true, true) -- Frost Armor
+ Aura(12472, true, true) -- Icy Veins
+ Aura(43010, true, true) -- Fire Ward
+ Aura(43046, true, true) -- Molten Armor
+ Aura(43020, true, true) -- Mana Shield
+ Aura(43024, true, true) -- Mage Armor
+ Aura(66, true, true) -- Invisibility
+ Aura(130, true, false) -- Slow Fall
+ Aura(11213, true, true) -- Arcane Concentration
+ Aura(12043, true, true) -- Presence of Mind
+ Aura(12042, true, true) -- Arcane Power
+ Aura(31579, true, true) -- Arcane Empowerment
+
+ Aura(61024, true, false, false, true) -- Dalaran Intellect
+ Aura(61316, true, false, false, true) -- Dalaran Brilliance
+ Aura(42995, true, false, false, true) -- Arcane Intellect
+ Aura(43002, true, false, false, true) -- Arcane Brilliance
+ Aura(43015, true, false, false, true) -- Dampen Magic
+ Aura(54646, true, false, false, true) -- Focus Magic
+elseif ( CLASS_PLAYER == "PALADIN" ) then
+ Aura(53601, true, false) -- Sacred Shield
+ Aura(53563, true, false) -- Beacon of Light
+ Aura(6940, true, false) -- Hand of Sacrifice
+ Aura(64205, true, false) -- Divine Sacrifice
+ Aura(31821, true, true) -- Aura Mastery
+ Aura(642, true, true) -- Divine Shield
+ Aura(1022, true, false) -- Hand of Protection
+ Aura(1044, true, false) -- Hand of Freedom
+ Aura(54428, true, true) -- Divine Plea
+ Aura(48952, true, true) -- Holy Shield
+ Aura(48942, true, true) -- Devotion Aura
+ Aura(54043, true, true) -- Retribution Aura
+ Aura(19746, true, true) -- Concentration Aura
+ Aura(48943, true, true) -- Shadow Resistance Aura
+ Aura(48945, true, true) -- Frost Resistance Aura
+ Aura(48947, true, true) -- Fire Resistance Aura
+ Aura(32223, true, true) -- Crusader Aura
+ Aura(31884, true, true) -- Avenging Wrath
+ Aura(54203, true, false) -- Sheath of Light
+ Aura(20053, true, true) -- Vengeance
+ Aura(59578, true, true) -- The Art of War
+
+ Aura(20217, true, false, false, true) -- Blessing of Kings
+ Aura(25898, true, false, false, true) -- Greater Blessing of Kings
+ Aura(48936, true, false, false, true) -- Blessing of Wisdom
+ Aura(48938, true, false, false, true) -- Greater Blessing of Wisdom
+ Aura(48932, true, false, false, true) -- Blessing of Might
+ Aura(48934, true, false, false, true) -- Greater Blessing of Might
+ Aura(25899, true, false, false, true) -- Greater Blessing of Sanctuary
+ Aura(20911, true, false, false, true) -- Blessing of Sanctuary
+elseif ( CLASS_PLAYER == "PRIEST" ) then
+ Aura(48111, true, false) -- Prayer of Mending
+ Aura(33206, true, false) -- Pain Suppression
+ Aura(48068, true, false) -- Renew
+ Aura(48066, true, false) -- Power Word: Shield
+ Aura(72418, true, true) -- Chilling Knowledge
+ Aura(47930, false, false) -- Grace
+ Aura(10060, true, false) -- Power Infusion
+ Aura(586, true, true) -- Fade
+ Aura(48168, true, true) -- Inner Fire
+ Aura(14751, true, true) -- Inner Focus
+ Aura(6346, true, false) -- Fear Ward
+ Aura(64901, true, false) -- Hymn of Hope
+ Aura(1706, true, false) -- Levitate
+ Aura(64843, false, false) -- Divine Hymn
+ Aura(59891, false, false) -- Borrowed Time
+ Aura(552, true, false) -- Abolish Disease
+ Aura(15473, true, true) -- Shadowform
+ Aura(15286, true, true) -- Vampiric Embrace
+ Aura(49694, true, true) -- Improved Spirit Tap
+ Aura(47788, true, false) -- Guardian Spirit
+ Aura(33151, true, true) -- Surge of Light
+ Aura(33151, true, true) -- Inspiration
+ Aura(7001, true, false) -- Lightwell Renew
+ Aura(27827, true, true) -- Spirit of Redemption
+ Aura(63734, true, true) -- Serendipity
+ Aura(65081, true, false) -- Body and Soul
+ Aura(63944, false, false) -- Renewed Hope
+
+ Aura(48073, true, false, false, true) -- Divine Spirit
+ Aura(48074, true, false, false, true) -- Prayer of Spirit
+ Aura(48169, true, false, false, true) -- Shadow Protection
+ Aura(48170, true, false, false, true) -- Prayer of Shadow Protection
+ Aura(48162, true, false, false, true) -- Prayer of Fortitude
+ Aura(48161, true, false, false, true) -- Power Word: Fortitude
+elseif ( CLASS_PLAYER == "ROGUE" ) then
+ Aura(1784, true, true) -- Stealth
+ Aura(31665, true, true) -- Master of Subtlety
+ Aura(26669, true, true) -- Evasion
+ Aura(11305, true, true) -- Sprint
+ Aura(26888, true, true) -- Vanish
+ Aura(36554, true, true) -- Shadowstep
+ Aura(48659, true, true) -- Feint
+ Aura(31224, true, true) -- Clock of Shadow
+ Aura(51713, true, true) -- Shadow dance
+ Aura(14177, true, true) -- Cold Blood
+ Aura(57934, true, false) -- Tricks of the Trade
+elseif ( CLASS_PLAYER == "SHAMAN" ) then
+ Aura(49284, true, false) -- Earth Shield
+ Aura(8515, false, false) -- Windfury Totem
+ Aura(8178, true, false) -- Grounding Totem
+ Aura(32182, true, false) -- Heroism
+ Aura(2825, true, false) -- Bloodlust
+ Aura(61301, true, false) -- Riptide
+ Aura(51466, true, false) -- Elemental Oath
+elseif ( CLASS_PLAYER == "WARLOCK" ) then
+ Aura(2947, true, false) -- Fire Shield
+ Aura(132, true, false) -- Detect Invisibility
+ Aura(19028, true, false) -- Soul Link
+ Aura(54424, true, false) -- Fel Intelligence
+elseif ( CLASS_PLAYER == "WARRIOR" ) then
+ Aura(2687, true, true) -- Bloodrage
+ Aura(18499, true, true) -- Berserker Rage
+ Aura(12328, true, true) -- Sweeping Strikes
+ Aura(23920, true, true) -- Spell Reflection
+ Aura(871, true, true) -- Shield Wall
+ Aura(2565, true, true) -- Shield Block
+ Aura(55694, true, true) -- Enraged Regeneration
+ Aura(1719, true, true) -- Recklessness
+ Aura(57522, true, true) -- Enrage
+ Aura(20230, true, true) -- Retaliation
+ Aura(46924, true, true) -- Bladestorm
+ Aura(47440, true, false) -- Commanding Shout
+ Aura(47436, true, false) -- Battle Shout
+ Aura(46913, true, true) -- Bloodsurge
+ Aura(12292, true, true) -- Death Wish
+ Aura(16492, true, true) -- Blood Craze
+ Aura(65156, true, true) -- Juggernaut
+ Aura(3411, true, false) -- Intervene
+end
+
+--[[
+
+ Global Auras
+
+]]
+
+Aura(69127, nil, nil, nil, nil, false, true) -- Chill of the Throne
+Aura(26013, nil, nil, nil, nil, false, true) -- Deserter
+Aura(31694, nil, nil, nil, nil, false, false) -- Strange Feeling
+Aura(70013, nil, nil, nil, nil, false, false) -- Quel'Delar's Compulsion
+
+-- TODO: Add PvP flags as a priority buff.
+
+--[[
+
+ UNITAURA / UNITBUFF / UNITDEBUFF
+ Modern payload.
+
+]]
+
+function C_UnitAura(...)
+ local Name, Rank, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID = UnitAura(...)
+ local Aura = AURA[Name]
+ return Name, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID, (Aura and Aura.CanApply)
+end
+
+function C_UnitBuff(...)
+ local Name, Rank, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID = UnitBuff(...)
+ local Aura = AURA[Name]
+ return Name, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID, (Aura and Aura.CanApply)
+end
+
+function C_UnitDebuff(...)
+ local Name, Rank, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID = UnitDebuff(...)
+ return Name, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID
+end
+
+--[[
+
+ CompactUnitFrame_UtilShouldDisplayDebuff
+ Note: Support if CompactRaidFrames not installed.
+
+]]
+local SpellGetVisibilityInfo = SpellGetVisibilityInfo
+
+function CompactUnitFrame_UtilShouldDisplayDebuff(...)
+ local _, _, _, _, _, _, _, Caster, _, _, SpellID = UnitDebuff(...)
+
+ if ( SpellID ) then
+ local HasCustom, AlwaysShowMine, ShowForMySpec = SpellGetVisibilityInfo(SpellID, UnitAffectingCombat("player") and "RAID_INCOMBAT" or "RAID_OUTOFCOMBAT")
+ if ( HasCustom ) then
+ return ShowForMySpec or (AlwaysShowMine and (Caster == "player" or Caster == "pet" or Caster == "vehicle"))
+ end
+
+ return true
+ end
+end
+
+--[[ TODO
+function UnitAuraBySlot(unit, slot)
+ if ( unit and LibAura ) then
+ return LibAura:UnitAuraBySlot(unit, slot)
+ end
+end
+
+function UnitAuraSlots(unit, filter, maxCount, continuationToken)
+ if ( unit and LibAura ) then
+ return LibAura:UnitAuraSlots(unit, filter, maxCount, continuationToken)
+ end
+end]]
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AuraUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AuraUtil.lua
new file mode 100644
index 0000000..9b77bda
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/AuraUtil.lua
@@ -0,0 +1,67 @@
+AuraUtil = AuraUtil or {}
+
+local UnitAura = UnitAura
+
+local function FindAuraRecurse(predicate, unit, filter, auraIndex, predicateArg1, predicateArg2, predicateArg3, ...)
+ if ... == nil then
+ return nil; -- Not found
+ end
+ if predicate(predicateArg1, predicateArg2, predicateArg3, ...) then
+ return ...;
+ end
+ auraIndex = auraIndex + 1;
+ local name, rank, icon, count, type, duration, expire, caster, steal, consolidate, id = UnitAura(unit, auraIndex, filter);
+ return FindAuraRecurse(predicate, unit, filter, auraIndex, predicateArg1, predicateArg2, predicateArg3, name, icon, count, type, duration, expire, caster, steal, consolidate, id);
+end
+-- Find an aura by any predicate, you can pass in up to 3 predicate specific parameters
+-- The predicate will also receive all aura params, if the aura data matches return true
+function AuraUtil.FindAura(predicate, unit, filter, predicateArg1, predicateArg2, predicateArg3)
+ local auraIndex = 1;
+ local name, rank, icon, count, type, duration, expire, caster, steal, consolidate, id = UnitAura(unit, auraIndex, filter);
+ return FindAuraRecurse(predicate, unit, filter, auraIndex, predicateArg1, predicateArg2, predicateArg3, name, icon, count, type, duration, expire, caster, steal, consolidate, id);
+end
+
+do
+
+ local function NamePredicate(auraNameToFind, _, _, auraName)
+ return auraNameToFind == auraName;
+ end
+ -- Finds the first aura that matches the name
+ -- Notes:
+ -- aura names are not unique!
+ -- aura names are localized, what works in one locale might not work in another
+ -- consider that in English two auras might have different names, but once localized they have the same name, so even using the localized aura name in a search it could result in different behavior
+ -- the unit could have multiple auras with the same name, this will only find the first
+ function AuraUtil.FindAuraByName(auraName, unit, filter)
+ return AuraUtil.FindAura(NamePredicate, unit, filter, auraName);
+ end
+
+end
+
+do
+
+ local function ForEachAuraHelper(unit, filter, func, continuationToken, ...)
+ -- continuationToken is the first return value of UnitAuraSlots()
+ local n = select('#', ...);
+ for i=1, n do
+ local slot = select(i, ...);
+ if func(UnitAuraBySlot(unit, slot)) then
+ -- if func returns true then no further slots are needed, so don't return continuationToken
+ return nil;
+ end
+ end
+ return continuationToken;
+ end
+
+ function AuraUtil.ForEachAura(unit, filter, maxCount, func)
+ if maxCount and maxCount <= 0 then
+ return;
+ end
+ local continuationToken;
+ repeat
+ -- continuationToken is the first return value of UnitAuraSltos
+ continuationToken = ForEachAuraHelper(unit, filter, func, UnitAuraSlots(unit, filter, maxCount, continuationToken));
+ until continuationToken == nil;
+ end
+
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_ChatInfo.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_ChatInfo.lua
new file mode 100644
index 0000000..6c71f25
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_ChatInfo.lua
@@ -0,0 +1,66 @@
+local _G = _G
+local C_ChatInfo = C_ChatInfo or {}
+
+local SendAddonMessage = _G.SendAddonMessage
+
+local CTL = _G.ChatThrottleLib
+local CTL_SendAddonMessage = CTL.SendAddonMessage
+CTL.version = 50 -- Force ClassicAPI CTL.
+CTL.SendAddonMessage = function(...)
+ local _, _, _, _, Type = ...
+ if ( Type ~= "PARTY" and Type ~= "RAID" and Type ~= "GUILD" and Type ~= "BATTLEGROUND" and Type ~= "WHISPER" ) then
+ return
+ end
+ return CTL_SendAddonMessage(...)
+end
+
+local function TODO() end
+
+C_ChatInfo.RegisterAddonMessagePrefix = TODO
+C_ChatInfo.CanPlayerSpeakLanguage = TODO
+C_ChatInfo.CanReportPlayer = TODO
+C_ChatInfo.GetChannelInfoFromIdentifier = TODO
+C_ChatInfo.GetChannelRosterInfo = TODO
+C_ChatInfo.GetChannelRuleset = TODO
+C_ChatInfo.GetChannelShortcut = TODO
+C_ChatInfo.GetChatLineSenderGUID = TODO
+C_ChatInfo.GetChatLineSenderName = TODO
+C_ChatInfo.GetChatLineText = TODO
+C_ChatInfo.GetChatTypeName = TODO
+C_ChatInfo.GetClubStreamIDs = TODO
+C_ChatInfo.GetColorForChatType = TODO
+C_ChatInfo.GetGeneralChannelID = TODO
+C_ChatInfo.GetGeneralChannelLocalID = TODO
+C_ChatInfo.GetMentorChannelID = TODO
+C_ChatInfo.GetNumActiveChannels = TODO
+C_ChatInfo.GetNumReservedChatWindows = TODO
+C_ChatInfo.GetRegisteredAddonMessagePrefixes = TODO
+
+function C_ChatInfo.IsAddonMessagePrefixRegistered()
+ return true
+end
+
+C_ChatInfo.IsChannelRegional = TODO
+C_ChatInfo.IsChatLineCensored = TODO
+C_ChatInfo.IsPartyChannelType = TODO
+C_ChatInfo.IsRegionalServiceAvailable = TODO
+C_ChatInfo.IsValidChatLine = TODO
+C_ChatInfo.RegisterAddonMessagePrefix = TODO
+C_ChatInfo.ReplaceIconAndGroupExpressions = TODO
+C_ChatInfo.ReportServerLag = TODO
+C_ChatInfo.RequestCanLocalWhisperTarget = TODO
+C_ChatInfo.ResetDefaultZoneChannels = TODO
+
+function C_ChatInfo.SendAddonMessage(...)
+ local _, _, Type = ...
+ if ( Type ~= "PARTY" and Type ~= "RAID" and Type ~= "GUILD" and Type ~= "BATTLEGROUND" and Type ~= "WHISPER" ) then
+ return
+ end
+ return SendAddonMessage(...)
+end
+
+C_ChatInfo.SwapChatChannelsByChannelIndex = TODO
+C_ChatInfo.UncensorChatLine = TODO
+
+-- Global
+_G.C_ChatInfo = C_ChatInfo
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_CreatureInfo.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_CreatureInfo.lua
new file mode 100644
index 0000000..1bc0024
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_CreatureInfo.lua
@@ -0,0 +1,108 @@
+C_CreatureInfo = C_CreatureInfo or {}
+
+local C_CreatureInfo_RaceData = {
+ [1] = {
+ raceName = "Human",
+ clientFileString = "Human",
+ },
+ [2] = {
+ raceName = "Orc",
+ clientFileString = "Orc",
+ },
+ [3] = {
+ raceName = "Dwarf",
+ clientFileString = "Dwarf",
+ },
+ [4] = {
+ raceName = "Night Elf",
+ clientFileString = "NightElf",
+ },
+ [5] = {
+ raceName = "Undead",
+ clientFileString = "Scourge",
+ },
+ [6] = {
+ raceName = "Tauren",
+ clientFileString = "Tauren",
+ },
+ [7] = {
+ raceName = "Gnome",
+ clientFileString = "Gnome",
+ },
+ [8] = {
+ raceName = "Troll",
+ clientFileString = "Troll",
+ },
+ [9] = {
+ raceName = "Goblin",
+ clientFileString = "Goblin",
+ },
+ [10] = {
+ raceName = "Blood Elf",
+ clientFileString = "BloodElf",
+ },
+ [11] = {
+ raceName = "Draenei",
+ clientFileString = "Draenei",
+ },
+}
+
+local C_CreatureInfo_ClassData = {
+ [1] = {
+ className = "Warrior",
+ classFile = "WARRIOR",
+ },
+ [2] = {
+ className = "Paladin",
+ classFile = "PALADIN",
+ },
+ [3] = {
+ className = "Hunter",
+ classFile = "HUNTER",
+ },
+ [4] = {
+ className = "Rogue",
+ classFile = "ROGUE",
+ },
+ [5] = {
+ className = "Priest",
+ classFile = "PRIEST",
+ },
+ [6] = {
+ className = "Death Knight",
+ classFile = "DEATHKNIGHT",
+ },
+ [7] = {
+ className = "Shaman",
+ classFile = "SHAMAN",
+ },
+ [8] = {
+ className = "Mage",
+ classFile = "MAGE",
+ },
+ [9] = {
+ className = "Warlock",
+ classFile = "WARLOCK",
+ },
+ --[10] = { MONK }
+ [11] = {
+ className = "Druid",
+ classFile = "DRUID",
+ },
+}
+
+function C_CreatureInfo.GetClassInfo(classID)
+ local classInfo = C_CreatureInfo_ClassData[classID]
+
+ if ( classInfo ) then
+ if ( not classInfo[classID] ) then
+ classInfo.classID = classID
+ end
+ end
+
+ return classInfo
+end
+
+function C_CreatureInfo.GetRaceInfo(raceID)
+ return C_CreatureInfo_RaceData[raceID]
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_FriendList.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_FriendList.lua
new file mode 100644
index 0000000..119adac
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_FriendList.lua
@@ -0,0 +1,12 @@
+local GetNumFriends = GetNumFriends
+local GetFriendInfo = GetFriendInfo
+
+C_FriendList = C_FriendList or {}
+
+C_FriendList.GetNumFriends = function()
+ return GetNumFriends()
+end
+
+C_FriendList.GetFriendInfoByIndex = function(index)
+ return GetFriendInfo(index)
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_GossipInfo.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_GossipInfo.lua
new file mode 100644
index 0000000..fd00f3b
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_GossipInfo.lua
@@ -0,0 +1,13 @@
+C_GossipInfo = C_GossipInfo or {}
+
+function C_GossipInfo.GetText()
+ return GetGossipText()
+end
+
+function C_GossipInfo.GetNumActiveQuests()
+ return GetNumGossipActiveQuests()
+end
+
+function C_GossipInfo.GetNumAvailableQuests()
+ return GetNumGossipAvailableQuests()
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_IncomingSummon.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_IncomingSummon.lua
new file mode 100644
index 0000000..998a6dc
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_IncomingSummon.lua
@@ -0,0 +1,9 @@
+C_IncomingSummon = C_IncomingSummon or {}
+
+function C_IncomingSummon.HasIncomingSummon(unit)
+ return false
+end
+
+function C_IncomingSummon.IncomingSummonStatus(unit)
+ return 0
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Item.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Item.lua
new file mode 100644
index 0000000..0dced51
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Item.lua
@@ -0,0 +1,66 @@
+local GetItemInfo = GetItemInfo
+
+C_Item = C_Item or {}
+
+function C_Item.GetItemInfo(itemInfo)
+ return GetItemInfo(itemInfo)
+end
+
+function C_Item.IsItemDataCachedByID(itemID)
+ local itemName, itemLink = GetItemInfo(itemID)
+ return itemLink ~= nil
+end
+
+function C_Item.DoesItemExistByID(itemID)
+ return true
+end
+
+function C_Item.DoesItemExist(itemInfo)
+ -- This can accept location, id, link or name...
+ return self.IsItemDataCachedByID(itemInfo)
+end
+
+function C_Item.GetItemName(itemInfo)
+ return GetItemInfo(itemInfo)
+end
+
+function C_Item.GetItemNameByID(itemID)
+ return GetItemInfo(itemID)
+end
+
+function C_Item.RequestLoadItemDataByID(itemID)
+ local item = Item:CreateFromItemID(itemID)
+ if item then
+ item:ContinueOnItemLoad(self.DoesItemExistByID)
+ end
+end
+
+local TODO = function()end
+
+C_Item.GetItemGUID = TODO
+C_Item.IsLocked = TODO
+C_Item.GetItemID = TODO -- arg: Pass the items location in bags/character etc, offical relies on itemLocationMixin
+C_Item.LockItem = TODO
+C_Item.UnlockItem = TODO
+C_Item.GetItemIconByID = TODO
+C_Item.GetItemIcon = TODO
+C_Item.GetItemLink = TODO
+C_Item.GetItemQualityByID = TODO
+C_Item.GetItemQuality = TODO
+C_Item.GetCurrentItemLevel = TODO
+C_Item.GetItemInventoryTypeByID = TODO
+C_Item.GetItemInventoryType = TODO
+C_Item.GetItemGUID = TODO
+C_Item.IsItemDataCached = TODO
+C_Item.IsBound = TODO
+
+
+function GetItemInfoInstant(itemInfo)
+ -- return itemID, itemType, itemSubType, itemEquipLoc, icon, classID, subclassID
+ -- TODO: if it is a link extract the id from the link text and send it back...
+ -- we can't return any of the other information like classic because its not ready
+ -- we have to read the tooltips
+ return itemInfo
+end
+
+GetItemSubClassInfo = TODO
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Map.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Map.lua
new file mode 100644
index 0000000..57ffa4c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Map.lua
@@ -0,0 +1,27 @@
+local Select = select
+local GetMapZones = GetMapZones
+
+C_Map = C_Map or {}
+C_Map.WorldMap = {}
+
+local function LoadZones(Obj, ...)
+ for i=1, Select('#', ...) do
+ Obj[i] = Select(i, ...)
+ end
+end
+
+for ContinentIndex = 1, 4 do
+ LoadZones(C_Map.WorldMap, GetMapZones(ContinentIndex))
+end
+
+function C_Map.IsWorldMap(UIMap)
+ for _, Zone in pairs(C_Map.WorldMap) do
+ if ( Zone == UIMap ) then
+ return true
+ end
+ end
+end
+
+function C_Map.GetBestMapForUnit()
+
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_NamePlate.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_NamePlate.lua
new file mode 100644
index 0000000..a52e0c3
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_NamePlate.lua
@@ -0,0 +1,3 @@
+if ( C_NamePlates ) then return end -- (https://github.com/FrostAtom/awesome_wotlk)
+
+C_NamePlates = {}
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_NewItems.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_NewItems.lua
new file mode 100644
index 0000000..3742d6f
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_NewItems.lua
@@ -0,0 +1,219 @@
+if ( C_NewItems ) then return end
+C_NewItems = CreateFrame("Frame")
+
+local SplitUI
+local CursorHasItem = CursorHasItem
+local GetContainerItemID = GetContainerItemID
+local GetContainerNumSlots = GetContainerNumSlots
+local GetContainerItemInfo = GetContainerItemInfo
+
+--[[
+
+ C_NewItems Helper
+ -----
+ Information:
+ An item is considered "new" if it goes to an empty slot, or
+ a stack is increased.
+
+ Known Issue(s):
+ Rapidly stacking/sorting items may cause it to consider an
+ item new. I have rewritten this about 100 times. Used
+ differnt methods. Events related to items are wonky, sadly.
+ The best solution is to call RemoveNewItem while sorting, or
+ ClearAll after.
+
+]]
+
+local INVENTORY, MOVE_ITEM_COUNT, MOVE_ITEM_CONTAINER, MOVE_ITEM_SLOT, MOVE_ITEM_TYPE
+local MIN, MAX = 0, 4
+
+local function GetSlotInfo(ContainerIndex, SlotIndex)
+ if ( INVENTORY ) then
+ local Container = INVENTORY[ContainerIndex]
+ return (Container and SlotIndex) and Container[SlotIndex]
+ end
+end
+
+local function ContainerUpdate(ContainerIndex, Remove)
+ if ( ContainerIndex >= MIN and ContainerIndex <= MAX ) then
+ if ( Remove ) then
+ INVENTORY[ContainerIndex] = false
+ else
+ local Size = GetContainerNumSlots(ContainerIndex)
+ local Container = INVENTORY[ContainerIndex]
+ local Start, End
+
+ if ( not Container ) then
+ Container = {}
+ INVENTORY[ContainerIndex] = Container
+ Start = 1
+ elseif ( Size ~= Container.Size ) then
+ if ( Size > Container.Size ) then
+ Start = Container.Size
+ else
+ Start = Size
+ End = Container.Size
+ end
+ end
+
+ if ( Start ) then
+ for i=Start,(End or Size) do
+ Container[i] = (Start == 1 or End) and {} or nil -- 40 bytes down the drain!
+ end
+ end
+
+ Container.Size = Size
+
+ return Container
+ end
+ end
+end
+
+local function SlotUpdate(ContainerIndex, Supress)
+ local Container = INVENTORY[ContainerIndex]
+
+ if ( not Supress ) then
+ Supress = not Container
+ Container = ContainerUpdate(ContainerIndex)
+ end
+
+ if ( Container ) then
+ for SlotIndex=1, Container.Size do
+ local Slot, ID, Stack, Locked, New, _ = Container[SlotIndex], GetContainerItemID(ContainerIndex, SlotIndex)
+
+ if ( Slot.Stack and Slot.Stack == -1 ) then
+ Stack = 0
+ elseif ( ID ) then
+ _, Stack, Locked = GetContainerItemInfo(ContainerIndex, SlotIndex)
+ New = ( not Supress and not Locked and (not Slot.Stack or (Slot.Stack > 0 and (Slot.New == ID or Stack > Slot.Stack))) ) and ID or nil
+ elseif ( Slot.Stack and Slot.Stack == -2 ) then
+ Stack = Slot.Stack -- Await ID
+ end
+
+ Slot.New = New
+ Slot.Stack = Locked and 0 or Stack
+ end
+ end
+end
+
+local function MoveClear(ContainerIndex, SlotIndex)
+ if ( ContainerIndex == true or (ContainerIndex == MOVE_ITEM_CONTAINER and SlotIndex == MOVE_ITEM_SLOT) ) then
+ MOVE_ITEM_COUNT = nil
+ MOVE_ITEM_CONTAINER = nil
+ MOVE_ITEM_SLOT = nil
+ MOVE_ITEM_TYPE = nil
+ SplitUI.split = nil
+ end
+end
+
+local function MoveSet(ContainerIndex, SlotIndex, Stack)
+ if ( Stack or (SplitUI.split and not MOVE_ITEM_SLOT) ) then
+ MOVE_ITEM_COUNT = Stack or SplitUI.split
+ MOVE_ITEM_CONTAINER = ContainerIndex
+ MOVE_ITEM_SLOT = SlotIndex
+ MOVE_ITEM_TYPE = (SplitUI.split) and true
+ end
+end
+
+local function MoveInfo()
+ return MOVE_ITEM_COUNT, MOVE_ITEM_CONTAINER, MOVE_ITEM_SLOT, MOVE_ITEM_TYPE
+end
+
+local function PickupContainerItem(ContainerIndex, SlotIndex)
+ local Slot = GetSlotInfo(ContainerIndex, SlotIndex)
+ if ( Slot ) then
+ local PlacementStack = Slot.Stack
+ if ( CursorHasItem() ) then
+ MoveSet(ContainerIndex, SlotIndex, PlacementStack) -- Click (Not Split)
+ else
+ local OriginStack, OriginContainerIndex, OriginSlotIndex, Split, Iteration = MoveInfo()
+
+ if ( OriginStack ) then
+ if ( PlacementStack and PlacementStack > 1 ) then
+ Iteration = -1 -- Merging
+ else
+ Iteration = (Split) and -2 or -1 -- Empty/Swap
+ end
+
+ local Origin = GetSlotInfo(OriginContainerIndex, OriginSlotIndex)
+ if ( Origin ) then
+ Origin.New = nil
+ Origin.Stack = 0
+ end
+ else
+ Iteration = 0 -- Source Unknown
+ end
+
+ Slot.New = nil
+ Slot.Stack = Iteration
+ MoveClear(true)
+ end
+ end
+end
+
+local function EventHandler(Self, Event, ...)
+ if ( Event == "BAG_UPDATE" ) then
+ SlotUpdate(...)
+ elseif ( Event == "BAG_CLOSED" ) then
+ ContainerUpdate(..., true)
+ elseif ( Event == "ITEM_LOCKED" ) then
+ MoveSet(...)
+ else
+ MoveClear(... or MoveInfo() and true)
+ end
+end
+
+function Construct()
+ local Self = C_NewItems
+
+ INVENTORY = {}
+ SplitUI = StackSplitFrame
+
+ for i=MIN,MAX do
+ SlotUpdate(i, true)
+ end
+
+ Self:RegisterEvent("BAG_CLOSED")
+ Self:RegisterEvent("ITEM_LOCKED")
+ Self:RegisterEvent("ITEM_UNLOCKED")
+ Self:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+
+ Self:SetScript("OnEvent", EventHandler)
+ hooksecurefunc("PickupContainerItem", PickupContainerItem)
+end
+
+--[[
+ I hate this, but we have to be first.
+ Or we pre-call a C_NewItems func.
+]]
+C_NewItems:RegisterEvent("BAG_UPDATE")
+
+--[[
+
+ C_NewItems
+
+]]
+
+function C_NewItems.ClearAll()
+ if ( not INVENTORY ) then return Construct() end
+
+ for i=MIN,MAX do
+ SlotUpdate(i, true)
+ end
+end
+
+function C_NewItems.IsNewItem(ContainerIndex, SlotIndex)
+ if ( not INVENTORY ) then return Construct() end
+
+ local Slot = GetSlotInfo(ContainerIndex, SlotIndex)
+ return Slot and Slot.New
+end
+
+function C_NewItems.RemoveNewItem(ContainerIndex, SlotIndex)
+ if ( not INVENTORY ) then return Construct() end
+
+ local Slot = GetSlotInfo(ContainerIndex, SlotIndex)
+ if ( Slot ) then
+ Slot.New = nil
+ end
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_PartyInfo.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_PartyInfo.lua
new file mode 100644
index 0000000..ee4b6af
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_PartyInfo.lua
@@ -0,0 +1,28 @@
+local IsInRaid = IsInRaid
+local SendChatMessage = SendChatMessage
+local strformat = string.format
+local NewTicker = C_Timer.NewTicker
+
+C_PartyInfo = C_PartyInfo or {}
+
+local countdownTicker
+function C_PartyInfo.DoCountdown(count)
+ if ( countdownTicker ) then
+ return
+ end
+
+ local channel = IsInRaid() and "RAID_WARNING" or "PARTY"
+ local countdown = count
+
+ local func = function()
+ local timer = strformat(COMPACT_UNIT_FRAME_COUNTDOWN_START, countdown)
+ local now = COMPACT_UNIT_FRAME_COUNTDOWN_NOW
+ if ( countdown == 0 ) then
+ countdownTicker = nil
+ end
+ SendChatMessage(countdown > 0 and timer or now, channel)
+ countdown = countdown - 1
+ end
+
+ countdownTicker = NewTicker(1, func, count + 1)
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_PvP.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_PvP.lua
new file mode 100644
index 0000000..af48eb7
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_PvP.lua
@@ -0,0 +1,35 @@
+local IsInInstance = IsInInstance
+local GetMapInfo = GetMapInfo
+
+C_PvP = C_PvP or {}
+
+local MAX_BATTLEGOUND_UNIT = {
+ AlteracValley = 40,
+ ArathiBasin = 15,
+ NetherstormArena = 15,
+ WarsongGulch = 10,
+ IsleofConquest = 40,
+ StrandoftheAncients = 15,
+ LakeWintergrasp = 40,
+}
+
+function C_PvP.IsRatedBattleground()
+ return false
+end
+
+function C_PvP.IsWarModeDesired()
+ return false
+end
+
+function C_PvP.IsPvPMap()
+ local inInstance, instanceType = IsInInstance()
+ if ( not inInstance ) then
+ return
+ end
+
+ return instanceType == "pvp" or instanceType == "arena"
+end
+
+function GetMaxUnitNumberBattleground()
+ return MAX_BATTLEGOUND_UNIT[GetMapInfo()]
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Spell.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Spell.lua
new file mode 100644
index 0000000..ca2d00b
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Spell.lua
@@ -0,0 +1,16 @@
+local _, Private = ...
+
+local EventHandler = Private.EventHandler
+local EventHandler_Fire = EventHandler.Fire
+EventHandler.AddEvent("SPELL_DATA_LOAD_RESULT")
+
+C_Spell = C_Spell or {}
+
+function C_Spell.RequestLoadSpellData(spellID)
+ -- Fire SPELL_DATA_LOAD_RESULT when requested.
+ EventHandler_Fire(nil, "SPELL_DATA_LOAD_RESULT", spellID, true)
+end
+
+function C_Spell.DoesSpellExist(spellInfo)
+ return GetSpellInfo(spellInfo)
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Talent.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Talent.lua
new file mode 100644
index 0000000..589d126
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Talent.lua
@@ -0,0 +1,14 @@
+C_Talent = C_Talent or {}
+
+function C_Talent.GetNumTalentGroups()
+ return 0 -- TODO
+end
+
+function C_Talent.GetTalentGroupSettings()
+ -- TODO
+end
+
+function C_Talent.GetSpecInfoCache()
+ -- Returns table: return "Spec"..C_Talent.GetSpecInfoCache().activeTalentGroup
+ return { activeTalentGroup = 0 }
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Texture.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Texture.lua
new file mode 100644
index 0000000..3ffc711
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_Texture.lua
@@ -0,0 +1,29 @@
+C_Texture = C_Texture or {};
+local CONST_ATLAS_WIDTH = 1;
+local CONST_ATLAS_HEIGHT = 2;
+local CONST_ATLAS_LEFT = 3;
+local CONST_ATLAS_RIGHT = 4;
+local CONST_ATLAS_TOP = 5;
+local CONST_ATLAS_BOTTOM = 6;
+local CONST_ATLAS_TILESHORIZ = 7;
+local CONST_ATLAS_TILESVERT = 8;
+local CONST_ATLAS_TEXTUREPATH = 9;
+
+function C_Texture.GetAtlasInfo( atlas )
+ assert(atlas, "C_Texture.GetAtlasInfo: AtlasName must be specified");
+ --assert(ATLAS_INFO_STORAGE[atlas], "C_Texture.GetAtlasInfo: Atlas named "..atlas.." does not exist");
+ local info = ATLAS_INFO_STORAGE[atlas];
+ if (info) then
+ return {
+ width = info[CONST_ATLAS_WIDTH],
+ height = info[CONST_ATLAS_HEIGHT],
+ leftTexCoord = info[CONST_ATLAS_LEFT],
+ rightTexCoord = info[CONST_ATLAS_RIGHT],
+ topTexCoord = info[CONST_ATLAS_TOP],
+ bottomTexCoord = info[CONST_ATLAS_BOTTOM],
+ tilesHorizontally = info[CONST_ATLAS_TILESHORIZ],
+ tilesVertically = info[CONST_ATLAS_TILESVERT],
+ filename = info[CONST_ATLAS_TEXTUREPATH],
+ };
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_TimerAugment.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_TimerAugment.lua
new file mode 100644
index 0000000..aa745d6
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_TimerAugment.lua
@@ -0,0 +1,106 @@
+local _G = _G
+local Call = pcall
+local Next = next
+
+local C_Timer = TimerFrame or CreateFrame("Frame", "TimerFrame")
+local ObjPool = {}
+
+local function Pool(Timer)
+ Timer.Ref = nil
+ Timer.Callback = nil
+ Timer.Iteration = nil
+ ObjPool[#ObjPool+1] = Timer
+end
+
+local function Caller(Self)
+ Call(Self.Callback)
+
+ if ( Self.Callback ) then
+ local Iteration = Self.Iteration
+ if ( Iteration ) then
+ if ( Iteration == 1 ) then
+ Self.Ref:Cancel()
+ else
+ Self.Iteration = Iteration - 1
+ end
+ elseif ( not Self.Ref ) then
+ Pool(Self)
+ end
+ end
+end
+
+local function New()
+ local Index, Timer = Next(ObjPool)
+
+ if ( Timer ) then
+ ObjPool[Index] = nil
+ else
+ local A = C_Timer:CreateAnimationGroup()
+ Timer = A:CreateAnimation("Animation")
+ Timer:SetScript("OnFinished", Caller)
+ end
+
+ return Timer
+end
+
+local function Cancel(Self)
+ if ( Self.__Timer ) then
+ Self.__Timer:Stop()
+ Pool(Self.__Timer)
+ Self.__Timer = nil
+ end
+end
+
+local function IsCancelled(Self)
+ return not Self.__Timer
+end
+
+local function Create(Duration, Callback, Iteration, Ticker)
+ local Timer = New()
+
+ Timer.Ref = Ticker and { __Timer = Timer, Cancel = Cancel, IsCancelled = IsCancelled }
+ Timer:GetParent():SetLooping(Ticker and "REPEAT" or "NONE")
+ Timer.Iteration = Iteration
+ Timer.Callback = Callback
+
+ Timer:SetDuration(Duration > 0 and Duration or .1)
+ Timer:Play()
+
+ return Timer.Ref
+end
+
+--[[
+ METHOD
+]]
+
+function C_Timer.After(Duration, Callback, _)
+ if ( _ ) then
+ Duration = Callback
+ Callback = _
+ end
+
+ Create(Duration, Callback)
+end
+
+function C_Timer.NewTimer(Duration, Callback, _)
+ if ( _ ) then
+ Duration = Callback
+ Callback = _
+ end
+
+ return Create(Duration, Callback, 1, true)
+end
+
+function C_Timer.NewTicker(Duration, Callback, Iteration, _)
+ if ( _ ) then
+ Duration = Callback
+ Callback = Iteration
+ Iteration = _
+ end
+
+ return Create(Duration, Callback, Iteration, true)
+end
+
+-- Global
+_G.C_Timer = C_Timer
+C_Timer._version = 2
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_UI.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_UI.lua
new file mode 100644
index 0000000..a6c291d
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_UI.lua
@@ -0,0 +1,5 @@
+C_UI = C_UI or {}
+
+function C_UI.Reload()
+ ReloadUI()
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_VoiceChat.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_VoiceChat.lua
new file mode 100644
index 0000000..e905fc3
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/C_VoiceChat.lua
@@ -0,0 +1,5 @@
+C_VoiceChat = C_VoiceChat or {}
+
+function C_VoiceChat.GetTtsVoices()
+ return {}
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/CallbackRegistry.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/CallbackRegistry.lua
new file mode 100644
index 0000000..cb4a9b1
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/CallbackRegistry.lua
@@ -0,0 +1,196 @@
+local secureexecuterange = secureexecuterange;
+local securecallfunction = securecallfunction;
+local unpack = unpack;
+local error = error;
+local pairs = pairs;
+local rawset = rawset;
+local next = next;
+
+-- Callbacks can be registered without an owner as a matter of convenience. Generally this is fine when you never
+-- intend to release the callback.
+local generateOwnerID = CreateCounter();
+
+local InsertEventAttribute = "insert-secure-event";
+local AttributeDelegate = CreateFrame("FRAME");
+AttributeDelegate:SetForbidden();
+AttributeDelegate:SetScript("OnAttributeChanged", function(self, attribute, value)
+ if attribute == InsertEventAttribute then
+ local registry, event = securecallfunction(unpack, value);
+ if type(event) ~= "string" then
+ error("AttributeDelegate OnAttributeChanged 'event' requires string type.")
+ end
+ for callbackType, callbackTable in pairs(registry:GetCallbackTables()) do
+ if not callbackTable[event] then
+ rawset(callbackTable, event, {});
+ end
+ end
+ end
+end);
+
+local CallbackType = EnumUtil.MakeEnum("Closure", "Function");
+
+CallbackRegistryMixin = {};
+
+function CallbackRegistryMixin:OnLoad()
+ local callbackTables = {};
+ for callbackType, value in pairs(CallbackType) do
+ callbackTables[value] = {};
+ end
+ self.callbackTables = callbackTables;
+end
+
+function CallbackRegistryMixin:SetUndefinedEventsAllowed(allowed)
+ self.isUndefinedEventAllowed = allowed;
+end
+
+function CallbackRegistryMixin:GetCallbackTables()
+ return self.callbackTables;
+end
+
+function CallbackRegistryMixin:GetCallbackTable(callbackType)
+ return self.callbackTables[callbackType];
+end
+
+function CallbackRegistryMixin:GetCallbacksByEvent(callbackType, event)
+ local callbackTable = self:GetCallbackTable(callbackType);
+ return callbackTable[event];
+end
+
+function CallbackRegistryMixin:HasRegistrantsForEvent(event)
+ for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
+ local callbacks = callbackTable[event];
+ if callbacks and securecallfunction(next, callbacks) then
+ return true;
+ end
+ end
+ return false;
+end
+
+function CallbackRegistryMixin:SecureInsertEvent(event)
+ if not self:HasRegistrantsForEvent(event) then
+ AttributeDelegate:SetAttribute(InsertEventAttribute, {self, event});
+ end
+end
+
+function CallbackRegistryMixin:RegisterCallback(event, func, owner, ...)
+ if type(event) ~= "string" then
+ error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.");
+ elseif type(func) ~= "function" then
+ error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.");
+ else
+ if owner == nil then
+ owner = generateOwnerID();
+ elseif type(owner) == "number" then
+ error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
+ end
+ end
+
+ -- Taint barrier for inserting event key into callback tables.
+ self:SecureInsertEvent(event);
+
+ for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
+ local callbacks = callbackTable[event];
+ callbacks[owner] = nil;
+ end
+
+ local count = select("#", ...);
+ if count > 0 then
+ local callbacks = self:GetCallbacksByEvent(CallbackType.Closure, event);
+ callbacks[owner] = GenerateClosure(func, owner, ...);
+ else
+ local callbacks = self:GetCallbacksByEvent(CallbackType.Function, event);
+ callbacks[owner] = func;
+ end
+
+ return owner;
+end
+
+local function CreateCallbackHandle(cbr, event, owner)
+ -- Wrapped in a table for future flexibility.
+ local handle =
+ {
+ Unregister = function()
+ cbr:UnregisterCallback(event, owner);
+ end,
+ };
+ return handle;
+end
+
+function CallbackRegistryMixin:RegisterCallbackWithHandle(event, func, owner, ...)
+ owner = self:RegisterCallback(event, func, owner, ...);
+ return CreateCallbackHandle(self, event, owner);
+end
+
+function CallbackRegistryMixin:TriggerEvent(event, ...)
+ if type(event) ~= "string" then
+ error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.");
+ elseif not self.isUndefinedEventAllowed and not self.Event[event] then
+ error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event));
+ end
+
+ local closures = self:GetCallbacksByEvent(CallbackType.Closure, event);
+ if closures then
+ local function CallbackRegistryExecuteClosurePair(owner, closure, ...)
+ securecallfunction(closure, ...);
+ end
+
+ secureexecuterange(closures, CallbackRegistryExecuteClosurePair, ...);
+ end
+
+ local funcs = self:GetCallbacksByEvent(CallbackType.Function, event);
+ if funcs then
+ local function CallbackRegistryExecuteOwnerPair(owner, func, ...)
+ securecallfunction(func, owner, ...);
+ end
+
+ secureexecuterange(funcs, CallbackRegistryExecuteOwnerPair, ...);
+ end
+end
+
+function CallbackRegistryMixin:UnregisterCallback(event, owner)
+ if type(event) ~= "string" then
+ error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.");
+ elseif owner == nil then
+ error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.");
+ end
+
+ for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
+ local callbacks = callbackTable[event];
+ if callbacks then
+ callbacks[owner] = nil;
+ end
+ end
+end
+
+function CallbackRegistryMixin:UnregisterEvents(eventTable)
+ if eventTable then
+ for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
+ for event in pairs(eventTable) do
+ if callbackTable[event] then
+ callbackTable[event] = nil;
+ end
+ end
+ end
+ else
+ for callbackType, callbackTable in pairs(self:GetCallbackTables()) do
+ wipe(callbackTable);
+ end
+ end
+end
+
+function CallbackRegistryMixin:GenerateCallbackEvents(events)
+ if not self.Event then
+ self.Event = {};
+ end
+
+ for eventIndex, eventName in ipairs(events) do
+ if self.Event[eventName] then
+ error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName));
+ end
+ self.Event[eventName] = eventName;
+ end
+end
+
+function CallbackRegistryMixin.DoesFrameHaveEvent(frame, event)
+ return frame.Event and frame.Event[event];
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Color.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Color.lua
new file mode 100644
index 0000000..d6405ff
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Color.lua
@@ -0,0 +1,64 @@
+if ( ColorMixin or CreateColor ) then return end
+
+ColorMixin = {}
+ColorMixin.___CAPI = 1
+
+function CreateColor(r, g, b, a)
+ local color = CreateFromMixins(ColorMixin)
+ color:OnLoad(r, g, b, a)
+ return color
+end
+
+function ColorMixin:OnLoad(r, g, b, a)
+ self:SetRGBA(r, g, b, a)
+end
+
+function ColorMixin:IsEqualTo(otherColor)
+ return self.r == otherColor.r
+ and self.g == otherColor.g
+ and self.b == otherColor.b
+ and self.a == otherColor.a
+end
+
+function ColorMixin:GetRGB()
+ return self.r, self.g, self.b
+end
+
+function ColorMixin:GetRGBAsBytes()
+ return self.r * 255, self.g * 255, self.b * 255
+end
+
+function ColorMixin:GetRGBA()
+ return self.r, self.g, self.b, self.a
+end
+
+function ColorMixin:GetRGBAAsBytes()
+ return self.r * 255, self.g * 255, self.b * 255, (self.a or 1) * 255
+end
+
+function ColorMixin:SetRGBA(r, g, b, a)
+ self.r = r
+ self.g = g
+ self.b = b
+ self.a = a
+end
+
+function ColorMixin:SetRGB(r, g, b)
+ self:SetRGBA(r, g, b, nil)
+end
+
+function ColorMixin:GenerateHexColor()
+ return ("ff%.2x%.2x%.2x"):format(self:GetRGBAsBytes())
+end
+
+function ColorMixin:GenerateHexColorMarkup()
+ return "|c"..self:GenerateHexColor()
+end
+
+function ColorMixin:WrapTextInColorCode(text)
+ return WrapTextInColorCode(text, self:GenerateHexColor())
+end
+
+function WrapTextInColorCode(text, colorHexString)
+ return ("|c%s%s|r"):format(colorHexString, text)
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/ColorUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/ColorUtil.lua
new file mode 100644
index 0000000..f353bb9
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/ColorUtil.lua
@@ -0,0 +1,61 @@
+local ColorMixin = ColorMixin
+if ( ColorMixin and not ColorMixin.___CAPI ) then return end
+ColorMixin.___CAPI = nil
+
+local RAID_CLASS_COLORS = RAID_CLASS_COLORS
+local Mixin = Mixin
+
+for _, classColor in pairs(RAID_CLASS_COLORS) do
+ if ( classColor.colorStr ) then break end
+ Mixin(classColor, ColorMixin);
+ classColor.colorStr = classColor:GenerateHexColor();
+end
+
+function ExtractColorValueFromHex(str, index)
+ return tonumber(str:sub(index, index + 1), 16) / 255;
+end
+
+function CreateColorFromHexString(hexColor)
+ if #hexColor == 8 then
+ local a, r, g, b = ExtractColorValueFromHex(hexColor, 1), ExtractColorValueFromHex(hexColor, 3), ExtractColorValueFromHex(hexColor, 5), ExtractColorValueFromHex(hexColor, 7);
+ return CreateColor(r, g, b, a);
+ else
+ --GMError("CreateColorFromHexString input must be hexadecimal digits in this format: AARRGGBB.");
+ end
+end
+
+function CreateColorFromBytes(r, g, b, a)
+ return CreateColor(r / 255, g / 255, b / 255, a / 255);
+end
+
+function AreColorsEqual(left, right)
+ if left and right then
+ return left:IsEqualTo(right);
+ end
+ return left == right;
+end
+
+function GetClassColor(classFilename)
+ local color = RAID_CLASS_COLORS[classFilename];
+ if color then
+ return color.r, color.g, color.b, color.colorStr;
+ end
+ return 1, 1, 1, "ffffffff";
+end
+
+function GetClassColorObj(classFilename)
+ -- TODO: Remove this, convert everything that's using GetClassColor to use the object instead, then begin using that again
+ return RAID_CLASS_COLORS[classFilename];
+end
+
+function GetClassColoredTextForUnit(unit, text)
+ local classFilename = select(2, UnitClass(unit));
+ local color = GetClassColorObj(classFilename);
+ if (color) then
+ return color:WrapTextInColorCode(text);
+ end
+end
+
+function GetFactionColor(factionGroupTag)
+ return PLAYER_FACTION_COLORS[PLAYER_FACTION_GROUP[factionGroupTag]];
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/CombatLog.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/CombatLog.lua
new file mode 100644
index 0000000..c433734
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/CombatLog.lua
@@ -0,0 +1,15 @@
+function CombatLogGetCurrentEventInfo(...)
+ if ( ... ) then
+ -- 3.3.5 payload
+ local Timestamp, SubEvent, SrcGUID, SrcName, SrcFlag, DstGUID, DstName, DstFlag = ...
+
+ -- Invalid modern payloads
+ local HideCaster = false
+ local SrcRaidFlag = nil
+ local DstRaidFlag = nil
+
+ -- Return modern payload
+ -- Note: Going to straight pass 9th on, may not work if blizz changed order.
+ return Timestamp, SubEvent, HideCaster, SrcGUID, SrcName, SrcFlag, SrcRaidFlag, DstGUID, DstName, DstFlag, DstRaidFlag, select(9, ...)
+ end
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Constants.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Constants.lua
new file mode 100644
index 0000000..8e194be
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Constants.lua
@@ -0,0 +1,56 @@
+local ExpansionLevel = GetAccountExpansionLevel()
+
+ WOW_PROJECT_MAINLINE = 1
+ WOW_PROJECT_CLASSIC = 2
+ WOW_PROJECT_WOWLABS = 3
+ WOW_PROJECT_BURNING_CRUSADE_CLASSIC = 5
+ WOW_PROJECT_WRATH_CLASSIC = 11
+ WOW_PROJECT_CATACLYSM_CLASSIC = 14
+WOW_PROJECT_ID = WOW_PROJECT_WRATH_CLASSIC
+
+ LE_EXPANSION_CLASSIC = 0
+ LE_EXPANSION_BURNING_CRUSADE = 1
+ LE_EXPANSION_WRATH_OF_THE_LICH_KING = 2
+ LE_EXPANSION_CATACLYSM = 3
+ LE_EXPANSION_MISTS_OF_PANDARIA = 4
+ LE_EXPANSION_WARLORDS_OF_DRAENOR = 5
+ LE_EXPANSION_LEGION = 6
+ LE_EXPANSION_BATTLE_FOR_AZEROTH = 7
+ LE_EXPANSION_SHADOWLANDS = 8
+ LE_EXPANSION_DRAGONFLIGHT = 9
+ LE_EXPANSION_WAR_WITHIN = 10
+LE_EXPANSION_LEVEL_CURRENT = ExpansionLevel
+
+-- RCE Expansion
+if ( ExpansionLevel == 0 ) then
+ WOW_PROJECT_ID_RCE = WOW_PROJECT_CLASSIC
+elseif ( ExpansionLevel == 1 ) then
+ WOW_PROJECT_ID_RCE = WOW_PROJECT_BURNING_CRUSADE_CLASSIC
+else
+ WOW_PROJECT_ID_RCE = WOW_PROJECT_WRATH_CLASSIC
+end
+
+LE_PARTY_CATEGORY_HOME = 1
+LE_PARTY_CATEGORY_INSTANCE = 2
+
+-- Panel Positions
+PANEL_INSET_LEFT_OFFSET = 4
+PANEL_INSET_RIGHT_OFFSET = -6
+PANEL_INSET_BOTTOM_OFFSET = 4
+PANEL_INSET_BOTTOM_BUTTON_OFFSET = 26
+PANEL_INSET_TOP_OFFSET = -24
+PANEL_INSET_ATTIC_OFFSET = -60
+
+-- Panel default size
+PANEL_DEFAULT_WIDTH = 338
+PANEL_DEFAULT_HEIGHT = 424
+
+-- Role icons
+INLINE_TANK_ICON = "|TInterface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES.blp:16:16:0:0:64:64:0:19:22:41|t"
+INLINE_HEALER_ICON = "|TInterface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES.blp:16:16:0:0:64:64:20:39:1:20|t"
+INLINE_DAMAGER_ICON = "|TInterface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES.blp:16:16:0:0:64:64:20:39:22:41|t"
+
+-- Strings (TODO: Locale)
+AURA_TYPE_DEBUFF = "DEBUFF"
+AURA_TYPE_BUFF = "BUFF"
+SPECIALIZATION = "Spec"
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Cooldown.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Cooldown.lua
new file mode 100644
index 0000000..13db6ec
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Cooldown.lua
@@ -0,0 +1,21 @@
+local GetTime = GetTime
+
+function CooldownFrame_Set(Self, Start, Duration, Enable, ForceShowDrawEdge, ModRate)
+ if Enable and Enable ~= 0 and Start > 0 and Duration > 0 then
+ --self:Show()
+ Self:SetDrawEdge(ForceShowDrawEdge)
+ Self:SetCooldown(Start, Duration, ModRate)
+ else
+ Self:Hide()
+ end
+end
+
+function CooldownFrame_Clear(Self)
+ Self:Hide()
+end
+
+function CooldownFrame_SetDisplayAsPercentage(Self, Percentage)
+ local Seconds = 100
+ --Self:Pause()
+ Self:SetCooldown(GetTime() - Seconds * Percentage, Seconds)
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EnumUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EnumUtil.lua
new file mode 100644
index 0000000..45f82be
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EnumUtil.lua
@@ -0,0 +1,22 @@
+EnumUtil = EnumUtil or {};
+
+local tInvert = tInvert
+local tContains = tContains
+local pairs = pairs
+
+function EnumUtil.MakeEnum(...)
+ return tInvert({...});
+end
+function EnumUtil.IsValid(enumClass, enumValue)
+ return tContains(enumClass, enumValue);
+end
+function EnumUtil.GenerateNameTranslation(enum)
+ return function (enumValue)
+ for key, value in pairs(enum) do
+ if value == enumValue then
+ return key;
+ end
+ end
+ return UNKNOWN..enumValue;
+ end
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EventHandler.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EventHandler.lua
new file mode 100644
index 0000000..5edda81
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EventHandler.lua
@@ -0,0 +1,195 @@
+local _, Private = ...
+
+local CreateFrame = CreateFrame
+local GetMetaTable = getmetatable
+local HookSecureFunc = hooksecurefunc
+
+local EventHandler = CreateFrame("Frame")
+local ___Register = EventHandler.RegisterEvent
+local ___Unregister = EventHandler.UnregisterEvent
+
+local EVENT_ATLAS = {}
+local EVENT_OBJECT = {}
+local EVENT_ARCHIVE = {}
+
+local function EventHandler_Fire(Self, Listener, ...)
+ local Event = EVENT_ATLAS[Listener] or Listener
+
+ local Trigger = EventHandler[Event.."_TRIGGER"]
+ if ( Trigger and Trigger(nil, Listener, ...) == false ) then return end
+
+ local Registered = EVENT_OBJECT[Event]
+ local Shuffle = 1
+
+ for i=1,#Registered do
+ local Obj = Registered[i]
+
+ if ( Obj ) then
+ local OnEvent = Obj:GetScript("OnEvent")
+ if ( OnEvent ) then
+ OnEvent(Obj, Event, ...)
+ end
+
+ if ( i ~= Shuffle ) then
+ Registered[Shuffle] = Obj
+ Registered[i] = nil
+ end
+
+ Shuffle = Shuffle + 1
+ else
+ Registered[i] = nil
+ end
+ end
+end
+
+local function EventHandler_Listener(Event, Func)
+ local Archive = EVENT_ARCHIVE[Event]
+ if ( Archive[1] ) then
+ for i=1,#Archive do
+ Func(EventHandler, Archive[i])
+ end
+ else
+ Func(EventHandler, Archive)
+ end
+end
+
+--[[
+ EventHandler: Method Hook(s)
+]]
+
+local function Method_RegisterEvent(Self, Event)
+ local Archive = EVENT_ARCHIVE[Event]
+
+ if ( Archive ) then
+ local Registered = EVENT_OBJECT[Event]
+ local RegisteredTotal = 0
+
+ if ( Registered ) then
+ RegisteredTotal = #Registered
+ for i=1,RegisteredTotal do
+ if ( Registered[i] == Self ) then
+ return
+ end
+ end
+ else
+ local Trigger = EventHandler[Event.."_REGISTER"]
+ if ( Trigger and Trigger() == false ) then return end
+
+ Registered = {}
+ EVENT_OBJECT[Event] = Registered
+ EventHandler_Listener(Event, ___Register)
+ end
+
+ Registered[RegisteredTotal+1] = Self
+ end
+end
+
+local function Method_UnregisterEvent(Self, Event)
+ local Registered = EVENT_OBJECT[Event]
+
+ if ( Registered ) then
+ local RegisteredTotal, RegisteredIndex = 0
+
+ for i=1,#Registered do
+ local Obj = Registered[i]
+ if ( Obj ) then
+ RegisteredTotal = RegisteredTotal+1
+
+ if ( Obj == Self ) then
+ RegisteredIndex = i
+ if ( RegisteredTotal > 1 ) then break end
+ end
+ end
+ end
+
+ if ( RegisteredIndex ) then
+ if ( RegisteredTotal == 1 ) then
+ local Trigger = EventHandler[Event.."_UNREGISTER"]
+ if ( Trigger and Trigger() == false ) then return end
+
+ EventHandler_Listener(Event, ___Unregister)
+ EVENT_OBJECT[Event] = nil
+ else
+ Registered[RegisteredIndex] = false
+ end
+ end
+ end
+end
+
+local function Method_RegisterUnitEvent(Self, Event, Unit1, Unit2)
+ local UnitEventFrame = Self.___UnitEventHandler
+
+ if ( not UnitEventFrame ) then
+ UnitEventFrame = CreateFrame("Frame")
+ Self.___UnitEventHandler = UnitEventFrame
+
+ UnitEventFrame:SetScript("OnEvent", function(_, Event, ...)
+ local Units = UnitEventFrame[Event]
+ if ( Units ) then
+ local Unit = ...
+ if ( Units[1] == Unit or Units[2] == Unit ) then
+ local OnEvent = Self:GetScript("OnEvent")
+ if ( OnEvent ) then
+ OnEvent(Self, Event, ...)
+ end
+ end
+ end
+ end)
+
+ HookSecureFunc(Self, "UnregisterEvent", function(_, Event)
+ if ( UnitEventFrame[Event] ) then
+ UnitEventFrame[Event] = nil
+ ___Unregister(UnitEventFrame, Event) -- Avoid our hook, call directly.
+ end
+ end)
+ end
+
+ local Units = UnitEventFrame[Event]
+ if ( not Units ) then
+ Units = {}
+ UnitEventFrame[Event] = Units
+ UnitEventFrame:RegisterEvent(Event)
+ end
+
+ Units[1] = Unit1
+ if ( Unit2 ) then
+ Units[2] = Unit2
+ end
+end
+
+--[[
+ EventHandler: AddModernEvent
+ ----------
+ arg1 (String): Modern Event
+ arg2 (String): Authentic Event
+]]
+
+local function EventHandler_AddEvent(Modern, Event)
+ EVENT_ARCHIVE[Modern] = (Event) and Event or Modern
+ if ( Event ) then
+ if ( Event[1] ) then
+ for i=1,#Event do
+ EVENT_ATLAS[Event[i]] = Modern
+ end
+ else
+ EVENT_ATLAS[Event] = Modern
+ end
+ end
+end
+
+local FrameMeta = GetMetaTable(EventHandler).__index
+local ButtonMeta = GetMetaTable(CreateFrame("Button")).__index
+FrameMeta.RegisterUnitEvent = Method_RegisterUnitEvent
+ButtonMeta.RegisterUnitEvent = Method_RegisterUnitEvent
+HookSecureFunc(FrameMeta, "RegisterEvent", Method_RegisterEvent)
+HookSecureFunc(FrameMeta, "UnregisterEvent", Method_UnregisterEvent)
+HookSecureFunc(ButtonMeta, "RegisterEvent", Method_RegisterEvent)
+HookSecureFunc(ButtonMeta, "UnregisterEvent", Method_UnregisterEvent)
+EventHandler:SetScript("OnEvent", EventHandler_Fire)
+
+-- Module
+EventHandler.Fire = EventHandler_Fire
+EventHandler.AddEvent = EventHandler_AddEvent
+
+-- Private Namespace
+Private.EventHandler = EventHandler
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EventHandlerSupport.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EventHandlerSupport.lua
new file mode 100644
index 0000000..e6defe5
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/EventHandlerSupport.lua
@@ -0,0 +1,69 @@
+--[[
+ EventHandler: Support
+ -------
+ Note:
+ Some events are nested within individual files.
+]]
+
+local _, Private = ...
+
+local EventHandler = Private.EventHandler
+local EventHandler_Fire = EventHandler.Fire
+local _ = EventHandler.AddEvent
+
+--local UnitIsConnected = UnitIsConnected
+
+-- SPELL_DATA_LOAD_RESULT : C_Spell.lua
+-- ITEM_DATA_LOAD_RESULT : ItemUtil.lua
+-- UNIT_HEAL_PREDICTION : HealPrediction.lua
+
+--[[
+ EventHandler: GROUP_ROSTER_UPDATE / GROUP_JOINED / GROUP_LEFT
+]]
+
+_("GROUP_ROSTER_UPDATE", "PARTY_MEMBERS_CHANGED")
+--_("GROUP_JOINED")
+--_("GROUP_LEFT")
+--_("GROUP_FORMED")
+
+--[[
+ EventHandler: UNIT_CONNECTION
+]]
+
+--[[local UNIT_CONNECTION_STATUS
+
+local function UNIT_CONNECTION_TRIGGER(_, Event, UnitID, Connected)
+ if ( not Connected ) then -- HACK: Prevent continuation within Handler, only callback after we add "Connected" to payload.
+ local Connected = UnitIsConnected(UnitID) and true or false
+
+ if ( Connected ~= UNIT_CONNECTION_STATUS[UnitID] ) then
+ EventHandler_Fire(nil, "UNIT_CONNECTION", UnitID, Connected)
+ UNIT_CONNECTION_STATUS[UnitID] = Connected or nil
+ end
+
+ return false
+ end
+end
+
+function EventHandler:UNIT_CONNECTION_REGISTER()
+ if ( not UNIT_CONNECTION_STATUS ) then
+ UNIT_CONNECTION_STATUS = {}
+ end
+end
+
+function EventHandler:UNIT_CONNECTION_UNREGISTER()
+ UNIT_CONNECTION_STATUS = nil
+end
+
+_("UNIT_CONNECTION", "UNIT_FACTION")]]
+
+--[[
+
+ EventHandler: INSPECT_READY
+ Notes:
+ Work in progress!
+ UnitTalentQuery
+
+]]
+
+_("INSPECT_READY", "INSPECT_TALENT_READY")
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/GlobalCallbackRegistry.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/GlobalCallbackRegistry.lua
new file mode 100644
index 0000000..4c394e1
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/GlobalCallbackRegistry.lua
@@ -0,0 +1,76 @@
+EventRegistry = CreateFromMixins(CallbackRegistryMixin);
+
+function EventRegistry:OnLoad()
+ CallbackRegistryMixin.OnLoad(self);
+ self:SetUndefinedEventsAllowed(true);
+
+ self.frameEventFrame = CreateFrame("Frame");
+ self.frameEventFrame:SetScript("OnEvent", function(frameEventFrame, event, ...)
+ self:TriggerEvent(event, ...);
+ end);
+
+ self.frameEventFrame:SetScript("OnAttributeChanged", self.OnAttributeChanged);
+ self.frameEventFrame.registry = self;
+end
+
+function EventRegistry:OnAttributeChanged(frameEvent, value)
+ self = self.registry;
+
+ if value == 0 then
+ self.frameEventFrame:UnregisterEvent(frameEvent);
+ elseif value == 1 then
+ self.frameEventFrame:RegisterEvent(frameEvent);
+ end
+end
+
+function EventRegistry:RegisterFrameEvent(frameEvent)
+ self.frameEventFrame:SetAttribute(frameEvent, (self.frameEventFrame:GetAttribute(frameEvent) or 0) + 1);
+end
+
+function EventRegistry:UnregisterFrameEvent(frameEvent)
+ local eventCount = self.frameEventFrame:GetAttribute(frameEvent) or 0;
+ if eventCount > 0 then
+ self.frameEventFrame:SetAttribute(frameEvent, eventCount - 1);
+ end
+end
+
+function EventRegistry:RegisterFrameEventAndCallback(frameEvent, ...)
+ self:RegisterFrameEvent(frameEvent);
+ return self:RegisterCallback(frameEvent, ...);
+end
+
+local function CreateCallbackHandle(cbr, cbrHandle, frameEvent)
+ -- Wrapped in a table for future flexibility.
+ local handle = {
+ Unregister = function()
+ cbr:UnregisterFrameEvent(frameEvent);
+ cbrHandle:Unregister();
+ end,
+ };
+ return handle;
+end
+
+
+function EventRegistry:RegisterFrameEventAndCallbackWithHandle(frameEvent, ...)
+ self:RegisterFrameEvent(frameEvent);
+ local cbrHandle = self:RegisterCallbackWithHandle(frameEvent, ...);
+ return CreateCallbackHandle(self, cbrHandle, frameEvent);
+end
+
+function EventRegistry:UnregisterFrameEventAndCallback(frameEvent, ...)
+ self:UnregisterFrameEvent(frameEvent);
+ self:UnregisterCallback(frameEvent, ...);
+end
+
+function EventRegistry:GetEventCounts(...)
+ local counts = {};
+ for i = 1, select("#", ...) do
+ local frameEvent = select(i, ...);
+ local count = self.frameEventFrame:GetAttribute(frameEvent) or "?";
+ table.insert(counts, ("%s : %s"):format(frameEvent, tostring(count)));
+ end
+
+ return table.concat(counts, "\n");
+end
+
+EventRegistry:OnLoad();
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Group.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Group.lua
new file mode 100644
index 0000000..03cf294
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Group.lua
@@ -0,0 +1,136 @@
+local GetNumRaidMembers = GetNumRaidMembers
+local GetNumPartyMembers = GetNumPartyMembers
+local GetPartyLeaderIndex = GetPartyLeaderIndex
+local UnitIsRaidOfficer = UnitIsRaidOfficer
+local UnitExists = UnitExists
+local UnitIsConnected = UnitIsConnected
+local UnitIsPlayer = UnitIsPlayer
+local UnitIsEnemy = UnitIsEnemy
+local GetRealNumRaidMembers = GetRealNumRaidMembers
+local GetRaidRosterInfo = GetRaidRosterInfo
+local UnitName = UnitName
+local PromoteToAssistant = PromoteToAssistant
+local DemoteAssistant = DemoteAssistant
+local NewTicker = C_Timer.NewTicker
+local LE_PARTY_CATEGORY_INSTANCE = LE_PARTY_CATEGORY_INSTANCE
+
+function GetDisplayedAllyFrames()
+end
+
+function IsInGroup(LE_CATEGORY)
+ if ( LE_CATEGORY and LE_CATEGORY == LE_PARTY_CATEGORY_INSTANCE ) then
+ return false
+ end
+ return GetNumRaidMembers() > 0 or GetNumPartyMembers() > 0
+end
+
+function IsInRaid(LE_CATEGORY)
+ if ( LE_CATEGORY and LE_CATEGORY ~= LE_PARTY_CATEGORY_INSTANCE ) then
+ return false
+ end
+ return GetNumRaidMembers() > 0
+end
+
+function GetNumSubgroupMembers()
+ return GetNumPartyMembers()
+end
+
+function GetNumGroupMembers()
+ local Total = GetNumRaidMembers()
+
+ if ( Total < 1 ) then
+ Total = GetNumPartyMembers()
+ Total = (Total > 0) and Total+1 or 0
+ end
+
+ return Total
+end
+
+function UnitIsGroupLeader(unit)
+ local isLeader
+
+ if ( not IsInGroup() ) then
+ isLeader = false
+ elseif ( unit == "player" ) then
+ isLeader = IsInRaid() and IsRaidLeader() or IsPartyLeader()
+ else
+ local index = unit:match("%d+")
+ isLeader = index and GetPartyLeaderIndex() == index
+ end
+
+ return isLeader
+end
+
+function UnitIsGroupAssistant(unit)
+ local isAssistant = false
+ if ( IsInRaid() ) then
+ -- UnitIsRaidOfficer return correctly also for party
+ isAssistant = UnitIsRaidOfficer(unit) and not UnitIsGroupLeader(unit)
+ end
+ return isAssistant
+end
+
+local isAllAssistant = false
+function IsEveryoneAssistant()
+ return isAllAssistant
+end
+
+local assistantTicker
+function SetEveryoneIsAssistant(enable)
+ local numMembers = GetRealNumRaidMembers()
+
+ if ( numMembers > 0 ) then
+ if ( assistantTicker ) then
+ assistantTicker:Cancel()
+ assistantTicker = nil
+ end
+
+ local index = 1
+ local func = function()
+ local unit = "raid"..index
+ if ( IsEveryoneAssistant() ) then
+ PromoteToAssistant(unit)
+ else
+ DemoteAssistant(unit)
+ end
+ index = index + 1
+ end
+
+ isAllAssistant = enable
+ assistantTicker = NewTicker(0.15, func, numMembers)
+ end
+end
+
+function CanBeRaidTarget(unit)
+ if ( not unit ) then
+ return
+ end
+
+ if ( UnitExists(unit) and UnitIsConnected(unit) ) then
+ return not ( UnitIsPlayer(unit) and UnitIsEnemy("player", unit) )
+ end
+end
+
+function UnitInOtherParty(unit)
+ if not C_Map.IsWorldMap(GetZoneText()) or UnitPhaseReason(unit) then
+ return false
+ end
+
+ if not ( IsInRaid() and UnitIsConnected(unit) ) then
+ return
+ end
+
+ for i = 1, GetRealNumRaidMembers() do
+ local name, rank, subgroup, level, class, fileName, zone = GetRaidRosterInfo(i)
+ if ( name == UnitName(unit) ) then
+ return not C_Map.IsWorldMap(zone)
+ end
+ end
+end
+
+function GetGroupMemberCountsForDisplay()
+ local data = GetGroupMemberCounts();
+ data.DAMAGER = data.DAMAGER + data.NOROLE; --People without a role count as damage
+ data.NOROLE = 0;
+ return data;
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/HealPrediction.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/HealPrediction.lua
new file mode 100644
index 0000000..14f79be
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/HealPrediction.lua
@@ -0,0 +1,136 @@
+local _, Private = ...
+
+local BitBand = bit.band
+local GetTime = GetTime
+local UnitGUID = UnitGUID
+local UnitName = UnitName
+local EventHandler = Private.EventHandler
+local EventHandler_Fire = EventHandler.Fire
+local GetNumRaidMembers = GetNumRaidMembers
+local GetNumPartyMembers = GetNumPartyMembers
+
+local RESCOMM
+local HEALCOMM
+local HEALCOMM_PLAYER_GUID
+
+local UNIT_INDEX = {"player", "pet", "target", "focus"}
+
+for i=1,4 do
+ local Index = #UNIT_INDEX+1
+ UNIT_INDEX[Index] = "party"..i
+ UNIT_INDEX[Index+1] = "partypet"..i
+end
+
+for i=1, 40 do
+ local Index = #UNIT_INDEX+1
+ UNIT_INDEX[Index] = "raid"..i
+ UNIT_INDEX[Index+1] = "raidpet"..i
+end
+
+--[[
+ FUNCTIONS
+]]
+
+function UnitGetIncomingHeals(Unit, Healer, GUID)
+ if ( Unit ) then
+ if ( HEALCOMM == nil and LibStub ) then
+ HEALCOMM = LibStub:GetLibrary("LibHealComm-4.0", true) or false
+
+ if ( HEALCOMM and not HEALCOMM_PLAYER_GUID ) then
+ HEALCOMM_PLAYER_GUID = UnitGUID("player")
+ end
+ end
+
+ if ( HEALCOMM ) then
+ if ( not GUID ) then
+ Unit = UnitGUID(Unit)
+ end
+
+ return HEALCOMM:GetHealAmount(Unit, HEALCOMM.CASTED_HEALS, GetTime() + 5, HEALCOMM_PLAYER_GUID)
+ end
+ end
+end
+
+function UnitGetTotalAbsorbs(Unit)
+ return 0
+end
+
+function UnitGetTotalHealAbsorbs(Unit)
+ return 0
+end
+
+function UnitHasIncomingResurrection(Unit)
+ if ( Unit ) then
+ if ( RESCOMM == nil and LibStub ) then
+ RESCOMM = LibStub:GetLibrary("LibResComm-1.0", true) or false
+ end
+ return (RESCOMM) and RESCOMM:IsUnitBeingRessed(UnitName(Unit))
+ end
+end
+
+--[[
+ EVENT HANDLER:
+ UNIT_HEAL_PREDICTION
+]]
+
+local function UNIT_HEAL_PREDICTION(Limit, GUID, ...)
+ if ( not Limit ) then
+ local Raid = GetNumRaidMembers()
+ Limit = 4 + (Raid > 0 and 8 + (Raid*2) or (GetNumPartyMembers()*2))
+ end
+
+ for i=1, Limit do
+ local UnitID = UNIT_INDEX[i]
+ if ( GUID == UnitGUID(UnitID) ) then
+ EventHandler_Fire(nil, "UNIT_HEAL_PREDICTION", UnitID)
+ end
+ end
+
+ if ( ... ) then
+ UNIT_HEAL_PREDICTION(Limit, ...)
+ end
+end
+
+local function HealComm_HealStarted(_, Event, SrcGUID, SpellID, Type, EndTime, ...)
+ if ( SrcGUID == HEALCOMM_PLAYER_GUID and BitBand(Type, HEALCOMM.CASTED_HEALS) > 0 ) then
+ UNIT_HEAL_PREDICTION(nil, ...)
+ end
+end
+
+local function HealComm_ModifierChanged(_, _, SrcGUID)
+ UNIT_HEAL_PREDICTION(nil, SrcGUID)
+end
+
+function EventHandler:UNIT_HEAL_PREDICTION_REGISTER()
+ if ( HEALCOMM == nil and LibStub ) then
+ HEALCOMM = LibStub:GetLibrary("LibHealComm-4.0", true) or false
+
+ if ( HEALCOMM and not HEALCOMM_PLAYER_GUID ) then
+ HEALCOMM_PLAYER_GUID = UnitGUID("player")
+ end
+ end
+
+ if ( HEALCOMM and not EventHandler.HealComm_HealStarted) then
+ EventHandler.HealComm_HealStarted = HealComm_HealStarted
+ EventHandler.HealComm_ModifierChanged = HealComm_ModifierChanged
+
+ HEALCOMM.RegisterCallback(EventHandler, "HealComm_HealStarted")
+ HEALCOMM.RegisterCallback(EventHandler, "HealComm_HealDelayed", "HealComm_HealStarted")
+ HEALCOMM.RegisterCallback(EventHandler, "HealComm_HealUpdated", "HealComm_HealStarted")
+ HEALCOMM.RegisterCallback(EventHandler, "HealComm_HealStopped", "HealComm_HealStarted")
+ end
+end
+
+function EventHandler:UNIT_HEAL_PREDICTION_UNREGISTER()
+ if ( HEALCOMM and EventHandler.HealComm_HealStarted ) then
+ HEALCOMM.UnregisterCallback(EventHandler, "HealComm_HealStarted")
+ HEALCOMM.UnregisterCallback(EventHandler, "HealComm_HealDelayed")
+ HEALCOMM.UnregisterCallback(EventHandler, "HealComm_HealUpdated")
+ HEALCOMM.UnregisterCallback(EventHandler, "HealComm_HealStopped")
+
+ EventHandler.HealComm_HealStarted = nil
+ EventHandler.HealComm_ModifierChanged = nil
+ end
+end
+
+EventHandler.AddEvent("UNIT_HEAL_PREDICTION")
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/ItemUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/ItemUtil.lua
new file mode 100644
index 0000000..64df235
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/ItemUtil.lua
@@ -0,0 +1,373 @@
+local _, Private = ...
+
+Item = Item or {}
+ItemMixin = ItemMixin or {}
+
+local ItemEventListener
+
+--[[static]] function Item:CreateFromItemLocation(itemLocation)
+ if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then
+ error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2)
+ end
+ local item = CreateFromMixins(ItemMixin)
+ item:SetItemLocation(itemLocation)
+ return item
+end
+
+--[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex)
+ if type(bagID) ~= "number" or type(slotIndex) ~= "number" then
+ error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2)
+ end
+ local item = CreateFromMixins(ItemMixin)
+ item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex))
+ return item
+end
+
+--[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex)
+ if type(equipmentSlotIndex) ~= "number" then
+ error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2)
+ end
+ local item = CreateFromMixins(ItemMixin)
+ item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex))
+ return item
+end
+
+--[[static]] function Item:CreateFromItemLink(itemLink)
+ if type(itemLink) ~= "string" then
+ error("Usage: Item:CreateFromItemLink(itemLinkString)", 2)
+ end
+ local item = CreateFromMixins(ItemMixin)
+ item:SetItemLink(itemLink)
+ return item
+end
+
+--[[static]] function Item:CreateFromItemID(itemID)
+ if type(itemID) ~= "number" then
+ error("Usage: Item:CreateFromItemID(itemID)", 2)
+ end
+ local item = CreateFromMixins(ItemMixin)
+ item:SetItemID(itemID)
+ return item
+end
+
+function ItemMixin:SetItemLocation(itemLocation)
+ self:Clear()
+ self.itemLocation = itemLocation
+end
+
+function ItemMixin:SetItemLink(itemLink)
+ self:Clear()
+ self.itemLink = itemLink
+end
+
+function ItemMixin:SetItemID(itemID)
+ self:Clear()
+ self.itemID = itemID
+end
+
+function ItemMixin:GetItemLocation()
+ return self.itemLocation
+end
+
+function ItemMixin:HasItemLocation()
+ return self.itemLocation ~= nil
+end
+
+function ItemMixin:Clear()
+ self.itemLocation = nil
+ self.itemLink = nil
+ self.itemID = nil
+end
+
+function ItemMixin:IsItemEmpty()
+ if self:GetStaticBackingItem() then
+ return not C_Item.DoesItemExistByID(self:GetStaticBackingItem())
+ end
+
+ return not self:IsItemInPlayersControl()
+end
+
+function ItemMixin:GetStaticBackingItem()
+ return self.itemLink or self.itemID
+end
+
+function ItemMixin:IsItemInPlayersControl()
+ local itemLocation = self:GetItemLocation()
+ return itemLocation and C_Item.DoesItemExist(itemLocation)
+end
+
+-- Item API
+function ItemMixin:GetItemID()
+ if self:GetStaticBackingItem() then
+ return (GetItemInfoInstant(self:GetStaticBackingItem()))
+ end
+
+ if not self:IsItemEmpty() and self.HasItemLocation() then
+ return C_Item.GetItemID(self:GetItemLocation())
+ end
+ return nil
+end
+
+function ItemMixin:IsItemLocked()
+ return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation())
+end
+
+function ItemMixin:LockItem()
+ if self:IsItemInPlayersControl() then
+ C_Item.LockItem(self:GetItemLocation())
+ end
+end
+
+function ItemMixin:UnlockItem()
+ if self:IsItemInPlayersControl() then
+ C_Item.UnlockItem(self:GetItemLocation())
+ end
+end
+
+function ItemMixin:GetItemIcon() -- requires item data to be loaded
+ if self:GetStaticBackingItem() then
+ return C_Item.GetItemIconByID(self:GetStaticBackingItem())
+ end
+
+ if not self:IsItemEmpty() then
+ return C_Item.GetItemIcon(self:GetItemLocation())
+ end
+end
+
+function ItemMixin:GetItemName() -- requires item data to be loaded
+ if self:GetStaticBackingItem() then
+ return C_Item.GetItemNameByID(self:GetStaticBackingItem())
+ end
+
+ if not self:IsItemEmpty() then
+ return C_Item.GetItemName(self:GetItemLocation())
+ end
+ return nil
+end
+
+function ItemMixin:GetItemLink() -- requires item data to be loaded
+ if self.itemLink then
+ return self.itemLink
+ end
+
+ if self.itemID then
+ return (select(2, GetItemInfo(self.itemID)))
+ end
+
+ if not self:IsItemEmpty() then
+ return C_Item.GetItemLink(self:GetItemLocation())
+ end
+ return nil
+end
+
+function ItemMixin:GetItemQuality() -- requires item data to be loaded
+ if self:GetStaticBackingItem() then
+ return C_Item.GetItemQualityByID(self:GetStaticBackingItem())
+ end
+
+ if not self:IsItemEmpty() then
+ return C_Item.GetItemQuality(self:GetItemLocation())
+ end
+ return nil
+end
+
+function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded
+ if self:GetStaticBackingItem() then
+ return (GetDetailedItemLevelInfo(self:GetStaticBackingItem()))
+ end
+
+ if not self:IsItemEmpty() then
+ return C_Item.GetCurrentItemLevel(self:GetItemLocation())
+ end
+ return nil
+end
+
+function ItemMixin:GetItemQualityColor() -- requires item data to be loaded
+ local itemQuality = self:GetItemQuality()
+ return ITEM_QUALITY_COLORS[itemQuality] -- may be nil if item data isn't loaded
+end
+
+function ItemMixin:GetInventoryType()
+ if self:GetStaticBackingItem() then
+ return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem())
+ end
+
+ if not self:IsItemEmpty() then
+ return C_Item.GetItemInventoryType(self:GetItemLocation())
+ end
+ return nil
+end
+
+function ItemMixin:GetItemGUID()
+ if self:GetStaticBackingItem() then
+ return nil
+ end
+
+ if not self:IsItemEmpty() then
+ return C_Item.GetItemGUID(self:GetItemLocation())
+ end
+ return nil
+end
+
+function ItemMixin:GetInventoryTypeName()
+ if not self:IsItemEmpty() then
+ return select(4, GetItemInfoInstant(self:GetItemID()))
+ end
+end
+
+function ItemMixin:IsItemDataCached()
+ if self:GetStaticBackingItem() then
+ return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem())
+ end
+
+ if not self:IsItemEmpty() then
+ return C_Item.IsItemDataCached(self:GetItemLocation())
+ end
+ return true
+end
+
+function ItemMixin:IsDataEvictable()
+ -- Item data could be evicted from the cache
+ return true
+end
+
+-- Add a callback to be executed when item data is loaded, if the item data is already loaded then execute it immediately
+function ItemMixin:ContinueOnItemLoad(callbackFunction)
+ if type(callbackFunction) ~= "function" or self:IsItemEmpty() then
+ error("Usage: NonEmptyItem:ContinueOnLoad(callbackFunction)", 2)
+ end
+ ItemEventListener:AddCallback(self:GetItemID(), callbackFunction)
+end
+
+-- Same as ContinueOnItemLoad, except it returns a function that when called will cancel the continue
+function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction)
+ if type(callbackFunction) ~= "function" or self:IsItemEmpty() then
+ error("Usage: NonEmptyItem:ContinueWithCancelOnItemLoad(callbackFunction)", 2)
+ end
+ return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction)
+end
+
+--[ Item Event Listener ]
+
+ItemEventListener = CreateFrame("GameTooltip", nil, UIParent)
+ItemEventListener.callbacks = {}
+
+ItemEventListener:SetScript("OnEvent",
+ function(self, event, ...)
+ self:QueryItemDataStart(true)
+ self.Pending = 2
+ self:UnregisterEvent("PLAYER_ENTERING_WORLD")
+ end
+)
+ItemEventListener:RegisterEvent("PLAYER_ENTERING_WORLD")
+
+local CANCELED_SENTINEL = -1
+local TIMEOUT_SENTINEL = 180
+local ELAPSE = 0
+
+function ItemEventListener:AddCallback(itemID, callbackFunction)
+ local callbacks = self:GetOrCreateCallbacks(itemID)
+ table.insert(callbacks, callbackFunction)
+ if not callbacks[0] then
+ table.insert(callbacks, 0, TIMEOUT_SENTINEL)
+ end
+ self:QueryItemDataStart()
+ --C_Item.RequestLoadItemDataByID(itemID)
+end
+
+function ItemEventListener:AddCancelableCallback(itemID, callbackFunction)
+ local callbacks = self:GetOrCreateCallbacks(itemID)
+ table.insert(callbacks, callbackFunction)
+ self:QueryItemDataStart()
+ --C_Item.RequestLoadItemDataByID(itemID)
+
+ local index = #callbacks
+ return function()
+ if index > 0 and callbacks[index] ~= CANCELED_SENTINEL then
+ callbacks[index] = CANCELED_SENTINEL
+ return true
+ end
+ return false
+ end
+end
+
+function ItemEventListener:FireCallbacks(itemID)
+ local callbacks = self:GetCallbacks(itemID)
+ if callbacks then
+ self:ClearCallbacks(itemID)
+ for i, callback in ipairs(callbacks) do
+ if callback ~= CANCELED_SENTINEL then
+ callback()
+ --xpcall(callback, CallErrorHandler)
+ end
+ end
+
+ for i = #callbacks, 0, -1 do
+ callbacks[i] = nil
+ end
+ end
+end
+
+function ItemEventListener:ClearCallbacks(itemID)
+ self.callbacks[itemID] = nil
+end
+
+function ItemEventListener:GetCallbacks(itemID)
+ return self.callbacks[itemID]
+end
+
+function ItemEventListener:GetOrCreateCallbacks(itemID)
+ local callbacks = self.callbacks[itemID]
+ if not callbacks then
+ callbacks = {}
+ self.callbacks[itemID] = callbacks
+ end
+ return callbacks
+end
+
+--[ Item Event Query Handler ]
+
+local EventHandler = Private.EventHandler
+local EventHandler_Fire = EventHandler.Fire
+EventHandler.AddEvent("ITEM_DATA_LOAD_RESULT")
+
+function ItemEventListener:QueryItemDataStart(init)
+ if init and self.Pending or self.Pending == 2 then
+ self:SetScript("OnUpdate", self.QueryItemData)
+ else
+ self.Pending = 1
+ end
+end
+
+function ItemEventListener:QueryItemData(elapsed)
+ ELAPSE = ELAPSE + elapsed
+
+ if ( ELAPSE > .3 ) then
+ local timeout
+
+ for itemID, itemCallbacks in pairs(self.callbacks) do
+ local itemName, itemLink = GetItemInfo(itemID)
+ timeout = itemCallbacks[0]
+
+ if itemName and itemLink then
+ EventHandler_Fire(nil, "ITEM_DATA_LOAD_RESULT", itemID, true)
+ self:FireCallbacks(itemID)
+ else
+ if timeout == TIMEOUT_SENTINEL then
+ self:SetHyperlink("item:".. tostring(itemID) ..":0:0:0:0:0:0:0")
+ elseif timeout < 1 then
+ EventHandler_Fire(nil, "ITEM_DATA_LOAD_RESULT", itemID, false)
+ self:ClearCallbacks(itemID)
+ end
+
+ itemCallbacks[0] = timeout - elapsed
+ end
+ end
+
+ if timeout == nil then
+ self:SetScript("OnUpdate", nil)
+ end
+
+ ELAPSE = 0
+ end
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/MathUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/MathUtil.lua
new file mode 100644
index 0000000..e62278d
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/MathUtil.lua
@@ -0,0 +1,98 @@
+local ceil = math.ceil
+local floor = math.floor
+local random = math.random
+local atan2 = math.atan2
+local sqrt = math.sqrt
+
+function Lerp(startValue, endValue, amount)
+ return (1 - amount) * startValue + amount * endValue;
+end
+
+function Clamp(value, min, max)
+ if value > max then
+ return max;
+ elseif value < min then
+ return min;
+ end
+ return value;
+end
+
+function Saturate(value)
+ return Clamp(value, 0.0, 1.0);
+end
+
+function Wrap(value, max)
+ return (value - 1) % max + 1;
+end
+
+function ClampDegrees(value)
+ return ClampMod(value, 360);
+end
+
+function ClampMod(value, mod)
+ return ((value % mod) + mod) % mod;
+end
+
+function NegateIf(value, condition)
+ return condition and -value or value;
+end
+
+function PercentageBetween(value, startValue, endValue)
+ if startValue == endValue then
+ return 0.0;
+ end
+ return (value - startValue) / (endValue - startValue);
+end
+
+function ClampedPercentageBetween(value, startValue, endValue)
+ return Saturate(PercentageBetween(value, startValue, endValue));
+end
+
+local TARGET_FRAME_PER_SEC = 60.0;
+function DeltaLerp(startValue, endValue, amount, timeSec)
+ return Lerp(startValue, endValue, Saturate(amount * timeSec * TARGET_FRAME_PER_SEC));
+end
+
+function FrameDeltaLerp(startValue, endValue, amount, elapsed)
+ return DeltaLerp(startValue, endValue, amount, elapsed);
+end
+
+function RandomFloatInRange(minValue, maxValue)
+ return Lerp(minValue, maxValue, random());
+end
+
+function Round(value)
+ if value < 0.0 then
+ return ceil(value - .5);
+ end
+ return floor(value + .5);
+end
+
+function Square(value)
+ return value * value;
+end
+
+function CalculateDistanceSq(x1, y1, x2, y2)
+ local dx = x2 - x1;
+ local dy = y2 - y1;
+ return (dx * dx) + (dy * dy);
+end
+
+function CalculateDistance(x1, y1, x2, y2)
+ return sqrt(CalculateDistanceSq(x1, y1, x2, y2));
+end
+
+function CalculateAngleBetween(x1, y1, x2, y2)
+ return atan2(y2 - y1, x2 - x1);
+end
+
+function CreateCounter(initialCount)
+ local count = initialCount or 0;
+ local counter = function()
+ count = count + 1;
+ return count;
+ end;
+ return function()
+ return securecallfunction(counter);
+ end;
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Mixin.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Mixin.lua
new file mode 100644
index 0000000..b243dde
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Mixin.lua
@@ -0,0 +1,23 @@
+local select = select
+
+function Mixin(object, ...)
+ for i = 1, select("#", ...) do
+ local mixin = select(i, ...)
+ if ( mixin ) then -- prevent errors on 3.3.5
+ for k, v in pairs(mixin) do
+ object[k] = v
+ end
+ end
+ end
+ return object
+end
+
+function CreateFromMixins(...)
+ return Mixin({}, ...)
+end
+
+function CreateAndInitFromMixin(mixin, ...)
+ local object = CreateFromMixins(mixin)
+ object:Init(...)
+ return object
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/NineSlice.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/NineSlice.lua
new file mode 100644
index 0000000..4da5f3f
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/NineSlice.lua
@@ -0,0 +1,361 @@
+local function GetNineSlicePiece(container, pieceName)
+ if container.GetNineSlicePiece then
+ local piece = container:GetNineSlicePiece(pieceName);
+ if piece then
+ return piece, true;
+ end
+ end
+ local piece = container[pieceName];
+ if piece then
+ return piece, true;
+ else
+ piece = container:CreateTexture()
+ container[pieceName] = piece;
+ return piece, false;
+ end
+end
+
+local function PropagateLayoutSettingsToPieceLayout(userLayout, pieceLayout)
+ -- Only apply mirrorLayout if it wasn't explicitly defined
+ if pieceLayout.mirrorLayout == nil then
+ pieceLayout.mirrorLayout = userLayout.mirrorLayout;
+ end
+ -- ... and other settings that apply to the whole nine-slice
+end
+
+local function SetupTextureCoordinates(piece, setupInfo, pieceLayout)
+ local left, right, top, bottom = 0, 1, 0, 1;
+ if pieceLayout.mirrorLayout then
+ if setupInfo.mirrorVertical then
+ top, bottom = bottom, top;
+ end
+ if setupInfo.mirrorHorizontal then
+ left, right = right, left;
+ end
+ end
+ piece:SetHorizTile(setupInfo.tileHorizontal);
+ piece:SetVertTile(setupInfo.tileVertical);
+ piece:SetTexCoord(left, right, top, bottom);
+end
+
+local function SetupPieceVisuals(piece, setupInfo, pieceLayout, textureKit, userLayout)
+ --- Change texture coordinates before applying atlas.
+ SetupTextureCoordinates(piece, setupInfo, pieceLayout, userLayout);
+ -- textureKit is optional, that's fine; but if it's nil the caller should ensure that there are no format specifiers in .atlas
+ local atlasName = GetFinalNameFromTextureKit(pieceLayout.atlas, textureKit);
+ local info = C_Texture.GetAtlasInfo(atlasName);
+ piece:SetHorizTile(info and info.tilesHorizontally or false);
+ piece:SetVertTile(info and info.tilesVertically or false);
+ piece:SetAtlas(atlasName, true);
+end
+
+local function SetupCorner(container, piece, setupInfo, pieceLayout)
+ piece:ClearAllPoints();
+ piece:SetPoint(pieceLayout.point or setupInfo.point, container, pieceLayout.relativePoint or setupInfo.point, pieceLayout.x, pieceLayout.y);
+end
+
+local function SetupEdge(container, piece, setupInfo, pieceLayout)
+ piece:ClearAllPoints();
+ piece:SetPoint(setupInfo.point, GetNineSlicePiece(container, setupInfo.relativePieces[1]), setupInfo.relativePoint, pieceLayout.x, pieceLayout.y);
+ piece:SetPoint(setupInfo.relativePoint, GetNineSlicePiece(container, setupInfo.relativePieces[2]), setupInfo.point, pieceLayout.x1, pieceLayout.y1);
+end
+
+local function SetupCenter(container, piece, setupInfo, pieceLayout)
+ piece:ClearAllPoints();
+ piece:SetPoint("TOPLEFT", GetNineSlicePiece(container, "TopLeftCorner"), "BOTTOMRIGHT", pieceLayout.x, pieceLayout.y);
+ piece:SetPoint("BOTTOMRIGHT", GetNineSlicePiece(container, "BottomRightCorner"), "TOPLEFT", pieceLayout.x1, pieceLayout.y1);
+end
+-- Defines the order in which each piece should be set up, and how to do the setup.
+--
+-- Mirror types: As a texture memory and effort savings, many borders are assembled from a single topLeft corner, and top/left edges.
+-- That's all that's required if everything is symmetrical (left edge is also superfluous, but allows for more detail variation)
+-- The mirror flags specify which texture coords to flip relative to the piece that would use default texture coordinates: left = 0, top = 0, right = 1, bottom = 1
+local nineSliceSetup =
+{
+ { pieceName = "TopLeftCorner", point = "TOPLEFT", fn = SetupCorner, },
+ { pieceName = "TopRightCorner", point = "TOPRIGHT", mirrorHorizontal = true, fn = SetupCorner, },
+ { pieceName = "BottomLeftCorner", point = "BOTTOMLEFT", mirrorVertical = true, fn = SetupCorner, },
+ { pieceName = "BottomRightCorner", point = "BOTTOMRIGHT", mirrorHorizontal = true, mirrorVertical = true, fn = SetupCorner, },
+ { pieceName = "TopEdge", point = "TOPLEFT", relativePoint = "TOPRIGHT", relativePieces = { "TopLeftCorner", "TopRightCorner" }, fn = SetupEdge, tileHorizontal = true },
+ { pieceName = "BottomEdge", point = "BOTTOMLEFT", relativePoint = "BOTTOMRIGHT", relativePieces = { "BottomLeftCorner", "BottomRightCorner" }, mirrorVertical = true, tileHorizontal = true, fn = SetupEdge, },
+ { pieceName = "LeftEdge", point = "TOPLEFT", relativePoint = "BOTTOMLEFT", relativePieces = { "TopLeftCorner", "BottomLeftCorner" }, tileVertical = true, fn = SetupEdge, },
+ { pieceName = "RightEdge", point = "TOPRIGHT", relativePoint = "BOTTOMRIGHT", relativePieces = { "TopRightCorner", "BottomRightCorner" }, mirrorHorizontal = true, tileVertical = true, fn = SetupEdge, },
+ { pieceName = "Center", fn = SetupCenter, },
+};
+
+local layouts =
+{
+ SimplePanelTemplate =
+ {
+ mirrorLayout = true,
+ TopLeftCorner = { atlas = "UI-Frame-SimpleMetal-CornerTopLeft", x = -5, y = 0, },
+ TopRightCorner = { atlas = "UI-Frame-SimpleMetal-CornerTopLeft", x = 2, y = 0, },
+ BottomLeftCorner = { atlas = "UI-Frame-SimpleMetal-CornerTopLeft", x = -5, y = -3, },
+ BottomRightCorner = { atlas = "UI-Frame-SimpleMetal-CornerTopLeft", x = 2, y = -3, },
+ TopEdge = { atlas = "_UI-Frame-SimpleMetal-EdgeTop", },
+ BottomEdge = { atlas = "_UI-Frame-SimpleMetal-EdgeTop", },
+ LeftEdge = { atlas = "!UI-Frame-SimpleMetal-EdgeLeft", },
+ RightEdge = { atlas = "!UI-Frame-SimpleMetal-EdgeLeft", },
+ },
+ PortraitFrameTemplate =
+ {
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-PortraitMetal-CornerTopLeft", x = -13, y = 16, },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRight", x = 4, y = 16, },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft", x = -13, y = -3, },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight", x = 4, y = -3, },
+ TopEdge = { layer="OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop", x = 0, y = 0, x1 = 0, y1 = 0, },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom", x = 0, y = 0, x1 = 0, y1 = 0, },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft", x = 0, y = 0, x1 = 0, y1 = 0 },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight", x = 0, y = 0, x1 = 0, y1 = 0, },
+ },
+ PortraitFrameTemplateMinimizable =
+ {
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-PortraitMetal-CornerTopLeft", x = -13, y = 16, },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRightDouble", x = 4, y = 16, },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft", x = -13, y = -3, },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight", x = 4, y = -3, },
+ TopEdge = { layer="OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop", x = 0, y = 0, x1 = 0, y1 = 0, },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom", x = 0, y = 0, x1 = 0, y1 = 0, },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft", x = 0, y = 0, x1 = 0, y1 = 0 },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight", x = 0, y = 0, x1 = 0, y1 = 0, },
+ },
+ ButtonFrameTemplateNoPortrait =
+ {
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopLeft", x = -12, y = 16, },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRight", x = 4, y = 16, },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft", x = -12, y = -3, },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight", x = 4, y = -3, },
+ TopEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop", },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom", },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft", },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight", },
+ },
+ ButtonFrameTemplateNoPortraitMinimizable =
+ {
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopLeft", x = -12, y = 16, },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRightDouble", x = 4, y = 16, },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft", x = -12, y = -3, },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight", x = 4, y = -3, },
+ TopEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop", },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom", },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft", },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight", },
+ },
+ InsetFrameTemplate =
+ {
+ TopLeftCorner = { layer = "BORDER", subLevel = -5, atlas = "UI-Frame-InnerTopLeft", },
+ TopRightCorner = { layer = "BORDER", subLevel = -5, atlas = "UI-Frame-InnerTopRight", },
+ BottomLeftCorner = { layer = "BORDER", subLevel = -5, atlas = "UI-Frame-InnerBotLeftCorner", x = 0, y = -1, },
+ BottomRightCorner = { layer = "BORDER", subLevel = -5, atlas = "UI-Frame-InnerBotRight", x = 0, y = -1, },
+ TopEdge = { layer = "BORDER", subLevel = -5, atlas = "_UI-Frame-InnerTopTile", },
+ BottomEdge = { layer = "BORDER", subLevel = -5, atlas = "_UI-Frame-InnerBotTile", },
+ LeftEdge = { layer = "BORDER", subLevel = -5, atlas = "!UI-Frame-InnerLeftTile", },
+ RightEdge = { layer = "BORDER", subLevel = -5, atlas = "!UI-Frame-InnerRightTile", },
+ },
+ BFAMissionHorde =
+ {
+ mirrorLayout = true,
+ TopLeftCorner = { atlas = "HordeFrame-Corner-TopLeft", x = -6, y = 6, },
+ TopRightCorner = { atlas = "HordeFrame-Corner-TopLeft", x = 6, y = 6, },
+ BottomLeftCorner = { atlas = "HordeFrame-Corner-TopLeft", x = -6, y = -6, },
+ BottomRightCorner = { atlas = "HordeFrame-Corner-TopLeft", x = 6, y = -6, },
+ TopEdge = { atlas = "_HordeFrameTile-Top", },
+ BottomEdge = { atlas = "_HordeFrameTile-Top", },
+ LeftEdge = { atlas = "!HordeFrameTile-Left", },
+ RightEdge = { atlas = "!HordeFrameTile-Left", },
+ },
+ BFAMissionAlliance =
+ {
+ mirrorLayout = true,
+ TopLeftCorner = { atlas = "AllianceFrameCorner-TopLeft", x = -6, y = 6, },
+ TopRightCorner = { atlas = "AllianceFrameCorner-TopLeft", x = 6, y = 6, },
+ BottomLeftCorner = { atlas = "AllianceFrameCorner-TopLeft", x = -6, y = -6, },
+ BottomRightCorner = { atlas = "AllianceFrameCorner-TopLeft", x = 6, y = -6, },
+ TopEdge = { atlas = "_AllianceFrameTile-Top", },
+ BottomEdge = { atlas = "_AllianceFrameTile-Top", },
+ LeftEdge = { atlas = "!AllianceFrameTile-Left", },
+ RightEdge = { atlas = "!AllianceFrameTile-Left", },
+ },
+ CovenantMissionFrame =
+ {
+ mirrorLayout = true,
+ TopLeftCorner = { atlas = "Oribos-NineSlice-CornerTopLeft", x = -6, y = 6, },
+ TopRightCorner = { atlas = "Oribos-NineSlice-CornerTopLeft", x = 6, y = 6, },
+ BottomLeftCorner = { atlas = "Oribos-NineSlice-CornerTopLeft", x = -6, y = -6, },
+ BottomRightCorner = { atlas = "Oribos-NineSlice-CornerTopLeft", x = 6, y = -6, },
+ TopEdge = { atlas = "_Oribos-NineSlice-EdgeTop", },
+ BottomEdge = { atlas = "_Oribos-NineSlice-EdgeTop", },
+ LeftEdge = { atlas = "!Oribos-NineSlice-EdgeLeft", },
+ RightEdge = { atlas = "!Oribos-NineSlice-EdgeLeft", },
+ },
+ GenericMetal =
+ {
+ TopLeftCorner = { atlas = "UI-Frame-GenericMetal-Corner", x = -6, y = 6, mirrorLayout = true, },
+ TopRightCorner = { atlas = "UI-Frame-GenericMetal-Corner", x = 6, y = 6, mirrorLayout = true, },
+ BottomLeftCorner = { atlas = "UI-Frame-GenericMetal-Corner", x = -6, y = -6, mirrorLayout = true, },
+ BottomRightCorner = { atlas = "UI-Frame-GenericMetal-Corner", x = 6, y = -6, mirrorLayout = true, },
+ TopEdge = { atlas = "_UI-Frame-GenericMetal-EdgeTop", },
+ BottomEdge = { atlas = "_UI-Frame-GenericMetal-EdgeBottom", },
+ LeftEdge = { atlas = "!UI-Frame-GenericMetal-EdgeLeft", },
+ RightEdge = { atlas = "!UI-Frame-GenericMetal-EdgeRight", },
+ },
+ Dialog =
+ {
+ TopLeftCorner = { atlas = "UI-Frame-DiamondMetal-CornerTopLeft", },
+ TopRightCorner = { atlas = "UI-Frame-DiamondMetal-CornerTopRight", },
+ BottomLeftCorner = { atlas = "UI-Frame-DiamondMetal-CornerBottomLeft", },
+ BottomRightCorner = { atlas = "UI-Frame-DiamondMetal-CornerBottomRight", },
+ TopEdge = { atlas = "_UI-Frame-DiamondMetal-EdgeTop", },
+ BottomEdge = { atlas = "_UI-Frame-DiamondMetal-EdgeBottom", },
+ LeftEdge = { atlas = "!UI-Frame-DiamondMetal-EdgeLeft", },
+ RightEdge = { atlas = "!UI-Frame-DiamondMetal-EdgeRight", },
+ },
+ WoodenNeutralFrameTemplate =
+ {
+ mirrorLayout = true,
+ TopLeftCorner = { atlas = "Neutral-NineSlice-Corner", x = -6, y = 6, },
+ TopRightCorner = { atlas = "Neutral-NineSlice-Corner", x = 6, y = 6, },
+ BottomLeftCorner = { atlas = "Neutral-NineSlice-Corner", x = -6, y = -6, },
+ BottomRightCorner = { atlas = "Neutral-NineSlice-Corner", x = 6, y = -6, },
+ TopEdge = { atlas = "_Neutral-NineSlice-EdgeTop", },
+ BottomEdge = { atlas = "_Neutral-NineSlice-EdgeBottom", mirrorLayout = false, },
+ LeftEdge = { atlas = "!Neutral-NineSlice-EdgeLeft", },
+ RightEdge = { atlas = "!Neutral-NineSlice-EdgeRight", mirrorLayout = false, },
+ },
+ Runeforge =
+ {
+ TopLeftCorner = { atlas = "UI-Frame-DiamondMetal-CornerTopLeft", },
+ TopRightCorner = { atlas = "UI-Frame-DiamondMetal-CornerTopRight", },
+ BottomLeftCorner = { atlas = "UI-Frame-DiamondMetal-CornerBottomLeft", },
+ BottomRightCorner = { atlas = "UI-Frame-DiamondMetal-CornerBottomRight", },
+ TopEdge = { atlas = "_UI-Frame-DiamondMetal-EdgeTop", },
+ BottomEdge = { atlas = "_UI-Frame-DiamondMetal-EdgeBottom", },
+ LeftEdge = { atlas = "!UI-Frame-DiamondMetal-EdgeLeft", },
+ RightEdge = { atlas = "!UI-Frame-DiamondMetal-EdgeRight", },
+ },
+ AdventuresMissionComplete =
+ {
+ TopLeftCorner = { atlas = "AdventuresFrame-Corner-Small-TopLeft", mirrorLayout = true, },
+ TopRightCorner = { atlas = "AdventuresFrame-Corner-Small-TopLeft", mirrorLayout = true, },
+ BottomLeftCorner = { atlas = "AdventuresFrame-Corner-Small-TopLeft", mirrorLayout = true, },
+ BottomRightCorner = { atlas = "AdventuresFrame-Corner-Small-TopLeft", mirrorLayout = true, },
+ TopEdge = { layer = "BACKGROUND", atlas = "_AdventuresFrame-Small-Top", x = -10, y = 3, x1 = 10, y1 = 3, },
+ BottomEdge = { layer = "BACKGROUND", atlas = "_AdventuresFrame-Small-Top", x = -10, y = -3, x1 = 10, y1 = -3, mirrorLayout = true, },
+ LeftEdge = { layer = "BACKGROUND", atlas = "!AdventuresFrame-Right", x = -3, y = 10, x1 = -3, y1 = -10,},
+ RightEdge = { layer = "BACKGROUND", atlas = "!AdventuresFrame-Left", x = 3, y = 10, x1 = 3, y1 = -10,},
+ },
+ CharacterCreateDropdown =
+ {
+ TopLeftCorner = { atlas = "CharacterCreateDropdown-NineSlice-CornerTopLeft", x=-30, y=20 },
+ TopRightCorner = { atlas = "CharacterCreateDropdown-NineSlice-CornerTopRight", x=30, y=20 },
+ BottomLeftCorner = { atlas = "CharacterCreateDropdown-NineSlice-CornerBottomLeft", x=-30, y=-20 },
+ BottomRightCorner = { atlas = "CharacterCreateDropdown-NineSlice-CornerBottomRight", x=30, y=-20 },
+ TopEdge = { atlas = "_CharacterCreateDropdown-NineSlice-EdgeTop", },
+ BottomEdge = { atlas = "_CharacterCreateDropdown-NineSlice-EdgeBottom", },
+ LeftEdge = { atlas = "!CharacterCreateDropdown-NineSlice-EdgeLeft", },
+ RightEdge = { atlas = "!CharacterCreateDropdown-NineSlice-EdgeRight", },
+ Center = { atlas = "CharacterCreateDropdown-NineSlice-Center", },
+ },
+ ChatBubble =
+ {
+ TopLeftCorner = { atlas = "ChatBubble-NineSlice-CornerTopLeft", },
+ TopRightCorner = { atlas = "ChatBubble-NineSlice-CornerTopRight", },
+ BottomLeftCorner = { atlas = "ChatBubble-NineSlice-CornerBottomLeft", },
+ BottomRightCorner = { atlas = "ChatBubble-NineSlice-CornerBottomRight", },
+ TopEdge = { atlas = "_ChatBubble-NineSlice-EdgeTop", },
+ BottomEdge = { atlas = "_ChatBubble-NineSlice-EdgeBottom", },
+ LeftEdge = { atlas = "!ChatBubble-NineSlice-EdgeLeft", },
+ RightEdge = { atlas = "!ChatBubble-NineSlice-EdgeRight", },
+ Center = { atlas = "ChatBubble-NineSlice-Center", },
+ },
+ UniqueCornersLayout =
+ {
+ ["TopRightCorner"] = { atlas = "%s-NineSlice-CornerTopRight" },
+ ["TopLeftCorner"] = { atlas = "%s-NineSlice-CornerTopLeft" },
+ ["BottomLeftCorner"] = { atlas = "%s-NineSlice-CornerBottomLeft" },
+ ["BottomRightCorner"] = { atlas = "%s-NineSlice-CornerBottomRight" },
+ ["TopEdge"] = { atlas = "_%s-NineSlice-EdgeTop" },
+ ["BottomEdge"] = { atlas = "_%s-NineSlice-EdgeBottom" },
+ ["LeftEdge"] = { atlas = "!%s-NineSlice-EdgeLeft" },
+ ["RightEdge"] = { atlas = "!%s-NineSlice-EdgeRight" },
+ ["Center"] = { atlas = "%s-NineSlice-Center" },
+ };
+ IdenticalCornersLayout =
+ {
+ ["TopRightCorner"] = { atlas = "%s-NineSlice-Corner", mirrorLayout = true, },
+ ["TopLeftCorner"] = { atlas = "%s-NineSlice-Corner", mirrorLayout = true,},
+ ["BottomLeftCorner"] = { atlas = "%s-NineSlice-Corner", mirrorLayout = true, },
+ ["BottomRightCorner"] = { atlas = "%s-NineSlice-Corner", mirrorLayout = true,},
+ ["TopEdge"] = { atlas = "_%s-NineSlice-EdgeTop" },
+ ["BottomEdge"] = { atlas = "_%s-NineSlice-EdgeBottom" },
+ ["LeftEdge"] = { atlas = "!%s-NineSlice-EdgeLeft" },
+ ["RightEdge"] = { atlas = "!%s-NineSlice-EdgeRight" },
+ ["Center"] = { atlas = "%s-NineSlice-Center" },
+ };
+};
+--------------------------------------------------
+-- NINE SLICE UTILS
+NineSliceUtil = {};
+function NineSliceUtil.ApplyUniqueCornersLayout(self, textureKit)
+ NineSliceUtil.ApplyLayout(self, layouts.UniqueCornersLayout, textureKit);
+end
+
+function NineSliceUtil.ApplyIdenticalCornersLayout(self, textureKit)
+ NineSliceUtil.ApplyLayout(self, layouts.IdenticalCornersLayout, textureKit);
+end
+
+function NineSliceUtil.ApplyLayout(container, userLayout, textureKit)
+ for pieceIndex, setup in ipairs(nineSliceSetup) do
+ local pieceName = setup.pieceName;
+ local pieceLayout = userLayout[pieceName];
+ if pieceLayout then
+ PropagateLayoutSettingsToPieceLayout(userLayout, pieceLayout);
+ local piece, pieceAlreadyExisted = GetNineSlicePiece(container, pieceName);
+ if not pieceAlreadyExisted then
+ container[pieceName] = piece;
+ piece:SetDrawLayer(pieceLayout.layer or "BORDER", pieceLayout.subLevel);
+ end
+ -- Piece setup can change arbitrary properties, do it before changing the texture.
+ setup.fn(container, piece, setup, pieceLayout);
+ if userLayout.setupPieceVisualsFunction then
+ userLayout.setupPieceVisualsFunction(container, piece, setup, pieceLayout, textureKit);
+ else
+ SetupPieceVisuals(piece, setup, pieceLayout, textureKit);
+ end
+ end
+ end
+end
+
+function NineSliceUtil.DisableSharpening(container)
+ for pieceIndex, setup in ipairs(nineSliceSetup) do
+ local piece = GetNineSlicePiece(container, setup.pieceName);
+ piece:SetTexelSnappingBias(0);
+ piece:SetSnapToPixelGrid(false);
+ end
+end
+
+function NineSliceUtil.ApplyLayoutByName(container, userLayoutName, textureKit)
+ return NineSliceUtil.ApplyLayout(container, NineSliceUtil.GetLayout(userLayoutName), textureKit);
+end
+
+function NineSliceUtil.GetLayout(layoutName)
+ return layouts[layoutName];
+end
+
+function NineSliceUtil.AddLayout(layoutName, layout)
+ layouts[layoutName] = layout;
+end
+--------------------------------------------------
+-- NINE SLICE PANEL MIXIN
+NineSlicePanelMixin = {};
+
+function NineSlicePanelMixin:GetFrameLayoutType()
+ return self.layoutType or self:GetParent().layoutType;
+end
+
+function NineSlicePanelMixin:OnLoad()
+ local layout = NineSliceUtil.GetLayout(self:GetFrameLayoutType());
+ if layout then
+ NineSliceUtil.ApplyLayout(self, layout, self.layoutTextureKit);
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/PartyUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/PartyUtil.lua
new file mode 100644
index 0000000..b71f5ae
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/PartyUtil.lua
@@ -0,0 +1,88 @@
+local huge = math.huge
+local min = math.min
+local UnitFactionGroup = UnitFactionGroup
+local UnitInPartyShard = UnitInPartyShard
+local GetGroupMemberCounts = GetGroupMemberCounts
+local UnitExists = UnitExists
+
+PartyUtil = {};
+
+local unitTags = { "player", "party1", "party2", "party3", "party4" };
+
+function PartyUtil.GetMinLevel()
+ local minLevel = huge;
+ for index, unit in ipairs(unitTags) do
+ if UnitExists(unit) then
+ minLevel = min(minLevel, UnitLevel(unit));
+ end
+ end
+ return minLevel;
+end
+
+local function GetChromieTimeLocationString(unitToken)
+ local expansionID = UnitChromieTimeID(unitToken);
+ local option = C_ChromieTime.GetChromieTimeExpansionOption(expansionID);
+ local expansion = option and option.name or "";
+ if unitToken == "player" then
+ return PARTY_PLAYER_CHROMIE_TIME_SELF_LOCATION:format(expansion);
+ else
+ return PARTY_PLAYER_CHROMIE_TIME_OTHER_LOCATION:format(expansion);
+ end
+end
+
+local function GetChromieTimeInstructionString(inChromieTime)
+ local factionGroup = UnitFactionGroup("player");
+ if factionGroup == "Horde" then
+ if inChromieTime then
+ return PARTY_PLAYER_CHROMIE_TIME_SELF_HORDE;
+ else
+ return PARTY_PLAYER_CHROMIE_TIME_OTHER_HORDE;
+ end
+ elseif factionGroup == "Alliance" then
+ if inChromieTime then
+ return PARTY_PLAYER_CHROMIE_TIME_SELF_ALLIANCE;
+ else
+ return PARTY_PLAYER_CHROMIE_TIME_OTHER_ALLIANCE;
+ end
+ end
+end
+
+local function GetChromieTimeString(unitToken)
+ local inChromieTime = C_PlayerInfo.IsPlayerInChromieTime();
+ if not (inChromieTime or C_PlayerInfo.CanPlayerEnterChromieTime()) then
+ local location = GetChromieTimeLocationString(unitToken);
+ return PARTY_PLAYER_CHROMIE_TIME_FMT:format(location, PARTY_PLAYER_CHROMIE_TIME_INELIGIBLE);
+ else
+ local location = GetChromieTimeLocationString(inChromieTime and "player" or unitToken);
+ local instruction = GetChromieTimeInstructionString(inChromieTime);
+ return PARTY_PLAYER_CHROMIE_TIME_FMT:format(location, instruction);
+ end
+end
+
+local function GetShardedString(unitToken)
+ if UnitInPartyShard("player") then
+ return PARTY_PLAYER_SHARDED_TARGET_NOT_IN_PARTY_PHASE;
+ else
+ if UnitInPartyShard(unitToken) then
+ return PARTY_PLAYER_SHARDED_NOT_IN_PARTY_SHARD;
+ else
+ return PARTY_PLAYER_SHARDED_NEITHER_IN_PARTY_SHARD;
+ end
+ end
+end
+
+function PartyUtil.GetPhasedReasonString(phaseReason, unitToken)
+ if phaseReason == Enum.PhaseReason.WarMode then
+ if C_PvP.IsWarModeDesired() then
+ return PARTY_PLAYER_WARMODE_DISABLED;
+ else
+ return PARTY_PLAYER_WARMODE_ENABLED;
+ end
+ elseif phaseReason == Enum.PhaseReason.ChromieTime then
+ return GetChromieTimeString(unitToken);
+ elseif phaseReason == Enum.PhaseReason.Phasing then
+ return PARTY_PHASED_MESSAGE;
+ elseif phaseReason == Enum.PhaseReason.Sharding then
+ return GetShardedString(unitToken);
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/PixelUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/PixelUtil.lua
new file mode 100644
index 0000000..82c7c9a
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/PixelUtil.lua
@@ -0,0 +1,76 @@
+local match = string.match
+local tonumber = tonumber
+local GetScreenResolutions = GetScreenResolutions
+local GetScreenWidth = GetScreenWidth
+local GetScreenHeight = GetScreenHeight
+
+PixelUtil = {};
+
+function PixelUtil.GetPixelToUIUnitFactor()
+ local physicalWidth, physicalHeight = match((({GetScreenResolutions()})[GetCurrentResolution()] or ""), "(%d+).-(%d+)");
+ physicalWidth, physicalHeight = tonumber(physicalWidth),tonumber(physicalHeight)
+ return 768.0 / physicalHeight;
+end
+
+function PixelUtil.GetNearestPixelSize(uiUnitSize, layoutScale, minPixels)
+ if uiUnitSize == 0 and (not minPixels or minPixels == 0) then
+ return 0;
+ end
+
+ local uiUnitFactor = PixelUtil.GetPixelToUIUnitFactor();
+ local numPixels = Round((uiUnitSize * layoutScale) / uiUnitFactor);
+ if minPixels then
+ if uiUnitSize < 0.0 then
+ if numPixels > -minPixels then
+ numPixels = -minPixels;
+ end
+ else
+ if numPixels < minPixels then
+ numPixels = minPixels;
+ end
+ end
+ end
+
+ return numPixels * uiUnitFactor / layoutScale;
+end
+
+function PixelUtil.SetWidth(region, width, minPixels)
+ region:SetWidth(PixelUtil.GetNearestPixelSize(width, region:GetEffectiveScale(), minPixels));
+end
+
+function PixelUtil.SetHeight(region, height, minPixels)
+ region:SetHeight(PixelUtil.GetNearestPixelSize(height, region:GetEffectiveScale(), minPixels));
+end
+
+function PixelUtil.SetSize(region, width, height, minWidthPixels, minHeightPixels)
+ PixelUtil.SetWidth(region, width, minWidthPixels);
+ PixelUtil.SetHeight(region, height, minHeightPixels);
+end
+
+function PixelUtil.SetPoint(region, point, relativeTo, relativePoint, offsetX, offsetY, minOffsetXPixels, minOffsetYPixels)
+ region:SetPoint(point, relativeTo, relativePoint,
+ PixelUtil.GetNearestPixelSize(offsetX, region:GetEffectiveScale(), minOffsetXPixels),
+ PixelUtil.GetNearestPixelSize(offsetY, region:GetEffectiveScale(), minOffsetYPixels)
+ );
+end
+
+function PixelUtil.SetStatusBarValue(statusBar, value)
+ local width = statusBar:GetWidth();
+ if width and width > 0.0 then
+ local min, max = statusBar:GetMinMaxValues();
+ local percent = ClampedPercentageBetween(value, min, max);
+ if percent == 0.0 or percent == 1.0 then
+ statusBar:SetValue(value);
+ else
+ local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetWidth() * percent, statusBar:GetEffectiveScale());
+ local roundedValue = Lerp(min, max, numPixels / width);
+ statusBar:SetValue(roundedValue);
+ end
+ else
+ statusBar:SetValue(value);
+ end
+end
+
+function GetPhysicalScreenSize()
+ return GetScreenWidth(), GetScreenHeight()
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Pools.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Pools.lua
new file mode 100644
index 0000000..9adb31e
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Pools.lua
@@ -0,0 +1,396 @@
+local next = next
+local tostring = tostring
+local type = type
+
+ObjectPoolMixin = {};
+
+function ObjectPoolMixin:OnLoad(creationFunc, resetterFunc)
+ self.creationFunc = creationFunc;
+ self.resetterFunc = resetterFunc;
+ self.activeObjects = {};
+ self.inactiveObjects = {};
+ self.numActiveObjects = 0;
+end
+function ObjectPoolMixin:Acquire()
+ local numInactiveObjects = #self.inactiveObjects;
+ if numInactiveObjects > 0 then
+ local obj = self.inactiveObjects[numInactiveObjects];
+ self.activeObjects[obj] = true;
+ self.numActiveObjects = self.numActiveObjects + 1;
+ self.inactiveObjects[numInactiveObjects] = nil;
+ return obj, false;
+ end
+ local newObj = self.creationFunc(self);
+ if self.resetterFunc and not self.disallowResetIfNew then
+ self.resetterFunc(self, newObj);
+ end
+ self.activeObjects[newObj] = true;
+ self.numActiveObjects = self.numActiveObjects + 1;
+ return newObj, true;
+end
+function ObjectPoolMixin:Release(obj)
+ if self:IsActive(obj) then
+ self.inactiveObjects[#self.inactiveObjects + 1] = obj;
+ self.activeObjects[obj] = nil;
+ self.numActiveObjects = self.numActiveObjects - 1;
+ if self.resetterFunc then
+ self.resetterFunc(self, obj);
+ end
+ return true;
+ end
+ return false;
+end
+function ObjectPoolMixin:ReleaseAll()
+ for obj in pairs(self.activeObjects) do
+ self:Release(obj);
+ end
+end
+function ObjectPoolMixin:SetResetDisallowedIfNew(disallowed)
+ self.disallowResetIfNew = disallowed;
+end
+function ObjectPoolMixin:EnumerateActive()
+ return pairs(self.activeObjects);
+end
+function ObjectPoolMixin:GetNextActive(current)
+ return (next(self.activeObjects, current));
+end
+function ObjectPoolMixin:GetNextInactive(current)
+ return (next(self.inactiveObjects, current));
+end
+function ObjectPoolMixin:IsActive(object)
+ return (self.activeObjects[object] ~= nil);
+end
+function ObjectPoolMixin:GetNumActive()
+ return self.numActiveObjects;
+end
+function ObjectPoolMixin:EnumerateInactive()
+ return ipairs(self.inactiveObjects);
+end
+function CreateObjectPool(creationFunc, resetterFunc)
+ local objectPool = CreateFromMixins(ObjectPoolMixin);
+ objectPool:OnLoad(creationFunc, resetterFunc);
+ return objectPool;
+end
+FramePoolMixin = CreateFromMixins(ObjectPoolMixin);
+local function FramePoolFactory(framePool)
+ return CreateFrame(framePool.frameType, nil, framePool.parent, framePool.frameTemplate);
+end
+local function ForbiddenFramePoolFactory(framePool)
+ return CreateForbiddenFrame(framePool.frameType, nil, framePool.parent, framePool.frameTemplate);
+end
+function FramePoolMixin:OnLoad(frameType, parent, frameTemplate, resetterFunc, forbidden, frameInitFunc)
+ if forbidden then
+ local creationFunc = ForbiddenFramePoolFactory;
+ if frameInitFunc ~= nil then
+ creationFunc = function(framePool)
+ local frame = CreateForbiddenFrame(framePool.frameType, nil, framePool.parent, framePool.frameTemplate);
+ frameInitFunc(frame);
+ return frame;
+ end
+ end
+ ObjectPoolMixin.OnLoad(self, creationFunc, resetterFunc);
+ else
+ local creationFunc = FramePoolFactory;
+ if frameInitFunc ~= nil then
+ creationFunc = function(framePool)
+ local frame = CreateFrame(framePool.frameType, nil, framePool.parent, framePool.frameTemplate);
+ frameInitFunc(frame);
+ return frame;
+ end
+ end
+ ObjectPoolMixin.OnLoad(self, creationFunc, resetterFunc);
+ end
+ self.frameType = frameType;
+ self.parent = parent;
+ self.frameTemplate = frameTemplate;
+end
+function FramePoolMixin:GetTemplate()
+ return self.frameTemplate;
+end
+function FramePool_Hide(framePool, frame)
+ frame:Hide();
+end
+function FramePool_HideAndClearAnchors(framePool, frame)
+ frame:Hide();
+ frame:ClearAllPoints();
+end
+function CreateFramePool(frameType, parent, frameTemplate, resetterFunc, forbidden, frameInitFunc)
+ local framePool = CreateFromMixins(FramePoolMixin);
+ framePool:OnLoad(frameType, parent, frameTemplate, resetterFunc or FramePool_HideAndClearAnchors, forbidden, frameInitFunc);
+ return framePool;
+end
+TexturePoolMixin = CreateFromMixins(ObjectPoolMixin);
+local function TexturePoolFactory(texturePool)
+ return texturePool.parent:CreateTexture(nil, texturePool.layer, texturePool.textureTemplate, texturePool.subLayer);
+end
+function TexturePoolMixin:OnLoad(parent, layer, subLayer, textureTemplate, resetterFunc)
+ ObjectPoolMixin.OnLoad(self, TexturePoolFactory, resetterFunc);
+ self.parent = parent;
+ self.layer = layer;
+ self.subLayer = subLayer;
+ self.textureTemplate = textureTemplate;
+end
+TexturePool_Hide = FramePool_Hide;
+TexturePool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
+function CreateTexturePool(parent, layer, subLayer, textureTemplate, resetterFunc)
+ local texturePool = CreateFromMixins(TexturePoolMixin);
+ texturePool:OnLoad(parent, layer, subLayer, textureTemplate, resetterFunc or TexturePool_HideAndClearAnchors);
+ return texturePool;
+end
+MaskPoolMixin = CreateFromMixins(ObjectPoolMixin);
+local function MaskPoolFactory(maskPool)
+ return maskPool.parent:CreateMaskTexture(nil, maskPool.layer, maskPool.maskTemplate, maskPool.subLayer);
+end
+function MaskPoolMixin:OnLoad(parent, layer, subLayer, maskTemplate, resetterFunc)
+ ObjectPoolMixin.OnLoad(self, MaskPoolFactory, resetterFunc);
+ self.parent = parent;
+ self.layer = layer;
+ self.subLayer = subLayer;
+ self.maskTemplate = maskTemplate;
+end
+MaskPool_Hide = FramePool_Hide;
+MaskPool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
+function CreateMaskPool(parent, layer, subLayer, maskTemplate, resetterFunc)
+ local maskPool = CreateFromMixins(MaskPoolMixin);
+ maskPool:OnLoad(parent, layer, subLayer, maskTemplate, resetterFunc or MaskPool_HideAndClearAnchors);
+ return maskPool;
+end
+FontStringPoolMixin = CreateFromMixins(ObjectPoolMixin);
+local function FontStringPoolFactory(fontStringPool)
+ return fontStringPool.parent:CreateFontString(nil, fontStringPool.layer, fontStringPool.fontStringTemplate, fontStringPool.subLayer);
+end
+function FontStringPoolMixin:OnLoad(parent, layer, subLayer, fontStringTemplate, resetterFunc)
+ ObjectPoolMixin.OnLoad(self, FontStringPoolFactory, resetterFunc);
+ self.parent = parent;
+ self.layer = layer;
+ self.subLayer = subLayer;
+ self.fontStringTemplate = fontStringTemplate;
+end
+FontStringPool_Hide = FramePool_Hide;
+FontStringPool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
+function CreateFontStringPool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
+ local fontStringPool = CreateFromMixins(FontStringPoolMixin);
+ fontStringPool:OnLoad(parent, layer, subLayer, fontStringTemplate, resetterFunc or FontStringPool_HideAndClearAnchors);
+ return fontStringPool;
+end
+ActorPoolMixin = CreateFromMixins(ObjectPoolMixin);
+local function ActorPoolFactory(actorPool)
+ return actorPool.parent:CreateActor(nil, actorPool.actorTemplate);
+end
+function ActorPoolMixin:OnLoad(parent, actorTemplate, resetterFunc)
+ ObjectPoolMixin.OnLoad(self, ActorPoolFactory, resetterFunc);
+ self.parent = parent;
+ self.actorTemplate = actorTemplate;
+end
+ActorPool_Hide = FramePool_Hide;
+function ActorPool_HideAndClearModel(actorPool, actor)
+ actor:ClearModel();
+ actor:Hide();
+end
+function CreateActorPool(parent, actorTemplate, resetterFunc)
+ local actorPool = CreateFromMixins(ActorPoolMixin);
+ actorPool:OnLoad(parent, actorTemplate, resetterFunc or ActorPool_HideAndClearModel);
+ return actorPool;
+end
+FramePoolCollectionMixin = {};
+function CreateFramePoolCollection()
+ local poolCollection = CreateFromMixins(FramePoolCollectionMixin);
+ poolCollection:OnLoad();
+ return poolCollection;
+end
+-- If different frames are used for specialized cases even though they have the same template,
+-- supply a specialization key to differentiate. If specialization is a function, it will be
+-- called the first time a frame is acquired. If specialization is a table, it will be mixed
+-- in with FrameUtil.SpecializeFrameWithMixins.
+local function FramePoolCollection_GetPoolKey(template, specialization)
+ return template..tostring(specialization);
+end
+local function FramePoolCollection_GetSpecializedFrameInit(specialization)
+ local specializationType = type(specialization);
+ if specializationType == "function" then
+ return specialization;
+ elseif specializationType == "table" then
+ local function SpecializationFrameInit(frame)
+ FrameUtil.SpecializeFrameWithMixins(frame, specialization);
+ end
+ return SpecializationFrameInit;
+ end
+ return nil;
+end
+function FramePoolCollectionMixin:OnLoad()
+ self.pools = {};
+end
+function FramePoolCollectionMixin:GetNumActive()
+ local numTotalActive = 0;
+ for _, pool in pairs(self.pools) do
+ numTotalActive = numTotalActive + pool:GetNumActive();
+ end
+ return numTotalActive;
+end
+-- Returns the pool, and whether or not the pool needed to be created.
+function FramePoolCollectionMixin:GetOrCreatePool(frameType, parent, template, resetterFunc, forbidden, specialization)
+ local pool = self:GetPool(template, specialization);
+ if not pool then
+ return self:CreatePool(frameType, parent, template, resetterFunc, forbidden, specialization), true;
+ end
+ return pool, false;
+end
+function FramePoolCollectionMixin:CreatePool(frameType, parent, template, resetterFunc, forbidden, specialization)
+ assert(self:GetPool(template, specialization) == nil);
+ local frameInitFunc = FramePoolCollection_GetSpecializedFrameInit(specialization);
+ local pool = CreateFramePool(frameType, parent, template, resetterFunc, forbidden, frameInitFunc);
+ local poolKey = FramePoolCollection_GetPoolKey(template, specialization);
+ self.pools[poolKey] = pool;
+ return pool;
+end
+function FramePoolCollectionMixin:CreatePoolIfNeeded(frameType, parent, template, resetterFunc, forbidden, specialization)
+ if not self:GetPool(template, specialization) then
+ self:CreatePool(frameType, parent, template, resetterFunc, forbidden, specialization);
+ end
+end
+function FramePoolCollectionMixin:GetPool(template, specialization)
+ local poolKey = FramePoolCollection_GetPoolKey(template, specialization);
+ return self.pools[poolKey];
+end
+function FramePoolCollectionMixin:Acquire(template, specialization)
+ local pool = self:GetPool(template, specialization);
+ assert(pool);
+ return pool:Acquire();
+end
+function FramePoolCollectionMixin:Release(object)
+ for _, pool in pairs(self.pools) do
+ if pool:Release(object) then
+ -- Found it! Just return
+ return;
+ end
+ end
+ -- Huh, we didn't find that object
+ assert(false);
+end
+function FramePoolCollectionMixin:ReleaseAllByTemplate(template, specialization)
+ local pool = self:GetPool(template, specialization);
+ if pool then
+ pool:ReleaseAll();
+ end
+end
+function FramePoolCollectionMixin:ReleaseAll()
+ for key, pool in pairs(self.pools) do
+ pool:ReleaseAll();
+ end
+end
+function FramePoolCollectionMixin:EnumerateActiveByTemplate(template, specialization)
+ local pool = self:GetPool(template, specialization);
+ if pool then
+ return pool:EnumerateActive();
+ end
+ return nop;
+end
+function FramePoolCollectionMixin:EnumerateActive()
+ local currentPoolKey, currentPool = next(self.pools, nil);
+ local currentObject = nil;
+ return function()
+ if currentPool then
+ currentObject = currentPool:GetNextActive(currentObject);
+ while not currentObject do
+ currentPoolKey, currentPool = next(self.pools, currentPoolKey);
+ if currentPool then
+ currentObject = currentPool:GetNextActive();
+ else
+ break;
+ end
+ end
+ end
+ return currentObject;
+ end, nil;
+end
+function FramePoolCollectionMixin:EnumerateInactiveByTemplate(template, specialization)
+ local pool = self:GetPool(template, specialization);
+ if pool then
+ return pool:EnumerateInactive();
+ end
+ return nop;
+end
+function FramePoolCollectionMixin:EnumerateInactive()
+ local currentPoolKey, currentPool = next(self.pools, nil);
+ local currentObject = nil;
+ return function()
+ if currentPool then
+ currentObject = currentPool:GetNextInactive(currentObject);
+ while not currentObject do
+ currentPoolKey, currentPool = next(self.pools, currentPoolKey);
+ if currentPool then
+ currentObject = currentPool:GetNextInactive();
+ else
+ break;
+ end
+ end
+ end
+ return currentObject;
+ end, nil;
+end
+FixedSizeFramePoolCollectionMixin = CreateFromMixins(FramePoolCollectionMixin);
+function CreateFixedSizeFramePoolCollection()
+ local poolCollection = CreateFromMixins(FixedSizeFramePoolCollectionMixin);
+ poolCollection:OnLoad();
+ return poolCollection;
+end
+function FixedSizeFramePoolCollectionMixin:OnLoad()
+ FramePoolCollectionMixin.OnLoad(self);
+ self.sizes = {};
+end
+function FixedSizeFramePoolCollectionMixin:CreatePool(frameType, parent, template, resetterFunc, forbidden, specialization, maxPoolSize, preallocate)
+ local pool = FramePoolCollectionMixin.CreatePool(self, frameType, parent, template, resetterFunc, forbidden, specialization);
+ if preallocate then
+ for i = 1, maxPoolSize do
+ pool:Acquire();
+ end
+ pool:ReleaseAll();
+ end
+ local poolKey = FramePoolCollection_GetPoolKey(template, specialization);
+ self.sizes[poolKey] = maxPoolSize;
+ return pool;
+end
+function FixedSizeFramePoolCollectionMixin:Acquire(template, specialization)
+ local pool = self:GetPool(template, specialization);
+ assert(pool);
+ local poolKey = FramePoolCollection_GetPoolKey(template, specialization);
+ if pool:GetNumActive() < self.sizes[poolKey] then
+ return pool:Acquire();
+ end
+ return nil;
+end
+FontStringPoolCollectionMixin = CreateFromMixins(FramePoolCollectionMixin);
+function CreateFontStringPoolCollection()
+ local poolCollection = CreateFromMixins(FontStringPoolCollectionMixin);
+ poolCollection:OnLoad();
+ return poolCollection;
+end
+function FontStringPoolCollectionMixin:GetOrCreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
+ local pool = self:GetPool(fontStringTemplate);
+ if not pool then
+ pool = self:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
+ end
+ return pool;
+end
+function FontStringPoolCollectionMixin:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
+ assert(self:GetPool(fontStringTemplate) == nil);
+ local pool = CreateFontStringPool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
+ self.pools[fontStringTemplate] = pool;
+ return pool;
+end
+function FontStringPoolCollectionMixin:CreatePoolIfNeeded(parent, layer, subLayer, fontStringTemplate, resetterFunc)
+ if not self:GetPool(fontStringTemplate) then
+ self:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
+ end
+end
+function FontStringPoolCollectionMixin:Acquire(fontStringTemplate, parent, layer, subLayer, resetterFunc)
+ local pool = self:GetOrCreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
+ local newString = pool:Acquire();
+ if parent then
+ newString:SetParent(parent);
+ end
+ if layer then
+ newString:SetDrawLayer(layer, subLayer);
+ end
+ return newString;
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SharedExtendedMethods.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SharedExtendedMethods.lua
new file mode 100644
index 0000000..76d6ea3
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SharedExtendedMethods.lua
@@ -0,0 +1,360 @@
+local _G = _G
+local IsType = type
+local Max = math.max
+local Assert = assert
+local GetTime = GetTime
+local Floor = math.floor
+local ToNumber = tonumber
+local Format = string.format
+local TableInsert = table.insert
+local GetMetaTable = getmetatable
+local Format = string.format
+local Date = date
+
+local FrameRef = CreateFrame("Frame")
+local ButtonRef = CreateFrame("Button")
+local SliderRef = CreateFrame("Slider")
+local CooldownRef = CreateFrame("Cooldown")
+local StatusBarRef = CreateFrame("StatusBar")
+local ScrollFrameRef = CreateFrame("ScrollFrame")
+local CheckButtonRef = CreateFrame("CheckButton")
+local PlayerModelRef = CreateFrame("PlayerModel")
+
+local Frame = getmetatable(FrameRef).__index
+local Button = getmetatable(ButtonRef).__index
+local Slider = getmetatable(SliderRef).__index
+local Cooldown = getmetatable(CooldownRef).__index
+local StatusBar = getmetatable(StatusBarRef).__index
+local ScrollFrame = getmetatable(ScrollFrameRef).__index
+local CheckButton = getmetatable(CheckButtonRef).__index
+local PlayerModel = getmetatable(PlayerModelRef).__index
+local FrameTexture = getmetatable(FrameRef:CreateTexture()).__index
+local FrameFontString = getmetatable(FrameRef:CreateFontString()).__index
+
+local CONST_ATLAS_WIDTH = 1
+local CONST_ATLAS_HEIGHT = 2
+local CONST_ATLAS_LEFT = 3
+local CONST_ATLAS_RIGHT = 4
+local CONST_ATLAS_TOP = 5
+local CONST_ATLAS_BOTTOM = 6
+local CONST_ATLAS_TILESHORIZ = 7
+local CONST_ATLAS_TILESVERT = 8
+local CONST_ATLAS_TEXTUREPATH = 9
+
+local GRAY_FONT_COLOR = GRAY_FONT_COLOR
+local NORMAL_FONT_COLOR = NORMAL_FONT_COLOR
+
+local PANELS = { "CollectionsJournal", "EncounterJournal" }
+
+-- We can also emulate SetReverse so it hides the cooldown frame by
+-- creating a hidden sub-cooldown and if onhide hook is set then hide on that.
+-- though idk how to tell if from xml...
+local function Method_SetCooldown(Self, Start, Duration, Modrate)
+ Self.___Start = Start
+ Self.___Duration = Duration
+end
+
+local function Method_GetCooldownTimes(Self)
+ -- This might not work entirely blizz-like, needs testing.
+ local Start = Self.___Start or 0
+ local Duration = Self.___Duration or 0
+
+ if ( Duration > 0 ) then
+ if ( ((Start + Duration) - GetTime()) <= 0 ) then
+ Start = 0
+ Duration = 0
+
+ Self.Start = nil
+ Self.Duration = nil
+ end
+ end
+
+ return Start, Duration
+end
+
+local function GetCooldownDuration(Self, ...)
+ local Duration = 0
+
+ if ( Self.___Duration ) then
+ Duration = Self.___Duration - (Self.___Start - GetTime())
+
+ if ( Duration <= 0 ) then
+ Self.Start = nil
+ Self.Duration = nil
+ end
+ end
+
+ return Duration
+end
+
+local function Method_SetShown(Self, ...)
+ if ... then
+ Self:Show()
+ else
+ Self:Hide()
+ end
+end
+
+local function Method_SetEnabled(Self, ...)
+ if ... then
+ Self:Enable()
+ else
+ Self:Disable()
+ end
+end
+
+local function Method_SetRemainingTime(Self, Time, DaysFormat)
+ local DayInSec = 86400
+ local Days = ""
+
+ Self:SetText("")
+
+ if ( Type(Time) ~= "number" ) then
+ -- printc("EROR: Method_SetRemainingTime time is not number. Frame "..Self:GetName())
+ return
+ end
+
+ if ( DaysFormat ) then
+ if ( Time > 86400 ) then
+ Self:SetText(Floor(Time / DayInSec)..Format(" |4день:дня:дней", Time % 10))
+ else
+ Self:SetText(Date("!%X", Time))
+ end
+ else
+ if ( Time > DayInSec ) then
+ Days = Floor(Time / DayInSec) .. "д "
+ Time = Time % DayInSec
+ end
+
+ if ( Time and Time >= 0 ) then
+ Self:SetText(Days .. Date("!%X", Time))
+ end
+ end
+end
+
+local function Method_SetSubTexCoord(Self, Left, Right, Top, Bottom)
+ local UL_X, UL_Y, LL_X, LL_Y, UR_X, UR_Y, LR_X, LR_Y = Self:GetTexCoord()
+
+ local LeftEdge = UL_X
+ local RightEdge = UR_X
+ local TopEdge = UL_Y
+ local BottomEdge = LL_Y
+
+ local Width = RightEdge - LeftEdge
+ local Height = BottomEdge - TopEdge
+
+ LeftEdge = UL_X + Width * Left
+ TopEdge = UL_Y + Height * Top
+ RightEdge = Max(RightEdge * Right, UL_X)
+ BottomEdge = Max(BottomEdge * Bottom, UL_Y)
+
+ UL_X = LeftEdge
+ UL_Y = TopEdge
+ LL_X = LeftEdge
+ LL_Y = BottomEdge
+ UR_X = RightEdge
+ UR_Y = TopEdge
+ LR_X = RightEdge
+ LR_Y = BottomEdge
+
+ Self:SetTexCoord(UL_X, UL_Y, LL_X, LL_Y, UR_X, UR_Y, LR_X, LR_Y)
+end
+
+local function Method_SetPortrait(Self, DisplayID)
+ Self:SetTexture("Interface\\PORTRAITS\\Portrait_model_"..ToNumber(DisplayID))
+end
+
+local function Method_FixOpenPanel(Self)
+ -- Hack, due to the oddities of the positioning system.
+ -- Necessary for the correct operation of the system of Professions.
+ for i = 1, #PANELS do
+ local Panel = _G[PANELS[i]]
+
+ if ( Panel ) then
+ if Panel:IsShown() then
+ HideUIPanel(Panel)
+ ShowUIPanel(Self)
+ return true
+ end
+ end
+ end
+end
+
+local function Method_SetAtlas(Self, AtlasName, UseAtlasSize, FilterMode)
+ Assert(Self, "SetAtlas: not found object")
+ Assert(AtlasName, "SetAtlas: AtlasName must be specified")
+
+ -- MoonWellClient owns additional Encounter Journal atlases that are not
+ -- part of ClassicAPI's generated storage. Resolve them through the explicit
+ -- system integration seam before consulting the ClassicAPI table.
+ local MoonWellResolver = _G.MoonWellApplyAtlasTexture
+ if MoonWellResolver then
+ local Ok, Handled = pcall(MoonWellResolver, Self, AtlasName, UseAtlasSize)
+ if Ok and Handled then return true end
+ end
+
+ -- An unknown optional atlas must not abort an XML OnLoad script. Consumers
+ -- can supply art later through their own resolver.
+ if not ATLAS_INFO_STORAGE[AtlasName] then return false end
+
+ local Atlas = ATLAS_INFO_STORAGE[AtlasName]
+
+ Self:SetTexture(Atlas[CONST_ATLAS_TEXTUREPATH] or "", Atlas[CONST_ATLAS_TILESHORIZ], Atlas[CONST_ATLAS_TILESVERT])
+
+ if ( UseAtlasSize ) then
+ Self:SetWidth(Atlas[CONST_ATLAS_WIDTH])
+ Self:SetHeight(Atlas[CONST_ATLAS_HEIGHT])
+ end
+
+ Self:SetTexCoord(Atlas[CONST_ATLAS_LEFT], Atlas[CONST_ATLAS_RIGHT], Atlas[CONST_ATLAS_TOP], Atlas[CONST_ATLAS_BOTTOM])
+
+ Self:SetHorizTile(Atlas[CONST_ATLAS_TILESHORIZ])
+ Self:SetVertTile(Atlas[CONST_ATLAS_TILESVERT])
+
+ Self.___AtlasName = AtlasName
+ return true
+end
+
+local function Method_GetAtlas(Self)
+ return Self.___AtlasName
+end
+
+local function Method_SmoothSetValue(Self, value)
+ -- local SmoothFrame = Self._SmoothUpdateFrame or CreateFrame("Frame")
+ -- Self._SmoothUpdateFrame = SmoothFrame
+end
+
+local function Method_SetDesaturated(Self, Toggle, Color)
+ if ( Toggle ) then
+ Self:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
+ else
+ if ( Color ) then
+ Self:SetTextColor(Color.r, Color.g, Color.b)
+ else
+ Self:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
+ end
+ end
+end
+
+local function Method_SetParentArray(Self, ArrayName, Element, SetInSelf)
+ local Parent = (not SetInSelf) and Self:GetParent() or Self
+
+ if ( not Parent[ArrayName] ) then
+ Parent[ArrayName] = {}
+ end
+
+ TableInsert(Parent[ArrayName], Element or Self)
+end
+
+local function Method_ClearAndSetPoint(Self, ...)
+ Self:ClearAllPoints()
+ Self:SetPoint(...)
+end
+
+local function Method_CreateLine(Self, ...)
+ -- Self is NineSlice
+ local Line = Self:CreateTexture(...)
+ Line.IsLine = true
+ return Line
+end
+
+local function Method_IsRectValid(Self, ...)
+ return true
+end
+
+local function Method_TODO(Self, ...)
+ -- Empty, like my soul.
+end
+
+-- FRAME
+Frame.SetShown = Method_SetShown
+Frame.FixOpenPanel = Method_FixOpenPanel
+Frame.SetParentArray = Method_SetParentArray
+Frame.ClearAndSetPoint = Method_ClearAndSetPoint
+Frame.IsRectValid = Method_IsRectValid
+Frame.SetIgnoreParentScale = Method_TODO
+Frame.CreateMaskTexture = Method_TODO
+Frame.SetClipsChildren = Method_TODO
+Frame.SetPortraitZoom = Method_TODO
+Frame.SetForbidden = Method_TODO
+Frame.IsForbidden = Method_TODO
+ -- Line
+ Frame.CreateLine = Method_CreateLine
+
+ -- TEXTURE (FRAME)
+ FrameTexture.SetShown = Method_SetShown
+ FrameTexture.SetSubTexCoord = Method_SetSubTexCoord
+ FrameTexture.SetPortrait = Method_SetPortrait -- This doesn't actually exist in WoW API
+ FrameTexture.SetAtlas = Method_SetAtlas
+ FrameTexture.GetAtlas = Method_GetAtlas
+ FrameTexture.SetParentArray = Method_SetParentArray
+ FrameTexture.ClearAndSetPoint = Method_ClearAndSetPoint
+ FrameTexture.SetMask = Method_TODO
+ FrameTexture.GetNumMaskTextures = function(Self) return 0 end
+ FrameTexture.SetSnapToPixelGrid = Method_TODO
+ FrameTexture.SetTexelSnappingBias = Method_TODO
+ FrameTexture.SetColorTexture = function(Self, ...) Self:SetTexture(...) end
+ -- Line
+ FrameTexture.SetThickness = Method_TODO
+ FrameTexture.SetStartPoint = Method_TODO
+ FrameTexture.SetEndPoint = Method_TODO
+ FrameTexture.SetIgnoreParentAlpha = Method_TODO
+
+ -- FONTSTRING (FRAME)
+ FrameFontString.SetShown = Method_SetShown
+ FrameFontString.SetRemainingTime = Method_SetRemainingTime
+ FrameFontString.SetDesaturated = Method_SetDesaturated
+ FrameFontString.SetParentArray = Method_SetParentArray
+ FrameFontString.ClearAndSetPoint = Method_ClearAndSetPoint
+
+-- BUTTON
+Button.SetShown = Method_SetShown
+Button.SetEnabled = Method_SetEnabled
+Button.SetParentArray = Method_SetParentArray
+Button.ClearAndSetPoint = Method_ClearAndSetPoint
+Button.SetNormalAtlas = function(Self, ...) Method_SetAtlas(Self:GetNormalTexture(), ...) end
+Button.SetPushedAtlas = function(Self, ...) Method_SetAtlas(Self:GetPushedTexture(), ...) end
+Button.SetDisabledAtlas = function(Self, ...) Method_SetAtlas(Self:GetDisabledTexture(), ...) end
+Button.SetHighlightAtlas = function(Self, ...) Method_SetAtlas(Self:GetHighlightTexture(), ...) end
+Button.SetForbidden = Method_TODO
+Button.IsForbidden = Method_TODO
+
+-- SLIDER
+Slider.SetShown = Method_SetShown
+Slider.SetParentArray = Method_SetParentArray
+Slider.ClearAndSetPoint = Method_ClearAndSetPoint
+
+-- STATUSBAR
+StatusBar.SetShown = Method_SetShown
+StatusBar.SmoothSetValue = Method_SmoothSetValue
+StatusBar.SetParentArray = Method_SetParentArray
+StatusBar.ClearAndSetPoint = Method_ClearAndSetPoint
+
+-- SCROLLFRAME
+ScrollFrame.SetShown = Method_SetShown
+ScrollFrame.SetParentArray = Method_SetParentArray
+ScrollFrame.ClearAndSetPoint = Method_ClearAndSetPoint
+
+-- CHECKBUTTON
+CheckButton.SetShown = Method_SetShown
+CheckButton.SetEnabled = Method_SetEnabled
+CheckButton.SetParentArray = Method_SetParentArray
+CheckButton.ClearAndSetPoint = Method_ClearAndSetPoint
+
+-- COOLDOWN
+hooksecurefunc(Cooldown, "SetCooldown", Method_SetCooldown)
+Cooldown.Clear = function(Self) Self:Hide() end
+Cooldown.SetHideCountdownNumbers = function(Self, Hide) Self.noCooldownCount = (Hide) and true or nil end
+Cooldown.SetDrawBling = Method_TODO
+Cooldown.SetDrawSwipe = Method_TODO
+Cooldown.IsPaused = Method_TODO
+Cooldown.Pause = Method_TODO
+Cooldown.Resume = Method_TODO
+Cooldown.SetSwipeTexture = Method_TODO
+Cooldown.SetSwipeColor = Method_TODO
+Cooldown.GetCooldownTimes = Method_GetCooldownTimes
+Cooldown.GetCooldownDuration = Method_GetCooldownDuration
+
+-- PLAYERMODEL
+PlayerModel.ClearTransform = Method_TODO
+PlayerModel.SetPortraitZoom = Method_TODO -- Just make it zoom into parent (code: WA => calc)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SmoothStatusBar.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SmoothStatusBar.lua
new file mode 100644
index 0000000..e6b311a
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SmoothStatusBar.lua
@@ -0,0 +1,82 @@
+-- https://github.com/tomrus88/BlizzardInterfaceCode/blob/master/Interface/SharedXML/SmoothStatusBar.lua
+if ( not SmoothStatusBarMixin ) then
+ local Pairs = pairs
+ local Abs = math.abs
+ local Clamp = Clamp
+ local FrameDeltaLerp = FrameDeltaLerp
+
+ local Bars = CreateFrame("Frame")
+ local BarsActive
+
+ local function IsCloseEnough(Bar, NewValue, TargetValue)
+ local Min, Max = Bar:GetMinMaxValues()
+ local Range = Max - Min
+ if ( Range > 0 ) then
+ return Abs((NewValue - TargetValue) / Range) < .00001
+ end
+
+ return true
+ end
+
+ local function ProcessSmoothStatusBars(Self, Elapsed)
+ BarsActive = 0
+
+ for Bar, TargetValue in Pairs(Bars) do
+ if ( Bar ~= 0 ) then
+ local EffectiveTargetValue = Clamp(TargetValue, Bar:GetMinMaxValues())
+ local NewValue = FrameDeltaLerp(Bar:GetValue(), EffectiveTargetValue, .25, Elapsed)
+
+ if ( IsCloseEnough(Bar, NewValue, EffectiveTargetValue) ) then
+ Bars[Bar] = nil
+ Bar:SetValue(EffectiveTargetValue)
+ else
+ Bar:SetValue(NewValue)
+ end
+
+ BarsActive = BarsActive + 1
+ end
+ end
+
+ if ( BarsActive == 0 ) then
+ Self:SetScript("OnUpdate", nil)
+ BarsActive = nil
+ end
+ end
+
+ SmoothStatusBarMixin = {}
+
+ function SmoothStatusBarMixin:ResetSmoothedValue(Value) --If nil, tries to set to the last target Value
+ local TargetValue = Bars[self]
+ if ( TargetValue ) then
+ Bars[self] = nil
+ self:SetValue(Value or TargetValue)
+ elseif ( Value ) then
+ self:SetValue(Value)
+ end
+ end
+
+ function SmoothStatusBarMixin:SetSmoothedValue(Value)
+ Bars[self] = Value
+
+ if ( not BarsActive ) then
+ Bars:SetScript("OnUpdate", ProcessSmoothStatusBars)
+ end
+ end
+
+ function SmoothStatusBarMixin:SetMinMaxSmoothedValue(Min, Max)
+ self:SetMinMaxValues(Min, Max)
+
+ local TargetValue = Bars[self]
+ if ( TargetValue ) then
+ local Ratio = 1
+ if ( Max ~= 0 and self.lastSmoothedMax and self.lastSmoothedMax ~= 0 ) then
+ Ratio = Max / self.lastSmoothedMax
+ end
+
+ Bars[self] = TargetValue * Ratio
+ end
+
+ self.lastSmoothedMin = Min
+ self.lastSmoothedMax = Max
+ end
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SoundKit.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SoundKit.lua
new file mode 100644
index 0000000..4f77d51
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SoundKit.lua
@@ -0,0 +1,11 @@
+SOUNDKIT = SOUNDKIT or {}
+
+-- https://github.com/liquidbase/wowui-source/blob/master/SoundKitConstants.lua
+
+SOUNDKIT.IG_MAINMENU_OPTION = "igMainMenuOption"
+SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON = "igMainMenuOptionCheckBoxOn"
+SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF = "igMainMenuOptionCheckBoxOff"
+SOUNDKIT.IG_SPELLBOOK_OPEN = "igSpellBookOpen"
+SOUNDKIT.IG_SPELLBOOK_CLOSE = "igSpellBookClose"
+SOUNDKIT.GS_LOGIN_CHANGE_REALM_OK = "uChatScrollButton"
+SOUNDKIT.GS_LOGIN_CHANGE_REALM_CANCEL = "uChatScrollButton"
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Spell.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Spell.lua
new file mode 100644
index 0000000..691d76a
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Spell.lua
@@ -0,0 +1,52 @@
+local PickupSpell = PickupSpell
+local GetSpellName = GetSpellName
+local GetSpellInfo = GetSpellInfo
+local GameTooltip = GameTooltip
+local UIParent = UIParent
+
+function PickupSpellBookItem(id, bookType)
+ return PickupSpell(id, bookType)
+end
+
+function GetSpellBookItemName(id, bookType)
+ return GetSpellName(id, bookType)
+end
+
+function GetSpellDescription(spellID)
+ GameTooltip:ClearLines()
+ GameTooltip:SetOwner(UIParent, "ANCHOR_NONE")
+ GameTooltip:SetHyperlink("spell:"..spellID)
+
+ local Num = GameTooltip:NumLines()
+ if ( Num > 0 ) then
+ return _G["GameTooltipTextLeft"..Num]:GetText()
+ end
+
+ GameTooltip:Hide()
+end
+
+function C_GetSpellTexture(spell, bookType)
+ local _, icon
+ if ( bookType ) then
+ icon = GetSpellTexture(spell, bookType)
+ else
+ _, _, icon = GetSpellInfo(spell)
+ end
+ return icon, icon
+end
+
+function C_GetSpellInfo(spell)
+ local name, rank, icon, cost, isFunnel, powerType, castTime, minRange, maxRange
+ if ( spell ) then
+ name, rank, icon, cost, isFunnel, powerType, castTime, minRange, maxRange = GetSpellInfo(spell)
+ end
+ return name, rank, icon, castTime, minRange, maxRange
+end
+
+function GetSpellSubtext(id)
+ if ( id ) then
+ local _, rank = GetSpellInfo(id)
+
+ return rank
+ end
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SpellUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SpellUtil.lua
new file mode 100644
index 0000000..0bc69a3
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/SpellUtil.lua
@@ -0,0 +1,85 @@
+Spell = Spell or {}
+SpellMixin = SpellMixin or {}
+
+--[[static]] function Spell:CreateFromSpellID(spellID)
+ local spell = CreateFromMixins(SpellMixin)
+ spell:SetSpellID(spellID)
+ return spell
+end
+
+function SpellMixin:SetSpellID(spellID)
+ --self:Clear()
+ self.spellID = spellID
+end
+
+function SpellMixin:GetSpellID()
+ return self.spellID
+end
+
+function SpellMixin:Clear()
+ self.spellID = nil
+end
+
+function SpellMixin:IsSpellEmpty()
+ return not self:GetSpellID()
+ --[[local spellID = self:GetSpellID()
+ return not spellID or not C_Spell.DoesSpellExist(spellID)]]
+end
+
+-- Spell API
+function SpellMixin:IsSpellDataCached()
+ --[[if not self:IsSpellEmpty() then
+ return C_Spell.IsSpellDataCached(self:GetSpellID())
+ end
+ return true]]
+end
+
+function SpellMixin:IsDataEvictable()
+ -- Spell data could be evicted from the cache
+ return true
+end
+
+function SpellMixin:GetSpellName()
+ return GetSpellInfo(self:GetSpellID())
+end
+
+function SpellMixin:GetSpellTexture()
+ return C_GetSpellTexture(self:GetSpellID())
+end
+
+function SpellMixin:GetSpellSubtext()
+ return GetSpellSubtext(self:GetSpellID())
+end
+
+function SpellMixin:GetSpellDescription()
+ return GetSpellDescription(self:GetSpellID())
+end
+
+-- Add a callback to be executed when spell data is loaded, if the spell data is already loaded then execute it immediately
+function SpellMixin:ContinueOnSpellLoad(callbackFunction)
+ if type(callbackFunction) ~= "function" or self:IsSpellEmpty() then
+ error("Usage: NonEmptySpell:ContinueOnLoad(callbackFunction)", 2)
+ end
+
+ --SpellEventListener:AddCallback(self:GetSpellID(), callbackFunction)
+ -- TODO: for performance, we need to delay this until in the world
+ callbackFunction()
+end
+
+-- Same as ContinueOnSpellLoad, except it returns a function that when called will cancel the continue
+function SpellMixin:ContinueWithCancelOnSpellLoad(callbackFunction)
+ --[[if type(callbackFunction) ~= "function" or self:IsSpellEmpty() then
+ error("Usage: NonEmptySpell:ContinueWithCancelOnSpellLoad(callbackFunction)", 2)
+ end
+
+ return SpellEventListener:AddCancelableCallback(self:GetSpellID(), callbackFunction)]]
+end
+
+-- Generic aliases for use with ContinuableContainer
+function SpellMixin:ContinueWithCancelOnRecordLoad(callbackFunction)
+ return self:ContinueWithCancelOnSpellLoad(callbackFunction)
+end
+
+function SpellMixin:IsRecordDataCached()
+ return self:IsSpellDataCached()
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/TableUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/TableUtil.lua
new file mode 100644
index 0000000..1e47612
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/TableUtil.lua
@@ -0,0 +1,594 @@
+local tRemove = table.remove;
+local tInsert = table.insert;
+local tWipe = table.wipe;
+
+TableUtil = TableUtil or {};
+
+TableUtil.Constants =
+{
+ AssociativePriorityTable = true,
+ ArraylikePriorityTable = false,
+ IsIndexTable = true,
+};
+
+function ipairs_reverse(table)
+ local function Enumerator(table, index)
+ index = index - 1;
+ local value = table[index];
+ if value ~= nil then
+ return index, value;
+ end
+ end
+ return Enumerator, table, #table + 1;
+end
+
+function CreateTableEnumerator(tbl, minIndex, maxIndex)
+ minIndex = minIndex and (minIndex - 1) or 0;
+ maxIndex = maxIndex or math.huge;
+
+ local function Enumerator(tbl, index)
+ index = index + 1;
+ if index <= maxIndex then
+ local value = tbl[index];
+ if value ~= nil then
+ return index, value;
+ end
+ end
+ end
+
+ return Enumerator, tbl, minIndex;
+end
+
+function CreateTableReverseEnumerator(tbl, minIndex, maxIndex)
+ minIndex = minIndex or 1;
+ maxIndex = (maxIndex or #tbl) + 1;
+
+ local function Enumerator(tbl, index)
+ index = index - 1;
+ if index >= minIndex then
+ local value = tbl[index];
+ if value ~= nil then
+ return index, value;
+ end
+ end
+ end
+
+ return Enumerator, tbl, maxIndex;
+end
+
+function tIndexOf(tbl, item)
+ for i, v in ipairs(tbl) do
+ if item == v then
+ return i;
+ end
+ end
+end
+
+function TableUtil.ContainsAllKeys(lhsTable, rhsTable)
+ for key, _ in pairs(lhsTable) do
+ if rhsTable[key] == nil then
+ return false;
+ end
+ end
+ return true;
+end
+
+function TableUtil.CompareValuesAsKeys(lhsTable, rhsTable, valueToKeyOp)
+ local lhsKeys = CopyTransformedValuesAsKeys(lhsTable, valueToKeyOp);
+ local rhsKeys = CopyTransformedValuesAsKeys(rhsTable, valueToKeyOp);
+ return TableUtil.ContainsAllKeys(lhsKeys, rhsKeys)
+end
+
+-- This is a deep compare on the values of the table (based on depth) but not a deep comparison
+-- of the keys, as this would be an expensive check and won't be necessary in most cases.
+function tCompare(lhsTable, rhsTable, depth)
+ depth = depth or 1;
+ for key, value in pairs(lhsTable) do
+ if type(value) == "table" then
+ local rhsValue = rhsTable[key];
+ if type(rhsValue) ~= "table" then
+ return false;
+ end
+ if depth > 1 then
+ if not tCompare(value, rhsValue, depth - 1) then
+ return false;
+ end
+ end
+ elseif value ~= rhsTable[key] then
+ return false;
+ end
+ end
+
+ -- Check for any keys that are in rhsTable and not lhsTable.
+ for key, value in pairs(rhsTable) do
+ if lhsTable[key] == nil then
+ return false;
+ end
+ end
+
+ return true;
+end
+
+function tInvert(tbl)
+ local inverted = {};
+ for k, v in pairs(tbl) do
+ inverted[v] = k;
+ end
+ return inverted;
+end
+
+function TableUtil.TrySet(tbl, key)
+ if not tbl[key] then
+ tbl[key] = true;
+ return true;
+ end
+ return false;
+end
+
+function TableUtil.CopyUnique(tbl, isIndexTable)
+ local found = {};
+ local function FilterPredicate(value)
+ return TableUtil.TrySet(found, value);
+ end
+
+ return tFilter(tbl, FilterPredicate, isIndexTable);
+end
+
+function TableUtil.CopyUniqueByPredicate(tbl, isIndexTable, unaryPredicate)
+ local found = {};
+ local function FilterPredicate(value)
+ return TableUtil.TrySet(found, unaryPredicate(value));
+ end
+
+ return tFilter(tbl, FilterPredicate, isIndexTable);
+end
+
+function tFilter(tbl, pred, isIndexTable)
+ local out = {};
+
+ if (isIndexTable) then
+ local currentIndex = 1;
+ for i, v in ipairs(tbl) do
+ if (pred(v)) then
+ out[currentIndex] = v;
+ currentIndex = currentIndex + 1;
+ end
+ end
+ else
+ for k, v in pairs(tbl) do
+ if (pred(v)) then
+ out[k] = v;
+ end
+ end
+ end
+
+ return out;
+end
+
+function tAppendAll(table, addedArray)
+ for i, element in ipairs(addedArray) do
+ tinsert(table, element);
+ end
+end
+
+function tInsertUnique(tbl, item)
+ if not tContains(tbl, item) then
+ table.insert(tbl, item);
+ return #tbl;
+ end
+ return nil;
+end
+
+function tUnorderedRemove(tbl, index)
+ if index ~= #tbl then
+ tbl[index] = tbl[#tbl];
+ end
+
+ tRemove(tbl);
+end
+
+function C_CopyTable(settings, shallow)
+ local copy = {};
+ for k, v in pairs(settings) do
+ if type(v) == "table" and not shallow then
+ copy[k] = CopyTable(v);
+ else
+ copy[k] = v;
+ end
+ end
+ return copy;
+end
+
+function MergeTable(destination, source)
+ for k, v in pairs(source) do
+ destination[k] = v;
+ end
+end
+
+-- Useful if there are external references to a table but we want to set
+-- that table's key-value pairs to be exactly the same as another table's key-value pairs.
+function SetTablePairsToTable(destination, source)
+ tWipe(destination);
+ MergeTable(destination, source);
+end
+
+function Accumulate(tbl)
+ local count = 0;
+ for k, v in pairs(tbl) do
+ count = count + v;
+ end
+ return count;
+end
+
+function AccumulateOp(tbl, op)
+ local count = 0;
+ for k, v in pairs(tbl) do
+ count = count + op(v);
+ end
+ return count;
+end
+
+function TableUtil.Execute(tbl, op)
+ for k, v in pairs(tbl) do
+ op(v);
+ end
+end
+
+function TableUtil.ExecuteUntil(tbl, op)
+ for k, v in pairs(tbl) do
+ local operationResult = op(v);
+ if operationResult then
+ return operationResult;
+ end
+ end
+
+ return nil;
+end
+
+function TableUtil.Transform(tbl, op)
+ local result = {};
+ for k, v in pairs(tbl) do
+ table.insert(result, op(v));
+ return result;
+ end
+end
+
+function ContainsIf(tbl, pred)
+ for k, v in pairs(tbl) do
+ if (pred(v)) then
+ return true;
+ end
+ end
+
+ return false;
+end
+
+function FindInTableIf(tbl, pred)
+ for k, v in pairs(tbl) do
+ if (pred(v)) then
+ return k, v;
+ end
+ end
+
+ return nil;
+end
+
+function FindValueInTableIf(tbl, pred)
+ local _, value = FindInTableIf(tbl, pred);
+ return value;
+end
+
+local function FindSortedIndexImplementation(tbl, searchComparison, startIndex, rangeStart, rangeEnd)
+ local comparisonResult = searchComparison(tbl[startIndex]);
+ if comparisonResult == 0 then
+ return startIndex;
+ end
+
+ if comparisonResult > 0 then
+ if startIndex >= rangeEnd then
+ return startIndex + 1;
+ end
+
+ rangeStart = startIndex + 1;
+ return FindSortedIndexImplementation(tbl, searchComparison, startIndex + math.ceil((rangeEnd - startIndex) / 2), rangeStart, rangeEnd);
+ end
+
+ -- comparisonResult < 0
+ if startIndex <= rangeStart then
+ return startIndex;
+ end
+
+ rangeEnd = startIndex - 1;
+ return FindSortedIndexImplementation(tbl, searchComparison, math.floor(startIndex / 2), rangeStart, rangeEnd);
+end
+
+function FindSortedIndex(tbl, searchComparison)
+ local numTable = #tbl;
+ local startingIndex = math.ceil(numTable / 2);
+ if startingIndex == 0 then
+ return 1;
+ end
+
+ return FindSortedIndexImplementation(tbl, searchComparison, startingIndex, 1, numTable);
+end
+
+function TableIsEmpty(tbl)
+ return next(tbl) == nil;
+end
+
+function TableHasAnyEntries(tbl)
+ return next(tbl) ~= nil;
+end
+
+function CopyValuesAsKeys(tbl)
+ local output = {};
+ for k, v in ipairs(tbl) do
+ output[v] = v;
+ end
+ return output;
+end
+
+function CopyTransformedValuesAsKeys(tbl, transformOp)
+ local output = {};
+ for _, v in ipairs(tbl) do
+ output[transformOp(v)] = v;
+ end
+ return output;
+end
+
+function SafePack(...)
+ local tbl = { ... };
+ tbl.n = select("#", ...);
+ return tbl;
+end
+
+function SafeUnpack(tbl)
+ return unpack(tbl, 1, tbl.n);
+end
+
+function GetOrCreateTableEntry(table, key, defaultValue)
+ local currentValue = table[key];
+ local isNewValue = (currentValue == nil);
+ if isNewValue then
+ if defaultValue ~= nil then
+ currentValue = defaultValue;
+ else
+ currentValue = {};
+ end
+ table[key] = currentValue;
+ end
+
+ return currentValue, isNewValue;
+end
+
+function GetOrCreateTableEntryByCallback(table, key, callback)
+ local currentValue = table[key];
+ local isNewValue = (currentValue == nil);
+ if isNewValue then
+ currentValue = callback(key);
+ table[key] = currentValue;
+ end
+
+ return currentValue, isNewValue;
+end
+
+function GetRandomArrayEntry(array)
+ return array[math.random(1, #array)];
+end
+
+function GetRandomTableValue(tbl)
+ local value;
+ local n = 0;
+ for k, v in pairs(tbl) do
+ n = n + 1;
+ local r = math.random();
+ if r <= (1 / n) then
+ value = v;
+ end
+ end
+ return value;
+end
+
+function GetKeysArray(tbl)
+ local keysArray = {};
+ for key in pairs(tbl) do
+ tInsert(keysArray, key);
+ end
+
+ return keysArray;
+end
+
+function GetValuesArray(tbl)
+ local valuesArray = {};
+ for key, value in pairs(tbl) do
+ tInsert(valuesArray, value);
+ end
+
+ return valuesArray;
+end
+
+function GetPairsArray(tbl)
+ local pairsArray = {};
+ for key, value in pairs(tbl) do
+ tInsert(pairsArray, { key = key, value = value, });
+ end
+
+ return pairsArray;
+end
+
+function SwapTableEntries(lhsTable, rhsTable, key)
+ local lhsValue = lhsTable[key];
+ lhsTable[key] = rhsTable[key];
+ rhsTable[key] = lhsValue;
+end
+
+function TableUtil.OperateOnKeys(tbl, operation)
+ for key, value in pairs(tbl) do
+ operation(key);
+ end
+end
+
+function TableUtil.GetTableValueListFromEnumeration(tableKey, ...)
+ local values = {};
+ for enumerationKey, tbl in ... do
+ table.insert(values, tbl[tableKey]);
+ end
+
+ return values;
+end
+
+--[[
+This utility creates and returns a table of elements that are sorted by value "priority".
+
+Arguments:
+comparator - comparator(A, B) returns whether A has higher priority than B
+isAssociative - whether the table should be associative (true) or array-like (false). Only values have priorities for associative tables, not keys.
+
+Return usage:
+t[k]/t:Get(k) - returns the value stored with key/index k
+t[k] = v (Associative only) - stores the value v at key k, sorting accordingly
+t:Insert(v) (Array-like only) - inserts the value v, sorting accordingly
+t:Remove(k) - removes the element at key/index k
+t:Iterate(cb) - calls function cb on each key/index value pair in sorted priority order. !!WARNING!! This must be used instead of pairs(t)/ipairs(t)
+t:GetTop() - returns the highest priority element
+t:Pop() - returns and removes the highest priority element
+t:GetBottom() - returns the lowest priority element
+t:Size() - returns the number of stored elements. !!WARNING!! This must be used instead of #t
+t:Clear() - removes all elements
+--]]
+function TableUtil.CreatePriorityTable(comparator, isAssociative)
+ local sortedArray = {};
+
+ local ShiftPositionMap;
+ local keyToPosMap;
+ if isAssociative then
+ keyToPosMap = {};
+
+ ShiftPositionMap = function(position, shiftUp)
+ local mod = shiftUp and 1 or -1;
+ for k, p in pairs(keyToPosMap) do
+ if p >= position then
+ keyToPosMap[k] = p + mod;
+ end
+ end
+ end
+ end
+
+ local function SortedInsert(k, v)
+ if isAssociative then
+ local prevPos = keyToPosMap[k];
+ if prevPos then
+ tRemove(sortedArray, prevPos);
+ local shiftUp = false;
+ ShiftPositionMap(prevPos, shiftUp);
+ end
+ end
+
+ local top = (#sortedArray > 0) and (#sortedArray + 1) or 1;
+ local bottom = 1;
+ while top ~= bottom do
+ local mid = math.floor((top - bottom) / 2) + bottom;
+ if not comparator(v, sortedArray[mid]) then
+ bottom = mid + 1;
+ else
+ top = mid;
+ end
+ end
+ local idx = bottom;
+ tInsert(sortedArray, idx, v);
+
+ if isAssociative then
+ local shiftUp = true;
+ ShiftPositionMap(idx, shiftUp);
+ keyToPosMap[k] = idx;
+ end
+ end
+
+ local t = {};
+
+ function t:Get(k)
+ local key = isAssociative and keyToPosMap[k] or k;
+ return sortedArray[key];
+ end
+
+ if not isAssociative then
+ function t:Insert(v)
+ SortedInsert(nil, v)
+ end
+ end
+
+ function t:Remove(k)
+ local key = isAssociative and keyToPosMap[k] or k;
+ tRemove(sortedArray, key);
+ if isAssociative then
+ keyToPosMap[k] = nil;
+ local shiftUp = false;
+ ShiftPositionMap(key, shiftUp);
+ end
+ end
+
+ function t:GetTop()
+ return #sortedArray > 0 and sortedArray[1];
+ end
+
+ function t:GetBottom()
+ return #sortedArray > 0 and sortedArray[#sortedArray];
+ end
+
+ function t:Pop()
+ if t:Size() == 0 then
+ return nil;
+ end
+
+ local top = t:GetTop();
+ local removalKey = isAssociative and tInvert(keyToPosMap)[1] or 1;
+ t:Remove(removalKey);
+ return top;
+ end
+
+ function t:Iterate(callback)
+ local posToKeyMap = isAssociative and tInvert(keyToPosMap) or nil;
+ for pos, v in ipairs(sortedArray) do
+ local key = isAssociative and posToKeyMap[pos] or pos;
+ local done = callback(key, v);
+ if done then
+ return;
+ end
+ end
+ end
+
+ function t:Size()
+ return #sortedArray;
+ end
+
+ function t:Clear()
+ sortedArray = {};
+ if isAssociative then
+ keyToPosMap = {};
+ end
+ end
+
+ local mt =
+ {
+ __index = function(t, k)
+ return t:Get(k);
+ end,
+ };
+ if isAssociative then
+ mt.__newindex = function(t, k, v)
+ if v ~= nil then
+ SortedInsert(k, v);
+ else
+ t:Remove(k);
+ end
+ end
+ else
+ mt.__newindex = function(t, k, v)
+ error("Attempted to assign a value to an array-like priority queue index. Use Insert()/Remove() instead.")
+ end
+ end
+
+ setmetatable(t, mt);
+
+ return t;
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/TextureUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/TextureUtil.lua
new file mode 100644
index 0000000..70d68d1
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/TextureUtil.lua
@@ -0,0 +1,251 @@
+function GetTextureInfo(obj)
+ if obj:GetObjectType() == "Texture" then
+ local assetName = obj:GetAtlas();
+ local assetType = "Atlas";
+
+ if not assetName then
+ assetName = obj:GetTextureFilePath();
+ assetType = "File";
+ end
+
+ if not assetName then
+ assetName = obj:GetTextureFileID();
+ assetType = "FileID";
+ end
+
+ if not assetName then
+ assetName = "UnknownAsset";
+ assetType = "Unknown";
+ end
+
+ local ulX, ulY, blX, blY, urX, urY, brX, brY = obj:GetTexCoord();
+ return assetName, assetType, ulX, ulY, blX, blY, urX, urY, brX, brY;
+ end
+end
+
+function SetClampedTextureRotation(texture, rotationDegrees)
+ if (rotationDegrees ~= 0 and rotationDegrees ~= 90 and rotationDegrees ~= 180 and rotationDegrees ~= 270) then
+ error("SetRotation: rotationDegrees must be 0, 90, 180, or 270");
+ return;
+ end
+
+ if not (texture.rotationDegrees) then
+ texture.origTexCoords = {texture:GetTexCoord()};
+ texture.origWidth = texture:GetWidth();
+ texture.origHeight = texture:GetHeight();
+ end
+
+ if (texture.rotationDegrees == rotationDegrees) then
+ return;
+ end
+
+ texture.rotationDegrees = rotationDegrees;
+
+ if (rotationDegrees == 0 or rotationDegrees == 180) then
+ texture:SetWidth(texture.origWidth);
+ texture:SetHeight(texture.origHeight);
+ else
+ texture:SetWidth(texture.origHeight);
+ texture:SetHeight(texture.origWidth);
+ end
+
+ if (rotationDegrees == 0) then
+ texture:SetTexCoord( texture.origTexCoords[1], texture.origTexCoords[2],
+ texture.origTexCoords[3], texture.origTexCoords[4],
+ texture.origTexCoords[5], texture.origTexCoords[6],
+ texture.origTexCoords[7], texture.origTexCoords[8] );
+ elseif (rotationDegrees == 90) then
+ texture:SetTexCoord( texture.origTexCoords[3], texture.origTexCoords[4],
+ texture.origTexCoords[7], texture.origTexCoords[8],
+ texture.origTexCoords[1], texture.origTexCoords[2],
+ texture.origTexCoords[5], texture.origTexCoords[6] );
+ elseif (rotationDegrees == 180) then
+ texture:SetTexCoord( texture.origTexCoords[7], texture.origTexCoords[8],
+ texture.origTexCoords[5], texture.origTexCoords[6],
+ texture.origTexCoords[3], texture.origTexCoords[4],
+ texture.origTexCoords[1], texture.origTexCoords[2] );
+ elseif (rotationDegrees == 270) then
+ texture:SetTexCoord( texture.origTexCoords[5], texture.origTexCoords[6],
+ texture.origTexCoords[1], texture.origTexCoords[2],
+ texture.origTexCoords[7], texture.origTexCoords[8],
+ texture.origTexCoords[3], texture.origTexCoords[4] );
+ end
+end
+
+function ClearClampedTextureRotation(texture)
+ if (texture.rotationDegrees) then
+ SetClampedTextureRotation(0);
+ texture.origTexCoords = nil;
+ texture.origWidth = nil;
+ texture.origHeight = nil;
+ end
+end
+
+
+function GetTexCoordsByGrid(xOffset, yOffset, textureWidth, textureHeight, gridWidth, gridHeight)
+ local widthPerGrid = gridWidth/textureWidth;
+ local heightPerGrid = gridHeight/textureHeight;
+ return (xOffset-1)*widthPerGrid, (xOffset)*widthPerGrid, (yOffset-1)*heightPerGrid, (yOffset)*heightPerGrid;
+end
+
+function GetTexCoordsForRole(role)
+ local textureHeight, textureWidth = 256, 256;
+ local roleHeight, roleWidth = 67, 67;
+
+ if ( role == "GUIDE" ) then
+ return GetTexCoordsByGrid(1, 1, textureWidth, textureHeight, roleWidth, roleHeight);
+ elseif ( role == "TANK" ) then
+ return GetTexCoordsByGrid(1, 2, textureWidth, textureHeight, roleWidth, roleHeight);
+ elseif ( role == "HEALER" ) then
+ return GetTexCoordsByGrid(2, 1, textureWidth, textureHeight, roleWidth, roleHeight);
+ elseif ( role == "DAMAGER" ) then
+ return GetTexCoordsByGrid(2, 2, textureWidth, textureHeight, roleWidth, roleHeight);
+ else
+ error("Unknown role: "..tostring(role));
+ end
+end
+
+function CreateTextureMarkup(file, fileWidth, fileHeight, width, height, left, right, top, bottom, xOffset, yOffset)
+ return ("|T%s:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d|t"):format(
+ file
+ , height
+ , width
+ , xOffset or 0
+ , yOffset or 0
+ , fileWidth
+ , fileHeight
+ , left * fileWidth
+ , right * fileWidth
+ , top * fileHeight
+ , bottom * fileHeight
+ );
+end
+
+function CreateAtlasMarkup(atlasName, width, height, offsetX, offsetY, rVertexColor, gVertexColor, bVertexColor)
+ if ( bVertexColor ) then
+ return ("|A:%s:%d:%d:%d:%d:%d:%d:%d|a"):format(
+ atlasName
+ , height or 0
+ , width or 0
+ , offsetX or 0
+ , offsetY or 0
+ , rVertexColor or 0
+ , gVertexColor or 0
+ , bVertexColor
+ );
+ else
+ return ("|A:%s:%d:%d:%d:%d|a"):format(
+ atlasName
+ , height or 0
+ , width or 0
+ , offsetX or 0
+ , offsetY or 0
+ );
+ end
+end
+function CreateAtlasMarkupWithAtlasSize(atlasName, offsetX, offsetY, rVertexColor, gVertexColor, bVertexColor)
+ local atlasInfo = C_Texture.GetAtlasInfo(atlasName);
+ return CreateAtlasMarkup(atlasName, atlasInfo.width, atlasInfo.height, offsetX, offsetY, rVertexColor, gVertexColor, bVertexColor);
+end
+
+-- NOTE: Many of the TextureKit functions below use the following parameters
+-- If setVisibilityOfRegions is true, the frame will be shown or hidden based on whether the textureKit and atlas element were found
+-- If useAtlasSize is true, the frame will be resized to be the same size as the atlas element.
+-- Use the constants in TextureKitConstants for both
+
+TextureKitConstants = {
+ SetVisibility = true;
+ DoNotSetVisibility = false;
+
+ UseAtlasSize = true;
+ IgnoreAtlasSize = false;
+}
+
+-- Pass in a frame and a table containing parentKeys (on frame) as keys and atlas member names as the values
+function SetupAtlasesOnRegions(frame, regionsToAtlases, useAtlasSize)
+ for region, atlas in pairs(regionsToAtlases) do
+ if frame[region] then
+ if frame[region]:GetObjectType() == "StatusBar" then
+ frame[region]:SetStatusBarAtlas(atlas);
+ elseif frame[region].SetAtlas then
+ frame[region]:SetAtlas(atlas, useAtlasSize);
+ end
+ end
+ end
+end
+
+function GetFinalNameFromTextureKit(fmt, textureKits)
+ if type(textureKits) == "table" then
+ return fmt:format(unpack(textureKits));
+ else
+ return fmt:format(textureKits);
+ end
+end
+
+-- Pass in a TextureKit name, a frame and a formatting string.
+-- The TextureKit name will be inserted into fmt (at the first %s). The resulting atlas name will be set on frame
+-- Use "%s" for fmt if the TextureKit name is the entire atlas element name
+function SetupTextureKitOnFrame(textureKit, frame, fmt, setVisibility, useAtlasSize)
+ if not frame then
+ return;
+ end
+
+ local success = false;
+
+ if textureKit then
+ if frame:GetObjectType() == "StatusBar" then
+ success = frame:SetStatusBarAtlas(GetFinalNameFromTextureKit(fmt, textureKit));
+ elseif frame.SetAtlas then
+ success = frame:SetAtlas(GetFinalNameFromTextureKit(fmt, textureKit), useAtlasSize);
+ end
+ end
+
+ if setVisibility then
+ frame:SetShown(success);
+ end
+end
+
+-- Pass in a TextureKit name and a table containing frames as keys and formatting strings as values
+-- For each frame key in frames, the TextureKit name will be inserted into fmt (at the first %s). The resulting atlas name will be set on frame
+-- Use "%s" for fmt if the TextureKit name is the entire atlas element name
+function SetupTextureKitOnFrames(textureKit, frames, setVisibilityOfRegions, useAtlasSize)
+ if not textureKit and not setVisibilityOfRegions then
+ return;
+ end
+
+ for frame, fmt in pairs(frames) do
+ SetupTextureKitOnFrame(textureKit, frame, fmt, setVisibilityOfRegions, useAtlasSize);
+ end
+end
+
+-- Pass in a TextureKit name, a frame and a table containing parentKeys (on frame) as keys and formatting strings as values
+-- For each frame key in frames, the TextureKit name will be inserted into fmt (at the first %s). The resulting atlas name will be set on frame
+-- Use "%s" for fmt if the TextureKit name is the entire atlas element name
+function SetupTextureKitOnRegions(textureKit, frame, regions, setVisibilityOfRegions, useAtlasSize)
+ if not textureKit and not setVisibilityOfRegions then
+ return;
+ end
+
+ local frames = {};
+ for region, fmt in pairs(regions) do
+ if frame[region] then
+ frames[frame[region]] = fmt;
+ end
+ end
+
+ return SetupTextureKitOnFrames(textureKit, frames, setVisibilityOfRegions, useAtlasSize);
+end
+
+-- Pass in a TextureKit name, a frame and a table containing parentKeys (on frame) as keys and a table as values
+-- The values table should contain formatString as a member (setVisibility and useAtlasSize can also be added if desired)
+-- For each frame key in frames, the TextureKit name will be inserted into formatString (at the first %s). The resulting atlas name will be set on frame
+-- Use "%s" for formatString if the TextureKit name is the entire atlas element name
+function SetupTextureKitsFromRegionInfo(textureKit, frame, regionInfoList)
+ if not frame or not regionInfoList then
+ return;
+ end
+
+ for region, regionInfo in pairs(regionInfoList) do
+ SetupTextureKitOnFrame(textureKit, frame[region], regionInfo.formatString, regionInfo.setVisibility, regionInfo.useAtlasSize);
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Unit.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Unit.lua
new file mode 100644
index 0000000..c9c61c0
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/!!!ClassicAPI/Util/Unit.lua
@@ -0,0 +1,52 @@
+local _, Private = ...
+
+local UnitIsTapped = UnitIsTapped
+local UnitIsConnected = UnitIsConnected
+local UnitIsTappedByPlayer = UnitIsTappedByPlayer
+local GetPlayerMapPosition = GetPlayerMapPosition
+local UnitIsTappedByAllThreatList = UnitIsTappedByAllThreatList
+
+function UnitPhaseReason(Unit)
+ -- return 0 -- Phasing -- If in instance?
+end
+
+function UnitDistanceSquared(Unit)
+ if ( UnitIsConnected(Unit) ) then
+ local PX, PY = GetPlayerMapPosition("player")
+ local UX, UY = GetPlayerMapPosition(Unit)
+ return CalculateDistance(PX, PY, UX, UY) * 100000, true
+ end
+ return 0, false
+end
+
+function UnitIsTapDenied(Unit)
+ return UnitIsTapped(Unit) and not UnitIsTappedByPlayer(Unit) and not UnitIsTappedByAllThreatList(Unit)
+end
+
+function UnitShouldDisplayName(Unit)
+ return Unit
+end
+
+function UnitNameplateShowsWidgetsOnly(Unit)
+ return false
+end
+
+function C_UnitCastingInfo(Unit)
+ local Name, Rank, Text, Texture, StartTime, EndTime, IsTradeskill, Cast, Interruptable, SpellID = UnitCastingInfo(Unit)
+ return Name, Text, Texture, StartTime, EndTime, IsTradeskill, Cast, Interruptable, SpellID
+end
+
+function C_UnitChannelInfo(Unit)
+ local Name, Rank, Text, Texture, StartTime, EndTime, IsTradeskill, Interruptable, SpellID = UnitChannelInfo(Unit)
+ return Name, Text, Texture, StartTime, EndTime, IsTradeskill, Interruptable, SpellID
+end
+
+function UnitFullName(Unit)
+ local Name, Realm = UnitName(Unit)
+
+ if ( Unit == "player" ) then
+ Realm = GetNormalizedRealmName()
+ end
+
+ return Name, Realm
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/DragonUI.toc b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/DragonUI.toc
index afcdf6e..3fac7cb 100644
--- a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/DragonUI.toc
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/DragonUI.toc
@@ -4,6 +4,8 @@
## Version: 2.4.1
## Author: NeticSoul
## SavedVariables: DragonUIDB
+## DefaultState: Enabled
+## X-DragonUI-API: 1
## Dependencies: LibStub, CallbackHandler-1.0, Ace3
## OptionalDeps: Blizzard_TimeManager, Blizzard_PartyUI
## X-License: MIT for project-authored code; bundled dependencies retain their own licenses
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core.lua
index 0262882..cef311a 100644
--- a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core.lua
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core.lua
@@ -39,6 +39,14 @@ function addon.core:OnInitialize()
addon:ApplyDatabaseMigrations()
end
+ -- Replacement addons declare their targets in TOC metadata. Discover and
+ -- suppress those targets after defaults exist but before legacy
+ -- ADDON_LOADED module handlers run.
+ if addon.OverrideRegistry then
+ addon.OverrideRegistry:DiscoverDeclarations()
+ addon.OverrideRegistry:SuppressDeclaredModules()
+ end
+
-- Register callbacks for configuration changes
addon.db.RegisterCallback(addon, "OnProfileChanged", "RefreshConfig");
addon.db.RegisterCallback(addon, "OnProfileCopied", "RefreshConfig");
@@ -59,7 +67,9 @@ function addon.core:OnEnable()
self:RegisterChatCommand("pi", "SlashCommand")
end
- -- Fire custom event to signal that DragonUI is fully initialized
+ -- Fire custom event to signal that DragonUI is fully initialized.
+ -- External override providers have loaded by PLAYER_LOGIN; the early TOC
+ -- declaration already prevents the stock module from applying meanwhile.
-- This ensures modules get the correct config values
self:SendMessage("DRAGONUI_READY");
end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/api.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/api.lua
index a0f93ce..9f17f67 100644
--- a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/api.lua
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/api.lua
@@ -1132,6 +1132,10 @@ end
-- @param name: Module identifier
-- @return boolean
function MR:IsEnabled(name)
+ if addon.IsModuleOverridden and addon:IsModuleOverridden(name) then
+ return false
+ end
+
local info = self.modules[name]
if info and info.isEnabled then
return info.isEnabled()
@@ -1257,6 +1261,12 @@ function MR:Enable(name)
addon:Error((L and L["ModuleRegistry: Unknown module -"]) or "ModuleRegistry: Unknown module -", name)
return false
end
+
+ if addon.IsModuleOverridden and addon:IsModuleOverridden(name) then
+ local owner = addon:GetModuleOverride(name)
+ addon:Error("Module is controlled by external addon:", name, owner)
+ return false
+ end
-- Update database
if addon.db and addon.db.profile and addon.db.profile.modules then
@@ -1283,6 +1293,10 @@ function MR:Disable(name)
addon:Error((L and L["ModuleRegistry: Unknown module -"]) or "ModuleRegistry: Unknown module -", name)
return false
end
+
+ if addon.IsModuleOverridden and addon:IsModuleOverridden(name) then
+ return false
+ end
-- Update database
if addon.db and addon.db.profile and addon.db.profile.modules then
@@ -1333,6 +1347,10 @@ function MR:PrintStatus()
local enabled = self:IsEnabled(name)
local status = enabled and ("|cFF00FF00" .. ((L and L["Enabled"]) or "Enabled") .. "|r") or ("|cFFFF0000" .. ((L and L["Disabled"]) or "Disabled") .. "|r")
local loaded = info.module and (info.module.initialized or info.module.applied) and ("|cFF00FF00" .. ((L and L["Loaded"]) or "Loaded") .. "|r") or "|cFFAAAAAA-|r"
+ local owner = addon.GetModuleOverride and addon:GetModuleOverride(name)
+ if owner then
+ status = "|cFFFFD200overridden by " .. owner .. "|r"
+ end
local mode = info.loadOnce and (" |cFFFFD200(" .. ((L and L["load-once"]) or "load-once") .. ")|r") or ""
print(string.format(" %s: %s (%s)%s", info.displayName, status, loaded, mode))
@@ -1524,6 +1542,9 @@ end
-- @param moduleName: string key matching database.lua modules table
-- @return boolean
function addon:IsModuleEnabled(moduleName)
+ if self.IsModuleOverridden and self:IsModuleOverridden(moduleName) then
+ return false
+ end
local cfg = self:GetModuleConfig(moduleName)
return cfg and cfg.enabled or false
end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/core.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/core.xml
index dce6f01..dbcc2fe 100644
--- a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/core.xml
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/core.xml
@@ -2,10 +2,11 @@
..\FrameXML\UI.xsd">
-
+
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/overrides.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/overrides.lua
new file mode 100644
index 0000000..cff2627
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/core/overrides.lua
@@ -0,0 +1,416 @@
+-- ============================================================================
+-- DragonUI - External module override contract
+--
+-- A replacement addon declares modules before DragonUI initializes:
+-- ## X-DragonUI-Overrides: minimap, mainbars
+-- ## X-DragonUI-Override-Priority: 100
+--
+-- It then registers a provider while its Lua files load:
+-- DragonUI:RegisterModuleOverride("minimap", "MyAddon", provider)
+--
+-- The TOC declaration is load-bearing. Several legacy DragonUI modules apply
+-- themselves during ADDON_LOADED, before a dependent addon can execute Lua.
+-- Discovering the declaration first lets DragonUI suppress those modules in
+-- time while preserving the user's configured state for the next session.
+-- ============================================================================
+
+local addon = select(2, ...)
+
+addon.PUBLIC_API_VERSION = 1
+
+local Overrides = {
+ claims = {},
+ winners = {},
+ desiredStates = {},
+ activeProviders = {},
+ initialized = false,
+ ready = false,
+}
+
+addon.OverrideRegistry = Overrides
+
+local function Trim(value)
+ if type(value) ~= "string" then
+ return value
+ end
+ return (value:gsub("^%s+", ""):gsub("%s+$", ""))
+end
+
+local function NormalizeModuleName(name)
+ name = Trim(name)
+ if type(name) ~= "string" or name == "" then
+ return nil
+ end
+ return string.lower(name)
+end
+
+local function ParseModuleList(value)
+ local modules = {}
+ if type(value) ~= "string" then
+ return modules
+ end
+
+ for token in string.gmatch(value, "[^,;]+") do
+ local name = NormalizeModuleName(token)
+ if name then
+ table.insert(modules, name)
+ end
+ end
+ return modules
+end
+
+local function IsAddonEnabled(index)
+ local character = UnitName and UnitName("player") or nil
+ local state = GetAddOnEnableState and GetAddOnEnableState(character, index) or 0
+ if (not state or state == 0) and GetAddOnEnableState then
+ state = GetAddOnEnableState(nil, index)
+ end
+ return state and state > 0
+end
+
+local MODULE_CONFIG_PATHS = {
+ buffs = { "buffs" },
+ castbar = { "castbar" },
+ player = { "unitframe", "player" },
+ boss = { "unitframe", "boss" },
+}
+
+local function GetModuleConfig(name)
+ local profile = addon.db and addon.db.profile
+ if not profile then
+ return nil
+ end
+
+ local path = MODULE_CONFIG_PATHS[name]
+ if path then
+ local cfg = profile
+ for _, key in ipairs(path) do
+ cfg[key] = cfg[key] or {}
+ cfg = cfg[key]
+ end
+ return cfg
+ end
+
+ profile.modules = profile.modules or {}
+ profile.modules[name] = profile.modules[name] or {}
+ return profile.modules[name]
+end
+
+local function ProviderMethod(provider, ...)
+ if type(provider) ~= "table" then
+ return nil
+ end
+ for index = 1, select("#", ...) do
+ local method = provider[select(index, ...)]
+ if type(method) == "function" then
+ return method
+ end
+ end
+ return nil
+end
+
+function Overrides:SelectWinner(moduleName)
+ local claims = self.claims[moduleName]
+ local winner
+ local hasProvider = false
+ if claims then
+ for _, claim in pairs(claims) do
+ if claim.provider then
+ hasProvider = true
+ break
+ end
+ end
+
+ for _, claim in pairs(claims) do
+ -- TOC declarations reserve and suppress a module before addon Lua
+ -- loads. As soon as at least one real provider registers, stale
+ -- declarations must not hide that usable implementation.
+ if not hasProvider or claim.provider then
+ if not winner
+ or claim.priority > winner.priority
+ or (claim.priority == winner.priority and claim.owner < winner.owner) then
+ winner = claim
+ end
+ end
+ end
+ end
+ self.winners[moduleName] = winner
+ return winner
+end
+
+function Overrides:AddClaim(moduleName, owner, priority, declared)
+ moduleName = NormalizeModuleName(moduleName)
+ owner = Trim(owner)
+ if not moduleName or type(owner) ~= "string" or owner == "" then
+ return nil
+ end
+
+ self.claims[moduleName] = self.claims[moduleName] or {}
+ local claim = self.claims[moduleName][owner]
+ if not claim then
+ claim = {
+ module = moduleName,
+ owner = owner,
+ priority = tonumber(priority) or 0,
+ declared = declared and true or false,
+ }
+ self.claims[moduleName][owner] = claim
+ else
+ claim.priority = tonumber(priority) or claim.priority or 0
+ claim.declared = claim.declared or (declared and true or false)
+ end
+
+ self:SelectWinner(moduleName)
+ return claim
+end
+
+function Overrides:DiscoverDeclarations()
+ if self.initialized then
+ return
+ end
+ self.initialized = true
+
+ for index = 1, GetNumAddOns() do
+ local name = GetAddOnInfo(index)
+ if name and name ~= "DragonUI" and IsAddonEnabled(index) then
+ local declared = GetAddOnMetadata(name, "X-DragonUI-Overrides")
+ local priority = tonumber(GetAddOnMetadata(name, "X-DragonUI-Override-Priority")) or 0
+ for _, moduleName in ipairs(ParseModuleList(declared)) do
+ self:AddClaim(moduleName, name, priority, true)
+ end
+ end
+ end
+end
+
+function Overrides:Suppress(moduleName)
+ local cfg = GetModuleConfig(moduleName)
+ if not cfg then
+ return false
+ end
+
+ if self.desiredStates[moduleName] == nil then
+ self.desiredStates[moduleName] = cfg.enabled and true or false
+ end
+ cfg.enabled = false
+ return true
+end
+
+function Overrides:SuppressDeclaredModules()
+ for moduleName in pairs(self.winners) do
+ self:Suppress(moduleName)
+ end
+end
+
+function Overrides:RestoreDesiredState(moduleName)
+ local desired = self.desiredStates[moduleName]
+ if desired == nil then
+ return
+ end
+ local cfg = GetModuleConfig(moduleName)
+ if cfg then
+ cfg.enabled = desired
+ end
+ self.desiredStates[moduleName] = nil
+end
+
+function Overrides:RestoreAllDesiredStates()
+ for moduleName in pairs(self.desiredStates) do
+ self:RestoreDesiredState(moduleName)
+ end
+end
+
+function Overrides:Activate(claim)
+ if not claim or not claim.provider or self.activeProviders[claim.module] == claim then
+ return true
+ end
+
+ local method = ProviderMethod(claim.provider, "Activate", "Apply", "Enable", "OnEnable")
+ if method then
+ local ok, err = pcall(method, claim.provider, addon, claim.module)
+ if not ok then
+ addon:Error("Override provider failed:", claim.owner, claim.module, err)
+ return false
+ end
+ end
+
+ self.activeProviders[claim.module] = claim
+ return true
+end
+
+function Overrides:Deactivate(claim)
+ if not claim or self.activeProviders[claim.module] ~= claim then
+ return true
+ end
+
+ local method = ProviderMethod(claim.provider, "Deactivate", "Restore", "Disable", "OnDisable")
+ local success = true
+ if method then
+ local ok, err = pcall(method, claim.provider, addon, claim.module)
+ if not ok then
+ addon:Error("Override provider cleanup failed:", claim.owner, claim.module, err)
+ success = false
+ end
+ end
+
+ self.activeProviders[claim.module] = nil
+ return success
+end
+
+function Overrides:FinalizeStartup()
+ if self.ready then
+ return
+ end
+ self.ready = true
+
+ for moduleName, claims in pairs(self.claims) do
+ -- Drop stale declarations first. A lower-priority working provider is
+ -- allowed to win after an incomplete higher-priority addon is removed.
+ for owner, claim in pairs(claims) do
+ if not claim.provider then
+ addon:Error("Addon", claim.owner, "declared an override for", moduleName,
+ "but did not register a provider; ignoring its claim.")
+ claims[owner] = nil
+ end
+ end
+
+ local winner = self:SelectWinner(moduleName)
+ while winner and not self:Activate(winner) do
+ claims[winner.owner] = nil
+ winner = self:SelectWinner(moduleName)
+ end
+
+ if winner then
+ self:Suppress(moduleName)
+ else
+ -- No usable provider must never leave the player without the
+ -- corresponding interface element.
+ self:RestoreDesiredState(moduleName)
+ if addon.ModuleRegistry then addon.ModuleRegistry:Refresh(moduleName) end
+ end
+ end
+end
+
+function Overrides:Register(moduleName, owner, provider, options)
+ options = options or {}
+ moduleName = NormalizeModuleName(moduleName)
+ owner = Trim(owner)
+ if not moduleName or type(owner) ~= "string" or owner == "" or type(provider) ~= "table" then
+ return false, "INVALID_ARGUMENT"
+ end
+
+ local previousWinner = self.winners[moduleName]
+ local claim = self:AddClaim(moduleName, owner, options.priority, false)
+ claim.provider = provider
+ local winner = self:SelectWinner(moduleName)
+
+ if previousWinner and previousWinner ~= winner then
+ self:Deactivate(previousWinner)
+ end
+
+ self:Suppress(moduleName)
+
+ local requiresReload = false
+ local info = addon.ModuleRegistry and addon.ModuleRegistry:GetInfo(moduleName)
+ if not claim.declared and info and info.loadOnce
+ and info.module and (info.module.initialized or info.module.applied) then
+ requiresReload = true
+ end
+
+ if self.ready and winner == claim and not requiresReload then
+ if not self:Activate(claim) then
+ self.claims[moduleName][owner] = nil
+ winner = self:SelectWinner(moduleName)
+ if winner and winner.provider then
+ self:Activate(winner)
+ else
+ self:RestoreDesiredState(moduleName)
+ if addon.ModuleRegistry then addon.ModuleRegistry:Refresh(moduleName) end
+ end
+ return false, "ACTIVATION_FAILED"
+ end
+ end
+
+ if addon.core and addon.core.SendMessage then
+ addon.core:SendMessage("DRAGONUI_OVERRIDE_CHANGED", moduleName, winner and winner.owner or nil)
+ end
+
+ if requiresReload then
+ return true, "RELOAD_REQUIRED"
+ end
+ return true
+end
+
+function Overrides:Unregister(moduleName, owner)
+ moduleName = NormalizeModuleName(moduleName)
+ owner = Trim(owner)
+ local claims = moduleName and self.claims[moduleName]
+ local claim = claims and claims[owner]
+ if not claim then
+ return false, "NOT_FOUND"
+ end
+
+ local wasWinner = self.winners[moduleName] == claim
+ if wasWinner then
+ self:Deactivate(claim)
+ end
+ claims[owner] = nil
+
+ local winner = self:SelectWinner(moduleName)
+ if winner then
+ self:Suppress(moduleName)
+ if self.ready and winner.provider then
+ self:Activate(winner)
+ end
+ else
+ self:RestoreDesiredState(moduleName)
+ if self.ready and addon.ModuleRegistry then
+ addon.ModuleRegistry:Refresh(moduleName)
+ end
+ end
+
+ if addon.core and addon.core.SendMessage then
+ addon.core:SendMessage("DRAGONUI_OVERRIDE_CHANGED", moduleName, winner and winner.owner or nil)
+ end
+ return true
+end
+
+function Overrides:GetWinner(moduleName)
+ return self.winners[NormalizeModuleName(moduleName)]
+end
+
+function Overrides:IsOverridden(moduleName)
+ return self:GetWinner(moduleName) ~= nil
+end
+
+function addon:RegisterModuleOverride(moduleName, owner, provider, options)
+ return Overrides:Register(moduleName, owner, provider, options)
+end
+
+function addon:UnregisterModuleOverride(moduleName, owner)
+ return Overrides:Unregister(moduleName, owner)
+end
+
+function addon:IsModuleOverridden(moduleName)
+ return Overrides:IsOverridden(moduleName)
+end
+
+function addon:GetModuleOverride(moduleName)
+ local winner = Overrides:GetWinner(moduleName)
+ return winner and winner.owner or nil, winner and winner.provider or nil
+end
+
+-- Registered before module files create their PLAYER_LOGIN handlers, so stale
+-- declarations can be released before legacy module startup handlers run.
+local eventFrame = CreateFrame("Frame")
+eventFrame:RegisterEvent("PLAYER_LOGIN")
+eventFrame:RegisterEvent("PLAYER_LOGOUT")
+eventFrame:SetScript("OnEvent", function(self, event)
+ if event == "PLAYER_LOGIN" then
+ Overrides:FinalizeStartup()
+ self:UnregisterEvent("PLAYER_LOGIN")
+ elseif event == "PLAYER_LOGOUT" then
+ -- Suppression is session-only. Restore the user's chosen DragonUI state
+ -- immediately before SavedVariables are serialized.
+ Overrides:RestoreAllDesiredStates()
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/modules/unitframes/uf_core.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/modules/unitframes/uf_core.lua
index 4c8ee3f..465a34a 100644
--- a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/modules/unitframes/uf_core.lua
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI/modules/unitframes/uf_core.lua
@@ -193,6 +193,9 @@ function UF.GetConfig(unitKey)
end
function UF.IsEnabled(unitKey)
+ if addon.IsModuleOverridden and addon:IsModuleOverridden(unitKey) then
+ return false
+ end
local config = UF.GetConfig(unitKey)
return config.enabled ~= false
end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Bindings.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Bindings.xml
new file mode 100644
index 0000000..e8de11e
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Bindings.xml
@@ -0,0 +1,30 @@
+
+
+
+ if DragonUI_NewEra and DragonUI_NewEra.guild and DragonUI_NewEra.guild.Toggle then
+ DragonUI_NewEra.guild.Toggle()
+ end
+
+
+ if DragonUI_NewEra and DragonUI_NewEra.social and DragonUI_NewEra.social.Toggle then
+ DragonUI_NewEra.social.Toggle()
+ end
+
+
+ if DragonUI_NewEra and DragonUI_NewEra.ej and DragonUI_NewEra.ej.Toggle then
+ DragonUI_NewEra.ej.Toggle()
+ end
+
+
+ if DragonUI_NewEra and DragonUI_NewEra.collections and DragonUI_NewEra.collections.Toggle then
+ DragonUI_NewEra.collections.Toggle()
+ end
+
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/CONTRACTS.md b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/CONTRACTS.md
new file mode 100644
index 0000000..be8770d
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/CONTRACTS.md
@@ -0,0 +1,166 @@
+# DragonUI_NewEra — Build Contracts (Sprint 0)
+
+**Read this first.** Every agent codes against these fixed interfaces, not against each other's
+in-progress output. The architect owns this file and the TOC; agents own their directories.
+
+Paths: addon = `/root/downport/DragonUI_NewEra/`. Downport SOURCE = `/root/downport/DOWNPORT THIS/NewEra/`
+(Classic 1.15). Base HUD = `/root/downport/DragonUI/DragonUI/` (3.3.5a). Shim lib =
+`/root/downport/classicapi/!!!ClassicAPI/`.
+
+---
+
+## 0. Global conventions (all agents)
+
+- **Namespace:** the addon table is `_G.DragonUI_NewEra`, aliased `NE`. Every Lua file starts with
+ `local NE = DragonUI_NewEra`. Never create a global `NE`. (Porting a NewEra file = replace its
+ top `NE = NE or {}` with `local NE = DragonUI_NewEra`; everything else `NE.*` stays.)
+- **Base addon handle:** `NE.dragon == _G.DragonUI`. Use it for: `NE.dragon.ModuleRegistry`,
+ `NE.dragon.MoversSystem`, `NE.dragon.OptionsPanel`, `NE.dragon.PanelControls`, `NE.dragon.Fonts`,
+ `NE.dragon.SafeCall`, `NE.dragon.CombatQueue`, and `SetAtlasTexture` (global from DragonUI Atlas.lua).
+- **3.3.5a hard rules (luac won't catch these):** no `SetShown` (use Show/Hide); `FauxScrollFrame`
+ must be NAMED; no `SetMask`/`ScrollBox`/native atlas; some Blizzard returns are at shifted arg
+ positions — `pcall` data getters. LoadOnDemand frames (Talents/Inspect/AH) hook on `ADDON_LOADED`.
+- **Do not edit** anything under `/root/downport/DragonUI/` (base) or the NewEra source. Read-only.
+- **Comment style for ported files:** keep NewEra's excellent header comments; add a one-line
+ `-- DOWNPORT: ` wherever you deviate.
+
+---
+
+## 1. compat/ — modern-API shim layer (owner: API Bridge Engineer)
+
+Goal: after `compat/` loads, every modern symbol NewEra v1 uses **exists and works on 3.3.5a**,
+delegating to `!!!ClassicAPI` when present, else a vendored shim copied/adapted from ClassicAPI
+or written fresh. Load order is fixed by the TOC (compat loads before core).
+
+**Files you own (exact names — TOC already lists them):**
+`compat/Compat.lua` (loader + ClassicAPI detection + `NE.compat` table of capability bools),
+`compat/Mixin.lua`, `compat/C_Timer.lua`, `compat/C_Texture.lua`, `compat/C_Container.lua`,
+`compat/C_Item.lua`, `compat/C_Spell.lua`, `compat/C_Map.lua`. Plus **`compat/COVERAGE.md`**.
+
+**Contract each shim guarantees (minimum for v1):**
+- `Mixin(obj, ...)`, `CreateFromMixins(...)`, `CreateAndInitFromMixin(...)`.
+- `C_Timer.After(sec, fn)`, `C_Timer.NewTimer`, `C_Timer.NewTicker`.
+- `C_Texture.GetAtlasInfo(name)`, `C_Texture.GetAtlasExists`. (Atlas DATA is registered by Asset
+ agent via `NE.tex`; this shim only needs to answer atlas queries NewEra makes directly.)
+- `C_Container.*` (GetContainerNumSlots/ItemInfo/ItemLink/NumFreeSlots/UseContainerItem/
+ PickupContainerItem) mapped to 3.3.5 `GetContainer*` globals.
+- `C_Item.*` subset used by v1 (GetItemInfo wrappers, GetItemSpell, icon/quality helpers).
+- `C_Spell.*` subset (GetSpellSubtext, GetSpellPowerCost, texture/cooldown helpers).
+- `C_Map.*` subset (GetBestMapForUnit, GetMapInfo, GetPlayerMapPosition) — best-effort; QuestFrame
+ uses some. Stub returning safe nils where 3.3.5 can't answer, and record it in COVERAGE.md.
+
+**Rule:** prefer DragonUI's own helper if it already exists; else ClassicAPI; else vendor a copy
+INTO compat/ (don't hard-depend on ClassicAPI). Detection: `if NE.hasClassicAPI and C_Timer then …`.
+
+**Deliverable `compat/COVERAGE.md`:** a matrix of every modern symbol NewEra v1 references →
+`{source: ClassicAPI | DragonUI | vendored | stub | not-needed-v1}` + a note on any partial/stub.
+Produce the symbol list by grepping the v1 module dirs in the NewEra source (CharacterPanel,
+Spellbook, Talents, QuestFrame, MerchantFrame, MailFrame) AND the Core/ files those depend on.
+
+---
+
+## 2. core/ — downported NewEra Core chrome toolkit (owner: Core/Chrome Engineer)
+
+Downport these NewEra `Core/` files onto 3.3.5a, wired to DragonUI. **Files you own (exact names):**
+`core/FrameUtil.lua`, `core/Texture.lua`, `core/NineSlice.lua`, `core/NineSliceLayouts.lua`,
+`core/PanelChrome.lua`, `core/Portrait.lua`, `core/Tabs.lua`, `core/ButtonSkin.lua`,
+`core/ScrollbarReskin.lua`, `core/ItemButton.lua`, `core/ItemGrid.lua`, `core/Modules.lua`,
+and the Sprint-0 proof `core/_HelloDemo.lua`.
+
+**Public interfaces you must expose (panels in later sprints depend on these — keep stable):**
+- `NE.tex.RegisterLocal(fdid, path)` and `NE.tex.Set(texture, fdidOrAtlasName[, ...])` — the
+ texture indirection NewEra uses (`Core/Texture.lua`). The Asset agent supplies the data; you
+ supply the API. Resolve atlas coords via the C_Texture shim / registered atlas table.
+- `NE.nineslice.Apply(frame, layoutName[, opts])` — applies a registered nineslice layout
+ (`Core/NineSlice.lua` + `NineSliceLayouts.lua`). Must support at least `PortraitFrameTemplate`,
+ `InsetFrameTemplate`, `ButtonFrameTemplate` for Sprint 1.
+- `NE.chrome.Apply(frame, opts)` — the PanelChrome entry that turns a Blizzard frame into a DF
+ portrait-frame (nineslice + portrait + title + close-button reskin). (`Core/PanelChrome.lua`.)
+- `NE.modules.Register{ name=, default=, onBoot=fn }` and the reload-gated boot dispatcher
+ (`Core/Modules.lua`). This drives per-module boot/enable. (The DragonUI-facing toggle is added
+ by the Integration agent and proxies into this.)
+- `NE.frameutil.PinPixelPerfect(...)`, `NE.portrait.*`, `NE.tabs.*`, `NE.buttonskin.*`,
+ `NE.scrollbar.*`, `NE.itembutton.*`, `NE.itemgrid.*` — keep NewEra's surface; downport bodies.
+
+**Wiring to DragonUI:** use `NE.dragon.Fonts` for fonts; prefer DragonUI `utils/nineslice.lua`
+patterns where they already solve a problem; register atlases through DragonUI's `Atlas.lua` /
+`SetAtlasTexture` OR the C_Texture shim — coordinate the choice with the Asset agent (see §3).
+
+**`core/_HelloDemo.lua` (Sprint-0 exit proof):** registers slash `/dnehello` that creates a
+standalone frame and calls `NE.chrome.Apply` to render a DF portrait-frame on it (no real Blizzard
+panel — that's Sprint 1). Also registers itself into `NE.qa.modules` so the harness can open it.
+
+---
+
+## 3. Textures/ — art + atlas registration (owner: Asset Pipeline Engineer)
+
+Inventory `/root/downport/DOWNPORT THIS/NewEra/Art/`. For Sprint 0, deliver the **shared/Core
+sheets** needed by the chrome toolkit + Hello demo (the NineSlice frame-metal sheets and shared
+backgrounds). **File you own:** `Textures/Assets.lua` — a list of `NE.tex.RegisterLocal(fdid, path)`
+calls (the API comes from Core `core/Texture.lua`). Copy the actual BLP files into
+`DragonUI_NewEra/Textures//...` preserving NewEra's per-panel folder structure, and point
+the registration paths at `Interface\AddOns\DragonUI_NewEra\Textures\...`.
+
+**Sprint-0 minimum atlas set** (from NewEra `Core/NineSliceLayouts.lua` + `Core/Assets.lua`):
+the UI-Frame-Metal corner/edge sheets (FDIDs 2406979/2406984/2406987), UI-Background-Rock (374155),
+and the InsetFrameTemplate inner-border sheets (1723831/1723832/1723833). Verify each BLP is valid
+3.3.5-loadable (dimensions power-of-two; use `mpyq`/existing tooling to sanity-check if unsure).
+
+**Deliverable:** `Textures/ASSETS.md` — what was copied, the FDID→path map, total size, and any
+art that's missing/needs extraction for later panels. Confirm one atlas renders end-to-end with the
+Core agent's Hello demo (coords correct, not stretched).
+
+**Coordinate with Core (§2) on the atlas mechanism:** decide together whether atlas coords are
+resolved via `C_Texture.RegisterAtlas` (ClassicAPI/shim) or a plain `NE.tex` coord table. Record
+the decision at the top of `Textures/Assets.lua`.
+
+---
+
+## 4. integration/ — DragonUI handshake (owner: Integration Liaison)
+
+**Files you own:** `integration/Register.lua`, `integration/Options.lua`.
+
+- `Register.lua`: define `NE.OnReady` (bootstrap calls it once SavedVariables are loaded). In it,
+ ensure `NE.dragon.db.profile.newera` exists (defaults `{ enabled=true }`), and provide
+ `NE.RegisterPanel(spec)` — the helper every panel module (later sprints) calls to:
+ (1) register a `NE.modules.Register` boot entry, (2) register a `NE.dragon.ModuleRegistry:Register`
+ entry so it shows in DragonUI's module list, (3) register a `NE.dragon.MoversSystem:RegisterMover`
+ for the panel's frame, and (4) append to `NE.qa.modules`. Spec shape:
+ `{ id, title, desc, frame, openFn, closeFn, defaultPoint, order }`.
+- `Options.lua`: when `DragonUI_Options` loads (hook `ADDON_LOADED` for `"DragonUI_Options"`, and
+ also handle the already-loaded case), call `NE.dragon.OptionsPanel:RegisterTab("newera",
+ "New Era", builder, 16)`. The builder lists a toggle per registered panel using
+ `NE.dragon.PanelControls` bound to `modules..enabled`, with a callback that calls the panel's
+ `Refresh*System` (or `NE.modules` enable). For Sprint 0 it shows just the Hello demo toggle.
+
+**Contract for later panels:** they only ever call `NE.RegisterPanel(spec)` + `NE.chrome.Apply` —
+they never touch DragonUI internals directly. You own that indirection.
+
+---
+
+## 5. qa/ — in-game harness + static gate (owner: QA/Harness Engineer)
+
+**Files you own:** `qa/Harness.lua` and `qa/staticcheck.sh` (+ `qa/QA.md`).
+
+- `qa/Harness.lua`: slash `/dnetest` that iterates `NE.qa.modules` and prints a per-module report:
+ does the frame global exist, does `openFn` run without error (wrap in `pcall`), any Lua errors
+ captured this session (install a lightweight error hook), and a taint note. Summarize PASS/FAIL.
+- `qa/staticcheck.sh`: run `luac -p` (3.3.5/Lua 5.1) on every `.lua` in the addon (skip if no luac
+ → say so), verify every file listed in the TOC exists, and grep for the known runtime traps
+ (`SetShown(`, unnamed `CreateFrame("...","FauxScrollFrame"` patterns, `:SetMask(`, `C_` calls not
+ covered by `compat/COVERAGE.md`). Emit a clear PASS/FAIL with file:line for each hit.
+- `qa/QA.md`: how to run both, and the Sprint-0 expected output.
+
+You may run `staticcheck.sh` yourself at the end and paste results.
+
+---
+
+## 6. Sprint-0 Definition of Done (architect verifies)
+
+1. TOC loads clean (all listed files exist; `staticcheck.sh` PASS).
+2. `compat/COVERAGE.md` matrix complete for v1 symbols; shims load without error.
+3. Core toolkit exposes the §2 interfaces; `/dnehello` renders a DF portrait-frame using downported
+ nineslice + one real registered atlas (Asset art), pixel-correct.
+4. `/dnetest` reports the Hello demo PASS.
+5. A "New Era" tab appears in DragonUI options (when DragonUI_Options is open) with the demo toggle.
+6. No global leaks (`NE` is local everywhere); base DragonUI untouched.
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/DragonUI_NewEra.toc b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/DragonUI_NewEra.toc
new file mode 100644
index 0000000..c9a0893
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/DragonUI_NewEra.toc
@@ -0,0 +1,289 @@
+## Interface: 30300
+## Title: |cff1784d1DragonUI|r |cffffffffNew Era|r
+## Notes: Dragonflight-style panels for 3.3.5a — downport of NewEra (Classic 1.15) onto DragonUI.
+## Author: DragonUI_NewEra team
+## Version: 0.1.0
+## DefaultState: Enabled
+## Dependencies: DragonUI, !!!ClassicAPI
+## OptionalDeps: DragonUI_Options
+## SavedVariables: DragonUI_NewEraDB
+## X-MoonWell-System: 1
+## X-Upstream-Commit: 37a29d640f97545ff1bb7d4b0ba7a5329cce1b8c
+
+bootstrap.lua
+
+# --- libs: embedded so features don't depend on other addons being loaded ---
+# LibCustomGlow provides the WeakAuras-style "Action Button Glow" (ants) used for the spellbook's
+# active-ability indicator. Embedded (not relying on WeakAuras); LibStub dedupes if another copy loads.
+Libs\LibStub\LibStub.lua
+Libs\LibCustomGlow-1.0\LibCustomGlow-1.0.xml
+
+# --- compat: thin bridge over the REQUIRED !!!ClassicAPI (owner: API Bridge Engineer) ---
+# !!!ClassicAPI is a hard dependency and loads first (via its !!! prefix + Dependencies), so it
+# owns the modern globals (C_Timer/C_Texture/C_Container/C_Item/C_Spell/C_SpellBook/C_Map/Mixin/…).
+# These files now only fill the handful of symbols ClassicAPI does NOT provide; C_Timer/C_Texture
+# are fully ClassicAPI-owned and no longer shipped here. See compat/COVERAGE.md.
+compat\Compat.lua
+# Events.lua adds Frame:RegisterUnitEvent (a 5.0 API; ClassicAPI implements the semantics but only
+# inside its private namespace) plus NE.EV_LEARNED_SPELL. Must follow Compat.lua for NE.compat.
+compat\Events.lua
+compat\C_Container.lua
+compat\C_Item.lua
+compat\C_Spell.lua
+compat\C_Map.lua
+compat\C_EquipmentSet.lua
+compat\SpellBook.lua
+
+compat\Locale.lua
+Locales\Load_Locales.xml
+
+# --- core: downported NewEra Core chrome toolkit (owner: Core/Chrome Engineer) ---
+core\FrameUtil.lua
+core\Scale.lua
+core\Texture.lua
+core\NineSlice.lua
+core\NineSliceLayouts.lua
+core\PanelChrome.lua
+core\Portrait.lua
+core\Tabs.lua
+# Menu = a MenuUtil-shaped context-menu builder over UIDropDownMenu. Resolves its backend lazily,
+# so it does not care where ClassicAPI sits in the load order.
+core\Menu.lua
+core\ButtonSkin.lua
+core\ScrollbarReskin.lua
+core\ItemButton.lua
+core\ItemGrid.lua
+core\Modules.lua
+# --- core: HUD-layer primitives (added for the Cooldown Manager port; reusable) ---
+# GridLayout = Lua reimplementation of retail's GridLayoutFrame template (absent on 3.3.5a).
+# CooldownNumbers = countdown text on Cooldown frames (the widget draws none on this client).
+# AuraSnapshot = one UnitBuff/UnitDebuff walk per unit per frame, shared addon-wide.
+# SpellRanks = per-ability rank ids from the live spellbook. Load-bearing: GetSpellInfo's 7th
+# return is castTime here, NOT the spellID it is on modern clients.
+core\GridLayout.lua
+core\CooldownNumbers.lua
+core\AuraSnapshot.lua
+core\SpellRanks.lua
+
+# --- textures: art + atlas registration data (owner: Asset Pipeline Engineer) ---
+Textures\Assets.lua
+
+# --- integration: DragonUI handshake/options/module bridge (owner: Integration Liaison) ---
+integration\Register.lua
+integration\Options.lua
+
+# --- qa: in-game harness + manifest (owner: QA/Harness Engineer) ---
+qa\Harness.lua
+
+# === Sprint 1: Character panel (downport of NewEra/CharacterPanel) ===
+# Wave 1 — assets + shell/nav. Wave 2 files inserted at their load positions when dispatched.
+modules\character\Assets.lua
+modules\character\CharacterPanel.lua
+modules\character\InsetFrames.lua
+modules\character\ModelArea.lua
+modules\character\ModelControls.lua
+modules\character\SlotFrames.lua
+modules\character\SlotQuality.lua
+modules\character\InnerBorder.lua
+modules\character\LevelText.lua
+modules\character\PaperDoll.lua
+modules\character\Sidebar.lua
+modules\character\SidebarTabs.lua
+modules\character\TabButtons.lua
+modules\character\EquipmentSets.lua
+modules\character\EquipmentManagerPane.lua
+modules\character\EquipmentFlyout.lua
+modules\character\TitlesPane.lua
+modules\character\Reputation.lua
+modules\character\Skills.lua
+modules\character\Honor.lua
+modules\character\Currency.lua
+modules\character\PetPaperDoll.lua
+modules\character\CloseButton.lua
+modules\character\SettingsCog.lua
+modules\character\EditModeRegister.lua
+
+# --- Collections (standalone retail-style Mounts & Pet Journal window) ---
+# Replaces the old Character-panel Pet-tab "Collection" view (modules/character/Companions.lua,
+# retired). Load order: Assets (art paths) -> Data (ported source-info DB) -> Window (shell + bottom
+# tabs + insets, registers the module) -> Journal (the shared Mounts/Pets pane factory, provides
+# C.BuildJournals which Window calls at login) -> Open (SHIFT-P binding + default) -> MicroButton
+# (standalone micromenu button, same shift-cluster technique as encounterjournal/MicroButton.lua).
+# Driven by the raw 3.3.5a Companions API, self-contained (no runtime dependency on the
+# EZCollections addon).
+modules\collections\Assets.lua
+modules\collections\Data.lua
+modules\collections\Window.lua
+modules\collections\Journal.lua
+modules\collections\Open.lua
+modules\collections\MicroButton.lua
+
+# --- Spellbook (standalone War-Within-style book; learned spells) ---
+modules\spellbook\Assets.lua
+modules\spellbook\Window.lua
+modules\spellbook\Spellbook.lua
+
+# --- Talents (standalone retail-style 3-tree panel over WotLK grid talents) ---
+# Load order: Assets (atlas rects) -> NodeData (shape layer) -> Talents (scaffold/chrome/pools)
+# -> Behavior (live data + preview/commit). RENDER-BEFORE-WIRE.
+modules\talents\Assets.lua
+modules\talents\NodeData.lua
+modules\talents\Talents.lua
+modules\talents\SpecTabs.lua
+modules\talents\Behavior.lua
+modules\talents\Loadouts.lua
+modules\talents\LoadoutsUI.lua
+modules\talents\Glyphs.lua
+
+# --- Bags (DF metal chrome, smart sort, rarity/usable cues) — WORK IN PROGRESS ---
+# CombinedBag.lua = the all-in-one grid window (default; can be turned off in the NewEra options).
+# Bags.lua = per-window restyle of the stock frames (superseded by the combined window by default).
+# Both build on core/ItemGrid + the chrome toolkit.
+modules\bags\BagSkin.lua
+modules\bags\Bags.lua
+modules\bags\CombinedBag.lua
+
+# --- Professions (retail-style crafting window; downport of NewEra/Professions/Crafting) ---
+# Load order: Assets (atlas + texcoord registration) -> Window (chrome + event wiring) ->
+# RecipeList (left panel, FauxScrollFrame) -> Crafting (schematic form, rank bar, controls).
+modules\professions\Assets.lua
+modules\professions\Window.lua
+modules\professions\RecipeList.lua
+modules\professions\Crafting.lua
+
+# --- Auction House (visual shell + optional Auctionator embedded tabs) ---
+modules\auctionhouse\Assets.lua
+modules\auctionhouse\Browse.lua
+modules\auctionhouse\Sell.lua
+modules\auctionhouse\Auctions.lua
+modules\auctionhouse\TabBridge.lua
+modules\auctionhouse\Window.lua
+modules\auctionhouse\AuctionatorEmbed.lua
+
+# --- Guild (modern Communities-style guild window; downport of NewEra/Guild) ---
+# Load order: Assets (BLPs + atlas rects) -> Window (shell + mode switching + the Roster-only left
+# GuildColumn) -> data layers (Roster/Info/Chat) -> Tabard (the GuildColumn's crest badge) -> Open
+# (route ToggleGuildFrame / native GuildFrame to NE_GuildFrame).
+modules\guild\Assets.lua
+modules\guild\Window.lua
+modules\guild\Roster.lua
+modules\guild\Info.lua
+modules\guild\Chat.lua
+modules\guild\Tabard.lua
+modules\guild\Open.lua
+
+# --- Social (modern friends window; downport of NewEra/Social) ---
+# Load order: Assets (portrait BLP) -> Window (shell + tabs + ToggleFriendsFrame redirect) -> the
+# per-tab list files. The Guild tab opens the Guild window above. Chat (channels) + Raid have no
+# NewEra counterpart to port — they're built natively on the classic channel/raid APIs.
+modules\social\Assets.lua
+modules\social\Window.lua
+modules\social\Friends.lua
+modules\social\Who.lua
+modules\social\Channels.lua
+modules\social\Raid.lua
+
+# --- LFG (unified retail-style Group Finder over the 3.3.5a Dungeon Finder + Raid Browser) ---
+# Load order: Assets (BLPs + atlas rects) -> Window (shell + bluemenu rail + Dungeons/Raids
+# category buttons + animated eye) -> Roles (shared role-row factory) -> Dungeons (LFD queue
+# pane) -> Raids (LFR queue/browse pane) -> Open (route ToggleLFDParentFrame /
+# ToggleLFRParentFrame to NE_GroupFinderFrame). The native hidden LFD/LFR frames stay loaded as
+# the state machine; these panes are a modern view over the same shared FrameXML lists.
+# NOTE: loads after guild (Textures/Guild ships the bluemenu rail sheets we reuse).
+modules\lfg\Assets.lua
+modules\lfg\Window.lua
+modules\lfg\Roles.lua
+modules\lfg\Dungeons.lua
+modules\lfg\Raids.lua
+modules\lfg\Open.lua
+
+# MoonWell keeps its server-backed Encounter Journal. NewEra's implementation
+# is intentionally not loaded because both addons own ToggleEncounterJournal.
+
+# --- Cooldown Manager (downport of NewEra/CooldownViewer) — PHASE 1: Essential + Utility ---
+# Retail's Cooldown Manager runs on C_CooldownViewer, which exists on no Classic-family client;
+# NewEra rebuilt it as retail's visual model over curated per-class data, and that is what ports.
+# Load order: ClassData (pure tables) -> CooldownViewer (core: settings store, spell-list
+# resolution) -> ItemMixins (the per-tile mixin) -> Viewers (frames built in Lua, replacing
+# CooldownViewer.xml — MaskTexture and GridLayoutFrame are both unavailable here) -> Register
+# (movers + options tab, replacing EditModeRegister.lua). See modules/cooldownviewer/PORT_PLAN.md.
+modules\cooldownviewer\ClassData.lua
+# WotLK seed: additive appends onto the vanilla lists above, plus the whole of Death Knight. Every
+# id is generated from this client's own Spell.dbc + SkillLineAbility.dbc, never hand-typed.
+# Must load AFTER ClassData (it appends to those tables) and BEFORE the viewer core reads them.
+modules\cooldownviewer\CdmSeedWotLK.lua
+# Phase 7d: the per-class catalog of trackable SELF-BUFFS — what the Tracked Buffs/Bars picker
+# OFFERS, standing in for retail's curated per-spec aura set. Also generated from this client's own
+# data (tools/cdm-spellgen/gen_auracatalog.py), and read by CooldownViewer's catalog accessor.
+modules\cooldownviewer\CdmAuraCatalog.lua
+modules\cooldownviewer\CooldownViewer.lua
+# Phase 8a: the VIEWERS' art (the /cdm window's own is SettingsAssets.lua, far below). Must load
+# before ItemMixins/AuraItemMixins, whose OnLoad resolves these atlases as each tile is built.
+modules\cooldownviewer\Assets.lua
+# On-use trinket discovery + the per-token assignment registry. Overrides CooldownViewer.lua's
+# GetEquipItemsForCategory fallback, so it must load after it and before Viewers reads it.
+modules\cooldownviewer\Equip.lua
+modules\cooldownviewer\ItemMixins.lua
+modules\cooldownviewer\Viewers.lua
+# Phase 3, the aura-driven viewers. AuraItemMixins needs M.CropIcon/M.TooltipSetSpell from
+# ItemMixins; BuffViewers needs M.BaseViewerMixin from Viewers — so both load after those two.
+modules\cooldownviewer\AuraItemMixins.lua
+modules\cooldownviewer\BuffViewers.lua
+# Phase 4a: per-cooldown alerts and ready sounds. The ENGINE and its stores only — assignment UI
+# belongs to the Phase 4b settings panel. AlertData is generated
+# (tools/cdm-spellgen/gen_alertdata.py) and must precede Alerts, which reads it.
+modules\cooldownviewer\AlertData.lua
+modules\cooldownviewer\SoundAlertData.lua
+modules\cooldownviewer\Alerts.lua
+# Phase 4b-1: the standalone /cdm settings window (shell). SettingsAssets registers the side-tab
+# art and must load before the panel builds tabs from it. Both need core/Tabs.lua and
+# core/PanelChrome.lua, which load far earlier.
+modules\cooldownviewer\SettingsAssets.lua
+modules\cooldownviewer\SettingsPanel.lua
+# 4b-2: the category model and the grids that render it. CdmArsenal is generated
+# (tools/cdm-spellgen/gen_arsenal.py) and feeds the adapter's Hidden catalog, so it loads first;
+# SettingsCategories replaces the panel's RefreshLayout stub, so it loads after the panel.
+modules\cooldownviewer\CdmArsenal.lua
+modules\cooldownviewer\SettingsAdapter.lua
+modules\cooldownviewer\SettingsCategories.lua
+# 4b-3: the right-click item menu and the settings cog — the assignment surface that finally makes
+# the Phase 4a alert and ready-sound stores reachable. Needs core/Menu.lua and CDS.adapter.
+modules\cooldownviewer\SettingsMenu.lua
+# 4b-4: drag to reorder and reassign. Needs the adapter's ReorderTo/AssignAt and the tiles built by
+# SettingsCategories, which supplies the OnDragStart that calls in here.
+modules\cooldownviewer\SettingsReorder.lua
+# 4b-5: named layouts, import/export, and the snapshot/restore pair that finally makes the panel's
+# Revert button real. Needs core/Menu.lua for the footer menu and CDS.RefreshLayout from
+# SettingsCategories, so it loads last of the settings files.
+modules\cooldownviewer\SettingsPresets.lua
+# 4c: the Settings tab. SettingsControls is the widget kit (checkbox / slider / menu-button over the
+# client's own templates, since DragonUI's AceGUI option controls only render inside an AceGUI
+# container); SettingsOptions is the page it builds. The page reads M.VIEWER_SPECS, which Register.lua
+# defines below — that is fine, it is read when the tab is first opened, not at load.
+modules\cooldownviewer\SettingsControls.lua
+modules\cooldownviewer\SettingsOptions.lua
+# Each viewer's own settings as a dialog beside the frame in edit mode, retail's shape.
+# Needs the SettingsControls kit above and NE.RegisterHUDFrame's `editorSettings` seam; Register.lua
+# below hands it one opener per viewer at PLAYER_LOGIN.
+modules\cooldownviewer\EditorPanel.lua
+modules\cooldownviewer\Register.lua
+
+# --- Level Up Display (retail's level-up banner, learned from the live server) ---
+# Rebuild rather than a port. Both prior versions ship a hand-typed spellList[CLASS][level] table
+# that is only correct for the server it was written against; this addon runs on many. Harvest.lua
+# reads the same facts off the trainer window (GetTrainerServiceInfo / ...LevelReq / ...Icon),
+# battleground brackets (GetBattlegroundInfo) and the client's dungeon lists (GetLFGDungeonInfo)
+# instead, so a custom server's own abilities and levels are picked up without any baked data.
+# Load order: Assets (art + entry-type presets) -> Data (the small Blizzlike fallback for what no
+# API reports) -> Harvest (writes the store) -> Unlocks (merges observed over fallback; reads both
+# of the above) -> Banner + SideDisplay (the two views, both call Unlocks) -> Register (events,
+# options, mover, /nelevelup).
+modules\levelup\Assets.lua
+modules\levelup\Data.lua
+modules\levelup\Harvest.lua
+modules\levelup\Unlocks.lua
+modules\levelup\Banner.lua
+modules\levelup\SideDisplay.lua
+modules\levelup\Register.lua
+
+# Sprint-0 Hello demo is intentionally not loaded in the MoonWell system build.
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/IconAlert.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/IconAlert.blp
new file mode 100644
index 0000000..54fdca0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/IconAlert.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/IconAlertAnts.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/IconAlertAnts.blp
new file mode 100644
index 0000000..2c90a38
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/IconAlertAnts.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/LibCustomGlow-1.0.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/LibCustomGlow-1.0.lua
new file mode 100644
index 0000000..38a1ad8
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/LibCustomGlow-1.0.lua
@@ -0,0 +1,1037 @@
+--[[
+This library contains work of Hendrick "nevcairiel" Leppkes
+https://www.wowace.com/projects/libbuttonglow-1-0
+]]
+
+-- luacheck: globals CreateFromMixins ObjectPoolMixin CreateTexturePool CreateFramePool
+
+local MAJOR_VERSION = "LibCustomGlow-1.0"
+local MINOR_VERSION = 21
+if not LibStub then error(MAJOR_VERSION .. " requires LibStub.") end
+local lib, oldversion = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION)
+if not lib then return end
+
+local pairs, ipairs = pairs, ipairs
+local ceil, floor, min, mod = math.ceil, math.floor, math.min, mod
+local tinsert, tremove = table.insert, table.remove
+
+-- ===============================================================================
+-- !!! IMPORTANT: Requires Pools.lua to be loaded before this file !!!
+-- ===============================================================================
+
+-- ===============================================================================
+-- !!! IMPORTANT: CHANGE `texturePath` BELOW TO POINT TO YOUR ADDON'S FILE LOCATION !!!
+-- DON'T FORGET THE BACKSLASH AT THE END (\)!
+-- Example: local texturePath = [[Interface\AddOns\YourAddon\Libs\]]
+-- ===============================================================================
+-- This is the path to the texture files used by this library.
+local texturePath = [[Interface\AddOns\DragonUI_NewEra\Libs\]]
+
+local textureList = {
+ ["empty"] = texturePath .. [[LibCustomGlow-1.0\AM_29]],
+ ["white"] = [[Interface\BUTTONS\WHITE8X8]],
+ ["shine"] = texturePath .. [[LibCustomGlow-1.0\Artifacts]]
+}
+
+function lib.RegisterTextures(texture, id)
+ textureList[id] = texture
+end
+
+lib.glowList = {}
+lib.startList = {}
+lib.stopList = {}
+
+local GlowParent = UIParent
+
+local TexPoolResetter = function(pool, tex)
+ tex:Hide()
+ tex:ClearAllPoints()
+end
+local GlowTexPool = CreateTexturePool(GlowParent, "ARTWORK", nil, nil, TexPoolResetter)
+lib.GlowTexPool = GlowTexPool
+
+local FramePoolResetter = function(framePool, frame)
+ frame:SetScript("OnSizeChanged", nil)
+ frame:SetScript("OnUpdate", nil)
+
+ local parent = frame:GetParent()
+ if parent[frame.name] then
+ parent[frame.name] = nil
+ end
+
+ if frame.textures then
+ for _, texture in ipairs(frame.textures) do
+ GlowTexPool:Release(texture)
+ end
+ end
+
+ frame.textures = {}
+ frame.info = {}
+ frame.name = nil
+ frame.timer = nil
+ frame:Hide()
+ frame:ClearAllPoints()
+end
+
+local GlowFramePool = CreateFramePool("Frame", GlowParent, "", FramePoolResetter)
+lib.GlowFramePool = GlowFramePool
+
+local function addFrameAndTex(r, color, name, key, N, xOffset, yOffset, texture, texCoord, desaturated, frameLevel)
+ key = key or ""
+ frameLevel = frameLevel or 8
+
+ if not r[name..key] then
+ r[name..key] = GlowFramePool:Acquire()
+ r[name..key]:SetParent(r)
+ r[name..key].name = name..key
+ end
+
+ local f = r[name..key]
+ f:SetFrameLevel(r:GetFrameLevel() + frameLevel)
+ f:SetPoint("TOPLEFT", r, "TOPLEFT", -xOffset, yOffset)
+ f:SetPoint("BOTTOMRIGHT", r, "BOTTOMRIGHT", xOffset, -yOffset)
+ f:Show()
+
+ if not f.textures then
+ f.textures = {}
+ end
+
+ for i = 1, N do
+ if not f.textures[i] then
+ f.textures[i] = GlowTexPool:Acquire()
+ f.textures[i]:SetTexture(texture)
+ f.textures[i]:SetTexCoord(texCoord[1], texCoord[2], texCoord[3], texCoord[4])
+ f.textures[i]:SetParent(f)
+ end
+
+ f.textures[i]:SetVertexColor(color[1], color[2], color[3], color[4])
+ f.textures[i]:Show()
+ end
+
+ while #f.textures > N do
+ GlowTexPool:Release(f.textures[#f.textures])
+ tremove(f.textures)
+ end
+end
+
+
+--Pixel Glow Functions--
+local pPoint = {
+ ["BOTTOMLEFT"] = "BOTTOMRIGHT",
+ ["BOTTOMRIGHT"] = "TOPRIGHT",
+ ["TOPRIGHT"] = "TOPLEFT",
+ ["TOPLEFT"] = "BOTTOMLEFT",
+}
+
+local function pWidth(position, width, length, thickness, line1, line2, point)
+ line1:ClearAllPoints()
+ line1:SetPoint(pPoint[point], point == "BOTTOMLEFT" and -position or position, 0)
+ position = width - position
+ if position > length then
+ line1:SetSize(length, thickness)
+ line2:Hide()
+ else
+ line2:ClearAllPoints()
+ line2:SetPoint(point)
+ line2:Show()
+
+ line1:SetSize(position, thickness)
+ line2:SetSize(thickness, length - position)
+ end
+end
+
+local function pHeight(position, height, length, thickness, line1, line2, point)
+ line1:ClearAllPoints()
+ line1:SetPoint(pPoint[point], 0, point == "BOTTOMRIGHT" and -position or position)
+ position = height - position
+ if position > length then
+ line1:SetSize(thickness, length)
+ line2:Hide()
+ else
+ line2:ClearAllPoints()
+ line2:SetPoint(point)
+ line2:Show()
+
+ line1:SetSize(thickness, position)
+ line2:SetSize(length - position, thickness)
+ end
+end
+
+--[[
+local function pSize(position, size, length, thickness, line1, line2, point, left)
+ line1:ClearAllPoints()
+ line1:SetPoint(pPoint[point], not left and (point == "BOTTOMLEFT" and -position or position) or 0, left and (point == "BOTTOMRIGHT" and -position or position) or 0)
+ position = size - position
+ if position > length then
+ line1:SetSize(left and thickness or length, left and length or thickness)
+ line2:Hide()
+ else
+ line2:ClearAllPoints()
+ line2:SetPoint(point)
+ line2:Show()
+
+ line1:SetSize(left and thickness or position, left and position or thickness)
+ line2:SetSize(left and (length - position) or thickness, not left and (length - position) or thickness)
+ end
+end
+]]
+
+local function pSizeChanged(self, width, height)
+ if not (width or height) then
+ width, height = self:GetSize()
+ end
+
+ if width ~= self.info.width or height ~= self.info.height then
+ self.info.width = width
+ self.info.height = height
+ self.info.perimeter = 2 * (width + height)
+ self.info.bottomlim = height * 2 + width
+ self.info.rightlim = height + width
+ self.info.space = self.info.perimeter / self.info.N
+ end
+end
+
+local function pUpdate(self, elapsed)
+ self.timer = self.timer + elapsed / self.info.period
+ if self.timer > 1 or self.timer < -1 then
+ self.timer = self.timer % 1
+ end
+
+ local info = self.info
+ for i = 1, info.N do
+ local position = (info.space * i + info.perimeter * self.timer) % info.perimeter
+ if position > info.bottomlim then -- BOTTOM
+ pWidth(position - info.bottomlim, info.width, info.length, info.th, self.textures[i], self.textures[info.N + i], "BOTTOMLEFT")
+ elseif position > info.rightlim then -- RIGHT
+ pHeight(position - info.rightlim, info.height, info.length, info.th, self.textures[i], self.textures[info.N + i], "BOTTOMRIGHT")
+ elseif position > info.height then -- TOP
+ pWidth(position - info.height, info.width, info.length, info.th, self.textures[i], self.textures[info.N + i], "TOPRIGHT")
+ else -- LEFT
+ pHeight(position, info.height, info.length, info.th, self.textures[i], self.textures[info.N + i], "TOPLEFT")
+ end
+ end
+end
+
+function lib.PixelGlow_Start(r, color, N, frequency, length, th, xOffset, yOffset, border, key, frameLevel)
+ if not r then return end
+ if not N or N <= 0 then N = 8 end
+
+ local width, height = r:GetSize()
+ length = length or floor((width + height) * (2 / N - 0.1))
+ length = min(length, min(width, height))
+ key = key or ""
+
+ addFrameAndTex(r, color or {.95, .95, .32, 1}, "_PixelGlow", key, N * 2, xOffset or 0, yOffset or 0, textureList.white, {0, 1, 0, 1}, nil, frameLevel)
+
+ local f = r["_PixelGlow"..key]
+
+ f.timer = f.timer or 0
+ f.info = f.info or {}
+ f.info.N = N
+ f.info.period = (not frequency or frequency == 0) and 4 or (1 / frequency)
+ f.info.th = th or 1
+
+ if f.info.length ~= length then
+ f.info.width = nil
+ f.info.length = length
+ end
+
+ pSizeChanged(f)
+ f:SetScript("OnSizeChanged", pSizeChanged)
+ pUpdate(f, 0)
+ f:SetScript("OnUpdate", pUpdate)
+end
+
+function lib.PixelGlow_Stop(r, key)
+ if not r then return end
+ key = key or ""
+ if not r["_PixelGlow"..key] then
+ return false
+ else
+ GlowFramePool:Release(r["_PixelGlow"..key])
+ end
+end
+
+tinsert(lib.glowList, "Pixel Glow")
+lib.startList["Pixel Glow"] = lib.PixelGlow_Start
+lib.stopList["Pixel Glow"] = lib.PixelGlow_Stop
+
+
+--Autocast Glow Funcitons--
+local acSizes = {7, 6, 5, 4}
+
+local function acSizeChanged(self, width, height)
+ if not (width or height) then
+ width, height = self:GetSize()
+ end
+
+ if width ~= self.info.width or height ~= self.info.height then
+ if width*height == 0 then return end
+ self.info.width = width
+ self.info.height = height
+ self.info.perimeter = 2 * (width + height)
+ self.info.bottomlim = height * 2 + width
+ self.info.rightlim = height + width
+ self.info.space = self.info.perimeter / self.info.N
+ end
+end
+
+local function acUpdate(self, elapsed)
+ local texIndex, info = 0, self.info
+ for k = 1, 4 do
+ self.timer[k] = self.timer[k] + elapsed / (info.period * k)
+ if self.timer[k] > 1 or self.timer[k] < -1 then
+ self.timer[k] = self.timer[k] % 1
+ end
+ for i = 1, info.N do
+ texIndex = texIndex + 1
+ local position = (info.space * i + info.perimeter * self.timer[k]) % info.perimeter
+ if position > info.bottomlim then
+ self.textures[texIndex]:SetPoint("CENTER", self, "BOTTOMRIGHT", -position + info.bottomlim, 0)
+ elseif position > info.rightlim then
+ self.textures[texIndex]:SetPoint("CENTER", self, "TOPRIGHT", 0, -position + info.rightlim)
+ elseif position > info.height then
+ self.textures[texIndex]:SetPoint("CENTER", self, "TOPLEFT", position - info.height, 0)
+ else
+ self.textures[texIndex]:SetPoint("CENTER", self, "BOTTOMLEFT", 0, position)
+ end
+ end
+ end
+end
+
+function lib.AutoCastGlow_Start(r, color, N, frequency, scale, xOffset, yOffset, key, frameLevel)
+ if not r then return end
+ if not (N and N > 0) then N = 4 end
+
+ scale = scale or 1
+ xOffset = xOffset or 0
+ yOffset = yOffset or 0
+ key = key or ""
+
+ addFrameAndTex(r, color or {.95, .95, .32, 1}, "_AutoCastGlow", key, N * 4, xOffset, yOffset, textureList.shine, {0.8115234375, 0.9169921875, 0.8798828125, 0.9853515625}, true, frameLevel)
+ local f = r["_AutoCastGlow"..key]
+ for k, size in pairs(acSizes) do
+ for i = 1, N do
+ f.textures[i + N * (k - 1)]:SetSize(size * scale, size * scale)
+ end
+ end
+
+ f.timer = f.timer or {0, 0, 0, 0}
+ f.info = f.info or {}
+ f.info.N = N
+ f.info.period = (not frequency or frequency == 0) and 4 or (1 / frequency)
+
+ acSizeChanged(f)
+ f:SetScript("OnSizeChanged", acSizeChanged)
+ f:SetScript("OnUpdate", acUpdate)
+ acUpdate(f, 0)
+end
+
+function lib.AutoCastGlow_Stop(r, key)
+ if not r then return end
+
+ key = key or ""
+ if not r["_AutoCastGlow"..key] then
+ return false
+ else
+ GlowFramePool:Release(r["_AutoCastGlow"..key])
+ end
+end
+
+tinsert(lib.glowList, "Autocast Shine")
+lib.startList["Autocast Shine"] = lib.AutoCastGlow_Start
+lib.stopList["Autocast Shine"] = lib.AutoCastGlow_Stop
+
+
+-- Animation Functions
+local function InitAlphaAnimation(self)
+ self.target = self.target or self:GetRegionParent()
+ self.change = self.change or 0
+
+ self.frameAlpha = self.target:GetAlpha()
+ self.alphaFactor = self.frameAlpha + self.change - self.frameAlpha
+end
+
+local function TidyAlphaAnimation(self)
+ self.alphaFactor = nil
+ self.frameAlpha = nil
+end
+
+local function AlphaAnimation_OnUpdate(self, elapsed)
+ local progress = self:GetSmoothProgress()
+ if progress ~= 0 then
+ if not self.played then
+ InitAlphaAnimation(self)
+ self.played = 1
+ end
+
+ if self.frameAlpha then
+ self.target:SetAlpha(self.frameAlpha + self.alphaFactor * progress)
+
+ if progress == 1 then
+ TidyAlphaAnimation(self)
+ end
+ end
+ end
+end
+
+local function AlphaAnimation_OnStop(self)
+ if self.frameAlpha then
+ TidyAlphaAnimation(self)
+ end
+
+ self.played = nil
+end
+
+local function CreateAlphaAnim(group, target, order, duration, change, delay, onPlay, onFinished)
+ local alpha = group:CreateAnimation()
+
+ if target then
+ alpha.target = alpha:GetRegionParent()[target]
+ end
+
+ if order then
+ alpha:SetOrder(order)
+ end
+
+ alpha:SetDuration(duration)
+ alpha.change = change
+
+ if delay then
+ alpha:SetStartDelay(delay)
+ end
+
+ if onPlay then
+ alpha:SetScript("OnPlay", onPlay)
+ end
+
+ alpha:SetScript("OnUpdate", AlphaAnimation_OnUpdate)
+ alpha:SetScript("OnStop", AlphaAnimation_OnStop)
+ alpha:SetScript("OnFinished", onFinished or AlphaAnimation_OnStop)
+end
+
+local function InitScaleAnimation(self)
+ self.target = self.target or self:GetRegionParent()
+ self.scaleX = self.scaleX or 0
+ self.scaleY = self.scaleY or 0
+
+ local _, _, width, height = self.target:GetRect()
+ if not width then return end
+
+ self.frameWidth = width
+ self.frameHeight = height
+
+ self.widthFactor = width * self.scaleX - width
+ self.heightFactor = height * self.scaleY - height
+
+ local setCenter
+ local parent = self.target:GetParent()
+ local numPoints = self.target:GetNumPoints()
+
+ if numPoints > 0 then
+ local point, relativeTo, relativePoint, xOffset, yOffset = self.target:GetPoint(1)
+
+ if numPoints == 1 and point == "CENTER" then
+ setCenter = false
+ else
+ local i = 1
+ while true do
+ if relativeTo ~= parent and yOffset then
+ local j = #self + 1
+ self[j], self[j + 1], self[j + 2], self[j + 3], self[j + 4] = point, relativeTo, relativePoint, xOffset, yOffset
+ end
+
+ i = i + 1
+ if numPoints >= i then
+ point, relativeTo, relativePoint, xOffset, yOffset = self.target:GetPoint(i)
+ else
+ break
+ end
+ end
+
+ setCenter = true
+ end
+ else
+ setCenter = true
+ end
+
+ if setCenter then
+ local x, y = self.target:GetCenter()
+ local parentX, parentY = parent:GetCenter()
+
+ self.target:ClearAllPoints()
+ self.target:SetPoint("CENTER", x - parentX, y - parentY)
+ end
+
+ return 1
+end
+
+local function TidyScaleAnimation(self)
+ local target = self.target
+
+ if #self ~= 0 then
+ target:ClearAllPoints()
+
+ for i = 1, #self, 5 do
+ target:SetPoint(self[i], self[i + 1], self[i + 2], self[i + 3], self[i + 4])
+ self[i] = nil
+ self[i + 1] = nil
+ self[i + 2] = nil
+ self[i + 3] = nil
+ self[i + 4] = nil
+ end
+ end
+
+ self.widthFactor = nil
+ self.heightFactor = nil
+
+ self.frameWidth = nil
+ self.frameHeight = nil
+end
+
+local function ScaleAnimation_OnUpdate(self, elapsed)
+ local progress = self:GetSmoothProgress()
+ if progress ~= 0 then
+ if not self.played then
+ if InitScaleAnimation(self) then
+ self.played = 1
+ end
+ end
+
+ if self.frameWidth then
+ self.target:SetSize(self.frameWidth + self.widthFactor * progress, self.frameHeight + self.heightFactor * progress)
+
+ if progress == 1 then
+ TidyScaleAnimation(self)
+ end
+ end
+ end
+end
+
+local function ScaleAnimation_OnStop(self)
+ if self.frameWidth then
+ TidyScaleAnimation(self)
+ end
+
+ self.played = nil
+end
+
+local function CreateScaleAnim(group, target, order, duration, x, y, delay, smoothing, onPlay)
+ local scale = group:CreateAnimation()
+
+ if target then
+ scale.target = scale:GetRegionParent()[target]
+ end
+
+ scale:SetOrder(order)
+ scale:SetDuration(duration)
+ scale.scaleX, scale.scaleY = x, y
+
+ if delay then
+ scale:SetStartDelay(delay)
+ end
+
+ if smoothing then
+ scale:SetSmoothing(smoothing)
+ end
+
+ if onPlay then
+ scale:SetScript("OnPlay", onPlay)
+ end
+
+ scale:SetScript("OnUpdate", ScaleAnimation_OnUpdate)
+ scale:SetScript("OnStop", ScaleAnimation_OnStop)
+ scale:SetScript("OnFinished", ScaleAnimation_OnStop)
+end
+
+local function AnimateTexCoords(texture, textureWidth, textureHeight, frameWidth, frameHeight, numFrames, elapsed, throttle)
+ if not texture.frame then
+ texture.frame = 1
+ texture.throttle = throttle
+ texture.numColumns = floor(textureWidth / frameWidth)
+ texture.numRows = floor(textureHeight / frameHeight)
+ texture.columnWidth = frameWidth / textureWidth
+ texture.rowHeight = frameHeight / textureHeight
+ end
+
+ if not texture.throttle or texture.throttle > throttle then
+ local frame = texture.frame
+ local framesToAdvance = floor(texture.throttle / throttle)
+ while frame + framesToAdvance > numFrames do
+ frame = frame - numFrames
+ end
+
+ frame = frame + framesToAdvance
+ texture.throttle = 0
+
+ local left = mod(frame - 1, texture.numColumns) * texture.columnWidth
+ local right = left + texture.columnWidth
+ local bottom = ceil(frame / texture.numColumns) * texture.rowHeight
+ local top = bottom - texture.rowHeight
+ texture:SetTexCoord(left, right, top, bottom)
+
+ texture.frame = frame
+ else
+ texture.throttle = texture.throttle + elapsed
+ end
+end
+
+--Action Button Glow--
+local function ButtonGlowResetter(framePool,frame)
+-- frame:SetScript("OnUpdate",nil)
+ local parent = frame:GetParent()
+ if parent._ButtonGlow then
+ parent._ButtonGlow = nil
+ end
+ frame:Hide()
+ frame:ClearAllPoints()
+end
+local ButtonGlowPool = CreateFramePool("Frame", GlowParent, "", ButtonGlowResetter)
+lib.ButtonGlowPool = ButtonGlowPool
+
+local function AnimIn_OnPlay(anim)
+ local frame = anim:GetRegionParent()
+ local frameWidth, frameHeight = frame:GetSize()
+ frame.spark:SetSize(frameWidth, frameHeight)
+ frame.spark:SetAlpha(not (frame.color) and 1 or 0.3 * frame.color[4])
+ frame.innerGlow:SetSize(frameWidth / 2, frameHeight / 2)
+ frame.innerGlow:SetAlpha(not (frame.color) and 1 or frame.color[4])
+ frame.innerGlowOver:SetAlpha(not (frame.color) and 1 or frame.color[4])
+ frame.outerGlow:SetSize(frameWidth * 2, frameHeight * 2)
+ frame.outerGlow:SetAlpha(not (frame.color) and 1 or frame.color[4])
+ frame.outerGlowOver:SetAlpha(not (frame.color) and 1 or frame.color[4])
+ frame.ants:SetSize(frameWidth * 0.85, frameHeight * 0.85)
+ frame.ants:SetAlpha(0)
+ frame:Show()
+end
+
+local function AnimIn_OnFinished(group)
+ local frame = group:GetParent()
+ local frameWidth, frameHeight = frame:GetSize()
+ frame.spark:SetAlpha(0)
+ frame.innerGlow:SetAlpha(0)
+ frame.innerGlow:SetSize(frameWidth, frameHeight)
+ frame.innerGlowOver:SetAlpha(0)
+ frame.outerGlow:SetSize(frameWidth, frameHeight)
+ frame.outerGlowOver:SetAlpha(0)
+ frame.outerGlowOver:SetSize(frameWidth, frameHeight)
+ frame.ants:SetAlpha(not (frame.color) and 1 or frame.color[4])
+end
+
+local function AnimIn_OnStop(group)
+ local frame = group:GetParent()
+ frame.spark:SetAlpha(0)
+ frame.innerGlow:SetAlpha(0)
+ frame.innerGlowOver:SetAlpha(0)
+ frame.outerGlowOver:SetAlpha(0)
+end
+
+local function bgHide(self)
+ if self.animOut:IsPlaying() then
+ self.animOut:Stop()
+ ButtonGlowPool:Release(self)
+ end
+end
+
+local function bgUpdate(self, elapsed)
+ AnimateTexCoords(self.ants, 256, 256, 48, 48, 22, elapsed, self.throttle)
+end
+
+local function IsAnimPlaying(self)
+ return self.isPlaying
+end
+
+local function configureButtonGlow(f, alpha)
+ f.spark = f:CreateTexture(nil, "BACKGROUND")
+ f.spark:SetPoint("CENTER")
+ f.spark:SetAlpha(0)
+ f.spark:SetTexture(texturePath .. [[LibCustomGlow-1.0\IconAlert]])
+ f.spark:SetTexCoord(0.00781250, 0.61718750, 0.00390625, 0.26953125)
+
+ -- inner glow
+ f.innerGlow = f:CreateTexture()
+ f.innerGlow:SetPoint("CENTER")
+ f.innerGlow:SetAlpha(0)
+ f.innerGlow:SetTexture(texturePath .. [[LibCustomGlow-1.0\IconAlert]])
+ f.innerGlow:SetTexCoord(0.00781250, 0.50781250, 0.27734375, 0.52734375)
+ f.innerGlow:Show()
+
+ -- inner glow over
+ f.innerGlowOver = f:CreateTexture()
+ f.innerGlowOver:SetPoint("TOPLEFT", f.innerGlow, "TOPLEFT")
+ f.innerGlowOver:SetPoint("BOTTOMRIGHT", f.innerGlow, "BOTTOMRIGHT")
+ f.innerGlowOver:SetAlpha(0)
+ f.innerGlowOver:SetTexture(texturePath .. [[LibCustomGlow-1.0\IconAlert]])
+ f.innerGlowOver:SetTexCoord(0.00781250, 0.50781250, 0.53515625, 0.78515625)
+
+ -- outer glow
+ f.outerGlow = f:CreateTexture()
+ f.outerGlow:SetPoint("CENTER")
+ f.outerGlow:SetAlpha(0)
+ f.outerGlow:SetTexture(texturePath .. [[LibCustomGlow-1.0\IconAlert]])
+ f.outerGlow:SetTexCoord(0.00781250, 0.50781250, 0.27734375, 0.52734375)
+
+ -- outer glow over
+ f.outerGlowOver = f:CreateTexture()
+ f.outerGlowOver:SetPoint("TOPLEFT", f.outerGlow, "TOPLEFT")
+ f.outerGlowOver:SetPoint("BOTTOMRIGHT", f.outerGlow, "BOTTOMRIGHT")
+ f.outerGlowOver:SetAlpha(0)
+ f.outerGlowOver:SetTexture(texturePath .. [[LibCustomGlow-1.0\IconAlert]])
+ f.outerGlowOver:SetTexCoord(0.00781250, 0.50781250, 0.53515625, 0.78515625)
+
+ -- ants
+ f.ants = f:CreateTexture(nil, "OVERLAY")
+ f.ants:SetPoint("CENTER")
+ f.ants:SetAlpha(0)
+ f.ants:SetTexture(texturePath .. [[LibCustomGlow-1.0\IconAlertAnts]])
+
+ f.animIn = f:CreateAnimationGroup()
+ f.animIn.appear = {}
+ f.animIn.fade = {}
+ CreateScaleAnim(f.animIn, "spark", 1, 0.2, 1.5, 1.5, nil, nil, AnimIn_OnPlay)
+ CreateAlphaAnim(f.animIn, "spark", 1, 0.2, alpha, nil, nil, nil, true)
+ CreateScaleAnim(f.animIn, "innerGlow", 1, 0.3, 2, 2)
+ CreateScaleAnim(f.animIn, "innerGlowOver", 1, 0.3, 2, 2)
+ CreateAlphaAnim(f.animIn, "innerGlowOver", 1, 0.3, alpha, nil, nil, nil, false)
+ CreateScaleAnim(f.animIn, "outerGlow", 1, 0.3, 0.5, 0.5)
+ CreateScaleAnim(f.animIn, "outerGlowOver", 1, 0.3, 0.5, 0.5)
+ CreateAlphaAnim(f.animIn, "outerGlowOver", 1, 0.3, alpha, nil, nil, nil, false)
+ CreateScaleAnim(f.animIn, "spark", 1, 0.2, 0.666666, 0.666666, 0.2)
+ CreateAlphaAnim(f.animIn, "spark", 1, 0.2, alpha, 0.2, nil, nil, false)
+ CreateAlphaAnim(f.animIn, "innerGlow", 1, 0.2, alpha, 0.3, nil, nil, false)
+ CreateAlphaAnim(f.animIn, "ants", 1, 0.2, alpha, 0.3, nil, nil, true)
+ f.animIn:SetScript("OnStop", AnimIn_OnStop)
+ f.animIn:SetScript("OnFinished", AnimIn_OnFinished)
+
+ f.animOut = f:CreateAnimationGroup()
+ f.animOut.appear = {}
+ f.animOut.fade = {}
+ CreateAlphaAnim(f.animOut, "outerGlowOver", 1, 0.2, alpha, nil, nil, nil, true)
+ CreateAlphaAnim(f.animOut, "ants", 1, 0.2, alpha, nil, nil, nil, false)
+ CreateAlphaAnim(f.animOut, "outerGlowOver", 2, 0.2, alpha, nil, nil, nil, false)
+ CreateAlphaAnim(f.animOut, "outerGlow", 2, 0.2, alpha, nil, nil, nil, false)
+ f.animOut:SetScript("OnPlay", function(self) self.isPlaying = true; end)
+ f.animOut:SetScript("OnStop", function(self) self.isPlaying = false; end)
+ f.animOut:SetScript("OnFinished", function(self) self.isPlaying = false; ButtonGlowPool:Release(f) end)
+
+ f.animOut.isPlaying = false
+ f.animOut.IsPlaying = IsAnimPlaying
+
+ f:SetScript("OnHide", bgHide)
+end
+
+local function updateAlphaAnim(f,alpha)
+ for _,anim in pairs(f.animIn.appear) do
+ anim.change = alpha
+ end
+ for _,anim in pairs(f.animIn.fade) do
+ anim.change = -alpha
+ end
+ for _,anim in pairs(f.animOut.appear) do
+ anim.change = alpha
+ end
+ for _,anim in pairs(f.animOut.fade) do
+ anim.change = -alpha
+ end
+end
+
+local ButtonGlowTextures = {["spark"] = true, ["innerGlow"] = true, ["innerGlowOver"] = true, ["outerGlow"] = true, ["outerGlowOver"] = true, ["ants"] = true}
+
+local function noZero(num)
+ if num == 0 then
+ return 0.001
+ else
+ return num
+ end
+end
+
+function lib.ButtonGlow_Start(r, color, frequency, frameLevel)
+ if not r then return end
+ frameLevel = frameLevel or 8;
+
+ local throttle = frequency and frequency > 0 and 0.25 / frequency * 0.01 or 0.01
+
+ if r._ButtonGlow then
+ local f = r._ButtonGlow
+ local width, height = r:GetSize()
+ f:SetFrameLevel(r:GetFrameLevel() + frameLevel)
+ f:SetSize(width * 1.4 , height * 1.4)
+ f:SetPoint("TOPLEFT", r, "TOPLEFT", -width * 0.2, height * 0.2)
+ f:SetPoint("BOTTOMRIGHT", r, "BOTTOMRIGHT", width * 0.2, -height * 0.2)
+ f.ants:SetSize(width * 1.4 * 0.85, height * 1.4 * 0.85)
+ AnimIn_OnFinished(f.animIn)
+ if f.animOut:IsPlaying() then
+ f.animOut:Stop()
+ f.animIn:Play()
+ end
+
+ if not color then
+ for texture in pairs(ButtonGlowTextures) do
+ f[texture]:SetVertexColor(1, 1, 1)
+ local alpha = math.min(f[texture]:GetAlpha()/noZero(f.color and f.color[4] or 1), 1)
+ f[texture]:SetAlpha(alpha)
+ updateAlphaAnim(f, 1)
+ end
+ f.color = false
+ else
+ for texture in pairs(ButtonGlowTextures) do
+ f[texture]:SetVertexColor(color[1], color[2], color[3])
+ local alpha = math.min(f[texture]:GetAlpha()/noZero(f.color and f.color[4] or 1)*color[4], 1)
+ f[texture]:SetAlpha(alpha)
+ updateAlphaAnim(f,color and color[4] or 1)
+ end
+ f.color = color
+ end
+ f.throttle = throttle
+ else
+ local f, new = ButtonGlowPool:Acquire()
+ if new then
+ configureButtonGlow(f,color and color[4] or 1)
+ else
+ updateAlphaAnim(f,color and color[4] or 1)
+ end
+ r._ButtonGlow = f
+ local width, height = r:GetSize()
+ f:SetParent(r)
+ f:SetFrameLevel(r:GetFrameLevel() + frameLevel)
+ f:SetSize(width * 1.4, height * 1.4)
+ f:SetPoint("TOPLEFT", r, "TOPLEFT", -width * 0.2, height * 0.2)
+ f:SetPoint("BOTTOMRIGHT", r, "BOTTOMRIGHT", width * 0.2, -height * 0.2)
+ if not color then
+ f.color = false
+ for texture in pairs(ButtonGlowTextures) do
+ f[texture]:SetVertexColor(1, 1, 1)
+ end
+ else
+ f.color = color
+ for texture in pairs(ButtonGlowTextures) do
+ f[texture]:SetVertexColor(color[1], color[2], color[3])
+ end
+ end
+ f.throttle = throttle
+ f:SetScript("OnUpdate", bgUpdate)
+
+ f.animIn:Play()
+ end
+end
+
+function lib.ButtonGlow_Stop(r)
+ if r._ButtonGlow then
+ if r._ButtonGlow.animOut:IsPlaying() then
+ -- Do nothing the animOut finishing will release
+ elseif r._ButtonGlow.animIn:IsPlaying() then
+ r._ButtonGlow.animIn:Stop()
+ ButtonGlowPool:Release(r._ButtonGlow)
+ elseif r:IsVisible() then
+ r._ButtonGlow.animOut:Play()
+ else
+ ButtonGlowPool:Release(r._ButtonGlow)
+ end
+ end
+end
+
+tinsert(lib.glowList, "Action Button Glow")
+lib.startList["Action Button Glow"] = lib.ButtonGlow_Start
+lib.stopList["Action Button Glow"] = lib.ButtonGlow_Stop
+
+
+-- ProcGlow
+
+local BaseTexCoord = {
+ ["Loop"] = {0.412598, 0.575195, 0.000976562, 0.391602},
+ ["Start"] = {0.000488281, 0.411621, 0.000976562, 0.987305},
+}
+
+local function SetTile(texture, frame, rows, columns, frameScaleW, frameScaleH, key)
+ frame = frame - 1
+ local row = math.floor(frame / columns)
+ local column = frame % columns
+
+ local leftStart, rightEnd, topStart, bottomEnd = BaseTexCoord[key][1], BaseTexCoord[key][2], BaseTexCoord[key][3], BaseTexCoord[key][4]
+
+ local fullWidth = rightEnd - leftStart
+ local fullHeight = bottomEnd - topStart
+
+ local baseDeltaX = fullWidth / columns
+ local baseDeltaY = fullHeight / rows
+
+ local deltaX = baseDeltaX * frameScaleW
+ local deltaY = baseDeltaY * frameScaleH
+
+ local left = leftStart + baseDeltaX * column + (baseDeltaX - deltaX) / 2
+ local right = left + deltaX
+
+ local top = topStart + baseDeltaY * row + (baseDeltaY - deltaY) / 2
+ local bottom = top + deltaY
+
+ pcall(function()
+ texture:SetTexCoord(left, right, top, bottom)
+ end)
+end
+
+local StartFlipbook
+local FlipbookAnimation_OnUpdate
+
+FlipbookAnimation_OnUpdate = function(self, elapsed)
+ local data = self.flipbookData
+ if not data then return end
+
+ if data.animElapsed then
+ data.animElapsed = data.animElapsed + elapsed
+ if data.animElapsed >= 0.7 then
+ if self:IsShown() then
+ StartFlipbook(self, self.ProcLoop, 6, 5, 30, ((data.animOptions and (30 / data.animOptions)) or 30), nil, nil, "Loop")
+ end
+ data.animElapsed = nil
+ data.animOptions = nil
+ end
+ end
+ data.elapsedTime = data.elapsedTime + elapsed
+ local frameDuration = 1 / data.frameRate
+
+ if data.elapsedTime >= frameDuration then
+ data.elapsedTime = data.elapsedTime - frameDuration
+ data.currentFrame = data.currentFrame + 1
+ if data.currentFrame > data.totalFrames then
+ data.currentFrame = 1
+ end
+ SetTile(data.texture, data.currentFrame, data.rows, data.columns, 1, 1, data.key)
+ end
+end
+
+local function StopFlipbook(f)
+ f:SetScript("OnUpdate", nil)
+ if f.flipbookData and f.flipbookData.texture then
+ f.flipbookData.texture:Hide()
+ end
+ f.flipbookData = nil
+end
+
+StartFlipbook = function(f, texture, rows, columns, totalFrames, frameRate, startAnim, startOptionsDur, key)
+ StopFlipbook(f)
+ f.flipbookData = {
+ key = key,
+ texture = texture,
+ rows = rows,
+ columns = columns,
+ totalFrames = totalFrames,
+ frameRate = frameRate,
+ currentFrame = 1,
+ elapsedTime = 0,
+ animElapsed = startAnim,
+ animOptions = startOptionsDur,
+ }
+ texture:Show()
+ f:SetScript("OnUpdate", FlipbookAnimation_OnUpdate)
+end
+
+local function ProcGlowResetter(framePool, frame)
+ frame:Hide()
+ frame:ClearAllPoints()
+ frame:SetScript("OnShow", nil)
+ frame:SetScript("OnHide", nil)
+ frame:SetScript("OnUpdate", nil)
+ local parent = frame:GetParent()
+ if frame.name and parent[frame.name] then
+ parent[frame.name] = nil
+ end
+end
+
+local ProcGlowPool = CreateFramePool("Frame", GlowParent, "", ProcGlowResetter)
+lib.ProcGlowPool = ProcGlowPool
+
+local function InitProcGlow(f)
+ -- Start-Flipbook
+ f.ProcStart = f:CreateTexture(nil, "ARTWORK")
+ f.ProcStart:SetBlendMode("ADD")
+ f.ProcStart:SetTexture(texturePath .. [[LibCustomGlow-1.0\UIActionBarFX]])
+ f.ProcStart:SetTexCoord(0.0827148248, 0.1649413686, 0.000976562, 0.165364635) -- First Frame
+ f.ProcStart:SetAlpha(1)
+ f.ProcStart:SetSize(150, 150)
+ f.ProcStart:SetPoint("CENTER")
+ f.ProcStart:Hide()
+
+ -- Loop-Flipbook
+ f.ProcLoop = f:CreateTexture(nil, "ARTWORK")
+ f.ProcLoop:SetTexture(texturePath .. [[LibCustomGlow-1.0\UIActionBarFX]])
+ f.ProcLoop:SetTexCoord(0.412598, 0.4451174, 0.000976562, 0.066080801666667) -- First Frame
+ f.ProcLoop:SetAlpha(1)
+ f.ProcLoop:SetAllPoints()
+ f.ProcLoop:Hide()
+end
+
+local function SetupProcGlow(f, options)
+ f.name = "_ProcGlow" .. options.key
+
+ f:SetScript("OnHide", function(self)
+ StopFlipbook(self)
+ end)
+
+ f:SetScript("OnShow", function(self)
+ StopFlipbook(self)
+ if self.startAnim then
+ local width, height = self:GetSize()
+ self.ProcStart:SetSize((width / 42 * 150) / 1.4, (height / 42 * 150) / 1.4)
+ StartFlipbook(self, self.ProcStart, 6, 5, 30, 30, 0, options.duration, "Start")
+ else
+ StartFlipbook(self, self.ProcLoop , 6, 5, 30, (30 / options.duration), nil, nil, "Loop")
+ end
+ end)
+
+ local color = options.color or {1, 1, 1, 1}
+ f.ProcStart:SetVertexColor(unpack(color))
+ f.ProcLoop:SetVertexColor(unpack(color))
+
+ f.startAnim = options.startAnim
+end
+
+local ProcGlowDefaults = {
+ frameLevel = 8,
+ color = nil,
+ startAnim = true,
+ xOffset = 0,
+ yOffset = 0,
+ duration = 1,
+ key = ""
+}
+
+function lib.ProcGlow_Start(r, options)
+ if not r then return end
+ options = options or {}
+ setmetatable(options, { __index = ProcGlowDefaults })
+ local key = "_ProcGlow" .. options.key
+ local f, new
+ if r[key] then
+ f = r[key]
+ else
+ f, new = ProcGlowPool:Acquire()
+ if new then
+ InitProcGlow(f)
+ end
+ r[key] = f
+ end
+
+ f:SetParent(r)
+ f:SetFrameLevel(r:GetFrameLevel() + options.frameLevel)
+
+ local width, height = r:GetSize()
+ local xOffset = options.xOffset + width * 0.2
+ local yOffset = options.yOffset + height * 0.2
+ f:SetPoint("TOPLEFT", r, "TOPLEFT", -xOffset, yOffset)
+ f:SetPoint("BOTTOMRIGHT", r, "BOTTOMRIGHT", xOffset, -yOffset)
+
+ SetupProcGlow(f, options)
+ f:Show()
+end
+
+function lib.ProcGlow_Stop(r, key)
+ key = key or ""
+ local f = r["_ProcGlow" .. key]
+ if f then
+ ProcGlowPool:Release(f)
+ end
+end
+
+table.insert(lib.glowList, "Proc Glow")
+lib.startList["Proc Glow"] = lib.ProcGlow_Start
+lib.stopList["Proc Glow"] = lib.ProcGlow_Stop
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/LibCustomGlow-1.0.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/LibCustomGlow-1.0.xml
new file mode 100644
index 0000000..c93a784
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/LibCustomGlow-1.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/UIActionBarFX.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/UIActionBarFX.blp
new file mode 100644
index 0000000..428d430
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/UIActionBarFX.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/am_29.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/am_29.blp
new file mode 100644
index 0000000..cf409fa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/am_29.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/artifacts.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/artifacts.blp
new file mode 100644
index 0000000..55b6dcf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibCustomGlow-1.0/artifacts.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibStub/LibStub.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibStub/LibStub.lua
new file mode 100644
index 0000000..65a29f2
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Libs/LibStub/LibStub.lua
@@ -0,0 +1,30 @@
+-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/addons/libstub/ for more info
+-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
+local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
+local LibStub = _G[LIBSTUB_MAJOR]
+
+if not LibStub or LibStub.minor < LIBSTUB_MINOR then
+ LibStub = LibStub or {libs = {}, minors = {} }
+ _G[LIBSTUB_MAJOR] = LibStub
+ LibStub.minor = LIBSTUB_MINOR
+
+ function LibStub:NewLibrary(major, minor)
+ assert(type(major) == "string", "Bad argument #1 to `NewLibrary' (string expected)")
+ minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
+
+ local oldminor = self.minors[major]
+ if oldminor and oldminor >= minor then return nil end
+ self.minors[major], self.libs[major] = minor, self.libs[major] or {}
+ return self.libs[major], oldminor
+ end
+
+ function LibStub:GetLibrary(major, silent)
+ if not self.libs[major] and not silent then
+ error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
+ end
+ return self.libs[major], self.minors[major]
+ end
+
+ function LibStub:IterateLibraries() return pairs(self.libs) end
+ setmetatable(LibStub, { __call = LibStub.GetLibrary })
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/Load_Locales.xml b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/Load_Locales.xml
new file mode 100644
index 0000000..ada712b
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/Load_Locales.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/deDE.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/deDE.lua
new file mode 100644
index 0000000..23fdcf1
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/deDE.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "deDE")
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/enUS.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/enUS.lua
new file mode 100644
index 0000000..bf1b5cc
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/enUS.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "enUS", true, true)
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/esES.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/esES.lua
new file mode 100644
index 0000000..2ac20fb
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/esES.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "esES")
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/esMX.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/esMX.lua
new file mode 100644
index 0000000..9bcd128
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/esMX.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "esMX")
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/frFR.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/frFR.lua
new file mode 100644
index 0000000..949c1eb
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/frFR.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "frFR")
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/koKR.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/koKR.lua
new file mode 100644
index 0000000..b75186f
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/koKR.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "koKR")
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/ptBR.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/ptBR.lua
new file mode 100644
index 0000000..ed2ea3c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/ptBR.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "ptBR")
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/ruRU.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/ruRU.lua
new file mode 100644
index 0000000..a5f85bc
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/ruRU.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - Russian Locale (ruRU)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "ruRU")
+if not L then return end
+
+-- ============================================================================
+-- ПРОФЕССИИ
+-- ============================================================================
+
+L["Alchemy"] = "Алхимия"
+L["Blacksmithing"] = "Кузнечное дело"
+L["Enchanting"] = "Наложение чар"
+L["Engineering"] = "Инженерное дело"
+L["Herbalism"] = "Травничество"
+L["Leatherworking"] = "Кожевничество"
+L["Mining"] = "Горное дело"
+L["Smelting"] = "Выплавка металла"
+L["Skinning"] = "Снятие шкур"
+L["Tailoring"] = "Портняжное дело"
+L["Inscription"] = "Начертание"
+L["Jewelcrafting"] = "Ювелирное дело"
+L["Prospecting"] = "Просеивание"
+L["Cooking"] = "Кулинария"
+L["First Aid"] = "Первая помощь"
+L["Fishing"] = "Рыбная ловля"
+L["Select a recipe to craft"] = "Выберите рецепт для создания"
+L["Hide item tooltips in list"] = "Скрывать подсказки в списке"
+L["Colour names by skill difficulty"] = "Окрашивать названия по сложности"
+L["Plain skill bar (no animation)"] = "Обычная полоса навыка (без анимации)"
+L["Create"] = "Создать"
+L["Create All"] = "Создать все"
+L["Show Learned"] = "Показывать изученные"
+L["Has Skill Up"] = "Повышают навык"
+L["Have Materials"] = "Есть реагенты"
+
+-- ============================================================================
+-- ПЕРСОНАЖ, ЗВАНИЯ, УПРАВЛЕНИЕ ЭКИПИРОВКОЙ
+-- ============================================================================
+
+L["Attributes"] = "Основные"
+L["Spell"] = "Магия"
+L["Resistances"] = "Сопротивление"
+L["Maximum Health. If your health reaches zero, you will die."] = "Максимальный запас здоровья. Если ваше здоровье упадет до нуля, вы погибнете."
+L["Your maximum %s."] = "Максимальное значение данного ресурса."
+L["Increases attack power for some classes and the damage you block with a shield."] = "Увеличивает силу атаки некоторых классов и показатель блока щитом."
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = "Увеличивает шанс критического удара, уклонение, броню и (для некоторых классов) силу атаки."
+L["Increases your maximum health."] = "Увеличивает максимальный запас здоровья."
+L["Increases your maximum mana and your spell critical strike chance."] = "Увеличивает максимальный запас маны и шанс критического удара заклинаниями."
+L["Increases your health and mana regeneration while out of combat."] = "Увеличивает скорость восполнения здоровья и маны вне боя."
+L["Seconds between melee swings."] = "Задержка между ударами ближнего боя (сек.)"
+L["Improves your chance to hit by %.2f%%."] = "Увеличивает вероятность попадания по цели на %.2f%%."
+L["Chance of melee attacks dealing extra damage."] = "Вероятность нанесения дополнительного урона при атаках ближнего боя."
+L["Armor Penetration"] = "Рейт. пробивание брони"
+L["Reduces the target's armor by up to %.2f%%."] = "Снижает броню цели максимум на %.2f%%."
+L["Reduces the chance your attacks are dodged or parried."] = "Снижает вероятность уклонения или парирования вашей атаки."
+L["DPS"] = "Урон в сек."
+L["Hit Chance"] = "Рейт. меткости"
+L["Reduces your chance to miss."] = "Снижает вероятность промаха."
+L["Crit Chance"] = "Крит. Удар"
+L["Chance of ranged attacks dealing extra damage."] = "Вероятность нанесения дополнительного урона при атаках дальнего боя."
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = "Наименьший дополнительный урон среди всех школ магии. Наведите курсор на характеристику персонажа для подробностей."
+L["Bonus power applied to healing spells."] = "Дополнительный эффект, применяемый к исцеляющим заклинаниям."
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = "Вне применения: %d / 5 сек.\nПри применении: %d / 5cек."
+L["Chance of spells dealing extra damage."] = "Вероятность нанесения дополнительного урона заклинаниями."
+L["Haste Rating"] = "Рейт. скорости"
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = "Снижает вероятность попадания и критического удара по вам; повышает шанс блока, уклонения и парирования."
+L["Chance to dodge enemy melee attacks."] = "Вероятность уклониться от атак противника в ближнем бою."
+L["Chance to parry. Requires a weapon."] = "Вероятность парировать атаку противника. Требуется оружие."
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = "Вероятность заблокировать атаку щитом. Требуется щит.\nПоказатель блока: %d"
+L["Resilience"] = "Устойчивость"
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = "Снижает вероятность получения критического удара на %.2f%% и уменьшает получаемый критический урон."
+L["Reduces magic damage taken."] = "Снижает получаемый урон от соответствующей школы магии."
+L["Character"] = "Персонаж"
+L["Titles"] = "Звания"
+L["Equipment Manager"] = "Управление экипировкой"
+L["None"] = "Нет"
+L["New Equipment Set"] = "Новый комплект"
+L["Edit"] = "Редактировать"
+L["equipped"] = "надето"
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/zhCN.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/zhCN.lua
new file mode 100644
index 0000000..110c49c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/zhCN.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "zhCN")
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/zhTW.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/zhTW.lua
new file mode 100644
index 0000000..6e69b6f
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Locales/zhTW.lua
@@ -0,0 +1,83 @@
+--[[
+================================================================================
+DragonUI_NewEra - English Locale (Default)
+================================================================================
+]]
+
+local L = LibStub("AceLocale-3.0"):NewLocale("DragonUI_NewEra", "zhTW")
+if not L then return end
+
+-- ============================================================================
+-- PROFESSIONS
+-- ============================================================================
+
+L["Alchemy"] = true
+L["Blacksmithing"] = true
+L["Enchanting"] = true
+L["Engineering"] = true
+L["Herbalism"] = true
+L["Leatherworking"] = true
+L["Mining"] = true
+L["Smelting"] = true
+L["Skinning"] = true
+L["Tailoring"] = true
+L["Inscription"] = true
+L["Jewelcrafting"] = true
+L["Prospecting"] = true
+L["Cooking"] = true
+L["First Aid"] = true
+L["Fishing"] = true
+L["Select a recipe to craft"] = true
+L["Hide item tooltips in list"] = true
+L["Colour names by skill difficulty"] = true
+L["Plain skill bar (no animation)"] = true
+L["Create"] = true
+L["Create All"] = true
+L["Show Learned"] = true
+L["Has Skill Up"] = true
+L["Have Materials"] = true
+
+-- ============================================================================
+-- CHARACTER, TITLES, EQUIPMENT MANAGER
+-- ============================================================================
+
+L["Attributes"] = true
+L["Spell"] = true
+L["Resistances"] = true
+L["Maximum Health. If your health reaches zero, you will die."] = true
+L["Your maximum %s."] = true
+L["Increases attack power for some classes and the damage you block with a shield."] = true
+L["Increases critical strike chance, dodge chance, armor, and (for some classes) attack power."] = true
+L["Increases your maximum health."] = true
+L["Increases your maximum mana and your spell critical strike chance."] = true
+L["Increases your health and mana regeneration while out of combat."] = true
+L["Seconds between melee swings."] = true
+L["Improves your chance to hit by %.2f%%."] = true
+L["Chance of melee attacks dealing extra damage."] = true
+L["Armor Penetration"] = true
+L["Reduces the target's armor by up to %.2f%%."] = true
+L["Reduces the chance your attacks are dodged or parried."] = true
+L["DPS"] = true
+L["Hit Chance"] = true
+L["Reduces your chance to miss."] = true
+L["Crit Chance"] = true
+L["Chance of ranged attacks dealing extra damage."] = true
+L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."] = true
+L["Bonus power applied to healing spells."] = true
+L["Not casting: %d / 5s\nWhile casting: %d / 5s"] = true
+L["Chance of spells dealing extra damage."] = true
+L["Haste Rating"] = true
+L["Reduces chance to be hit and crit; raises block/dodge/parry."] = true
+L["Chance to dodge enemy melee attacks."] = true
+L["Chance to parry. Requires a weapon."] = true
+L["Chance to block. Requires a shield.\nBlock Value: %d"] = true
+L["Resilience"] = true
+L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."] = true
+L["Reduces magic damage taken."] = true
+L["Character"] = true
+L["Titles"] = true
+L["Equipment Manager"] = true
+L["None"] = true
+L["New Equipment Set"] = true
+L["Edit"] = true
+L["equipped"] = true
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/README.md b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/README.md
new file mode 100644
index 0000000..bf556b3
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/README.md
@@ -0,0 +1,229 @@
+# DragonUI_NewEra
+
+A support / extension module for **[DragonUI](https://github.com/NeticSoul/DragonUI)** — the World of Warcraft **3.3.5a (WotLK)** port of the Dragonflight UI.
+
+DragonUI ports the Dragonflight **HUD** to 3.3.5a. **DragonUI_NewEra** fills in the rest: it faithfully downports the panel work from **NewEra** (Ashgaroth's Classic Era 1.15 Dragonflight-style addon) to 3.3.5a, rebuilding the panels DragonUI hasn't ported yet so the whole interface matches the modern look — not just the action bars.
+
+> **Requires DragonUI and ClassicAPI.** This is an add-on *to* DragonUI, not a standalone UI — it reuses DragonUI's textures, atlases, and chrome where possible and only rebuilds what's missing. It also depends on **`!!!ClassicAPI`**, a compatibility layer that backports the modern API (`C_Timer`, `C_Texture`, `C_Container`, `C_Spell`, `C_Map`, `Mixin`, …) onto the 3.3.5a client; the panels are built against those modern globals. Both are hard dependencies (declared in the `.toc`) and must be installed and enabled.
+
+## What's inside
+
+### Character panel
+
+
+
+A full custom replacement for the 3.3.5a `CharacterFrame`, styled to match Dragonflight:
+
+- Paperdoll (3D model + all equipment slots) with modern model controls (zoom, click-drag rotate / pan)
+- **Stats sidebar** (General / Attributes / Melee / Ranged / Spell / Defense / Resistances) with proper tooltips - compatible with [EnhancedCharStats](https://github.com/thezephyrsong/EnhancedCharStats/)
+- Tabs for **Character, Pet, Skills, Honor, Reputation** (Mounts & Companions now live in the standalone Collections window — see below)
+- **Titles** picker — set your title from the panel; the window header shows `Name `
+- **Equipment Manager** — a fully client-side gear-set manager (works on any server, no reliance on the native equipment-manager API)
+
+### `/gearset` — equip a saved set by name
+
+Save gear sets in the Equipment Manager, then swap to one from chat or a macro:
+
+```
+/gearset "Tank Set" -- equip the set named "Tank Set" (case-insensitive)
+/dnequip "Healer Set" -- alias of /gearset
+```
+
+- Matches by name (quoted or bare); errors and lists your sets if no match.
+- If the set is already fully equipped, it does nothing (no redundant request to the server).
+- Sets are stored client-side in SavedVariables and applied with a physical item swap.
+
+*(Note: `/equip` and `/equipset` are reserved built-in WotLK macro commands, hence `/gearset` / `/dnequip`.)*
+
+### Spellbook
+
+
+
+A standalone War-Within-style two-page spellbook, replacing the 3.3.5a `SpellBookFrame`:
+
+- **Card layout** — every learned spell as a Dragonflight-style card (icon + name + rank), flowing across a two-page evergreen book; a min/max button collapses it to a single page (↗ Expand / ↙ Condense).
+- **Category tabs** — General, your class (sectioned by spec), and a live Pet tab, styled to match the Character panel.
+- **Active vs passive** — active spells use the gold spellbook frame; passives use the dark square talent-node socket. Passive cells are click-inert (hover for tooltip only); pet cells ignore right-click.
+- **Whole-cell interaction** — click anywhere on a cell to cast, drag to place it on a bar, hover anywhere for the tooltip.
+- **Search + options** — filter spells by name; a cog menu toggles *Hide Passives* and *Show All Ranks* (off = highest rank only).
+
+Built natively for 3.3.5a's index-based spellbook API (a compat shim maps the Cataclysm `GetSpellBookItem*` family onto it).
+
+### Collections
+
+
+
+A standalone retail-style Collections journal (**Mounts** and **Pet Journal** bottom tabs), replacing the old Mounts & Companions grid that used to sit inside the Character panel's Pet tab:
+
+- **List + preview layout** — a scrollable icon+name list on the left (gold selection highlight, favourite star), a rotatable 3D model on the right over the retail model backdrop, and an info panel with the mount/pet's name, source (*Vendor / Zone / Cost*) and lore.
+- **Search, filter & favourites** — filter by name, toggle favourites-only, and star companions to keep them at the top; favourites are per-character and carry over from the old view.
+- **Summon** — a *Mount / Summon* button plus a *Summon Random Favourite* button; right-click a row to summon or dismiss.
+- **Opens on `Shift-P`** by default (rebindable under *Key Bindings → DragonUI New Era*).
+
+Driven by the raw 3.3.5a Companions API; the source/lore text comes from a bundled database, so no other addon is required. The retail-look art and the mount/companion source-info database are ported from **EZCollections** (see [Credits](#credits)).
+
+### Talents
+
+
+
+A standalone War-Within-style talent window over WotLK's classic grid talents, opened with **`/netalents`** or the default talent key:
+
+- **Three trees** on the Dragonflight metal chrome, with real talent data, per-tree point readouts, and a live **preview → Apply / Reset** flow — left-click to spend, right-click to refund, nothing is permanent until you Apply (behind a confirm).
+- **Retail-style nodes** — square / circle / capstone art derived per talent, with a hover highlight, a gold flash when a rank lands, and a subtle random glint that wanders across the talents you've spent points in.
+- **Per-tier centering** — rows with fewer than four talents are packed and centred (the way retail lays them out), and the three trees are centred in the window.
+- **Spec-art backgrounds** — each class/spec paints its own artwork behind the trees.
+- **Animated connectors** — prerequisite links draw as a flowing dotted line straight from one talent to the talent that needs it.
+- **Multi-spec** — bottom tabs switch between up to 4 specs depending on server configurations; rename the specs from the cog (custom names persist per character). View your other spec read-only (dimmed) and hit **Activate** to switch to it.
+- **Pet talents** — hunters with a talented pet out get a **Pet** tab (Ferocity / Tenacity / Cunning) on the pet's own family artwork, with the same live preview → Apply / Reset flow, the pet's circular portrait, the correct 3-points-per-tier gating, and the single tree centred in the window.
+- Sound cues for spending, refunding, applying, and switching specs.
+
+Built natively for 3.3.5a's talent + preview-talent API (`GetTalentInfo`, `AddPreviewTalentPoints`, `LearnPreviewTalents`, dual `GetActiveTalentGroup`, and their `isPet` variants).
+
+### Glyphs
+
+
+
+The **Glyphs** tab shares the talent window — a hexagon of major/minor glyph sockets you socket, swap, and clear directly (the stock glyph frame is suppressed):
+
+- **Class artwork** — each class gets its own full-window Legion-artifact-style backdrop behind the sockets.
+- **One title, per-spec labels** — a single **GLYPHS** header; under dual spec each spec's name (**PRIMARY** / **SECONDARY**, or your custom rename, in caps) is lined up above its own socket cluster instead of repeating the title.
+- **Animated links** — the same flowing dotted connectors as the talent trees tie the sockets together.
+- ***Optional*** - see a list of the effects of each glyphs next to the glyphs
+
+### Professions
+
+
+
+A standalone Dragonflight-style profession window replacing the 3.3.5a `TradeSkillFrame`, with a modern recipe list, item/reagent details, a generic skill bar, and cog options — plus optional **Auctionator** integration via an AH scan button.
+
+### Auction House
+
+
+
+A standalone Dragonflight-style Auction House window replacing the 3.3.5a `AuctionFrame`:
+
+- **Buy** — search the market; results aggregate by item, drilling into a per-item detail page for bid/buyout.
+- **Sell** — drag an item into the sell slot, set quantity/price/duration against a live view of that item's current market listings, and post.
+- **Auctions** — Auctions/Bids sub-tabs with a retail-style summary of your listings grouped by item, plus the full owner/bidder list and Cancel Auction.
+- **Auctionator reskin** — when Auctionator is installed, its Buy/Sell/More panel is reparented straight into this window and fully restyled to match (dark fill, gold-trim insets, zebra-striped rows, reskinned scrollbars/tabs/dialogs) instead of popping open its own separate parchment-style frame. Its tabs sit alongside the shell's own Buy/Sell/Auctions tabs and drive the same window.
+
+### Bags — *work in progress*
+
+
+
+A retail-style **combined bag** plus a per-window restyle for the **individual** Blizzard bag frames, both sharing the same metal chrome, recessed slots, portrait treatment, and item cues as the rest of the addon. **This one is still being built and polished — expect rough edges.**
+
+- **Combined window** (default) — every backpack/bag slot in one movable Dragonflight-style grid, with a search box, a smart sort, and a bottom band showing your money + watched currencies. It takes over bag opening; a toggle in the NewEra options turns it off (→ stock Blizzard bags, needs `/reload`).
+- **Individual bags** — a lighter restyle that skins the stock per-window bags in place (metal frame, portrait, recessed slots, rarity/usable cues) for players who prefer separate windows. Superseded by the combined window by default.
+- **Smart sort** — consolidates partial stacks, routes specialty items into their bags (arrows → quiver, bullets → ammo pouch, herbs → herb bag, soul shards → soul bag, profession mats → their bags), then arranges by category → subtype → quality, with the Hearthstone pinned first and same-item stacks ordered fullest-first. The sort runs behind a "Sorting…" cover and keeps going until the layout settles rather than for a fixed number of passes.
+- **Separate specialty bags** *(optional toggle)* — split quivers, ammo pouches, soul bags, and profession bags out of the general grid into their own labeled sections below it (**QUIVER**, **MINING BAG**, **HERB BAG**, …), mirroring the keyring row.
+- **Keyring row** *(optional)* — show your keys as a **KEYS** row inside the window.
+- **At-a-glance cues** — item-rarity glow, a red tint on anything you can't use (missing weapon/armor proficiency, too low a level, or a recipe whose profession/skill you don't have), the merchant "sell" cursor, a quest-item glow, and optional auto-sell-junk at vendors.
+
+### Social & Guild
+
+
+
+Standalone Dragonflight-style windows replacing the 3.3.5a `GuildFrame` and `FriendsFrame`:
+
+**Guild** — a **Communities**-look window scoped to what 3.3.5a actually serves (no Benefits/Rewards, ClubFinder, or calendar — those are Cata+ systems):
+
+- **Roster** — sortable member list (Level / Class icon / Name / Zone / Rank / Note) with a guild-tabard badge, member detail, and the full permission-gated action set: public/officer notes, promote/demote/remove, and party invite.
+- **Guild Info** — the Message of the Day and Guild Information text wells, editable in place when you have the right, shown side by side with Guild Chat.
+- **Guild Chat, with history sync** — regular guild and officer chat rendered in the modern chrome, with **class-colored names** (live and backlog). Since 3.3.5a keeps no server-side chat log, a rolling per-guild history is saved locally and, on login, synced from other online guildmates over an addon message so a fresh `/reload` doesn't show an empty window — deduplicated and correctly ordered even across guildmates' differing system clocks. History is shared across all characters on your account in the same guild.
+
+**Social** — built on the classic friends/ignore/who/channels/raid APIs:
+
+- **Friends** — two-line entries (status icon + name/level/class over zone) with Friends and Ignore sub-tabs.
+- **Who** — the stock Name / Zone / Lvl / Class columns, with a switchable second column (zone/guild/race) and full filter support.
+- **Guild** — opens the standalone Guild window above rather than hosting a duplicate view inline.
+- **Chat** — the chat-channels tab: a grouped channel list (Group / World / Custom headers) on the left, a live roster for the selected channel on the right, and an Add-channel button.
+- **Raid** — a native raid roster grid with convert-to-raid and a full right-click context menu (promote, demote, assign main tank/assist, remove), permission-gated the same as stock.
+
+### Looking For Group
+
+
+
+A unified Dragonflight-style Group Finder window merging the 3.3.5a Dungeon Finder (LFD) and Raid Browser (LFR) into one panel, replacing the two separate stock windows:
+
+- **Left-hand category rail** — Dungeons / Raids, bluemenu chrome, and an animated searching-eye portrait (idle when closed, an eye-opening/searching flipbook while you're queued, role-checking, in a proposal, or listed).
+- **Dungeons** — the full LFD queue: Random/type dropdown, specific-dungeon list with lock and reward icons, the random-dungeon reward panel (money/XP/loot), role selection, and join/backfill.
+- **Raids** — both LFR modes on one pane: **Queue** (multi-select raid list + comment, list/unlist) and **Browse** (raid dropdown plus a live results list with whisper/invite).
+- **Role selection** — modern round tank/healer/dps/leader medallions sharing one row component between the Dungeons and Raids panes.
+- Drives the same native `LFDParentFrame` / `LFRParentFrame` queue state under the hood — this is a modern view over 3.3.5a's real Dungeon Finder and Raid Browser APIs, not a reimplementation, so it works with whatever LFD/LFR rules the server actually runs.
+
+### Adventure Guide (Encounter Journal)
+
+
+
+A standalone Dragonflight-style **Adventure Guide**, downported from NewEra's Encounter Journal — 3.3.5a never shipped one natively (the Encounter Journal is a Cataclysm+ feature), so this is entirely new UI over hand-seeded boss/loot data. Open it with **`/aguide`** (aliases `/adventureguide`, `/ej`), the micro button next to the main menu icons, or the `ToggleEncounterJournal()` global this addon creates for other addons to hook into:
+
+- **Classic, Burning Crusade, and Wrath of the Lich King** instances — dungeons and raids, browsable by expansion tier.
+- **Per-boss pages** — abilities and full loot table, tabbed like retail's journal.
+- **Loot browsing** — every boss's drops with icon, name, and armor/weapon type, split correctly across 10/25-man and Normal/Heroic where the source data distinguishes them.
+- **Search + breadcrumb navigation** — filter the instance grid by name, drill into an instance and back out via the nav bar.
+
+### Cooldown Manager
+
+
+
+Retail's **Cooldown Manager** (a *War Within* feature) downported from NewEra — four HUD viewers that track what's ready, what's running, and what's on you, plus the window that decides what goes in them. Open it with **`/cdm`**; the viewers themselves are placed from **`/dragonui edit`**.
+
+- **Four viewers** — **Essential** and **Utility** cooldown icon rows, a **Buff Icons** row, and **Tracked Bars** with names and countdowns. Each one is independently placed, sized, and toggled.
+- **Curated per class, then filtered by you** — spell lists come from the client's own DBC data (never hand-typed IDs), and grouped into *Essential* / *Utility* / *Not Displayed*. Drag a tile between sections to move it; search to find one.
+- **The picker is your character's arsenal, not your class's** — anything you haven't learned yet is hidden, talents you didn't take included, so *Not Displayed* means "you chose not to show this" rather than "you can't cast this". Newly trained abilities appear on their own. Turn on **Show Unlearned** in the cog to see the full class list anyway; unlearned entries there are tinted red.
+- **Tracked buffs** — a generated per-class aura catalog plus a live registry of every buff the scanner has actually seen on you, so procs that aren't in any list still turn up and can be tracked.
+- **Trinkets** — on-use trinkets are discovered from what you're wearing, no configuration.
+- **A spell's frame lights gold while its own buff is up**, so you can see at a glance which of your cooldowns is currently *doing* something rather than merely off cooldown.
+- **Alerts** — right-click any tile to pick a trigger (*Available*, *Refresh*, *Active*, *Usable*), an **FX Style**, a refresh window, and a **ready sound** from the client's full sound catalogue. A small badge marks every tile that has one configured.
+- **Edit mode** — `/dragonui edit` gives each viewer a mover and a settings dialog: orientation, icon limit and direction, icon size, padding, opacity, visibility, timers, and tooltips.
+- **Starter layouts per spec** — 30 curated starting points, one per talent tree. A new spec gets its own automatically the first time you play it; you can load any of them by hand from the Layout menu, and off-spec spells are switched *off* rather than removed, so anything you want back is one drag away in the picker. Your spec is read from talent points spent — with no points, or an exact tie, it asks rather than guessing.
+- **Layouts** — save and apply layouts, share them as import/export strings, drop back to a starter layout, and undo the last layout change with **Revert**. Layouts are stored **per spec**, so your dual-spec setups don't fight each other.
+
+Note: Module is off by default
+
+### Level Up Display
+
+
+
+Retail's **level-up banner**, which 3.3.5a never shipped — "You have reached level N", followed by what that level actually gave you. Preview it any time with **`/nelevelup `**; place it from **`/dragonui edit`**.
+
+The difference from every other version of this addon is where the data comes from. Others ship a hand-typed `spellList[CLASS][level]` table of spell IDs, which is only correct on the server it was written against — private servers move trainer levels, add abilities and remove others, and a baked table is then wrong *silently*, announcing spells that don't exist and missing the ones that do. **This one asks your server instead.**
+
+- **Abilities are read from your own class trainer.** Opening a trainer harvests every service it offers — name, rank, icon, and the level *your server* requires — including the ones you can't train yet, so a single visit at level 20 fills in the requirements up to 80. Nothing is keyed on a spell ID, so a server's custom abilities are picked up correctly without this addon knowing they could exist.
+- **Battlegrounds, dungeons and raids come from the client's own brackets**, already localized. A realm that opens Alterac Valley at 20 needs no configuration.
+- **Talent points** are derived from your actual point totals, so a server granting a different rate is followed rather than assumed.
+- **Auto-granted spells** are caught by a spellbook diff — abilities a server hands you on level-up appear at no trainer, and would otherwise be invisible.
+- **Everything is stored per realm**, so one installed copy serves several servers without their data bleeding into each other.
+- **Long lists go to a grid.** A single level can grant a dozen unlocks; rather than parade them one at a time for a minute, anything over four is shown at once in a panel.
+- **A small Blizzlike fallback** covers only what no API can report — glyph slots, dual spec, riding — and withdraws the moment the harvest observes the server's own answer.
+
+`/nelevelup coverage` reports what has been learned for the current realm and class; `/nelevelup harvest` re-reads an open trainer on demand.
+
+## Roadmap
+
+Faithfully downporting the remaining NewEra panels to 3.3.5a:
+
+- [x] ~~**Character panel**~~ — *done* (paperdoll, stats sidebar, Skills / Honor / Reputation / Pet tabs, Titles, Equipment Manager + `/gearset`)
+- [x] ~~**Spellbook**~~ — *done* (two-page book, category tabs, active/passive frames, search + Hide Passives / Show All Ranks, single/double-page toggle)
+- [x] ~~**Talents**~~ — *done* (3-tree panel, live preview/Apply/Reset, retail-style nodes, per-tier centering, spec-art backgrounds, animated connectors, dual-spec tabs with custom names, hunter **pet talents** tab, and a **glyphs** page with per-class artwork)
+- [x] ~~**Professions**~~ — *done* (Main profession Window, extra integration with Auctionator via AH Scan Button)
+- [x] ~~**Auction House**~~ — *done* (Buy/Sell/Auctions panel, plus a full Auctionator embed + reskin when it's installed)
+- [x] ~~**Guild**~~ — *done* (Roster with member actions, Guild Info, Guild Chat with class-colored names + cross-session/cross-character history sync)
+- [x] ~~**Social**~~ — *done* (Friends + Ignore, Who, Chat channels, Raid roster; Guild tab opens the standalone Guild window)
+- [x] ~~**Looking For Group**~~ — *done* (unified Dungeon Finder + Raid Browser window: Dungeons/Raids category rail, role selection, raid queue + browse)
+- [x] ~~**Adventure Guide (Encounter Journal)**~~ — *done* (Classic/TBC/Wrath instances, per-boss abilities + loot pages, search + breadcrumb nav)
+- [x] ~~**Collections**~~ — *done* (standalone Mounts + Pet Journal window with list/model/info layout, search, filter, favourites, summon)
+- [x] ~~**Cooldown Manager**~~ — *done* (Essential/Utility/Buff-icon/Tracked-bar viewers, DBC-sourced talent-gated spell lists, tracked buffs + trinkets, right-click alerts and ready sounds, edit-mode movers and per-viewer settings, per-spec layouts with import/export)
+- [x] ~~**Level Up Display**~~ — *done* (retail's level-up banner, with unlocks harvested from the live server's own trainer/battleground/dungeon data rather than a baked spell table, per-realm storage, grid overflow panel, `/nelevelup`)
+- [ ] **Bags** — *work in progress* (retail combined bag + individual-bag restyle: grid, smart sort, separated specialty-bag sections, keyring row, rarity/usable cues, money + currency band)
+- [ ] **Quest Log**
+- [ ] **Merchant**
+- [ ] **Mail**
+
+## Credits
+
+- **[DragonUI](https://github.com/NeticSoul/DragonUI)** by NeticSoul — the base 3.3.5a Dragonflight UI port this builds on.
+- **NewEra** by Ashgaroth — the Classic Era Dragonflight-style addon these panels are downported from.
+- **EZCollections** by ZEUStiger — Mount & companion source-info database (name / lore / source strings) are ported from EZCollections.
+- Dragonflight UI © Blizzard Entertainment.
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464792.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464792.ogg
new file mode 100644
index 0000000..3964ddb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464792.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464794.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464794.ogg
new file mode 100644
index 0000000..a939dab
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464794.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464798.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464798.ogg
new file mode 100644
index 0000000..cb86593
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464798.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464800.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464800.ogg
new file mode 100644
index 0000000..45a04a0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7464800.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466002.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466002.ogg
new file mode 100644
index 0000000..4012e25
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466002.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466004.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466004.ogg
new file mode 100644
index 0000000..8271ea8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466004.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466006.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466006.ogg
new file mode 100644
index 0000000..b8cc92e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466006.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466010.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466010.ogg
new file mode 100644
index 0000000..27d95d6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466010.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466012.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466012.ogg
new file mode 100644
index 0000000..071f416
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466012.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466014.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466014.ogg
new file mode 100644
index 0000000..84f2724
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466014.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466016.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466016.ogg
new file mode 100644
index 0000000..2d507e8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466016.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466018.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466018.ogg
new file mode 100644
index 0000000..089f436
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466018.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466020.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466020.ogg
new file mode 100644
index 0000000..b8ee4d1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466020.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466026.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466026.ogg
new file mode 100644
index 0000000..585bd98
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466026.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466036.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466036.ogg
new file mode 100644
index 0000000..f4355c9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466036.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466046.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466046.ogg
new file mode 100644
index 0000000..cc5fe2f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466046.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466048.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466048.ogg
new file mode 100644
index 0000000..8aa2251
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466048.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466050.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466050.ogg
new file mode 100644
index 0000000..ad21007
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466050.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466054.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466054.ogg
new file mode 100644
index 0000000..b3953d5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466054.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466062.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466062.ogg
new file mode 100644
index 0000000..0dc3947
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466062.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466070.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466070.ogg
new file mode 100644
index 0000000..29e4b57
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466070.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466072.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466072.ogg
new file mode 100644
index 0000000..ba05ce2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466072.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466082.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466082.ogg
new file mode 100644
index 0000000..02a8e77
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466082.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466092.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466092.ogg
new file mode 100644
index 0000000..58b5f2d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466092.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466096.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466096.ogg
new file mode 100644
index 0000000..fbcaeb6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466096.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466098.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466098.ogg
new file mode 100644
index 0000000..74d5aa9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466098.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466108.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466108.ogg
new file mode 100644
index 0000000..b1d0962
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466108.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466112.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466112.ogg
new file mode 100644
index 0000000..816097e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466112.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466116.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466116.ogg
new file mode 100644
index 0000000..fbac47b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466116.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466124.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466124.ogg
new file mode 100644
index 0000000..3c16bfc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466124.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466126.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466126.ogg
new file mode 100644
index 0000000..07e783a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466126.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466138.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466138.ogg
new file mode 100644
index 0000000..73df8c4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466138.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466148.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466148.ogg
new file mode 100644
index 0000000..da82140
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466148.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466899.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466899.ogg
new file mode 100644
index 0000000..b6a0575
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466899.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466901.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466901.ogg
new file mode 100644
index 0000000..b14cc14
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466901.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466903.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466903.ogg
new file mode 100644
index 0000000..98b2dae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466903.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466911.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466911.ogg
new file mode 100644
index 0000000..6512de8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466911.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466913.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466913.ogg
new file mode 100644
index 0000000..61e0731
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466913.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466915.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466915.ogg
new file mode 100644
index 0000000..8b6f280
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466915.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466945.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466945.ogg
new file mode 100644
index 0000000..6bb6740
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466945.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466947.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466947.ogg
new file mode 100644
index 0000000..cfbdc5c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466947.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466951.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466951.ogg
new file mode 100644
index 0000000..12d7e55
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466951.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466955.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466955.ogg
new file mode 100644
index 0000000..b0d47e1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466955.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466957.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466957.ogg
new file mode 100644
index 0000000..21b32d6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466957.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466959.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466959.ogg
new file mode 100644
index 0000000..058004a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466959.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466965.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466965.ogg
new file mode 100644
index 0000000..ffd6c54
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466965.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466967.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466967.ogg
new file mode 100644
index 0000000..857b93e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7466967.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467017.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467017.ogg
new file mode 100644
index 0000000..ee25d5e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467017.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467021.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467021.ogg
new file mode 100644
index 0000000..5cc2b97
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467021.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467023.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467023.ogg
new file mode 100644
index 0000000..d7bb59f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467023.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467025.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467025.ogg
new file mode 100644
index 0000000..f77a25e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467025.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467027.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467027.ogg
new file mode 100644
index 0000000..056930a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467027.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467029.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467029.ogg
new file mode 100644
index 0000000..6a79b4a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467029.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467031.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467031.ogg
new file mode 100644
index 0000000..e74b1f1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467031.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467033.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467033.ogg
new file mode 100644
index 0000000..39c3a11
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467033.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467072.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467072.ogg
new file mode 100644
index 0000000..93917d1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467072.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467074.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467074.ogg
new file mode 100644
index 0000000..4363ed3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467074.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467076.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467076.ogg
new file mode 100644
index 0000000..6141ea4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467076.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467080.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467080.ogg
new file mode 100644
index 0000000..c0159d2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467080.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467082.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467082.ogg
new file mode 100644
index 0000000..2131c19
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467082.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467084.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467084.ogg
new file mode 100644
index 0000000..d4ff9ee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467084.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467088.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467088.ogg
new file mode 100644
index 0000000..965e76c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467088.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467090.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467090.ogg
new file mode 100644
index 0000000..cfdc845
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467090.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467092.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467092.ogg
new file mode 100644
index 0000000..82c0c9f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467092.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467094.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467094.ogg
new file mode 100644
index 0000000..b270758
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467094.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467098.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467098.ogg
new file mode 100644
index 0000000..b06f5ac
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467098.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467100.ogg b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467100.ogg
new file mode 100644
index 0000000..24c9565
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Sounds/cdm/7467100.ogg differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/ASSETS.md b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/ASSETS.md
new file mode 100644
index 0000000..1387b77
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/ASSETS.md
@@ -0,0 +1,180 @@
+# DragonUI_NewEra — Textures / Art Pipeline (Sprint 0)
+
+Owner: Asset Pipeline Engineer (CONTRACTS.md §3).
+Source art: `/root/downport/DOWNPORT THIS/NewEra/Art/` (Classic 1.15 / retail-extracted BLPs).
+Dest: `/root/downport/DragonUI_NewEra/Textures/` — **the only path this agent writes to.**
+
+---
+
+## 1. Pipeline / architecture decision
+
+- Art lives under `Textures//...` mirroring NewEra's `Art/` layout
+ (Sprint-0 set is all in `Art/Common`, so it lands in `Textures/Common/`).
+- `Textures/Assets.lua` is a **pure fdid → file-path index** of `NE.tex.RegisterLocal(fdid, path)`
+ calls. Paths point at `Interface\AddOns\DragonUI_NewEra\Textures\...`.
+- **ARCHITECT DECISION (atlas mechanism):** atlas *coordinates* come from the Core NineSlice
+ coord tables (`core/NineSliceLayouts.lua` + the transcribed atlas-coord data), **not** from
+ `C_Texture.RegisterAtlas`. The 3.3.5a client has no native atlas DB and the ClassicAPI
+ `C_Texture` shim only answers existence/info queries — it does not feed slice geometry to the
+ render path (retail atlas nicknames would stretch as flat strips). So:
+ - geometry (atlas-name → fdid + texcoord rect, slice sizes) = **Core** coord tables;
+ - art location (fdid → BLP on disk) = **this file**.
+ This is recorded verbatim at the top of `Textures/Assets.lua`.
+- `NE.tex.RegisterLocal` / `NE.tex.Set` are provided by the Core agent's `core/Texture.lua`;
+ this agent only *calls* the API.
+
+---
+
+## 2. Art/ inventory (source, read-only)
+
+Total: **1238 files, ~131 MB**. Formats: **1236 BLP**, 1 PNG, 1 CSV (no TGA).
+46 top-level panel folders. Largest are per-panel art for LATER sprints.
+
+| Folder | Files | Size | Covers |
+|---|---|---|---|
+| Common | 119 | 21M | **shared chrome** (Core-owned): frame-metal, inset border, rock bg, tabs, slots, close btn, scrollbars, class/spec icons, etc. |
+| Professions | 37 | 44M | profession UI (later) |
+| Talents | 35 | 16M | talent tree art (later) |
+| EncounterJournal | 365 | 13M | dungeon journal (later) |
+| AchievementIcons | 337 | 1.5M | achievement icons |
+| LFG | 9 | 8.5M | LFG |
+| PvPMatch | 13 | 5.2M | PvP scoreboard |
+| PlayerFrame / UnitFrame / TargetFrame / PartyFrame / RaidFrame / NamePlates | — | — | HUD unit frames (base DragonUI mostly owns these) |
+| CharacterPanel | 3 | 590K | paperdoll class bgs (later, Sprint 1+) |
+| Spellbook | 6 | 1.1M | spellbook reskin (later) |
+| Merchant / Bank / TradeFrame / AuctionHouse / Mail* | — | — | vendor/bag panels (later) |
+| RaceBackground | 32 | 625K | dress-up race backdrops (later) |
+| ActionBar | 2 | 1.2M | action bar -2x sheets (HUD; base DragonUI) |
+| Calendar / Guild / Social / Honor / Achievements / TabardFrame / Tooltip / EditMode / BossMods / Minimap / ComboPoint / BuffFrame / CastingBar / CooldownViewerSettings / Campaign / Durability / FullAlert / DressUp / LootRoll / PetStable / QuestTracker / RaidManager / SpellActivationOverlay | — | — | misc panels/HUD (later or base-owned) |
+
+*(Full per-folder counts available via `find Art/ -type f`.)*
+
+---
+
+## 3. Copied this sprint (fdid → path map)
+
+All 7 Sprint-0 shared/Core sheets copied to `Textures/Common/`, registered in `Textures/Assets.lua`:
+
+| FileDataID | File (`Textures/Common/`) | Dimensions | Encoding | Bytes | Role |
+|---|---|---|---|---|---|
+| 2406979 | 2406979-uiframe-metal-corners.blp | 512×512 | BGRA (raw3) aDepth8 | 1,049,748 | UI-Frame-Metal corners (incl. PortraitMetal / Double) |
+| 2406984 | 2406984-uiframe-metal-edges-vert.blp | 512×32 | BGRA (raw3) aDepth8 | 66,708 | `!`-tile Left/Right metal edges |
+| 2406987 | 2406987-uiframe-metal-edges-horiz.blp | 64×256 | BGRA (raw3) aDepth8 | 66,708 | `_`-tile Top/Bottom metal edges |
+| 374155 | 374155-uibackground-rock.blp | 1024×1024 | DXT1 aDepth0 | 525,460 | UI-Background-Rock fill (ButtonFrameTemplate) |
+| 1723831 | 1723831-uiframe-inner.blp | 128×128 | DXT5 aDepth8 | 17,556 | InsetFrameTemplate inner corners (6×6) |
+| 1723832 | 1723832-uiframe-inner.blp | 64×256 | DXT5 aDepth8 | 17,556 | Inset Left/Right inner tiles (3×256) |
+| 1723833 | 1723833-uiframe-inner.blp | 256×128 | DXT5 aDepth8 | 33,940 | Inset Top/Bottom inner band |
+
+**Total copied: 1,777,685 bytes (~1.7 MB), 7 files.**
+
+> Filename note: source `Art/Common/` also contains a near-duplicate `1723831-uiframe.blp`
+> (17,556 bytes, identical size). The registered path used by NewEra's `Core/Assets.lua` is
+> `1723831-uiframe-inner.blp`, so that is the variant copied. The `-uiframe.blp` variant is
+> unregistered and was NOT copied.
+
+---
+
+## 4. Validation results
+
+Validated by reading BLP2 headers (Python struct, wowdev BLP2 layout). Result: **ALL PASS.**
+
+- **Magic:** all 7 are valid `BLP2`.
+- **Power-of-two:** all dimensions POT (512/1024/256/128/64/32). ✅
+- **Encoding:** valid — metal sheets = uncompressed BGRA8888 (raw3); rock = DXT1; inset = DXT5.
+ All three are 3.3.5a-loadable formats.
+- **Lua:** `Textures/Assets.lua` passes `luac5.1 -p` (Lua 5.1, 3.3.5 dialect) clean.
+
+Caveats (non-blocking, documented assumptions):
+- **No mipmaps** (`hasMips=0`) on all 7 sheets. UI textures don't require mips on 3.3.5a; they
+ render at native size via nineslice texcoords, so this is fine. If any sheet is ever down-scaled
+ far below native it may shimmer — not a concern for the fixed-size chrome here.
+- The two metal-edge sheets (2406984/2406987) are uncompressed BGRA (no DXT). Heavier on VRAM than
+ DXT but byte-faithful to the extracted retail art and within budget for chrome.
+- End-to-end render check (one atlas drawn pixel-correct via Core's `/dnehello`) is a **joint
+ Sprint-0 exit step with the Core agent** — pending their `_HelloDemo.lua`. Header/format/POT
+ validation done here is the static half.
+
+---
+
+## 5. MISSING / TODO for later panels
+
+Nothing is *missing* from the source `Art/` tree — all later-panel art is present and located.
+Future sprints grab from `/root/downport/DOWNPORT THIS/NewEra/Art/` and register in either a
+per-panel `Textures//Assets.lua` (panel-owned) or extend `Textures/Assets.lua` (shared Core).
+
+### CharacterPanel (Sprint 1+) — `Art/CharacterPanel/` and `Art/Common/`
+| FDID | Source path | Role |
+|---|---|---|
+| 1400895 | `Art/CharacterPanel/1400895-character-info-classes-a.blp` (1.0M) | class-themed paperdoll bg (Mage/Monk/Pal/Priest/Rogue/Sham/Lock/War + title/itemlevel bounce) |
+| 1400896 | `Art/CharacterPanel/1400896-character-info-classes-b.blp` (525K) | class paperdoll bg (DK/DH/Druid/Hunter) |
+| 5882640 | `Art/CharacterPanel/5882640-character-panel-background.blp` (525K) | overall character-pane bg |
+| 1662186 | `Art/Common/1662186-classicon.blp` (2.1M) | UI-Classes-Circles class-icon sheet (portrait swap) |
+
+> Other CharacterPanel sheets referenced by NewEra `CharacterPanel/Assets.lua` and living in
+> `Art/Common/`: 3534438/3487944 (model-control buttons+icons), 410247/410248/410249
+> (Char-Paperdoll Parts/Horiz/Vert), 136567 (reputation bar), 136565/131074 (rep detail bg +
+> divider), 4499236 (campaign header icon), 4571485 (options list-expand), 4331838/4332072/
+> 7367534/5142787/5142784 (minimal-scrollbar family), 461112/236179/461113/236264/236270/236286/
+> 237542/237581 (8 spec icons). Plus `Art/RaceBackground/` (32 dress-up race bg quarters, FDIDs
+> 131089-131112 / 455998-456009 — see NewEra CharacterPanel/Assets.lua loop).
+
+### Spellbook (later) — `Art/Spellbook/` (6 files, 1.1M)
+4200162 (skilllinetab), 5506565 (items), 5794906 (sheen-mask), 5834697 (backgrounds),
+5899876 (spellicon-mask), 5922242 (petautocast-mask).
+
+### Talents (later) — `Art/Talents/` (35 files, 16M)
+Tree backgrounds (4631299…4631395 family, ~18 sheets), 4556093 (talents sheet), masks
+(4633068 circle, 4731572/4731579 sheen, 7532041 apex, 4722778 anim-mask), dependency bars
+(1126606/1126607/1134738/1134739), apex bar 7532048, warmode art (450901/514317/921230/
+`warmode-flame-mask.blp`), anim clouds/particles (4723109/4732064).
+
+### Shared-Core candidates not yet needed in Sprint 0 — `Art/Common/`
+Tab sheet 4707839, inner top-streak 1723833 *(done)*, DiamondMetal dialog chrome
+(3056750/3056755/3058483), item-slot 4701874, close button 4698972, vendor slots
+(130766/130841), action-bar sheets 4613342/4615764 (`Art/ActionBar/`). Register into the
+shared `Textures/Assets.lua` when a Core consumer (Dialog layout, item grid, vendor) is downported.
+
+---
+
+## 5b. LFG (Group Finder) — added 2026-07-19
+
+Copied from `ReferenceAddons/NewEra/Art/LFG/` into `Textures/LFG/`, registered by
+`modules/lfg/Assets.lua` (per-panel registration, same pattern as Guild):
+
+| FileDataID | File (`Textures/LFG/`) | Dimensions | Encoding | Bytes | Role |
+|---|---|---|---|---|---|
+| 4616456 | 4616456-groupfinder-eye.blp | 2048×1024 | BGRA (raw3) aDepth8 | 8,389,780 | eye-frame portrait + searching/initial flipbook strips |
+| 5171843 | 5171843-roleicons.blp | 2048×2048 | BGRA (raw3) aDepth8 | 16,778,388 | modern round role medallions (+disabled) |
+| 985877 | 985877-groupfinder.blp | 2048×1024 | BGRA (raw3) aDepth8 | 8,389,780 | micro role/lock/leader/check icons + dark panel bg |
+
+All BLP2, POT, 3.3.5a-loadable encodings (validated with the same header pass as §4).
+The bluemenu rail sheets (593917/593918/593919) are NOT duplicated here — `Textures/Guild/`
+ships them and `modules/guild/Assets.lua` registers the fdids globally. Retail's standalone
+`bluemenu-ring` file (922034) is not shipped anywhere (Era CASC-only); the rail buttons mirror
+the DF PortraitMetal ring quadrant out of `Textures/Common/2406979` instead. The Raids rail-button
+icon is not a shipped asset either — it references the native 3.3.5a client texture directly
+(`Interface\LFGFrame\UI-LFR-PORTRAIT`, the same art the game's own Raid Browser window uses).
+(`ne-lfg-ring-quadrant` in modules/lfg/Assets.lua).
+
+## 6. Files delivered this sprint
+- `Textures/Assets.lua` — 7 `RegisterLocal` calls + architect-decision comment block.
+- `Textures/Common/` — 7 BLP sheets (1,777,685 bytes).
+- `Textures/ASSETS.md` — this file.
+
+---
+
+## 7. Encounter Journal (Adventure Guide) — `Textures/EncounterJournal/` (501 BLPs, 33M)
+Copied wholesale from `ReferenceAddons/NewEra/Art/EncounterJournal/` for the EJ downport
+(modules/encounterjournal). Registration is split across the module (not this folder's
+`Textures/Assets.lua`):
+- `modules/encounterjournal/Assets.lua` — chrome sheets (522972/522973 master+tile, 521743/
+ 521744/521748/521749/521750/521753, 527422/527690, tier bgs 605326/605327) + the Era
+ instance-button splashes + Era backdrops/lore loops.
+- `modules/encounterjournal/Data.lua` (tail) — 152 Era boss portraits (`Bosses/`) + lore loop.
+- `modules/encounterjournal/DataTBC.lua` (tail) — TBC button splashes, backdrops, lore,
+ ~98 TBC boss portraits.
+- `modules/encounterjournal/PortraitOverrides.lua` — 69 generated standin portraits
+ (`BossesGen/.blp`, registered under their displayID as the key).
+Subfolders: `Backdrops/` (37), `Lore/` (37), `Bosses/` (304), `BossesGen/` (69), 54 root
+files (chrome + ejbutton splashes). All FDID-named per the extract pipeline; provenance is
+documented in the NewEra source headers (retail CASC 12.0.5.67451 via wago.tools).
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Assets.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Assets.lua
new file mode 100644
index 0000000..e0a29f2
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Assets.lua
@@ -0,0 +1,95 @@
+-- DragonUI_NewEra/Textures/Assets.lua — SHARED / CORE sheet registration (Sprint 0).
+--
+-- Owner: Asset Pipeline Engineer (CONTRACTS.md §3). This file ONLY maps
+-- FileDataID -> shipped local BLP path via NE.tex.RegisterLocal. The API itself
+-- (NE.tex.RegisterLocal / NE.tex.Set) is provided by the Core agent's
+-- core/Texture.lua; we only CALL it. Loads after core/Texture.lua, after
+-- core/NineSliceLayouts.lua's own RegisterLocal calls were removed/owned here,
+-- and before any consumer (PanelChrome, _HelloDemo).
+--
+-- ============================================================================
+-- ARCHITECT DECISION (atlas mechanism) — coordinated with Core agent (§2/§3):
+--
+-- Atlas COORDINATES come from the Core NineSlice coord tables
+-- (core/NineSliceLayouts.lua, transcribed from retail's NineSliceLayouts +
+-- the NE_ATLAS coord data), resolved at draw time by NE.nineslice.Apply /
+-- NE.tex.Set. We DO NOT use C_Texture.RegisterAtlas to carry the slice
+-- geometry: the 3.3.5a client has no native atlas DB, and the ClassicAPI
+-- shim's C_Texture only answers existence/info queries — it does not feed
+-- real slice metadata to the render path (retail nicknames would stretch as
+-- flat strips, exactly the failure mode NineSliceLayouts.lua documents).
+--
+-- THIS FILE is therefore a pure fdid -> file-path index. The atlas-name ->
+-- (fdid + texcoord rect) mapping lives in the Core coord tables; this file
+-- answers "given the fdid that coord table named, where is the BLP on disk".
+-- That split keeps art (here) and geometry (Core) independently editable.
+-- ============================================================================
+
+local NE = DragonUI_NewEra
+if not (NE and NE.tex and NE.tex.RegisterLocal) then return end
+
+local P = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\"
+
+-- --- UI-Frame-Metal nineslice sheets (PortraitFrameTemplate / ButtonFrameTemplate
+-- family). Three sheets: corners, vertical-tile edges, horizontal-tile edges.
+-- Consumed by core/NineSliceLayouts.lua layouts + the /dnehello demo.
+NE.tex.RegisterLocal(2406979, P .. "Common\\2406979-uiframe-metal-corners.blp") -- 512x512 BGRA; all metal corners (incl. PortraitMetal/Double)
+NE.tex.RegisterLocal(2406984, P .. "Common\\2406984-uiframe-metal-edges-vert.blp") -- 512x32 BGRA; !-tile L/R edges
+NE.tex.RegisterLocal(2406987, P .. "Common\\2406987-uiframe-metal-edges-horiz.blp") -- 64x256 BGRA; _-tile Top/Bottom edges
+
+-- --- Common-currencybox — the rounded 3-piece pill border behind each watched-currency token in
+-- the combined bag's bottom money band (modules/bags/CombinedBag.lua). One 32x64 sheet holds
+-- left/right end caps + a stretched center (atlas coords registered by the bags module).
+NE.tex.RegisterLocal(4701880, P .. "Common\\4701880-common-currencybox.blp") -- 32x64 BGRA; common-currencybox-{left,right,center}
+
+-- --- UI-Background-Rock — ButtonFrameTemplate rock fill (shared: CharacterPanel + Social).
+NE.tex.RegisterLocal(374155, P .. "Common\\374155-uibackground-rock.blp") -- 1024x1024 DXT1
+
+-- --- InsetFrameTemplate inner-border (the thin gold inner-recess trim). Shared Core
+-- layout (CONTRACTS §3) — its 3 sheets belong in Core, never a single module's
+-- Assets (else disabling that module drops the inset border everywhere).
+NE.tex.RegisterLocal(1723831, P .. "Common\\1723831-uiframe-inner.blp") -- 128x128 DXT5; UI-Frame-Inner* corners (6x6)
+NE.tex.RegisterLocal(1723832, P .. "Common\\1723832-uiframe-inner.blp") -- 64x256 DXT5; !UI-Frame-Inner{Left,Right}Tile (3x256)
+NE.tex.RegisterLocal(1723833, P .. "Common\\1723833-uiframe-inner.blp") -- 256x128 DXT5; _UI-Frame-Inner{Top,Bot}Tile band
+
+-- --- BigRedThreeSliceButton (1536801) — the red dialog button. core/ButtonSkin.lua has been written
+-- against this sheet since Sprint 0 and, without it, fail-safed to native art on every call: the
+-- LFG role buttons and the edit-mode dialog's Revert/Reset were all silently unskinned.
+NE.tex.RegisterLocal(1536801, P .. "Common\\1536801-128redbutton.blp") -- 512x2048; caps, tile, 3 states + highlight
+
+-- --- DiamondMetal "Dialog" border — retail's clean thin dialog frame (the ESC menu and every
+-- modern dialog wear it). Core art, not a module's: the Metal family above is panel chrome and
+-- wrong on a small floating dialog, so anything dialog-shaped needs this instead.
+NE.tex.RegisterLocal(3056750, P .. "Common\\3056750-diamondmetal-frame.blp") -- corners + top/bottom edges
+NE.tex.RegisterLocal(3056755, P .. "Common\\3056755-diamondmetal-edges.blp") -- !-tile left/right edges
+
+-- --- Modern dropdown trigger (5390329) — common-dropdown-textholder (the body) plus the
+-- a-button arrow and its five states. 3.3.5a has no WowStyle1DropdownTemplate to carry these,
+-- so the widget is assembled from them in SettingsControls.
+NE.tex.RegisterLocal(5390329, P .. "Common\\5390329-common-dropdown-bg.blp") -- 512x512; textholder + a-button states
+
+-- --- RedButton close-button sheet (4698972) — the Dragonflight red "X". One sheet carries all
+-- four states (exit / pressed / highlight / disabled) via texcoords in core/NineSliceLayouts.
+-- Consumed by PanelChrome.ModernizeCloseButton; without it the close button falls back to the
+-- native 3.3.5a art (which renders blank under our chrome), so it's shared Core art.
+NE.tex.RegisterLocal(4698972, P .. "Common\\4698972-redbutton-exit-2x.blp") -- 36x38 states grid
+
+-- --- DF metal panel-tab sheet (4707839) — the modern bottom-tab look (uiframe-tab / -activetab),
+-- used by core/Tabs.lua (NE.tabs.ReskinClassicTab) for the CharacterPanel tabs and any other
+-- reskinned classic tabs. Coords live in core/NineSliceLayouts.lua; this ships the art.
+NE.tex.RegisterLocal(4707839, P .. "Common\\4707839-uiframe-tab.blp") -- 64x256 tab/activetab grid
+
+-- --- Minimal-scrollbar art (the DF "MinimalScrollBar" look), consumed by core/ScrollbarReskin.lua
+-- over the classic Slider-based UIPanelScrollBar. Coords live in core/NineSliceLayouts.lua;
+-- these ship the four backing sheets so the reskin lights up (track + arrows + thumb caps +
+-- thumb middle). Shared Core art — every character panel's FauxScrollFrame reskins through it.
+NE.tex.RegisterLocal(4331838, P .. "Common\\4331838-minimal-scrollbar.blp") -- track top/bottom + arrows
+NE.tex.RegisterLocal(4332072, P .. "Common\\4332072-minimal-scrollbar-track-middle.blp") -- !track-middle 1px tile
+NE.tex.RegisterLocal(5142784, P .. "Common\\5142784-minimal-scrollbar-small-thumb-middle.blp") -- small thumb middle strip
+NE.tex.RegisterLocal(5142787, P .. "Common\\5142787-minimal-scrollbar-small.blp") -- small thumb top/bottom caps
+
+-- MinimalSliderWithSteppers (sheet 4567914) — the whole horizontal slider on one 32x128 sheet: both
+-- rounded track caps, the 1px tiled run between them, the DIAMOND thumb, and both chevron steppers.
+-- Supplied by the owner; it is not in the NewEra Art set, which is why NE.scrollbar.SkinSlider first
+-- shipped built out of the vertical scrollbar's pieces turned on their side.
+NE.tex.RegisterLocal(4567914, P .. "Common\\4567914-minimalsliderbar.blp") -- track caps + run + diamond + steppers
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/AuctionHouse/3046538-auctionhouse-chrome.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/AuctionHouse/3046538-auctionhouse-chrome.blp
new file mode 100644
index 0000000..9c1fa64
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/AuctionHouse/3046538-auctionhouse-chrome.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/AuctionHouse/3054898-auctionhouse-backgrounds.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/AuctionHouse/3054898-auctionhouse-backgrounds.blp
new file mode 100644
index 0000000..22671f7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/AuctionHouse/3054898-auctionhouse-backgrounds.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/136567-ui-character-reputationbar.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/136567-ui-character-reputationbar.blp
new file mode 100644
index 0000000..5f10a00
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/136567-ui-character-reputationbar.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1400895-character-info-classes-a.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1400895-character-info-classes-a.blp
new file mode 100644
index 0000000..944e763
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1400895-character-info-classes-a.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1400896-character-info-classes-b.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1400896-character-info-classes-b.blp
new file mode 100644
index 0000000..51e6345
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1400896-character-info-classes-b.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1662186-classicon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1662186-classicon.blp
new file mode 100644
index 0000000..2e5e32b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/1662186-classicon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/3487944-common-buttons-icons.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/3487944-common-buttons-icons.blp
new file mode 100644
index 0000000..2a9b3ab
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/3487944-common-buttons-icons.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/3534438-common-buttons-icons.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/3534438-common-buttons-icons.blp
new file mode 100644
index 0000000..208cf7b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/3534438-common-buttons-icons.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410247-charpaperdoll.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410247-charpaperdoll.blp
new file mode 100644
index 0000000..7f2a21f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410247-charpaperdoll.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410248-charpaperdoll.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410248-charpaperdoll.blp
new file mode 100644
index 0000000..87da434
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410248-charpaperdoll.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410249-charpaperdoll.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410249-charpaperdoll.blp
new file mode 100644
index 0000000..e02f753
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/410249-charpaperdoll.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/4499236-campaign-headericon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/4499236-campaign-headericon.blp
new file mode 100644
index 0000000..afcf959
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/4499236-campaign-headericon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/4571485-options-listexpand.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/4571485-options-listexpand.blp
new file mode 100644
index 0000000..55f77c3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/4571485-options-listexpand.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/5882640-character-panel-background.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/5882640-character-panel-background.blp
new file mode 100644
index 0000000..e4adebf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/5882640-character-panel-background.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/ASSETS.md b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/ASSETS.md
new file mode 100644
index 0000000..f942285
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/ASSETS.md
@@ -0,0 +1,69 @@
+# CharacterPanel Assets (Sprint 1, Agent F)
+
+Art shipped + registered for the CharacterPanel reskin. Registration lives in
+`modules/character/Assets.lua` (`NE.tex.RegisterLocal` + `NE.tex.RegisterAtlases`).
+Atlas COORDS transcribed from `NewEra/Generated/AtlasData.lua` (retail build 12.0.5.67451).
+Architect decision: coords live in `NE.tex.atlases`, **not** `C_Texture.RegisterAtlas`.
+
+## fdid -> path map (44 BLPs)
+
+All paths are `Interface\AddOns\DragonUI_NewEra\Textures\CharacterPanel\`.
+
+| FDID | File | Dims | Enc | Purpose |
+|------|------|------|-----|---------|
+| 1400895 | 1400895-character-info-classes-a.blp | 1024x1024 | DXT5 | Class BGs: Mage/Monk/Paladin/Priest/Rogue/Shaman/Warlock/Warrior |
+| 1400896 | 1400896-character-info-classes-b.blp | 1024x512 | DXT5 | Class BGs: DeathKnight/DemonHunter/Druid/Hunter |
+| 5882640 | 5882640-character-panel-background.blp | 1024x512 | DXT5 | Overall pane background |
+| 1662186 | 1662186-classicon.blp | 2048x1024 | DXT5 | 12 class icons (portrait swap) |
+| 410247 | 410247-charpaperdoll.blp | 32x16 | DXT5 | Char-Paperdoll-Horizontal inner-border h-tile |
+| 410248 | 410248-charpaperdoll.blp | 256x128 | DXT5 | Char-Paperdoll-Parts (corners, slot frames) |
+| 410249 | 410249-charpaperdoll.blp | 16x32 | DXT5 | Char-Paperdoll-Vertical inner-border v-tile |
+| 3534438 | 3534438-common-buttons-icons.blp | 1024x1024 | DXT5 | common-button-square-gray-{up,down} |
+| 3487944 | 3487944-common-buttons-icons.blp | 2048x1024 | **ARGB8888** | common-icon-* (zoom/rotate/undo) |
+| 4571485 | 4571485-options-listexpand.blp | 128x128 | **ARGB8888** | options_listexpand_* (collapsible headers) |
+| 136567 | 136567-ui-character-reputationbar.blp | 256x64 | DXT5 | Reputation bar end caps |
+| 4499236 | 4499236-campaign-headericon.blp | 2048x1024 | **ARGB8888** | campaign_headericon_* (Skills collapse) |
+| 131081-131088 | RaceBackground/*-DressUpBackground-{BloodElf,Draenei}<1-4>.blp | (same quarters) | DXT5 | TBC race model BGs (8 files) |
+| 131089-131112 | RaceBackground/*-DressUpBackground-<1-4>.blp | 256x256 / 64x256 / 256x128 / 64x128 | DXT5 | Race model BGs: Dwarf/Human/NightElf/Orc/Scourge/Tauren (24 files) |
+| 455998-456001 | RaceBackground/*-DressUpBackground-Gnome<1-4>.blp | (same quarters) | DXT5 | Gnome model BG (4 files) |
+| 456006-456009 | RaceBackground/*-DressUpBackground-Troll<1-4>.blp | (same quarters) | DXT5 | Troll model BG (4 files) |
+
+Total: **9,540,548 bytes (9.10 MB)**, 53 files (13 top-level + 40 RaceBackground).
+
+## Atlas-coord families registered (via NE.tex.RegisterAtlases)
+
+All rects transcribed verbatim from `NewEra/Generated/AtlasData.lua`:
+
+| Family | Count | Sheet FDID | AtlasData.lua lines |
+|--------|-------|-----------|---------------------|
+| `ui-character-info--bg` | 12 | 1400895 / 1400896 | 13756-13770 |
+| `classicon-` | 13 (incl. evoker, never selected on 3.3.5a) | 1662186 | 2402-2414 |
+| `options_listexpand_{left,right,right_expanded}` + `_options_listexpand_middle` | 4 | 4571485 | 519, 8672-8674 |
+| `common-icon-{zoomin,zoomout,rotateleft,rotateright,undo}` | 5 | 3487944 | 2762-2779 |
+| `common-button-square-gray-{up,down}` | 2 | 3534438 | 2570-2571 |
+
+## Validation results (python BLP2 header read)
+
+- **All 44 BLPs are valid BLP2** (magic `BLP2`), **all power-of-two**, all `type/version=1`.
+- 41 sheets use DXT (encoding 2, DXT5/BC3 alpha) — the well-supported 3.3.5a path.
+- **3 sheets are uncompressed ARGB8888 (BLP2 encoding 3, alphaDepth 8):**
+ `3487944`, `4499236`, `4571485`. These are 3.3.5a-loadable (the client supports
+ uncompressed BGRA addon BLPs) but heavyweight (8.4 MB each for the two 2048x1024 ones).
+ Flagged for Wave 2: if texture memory / load time is a concern, the consuming modules
+ (ModelControls, Skills collapse) can degrade gracefully — `NE.tex.SetAtlas` already
+ returns false + `NE.Log`s a MISS if a sheet ever fails to load.
+- `mips=0` on all the large sheets (matches Sprint-0 sheets like 2406979) — accepted by
+ the 3.3.5a addon loader; not a problem.
+
+## Missing / uncertain art (for Wave 2 to degrade)
+
+- **136570** (rep/skill bar — contract listed "if present"): **NOT in source**, not shipped.
+ Only 136567 (reputation bar) exists. Wave 2: do not reference 136570.
+- **evoker** class icon is registered for parity but there is no Evoker class on 3.3.5a —
+ it will never be selected by `classicon-` lookup.
+- The 3 ARGB8888 sheets above are the only load-risk items; everything else is standard DXT5.
+- All other CharacterPanel sheets named in the source `Assets.lua` were located and shipped.
+- Shared chrome (UI-Frame-Metal nineslice, rock bg 374155, InsetFrame inner-border
+ 1723831/2/3, tab sheet 4707839, RedButton 4698972, minimal-scrollbar family,
+ spec-icons) is NOT in this module — it is owned by Core `Textures/Assets.lua` and consumed
+ from there (per contract: disabling CharacterPanel must not drop shared art).
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/PaperDollSidebarTabs.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/PaperDollSidebarTabs.blp
new file mode 100644
index 0000000..cc9800d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/PaperDollSidebarTabs.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131081-DressUpBackground-BloodElf1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131081-DressUpBackground-BloodElf1.blp
new file mode 100644
index 0000000..bb534b0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131081-DressUpBackground-BloodElf1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131082-DressUpBackground-BloodElf2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131082-DressUpBackground-BloodElf2.blp
new file mode 100644
index 0000000..351f2eb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131082-DressUpBackground-BloodElf2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131083-DressUpBackground-BloodElf3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131083-DressUpBackground-BloodElf3.blp
new file mode 100644
index 0000000..d20b7a2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131083-DressUpBackground-BloodElf3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131084-DressUpBackground-BloodElf4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131084-DressUpBackground-BloodElf4.blp
new file mode 100644
index 0000000..30e97ae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131084-DressUpBackground-BloodElf4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131085-DressUpBackground-Draenei1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131085-DressUpBackground-Draenei1.blp
new file mode 100644
index 0000000..4b5016a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131085-DressUpBackground-Draenei1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131086-DressUpBackground-Draenei2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131086-DressUpBackground-Draenei2.blp
new file mode 100644
index 0000000..1ddbfe1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131086-DressUpBackground-Draenei2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131087-DressUpBackground-Draenei3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131087-DressUpBackground-Draenei3.blp
new file mode 100644
index 0000000..0e18125
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131087-DressUpBackground-Draenei3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131088-DressUpBackground-Draenei4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131088-DressUpBackground-Draenei4.blp
new file mode 100644
index 0000000..23ec7c9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131088-DressUpBackground-Draenei4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131089-DressUpBackground-Dwarf1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131089-DressUpBackground-Dwarf1.blp
new file mode 100644
index 0000000..0b86b09
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131089-DressUpBackground-Dwarf1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131090-DressUpBackground-Dwarf2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131090-DressUpBackground-Dwarf2.blp
new file mode 100644
index 0000000..bcd4161
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131090-DressUpBackground-Dwarf2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131091-DressUpBackground-Dwarf3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131091-DressUpBackground-Dwarf3.blp
new file mode 100644
index 0000000..7873fdb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131091-DressUpBackground-Dwarf3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131092-DressUpBackground-Dwarf4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131092-DressUpBackground-Dwarf4.blp
new file mode 100644
index 0000000..e76c068
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131092-DressUpBackground-Dwarf4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131093-DressUpBackground-Human1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131093-DressUpBackground-Human1.blp
new file mode 100644
index 0000000..dd4d840
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131093-DressUpBackground-Human1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131094-DressUpBackground-Human2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131094-DressUpBackground-Human2.blp
new file mode 100644
index 0000000..23fa927
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131094-DressUpBackground-Human2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131095-DressUpBackground-Human3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131095-DressUpBackground-Human3.blp
new file mode 100644
index 0000000..f77fe13
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131095-DressUpBackground-Human3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131096-DressUpBackground-Human4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131096-DressUpBackground-Human4.blp
new file mode 100644
index 0000000..ecc54ee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131096-DressUpBackground-Human4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131097-DressUpBackground-NightElf1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131097-DressUpBackground-NightElf1.blp
new file mode 100644
index 0000000..88ef375
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131097-DressUpBackground-NightElf1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131098-DressUpBackground-NightElf2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131098-DressUpBackground-NightElf2.blp
new file mode 100644
index 0000000..045021f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131098-DressUpBackground-NightElf2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131099-DressUpBackground-NightElf3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131099-DressUpBackground-NightElf3.blp
new file mode 100644
index 0000000..134f932
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131099-DressUpBackground-NightElf3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131100-DressUpBackground-NightElf4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131100-DressUpBackground-NightElf4.blp
new file mode 100644
index 0000000..516103c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131100-DressUpBackground-NightElf4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131101-DressUpBackground-Orc1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131101-DressUpBackground-Orc1.blp
new file mode 100644
index 0000000..3e6471e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131101-DressUpBackground-Orc1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131102-DressUpBackground-Orc2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131102-DressUpBackground-Orc2.blp
new file mode 100644
index 0000000..2b88109
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131102-DressUpBackground-Orc2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131103-DressUpBackground-Orc3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131103-DressUpBackground-Orc3.blp
new file mode 100644
index 0000000..96c1b01
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131103-DressUpBackground-Orc3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131104-DressUpBackground-Orc4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131104-DressUpBackground-Orc4.blp
new file mode 100644
index 0000000..2600255
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131104-DressUpBackground-Orc4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131105-DressUpBackground-Scourge1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131105-DressUpBackground-Scourge1.blp
new file mode 100644
index 0000000..222e1c8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131105-DressUpBackground-Scourge1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131106-DressUpBackground-Scourge2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131106-DressUpBackground-Scourge2.blp
new file mode 100644
index 0000000..e0edc37
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131106-DressUpBackground-Scourge2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131107-DressUpBackground-Scourge3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131107-DressUpBackground-Scourge3.blp
new file mode 100644
index 0000000..32f2ce6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131107-DressUpBackground-Scourge3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131108-DressUpBackground-Scourge4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131108-DressUpBackground-Scourge4.blp
new file mode 100644
index 0000000..2b37dc2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131108-DressUpBackground-Scourge4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131109-DressUpBackground-Tauren1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131109-DressUpBackground-Tauren1.blp
new file mode 100644
index 0000000..eb75619
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131109-DressUpBackground-Tauren1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131110-DressUpBackground-Tauren2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131110-DressUpBackground-Tauren2.blp
new file mode 100644
index 0000000..b0b3300
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131110-DressUpBackground-Tauren2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131111-DressUpBackground-Tauren3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131111-DressUpBackground-Tauren3.blp
new file mode 100644
index 0000000..450b500
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131111-DressUpBackground-Tauren3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131112-DressUpBackground-Tauren4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131112-DressUpBackground-Tauren4.blp
new file mode 100644
index 0000000..18cb0e2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/131112-DressUpBackground-Tauren4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/455998-DressUpBackground-Gnome1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/455998-DressUpBackground-Gnome1.blp
new file mode 100644
index 0000000..5f0d3ee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/455998-DressUpBackground-Gnome1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/455999-DressUpBackground-Gnome2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/455999-DressUpBackground-Gnome2.blp
new file mode 100644
index 0000000..c1461a6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/455999-DressUpBackground-Gnome2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456000-DressUpBackground-Gnome3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456000-DressUpBackground-Gnome3.blp
new file mode 100644
index 0000000..cdabd93
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456000-DressUpBackground-Gnome3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456001-DressUpBackground-Gnome4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456001-DressUpBackground-Gnome4.blp
new file mode 100644
index 0000000..830fc82
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456001-DressUpBackground-Gnome4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456006-DressUpBackground-Troll1.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456006-DressUpBackground-Troll1.blp
new file mode 100644
index 0000000..a2313ae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456006-DressUpBackground-Troll1.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456007-DressUpBackground-Troll2.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456007-DressUpBackground-Troll2.blp
new file mode 100644
index 0000000..403d698
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456007-DressUpBackground-Troll2.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456008-DressUpBackground-Troll3.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456008-DressUpBackground-Troll3.blp
new file mode 100644
index 0000000..50d03a9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456008-DressUpBackground-Troll3.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456009-DressUpBackground-Troll4.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456009-DressUpBackground-Troll4.blp
new file mode 100644
index 0000000..314ee43
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/RaceBackground/456009-DressUpBackground-Troll4.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/_classicon_decoded.png b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/_classicon_decoded.png
new file mode 100644
index 0000000..2ccee94
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CharacterPanel/_classicon_decoded.png differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/FavoritesIcon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/FavoritesIcon.blp
new file mode 100644
index 0000000..8599088
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/FavoritesIcon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/ListButtons.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/ListButtons.blp
new file mode 100644
index 0000000..0646fdd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/ListButtons.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournal-BG.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournal-BG.blp
new file mode 100644
index 0000000..4eabc10
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournal-BG.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournal-NoMounts.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournal-NoMounts.blp
new file mode 100644
index 0000000..df66b08
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournal-NoMounts.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalEmptyIcon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalEmptyIcon.blp
new file mode 100644
index 0000000..9a283bf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalEmptyIcon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalIcons.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalIcons.blp
new file mode 100644
index 0000000..f66ef19
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalIcons.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalPortrait.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalPortrait.blp
new file mode 100644
index 0000000..eb1b33e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountJournalPortrait.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountUpFavourites.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountUpFavourites.blp
new file mode 100644
index 0000000..43a8c90
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/MountUpFavourites.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/PetJournalPortrait.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/PetJournalPortrait.blp
new file mode 100644
index 0000000..7cf0fc2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/PetJournalPortrait.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/UI-Searchbox-Icon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/UI-Searchbox-Icon.blp
new file mode 100644
index 0000000..0ab7ae5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/UI-Searchbox-Icon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/WhiteIconFrame.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/WhiteIconFrame.blp
new file mode 100644
index 0000000..c967f38
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Collections/WhiteIconFrame.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1536801-128redbutton.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1536801-128redbutton.blp
new file mode 100644
index 0000000..e63ad07
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1536801-128redbutton.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723831-uiframe-inner.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723831-uiframe-inner.blp
new file mode 100644
index 0000000..a70fb76
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723831-uiframe-inner.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723832-uiframe-inner.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723832-uiframe-inner.blp
new file mode 100644
index 0000000..0f84558
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723832-uiframe-inner.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723833-uiframe-inner.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723833-uiframe-inner.blp
new file mode 100644
index 0000000..66135b1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/1723833-uiframe-inner.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406979-uiframe-metal-corners.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406979-uiframe-metal-corners.blp
new file mode 100644
index 0000000..44c2f12
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406979-uiframe-metal-corners.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406984-uiframe-metal-edges-vert.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406984-uiframe-metal-edges-vert.blp
new file mode 100644
index 0000000..837ea8c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406984-uiframe-metal-edges-vert.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406987-uiframe-metal-edges-horiz.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406987-uiframe-metal-edges-horiz.blp
new file mode 100644
index 0000000..86d76fc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2406987-uiframe-metal-edges-horiz.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2922105-common-iconmask.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2922105-common-iconmask.blp
new file mode 100644
index 0000000..01dbf4f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/2922105-common-iconmask.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/3056750-diamondmetal-frame.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/3056750-diamondmetal-frame.blp
new file mode 100644
index 0000000..bf59404
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/3056750-diamondmetal-frame.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/3056755-diamondmetal-edges.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/3056755-diamondmetal-edges.blp
new file mode 100644
index 0000000..0fde5d9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/3056755-diamondmetal-edges.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/374155-uibackground-rock.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/374155-uibackground-rock.blp
new file mode 100644
index 0000000..7d953d8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/374155-uibackground-rock.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/423808-squarebuttontextures.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/423808-squarebuttontextures.blp
new file mode 100644
index 0000000..af75193
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/423808-squarebuttontextures.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4331838-minimal-scrollbar.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4331838-minimal-scrollbar.blp
new file mode 100644
index 0000000..abb7819
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4331838-minimal-scrollbar.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4332072-minimal-scrollbar-track-middle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4332072-minimal-scrollbar-track-middle.blp
new file mode 100644
index 0000000..ded6af3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4332072-minimal-scrollbar-track-middle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4567914-minimalsliderbar.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4567914-minimalsliderbar.blp
new file mode 100644
index 0000000..2c71d5a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4567914-minimalsliderbar.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4698972-redbutton-exit-2x.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4698972-redbutton-exit-2x.blp
new file mode 100644
index 0000000..21111c8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4698972-redbutton-exit-2x.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4701880-common-currencybox.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4701880-common-currencybox.blp
new file mode 100644
index 0000000..dbb7e3f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4701880-common-currencybox.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4707839-uiframe-tab.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4707839-uiframe-tab.blp
new file mode 100644
index 0000000..da20c7b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4707839-uiframe-tab.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4708813-ui-hud-micromenu.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4708813-ui-hud-micromenu.blp
new file mode 100644
index 0000000..b2e5df5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/4708813-ui-hud-micromenu.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5142784-minimal-scrollbar-small-thumb-middle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5142784-minimal-scrollbar-small-thumb-middle.blp
new file mode 100644
index 0000000..e52a8e7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5142784-minimal-scrollbar-small-thumb-middle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5142787-minimal-scrollbar-small.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5142787-minimal-scrollbar-small.blp
new file mode 100644
index 0000000..c7319f7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5142787-minimal-scrollbar-small.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5390329-common-dropdown-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5390329-common-dropdown-bg.blp
new file mode 100644
index 0000000..76535dd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/5390329-common-dropdown-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/9900002-ui-background-marble.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/9900002-ui-background-marble.blp
new file mode 100644
index 0000000..9d6f44f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/9900002-ui-background-marble.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/BagsItemSlot.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/BagsItemSlot.blp
new file mode 100644
index 0000000..a011177
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/BagsItemSlot.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/BagsItemSlotClose.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/BagsItemSlotClose.blp
new file mode 100644
index 0000000..5acecb9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/BagsItemSlotClose.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamBackground.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamBackground.blp
new file mode 100644
index 0000000..69710c9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamBackground.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamBackground.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamBackground.tga
new file mode 100644
index 0000000..6382d54
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamBackground.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamCircle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamCircle.blp
new file mode 100644
index 0000000..69abbbd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamCircle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamCircle.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamCircle.tga
new file mode 100644
index 0000000..59ab155
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamCircle.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamFrame.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamFrame.blp
new file mode 100644
index 0000000..5255d5a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamFrame.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamFrame.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamFrame.tga
new file mode 100644
index 0000000..3ae4461
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamFrame.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamSpark.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamSpark.blp
new file mode 100644
index 0000000..f7aa996
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamSpark.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamSpark.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamSpark.tga
new file mode 100644
index 0000000..c2cf523
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/StreamSpark.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/inv_pet_broom.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/inv_pet_broom.blp
new file mode 100644
index 0000000..9ac1ebb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Common/inv_pet_broom.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/5199404-ui-hud-actionbar-gcd-flipbook.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/5199404-ui-hud-actionbar-gcd-flipbook.blp
new file mode 100644
index 0000000..bf6e099
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/5199404-ui-hud-actionbar-gcd-flipbook.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/6685874-ui-cooldownmanager-oorshadow.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/6685874-ui-cooldownmanager-oorshadow.blp
new file mode 100644
index 0000000..4d8c388
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/6685874-ui-cooldownmanager-oorshadow.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/6704514-ui-hud-cooldownmanager-iconoverlay.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/6704514-ui-hud-cooldownmanager-iconoverlay.blp
new file mode 100644
index 0000000..e1064a1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewer/6704514-ui-hud-cooldownmanager-iconoverlay.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewerSettings/7289697-cdmadvanced.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewerSettings/7289697-cdmadvanced.blp
new file mode 100644
index 0000000..cd46658
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/CooldownViewerSettings/7289697-cdmadvanced.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396579-ejbutton-black-temple.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396579-ejbutton-black-temple.blp
new file mode 100644
index 0000000..5c71903
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396579-ejbutton-black-temple.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396580-ejbutton-blackwing-lair.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396580-ejbutton-blackwing-lair.blp
new file mode 100644
index 0000000..f8e66e9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396580-ejbutton-blackwing-lair.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396582-ejbutton-gruuls-lair.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396582-ejbutton-gruuls-lair.blp
new file mode 100644
index 0000000..3c38b4b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396582-ejbutton-gruuls-lair.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396584-ejbutton-karazhan.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396584-ejbutton-karazhan.blp
new file mode 100644
index 0000000..df5776d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396584-ejbutton-karazhan.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396585-ejbutton-magtheridons-lair.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396585-ejbutton-magtheridons-lair.blp
new file mode 100644
index 0000000..c34bc1f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396585-ejbutton-magtheridons-lair.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396586-ejbutton-molten-core.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396586-ejbutton-molten-core.blp
new file mode 100644
index 0000000..4e64f72
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396586-ejbutton-molten-core.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396587-ejbutton-naxxramas.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396587-ejbutton-naxxramas.blp
new file mode 100644
index 0000000..ee4a1d9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396587-ejbutton-naxxramas.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396589-ejbutton-onyxias-lair.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396589-ejbutton-onyxias-lair.blp
new file mode 100644
index 0000000..b2790be
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396589-ejbutton-onyxias-lair.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396591-ejbutton-ruins-of-ahnqiraj.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396591-ejbutton-ruins-of-ahnqiraj.blp
new file mode 100644
index 0000000..33091c0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396591-ejbutton-ruins-of-ahnqiraj.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396592-ejbutton-sunwell-plateau.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396592-ejbutton-sunwell-plateau.blp
new file mode 100644
index 0000000..1b7e44c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396592-ejbutton-sunwell-plateau.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396593-ejbutton-temple-of-ahnqiraj.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396593-ejbutton-temple-of-ahnqiraj.blp
new file mode 100644
index 0000000..d5d1c0e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/1396593-ejbutton-temple-of-ahnqiraj.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237592-ejbutton-ahnkalet.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237592-ejbutton-ahnkalet.blp
new file mode 100644
index 0000000..c1b3f8b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237592-ejbutton-ahnkalet.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237593-ejbutton-azjolnerub.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237593-ejbutton-azjolnerub.blp
new file mode 100644
index 0000000..f5a394a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237593-ejbutton-azjolnerub.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237594-ejbutton-chamberofaspects.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237594-ejbutton-chamberofaspects.blp
new file mode 100644
index 0000000..4bb314b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237594-ejbutton-chamberofaspects.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237595-ejbutton-draktharon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237595-ejbutton-draktharon.blp
new file mode 100644
index 0000000..f3f5ed9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237595-ejbutton-draktharon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237596-ejbutton-gundrak.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237596-ejbutton-gundrak.blp
new file mode 100644
index 0000000..7d25205
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237596-ejbutton-gundrak.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237598-ejbutton-hallsoflightning.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237598-ejbutton-hallsoflightning.blp
new file mode 100644
index 0000000..87802ae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237598-ejbutton-hallsoflightning.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237599-ejbutton-hallsofstone.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237599-ejbutton-hallsofstone.blp
new file mode 100644
index 0000000..3f02e60
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237599-ejbutton-hallsofstone.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237600-ejbutton-malygos.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237600-ejbutton-malygos.blp
new file mode 100644
index 0000000..3192ae5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237600-ejbutton-malygos.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237601-ejbutton-oldstratholme.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237601-ejbutton-oldstratholme.blp
new file mode 100644
index 0000000..52b8e55
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237601-ejbutton-oldstratholme.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237602-ejbutton-thenexus.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237602-ejbutton-thenexus.blp
new file mode 100644
index 0000000..81d94ab
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237602-ejbutton-thenexus.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237603-ejbutton-theoculus.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237603-ejbutton-theoculus.blp
new file mode 100644
index 0000000..420373f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237603-ejbutton-theoculus.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237604-ejbutton-theviolethold.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237604-ejbutton-theviolethold.blp
new file mode 100644
index 0000000..dc529e2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237604-ejbutton-theviolethold.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237605-ejbutton-utgarde.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237605-ejbutton-utgarde.blp
new file mode 100644
index 0000000..7eab14f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237605-ejbutton-utgarde.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237606-ejbutton-utgardepinnacle.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237606-ejbutton-utgardepinnacle.blp
new file mode 100644
index 0000000..fda4123
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/237606-ejbutton-utgardepinnacle.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/303841-ejbutton-vaultofarchavon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/303841-ejbutton-vaultofarchavon.blp
new file mode 100644
index 0000000..8056f98
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/303841-ejbutton-vaultofarchavon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/304502-ejbutton-ulduarraid.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/304502-ejbutton-ulduarraid.blp
new file mode 100644
index 0000000..7a6002e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/304502-ejbutton-ulduarraid.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/311220-ejbutton-argentdungeon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/311220-ejbutton-argentdungeon.blp
new file mode 100644
index 0000000..81e0297
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/311220-ejbutton-argentdungeon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/311221-ejbutton-argentraid.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/311221-ejbutton-argentraid.blp
new file mode 100644
index 0000000..77d51fe
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/311221-ejbutton-argentraid.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336389-ejbutton-hallsofreflection.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336389-ejbutton-hallsofreflection.blp
new file mode 100644
index 0000000..f4aa17e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336389-ejbutton-hallsofreflection.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336390-ejbutton-icecrowncitadel.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336390-ejbutton-icecrowncitadel.blp
new file mode 100644
index 0000000..1a47314
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336390-ejbutton-icecrowncitadel.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336391-ejbutton-pitofsaron.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336391-ejbutton-pitofsaron.blp
new file mode 100644
index 0000000..7c1c57e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336391-ejbutton-pitofsaron.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336392-ejbutton-theforgeofsouls.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336392-ejbutton-theforgeofsouls.blp
new file mode 100644
index 0000000..21e0899
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/336392-ejbutton-theforgeofsouls.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/366689-ejbutton-rubysanctum.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/366689-ejbutton-rubysanctum.blp
new file mode 100644
index 0000000..b9a84da
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/366689-ejbutton-rubysanctum.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/516763-cs-helptextures.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/516763-cs-helptextures.blp
new file mode 100644
index 0000000..43b9577
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/516763-cs-helptextures.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/516764-cs-helptextures-tile.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/516764-cs-helptextures-tile.blp
new file mode 100644
index 0000000..e7fdd07
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/516764-cs-helptextures-tile.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521743-ui-ej-background-default.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521743-ui-ej-background-default.blp
new file mode 100644
index 0000000..32844b5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521743-ui-ej-background-default.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521744-ui-ej-boss-default.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521744-ui-ej-boss-default.blp
new file mode 100644
index 0000000..6d01259
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521744-ui-ej-boss-default.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521748-ui-ej-heroictexticon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521748-ui-ej-heroictexticon.blp
new file mode 100644
index 0000000..3517ab8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521748-ui-ej-heroictexticon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521749-ui-ej-icons.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521749-ui-ej-icons.blp
new file mode 100644
index 0000000..6dbd0b5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521749-ui-ej-icons.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521750-ui-ej-journalbg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521750-ui-ej-journalbg.blp
new file mode 100644
index 0000000..939483c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521750-ui-ej-journalbg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521753-ui-ej-portraiticon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521753-ui-ej-portraiticon.blp
new file mode 100644
index 0000000..5401430
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/521753-ui-ej-portraiticon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522352-ejbutton-deadmines.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522352-ejbutton-deadmines.blp
new file mode 100644
index 0000000..a13bcbb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522352-ejbutton-deadmines.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522358-ejbutton-shadowfang-keep.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522358-ejbutton-shadowfang-keep.blp
new file mode 100644
index 0000000..8c7d622
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522358-ejbutton-shadowfang-keep.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522363-ejbutton-zulaman.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522363-ejbutton-zulaman.blp
new file mode 100644
index 0000000..3dff6e5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522363-ejbutton-zulaman.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522364-ejbutton-zulgurub.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522364-ejbutton-zulgurub.blp
new file mode 100644
index 0000000..0e20d18
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522364-ejbutton-zulgurub.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522972-ui-encounterjournaltextures.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522972-ui-encounterjournaltextures.blp
new file mode 100644
index 0000000..6d8f067
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522972-ui-encounterjournaltextures.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522973-ui-encounterjournaltextures-tile.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522973-ui-encounterjournaltextures-tile.blp
new file mode 100644
index 0000000..5ebe4e6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/522973-ui-encounterjournaltextures-tile.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/527422-ui-ej-lorebg-default.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/527422-ui-ej-lorebg-default.blp
new file mode 100644
index 0000000..2dc06c5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/527422-ui-ej-lorebg-default.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/527690-ui-ej-bossmodelpaperframe.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/527690-ui-ej-bossmodelpaperframe.blp
new file mode 100644
index 0000000..b0951e5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/527690-ui-ej-bossmodelpaperframe.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605326-ui-ej-burningcrusade.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605326-ui-ej-burningcrusade.blp
new file mode 100644
index 0000000..9aa7a40
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605326-ui-ej-burningcrusade.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605327-ui-ej-classic.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605327-ui-ej-classic.blp
new file mode 100644
index 0000000..e672155
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605327-ui-ej-classic.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605329-ui-ej-wrathofthelichking.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605329-ui-ej-wrathofthelichking.blp
new file mode 100644
index 0000000..a4c0726
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/605329-ui-ej-wrathofthelichking.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608193-ejbutton-auchindoun.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608193-ejbutton-auchindoun.blp
new file mode 100644
index 0000000..a662107
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608193-ejbutton-auchindoun.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608195-ejbutton-blackfathom-deeps.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608195-ejbutton-blackfathom-deeps.blp
new file mode 100644
index 0000000..1cacb4b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608195-ejbutton-blackfathom-deeps.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608196-ejbutton-blackrock-depths.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608196-ejbutton-blackrock-depths.blp
new file mode 100644
index 0000000..c2fc53f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608196-ejbutton-blackrock-depths.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608197-ejbutton-lower-blackrock-spire.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608197-ejbutton-lower-blackrock-spire.blp
new file mode 100644
index 0000000..d6f3f7d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608197-ejbutton-lower-blackrock-spire.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608198-ejbutton-caverns-of-time.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608198-ejbutton-caverns-of-time.blp
new file mode 100644
index 0000000..88dfbc7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608198-ejbutton-caverns-of-time.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608199-ejbutton-coilfang.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608199-ejbutton-coilfang.blp
new file mode 100644
index 0000000..2fd963e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608199-ejbutton-coilfang.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608200-ejbutton-dire-maul.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608200-ejbutton-dire-maul.blp
new file mode 100644
index 0000000..8ad0d3d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608200-ejbutton-dire-maul.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608202-ejbutton-gnomeregan.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608202-ejbutton-gnomeregan.blp
new file mode 100644
index 0000000..077d03e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608202-ejbutton-gnomeregan.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608207-ejbutton-hellfire-citadel.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608207-ejbutton-hellfire-citadel.blp
new file mode 100644
index 0000000..a639062
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608207-ejbutton-hellfire-citadel.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608208-ejbutton-magisters-terrace.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608208-ejbutton-magisters-terrace.blp
new file mode 100644
index 0000000..240f279
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608208-ejbutton-magisters-terrace.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608209-ejbutton-maraudon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608209-ejbutton-maraudon.blp
new file mode 100644
index 0000000..5e6d940
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608209-ejbutton-maraudon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608211-ejbutton-ragefire-chasm.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608211-ejbutton-ragefire-chasm.blp
new file mode 100644
index 0000000..72ce333
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608211-ejbutton-ragefire-chasm.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608212-ejbutton-razorfen-downs.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608212-ejbutton-razorfen-downs.blp
new file mode 100644
index 0000000..cd88209
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608212-ejbutton-razorfen-downs.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608213-ejbutton-razorfen-kraul.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608213-ejbutton-razorfen-kraul.blp
new file mode 100644
index 0000000..1fef85b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608213-ejbutton-razorfen-kraul.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608214-ejbutton-scarlet-monastery.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608214-ejbutton-scarlet-monastery.blp
new file mode 100644
index 0000000..983666a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608214-ejbutton-scarlet-monastery.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608215-ejbutton-scholomance.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608215-ejbutton-scholomance.blp
new file mode 100644
index 0000000..6e61863
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608215-ejbutton-scholomance.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608216-ejbutton-stratholme.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608216-ejbutton-stratholme.blp
new file mode 100644
index 0000000..1d9f1ee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608216-ejbutton-stratholme.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608217-ejbutton-sunken-temple.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608217-ejbutton-sunken-temple.blp
new file mode 100644
index 0000000..a52a06c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608217-ejbutton-sunken-temple.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608218-ejbutton-tempest-keep.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608218-ejbutton-tempest-keep.blp
new file mode 100644
index 0000000..3e0db9e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608218-ejbutton-tempest-keep.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608223-ejbutton-stockade.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608223-ejbutton-stockade.blp
new file mode 100644
index 0000000..9c5b554
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608223-ejbutton-stockade.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608225-ejbutton-uldaman.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608225-ejbutton-uldaman.blp
new file mode 100644
index 0000000..341db64
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608225-ejbutton-uldaman.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608229-ejbutton-wailing-caverns.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608229-ejbutton-wailing-caverns.blp
new file mode 100644
index 0000000..f024681
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608229-ejbutton-wailing-caverns.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608230-ejbutton-zulfarrak.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608230-ejbutton-zulfarrak.blp
new file mode 100644
index 0000000..79ca473
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/608230-ejbutton-zulfarrak.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/7494373-uicombattimelinewarningicons.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/7494373-uicombattimelinewarningicons.blp
new file mode 100644
index 0000000..fd79824
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/7494373-uicombattimelinewarningicons.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396453.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396453.blp
new file mode 100644
index 0000000..0f207f9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396453.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396454.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396454.blp
new file mode 100644
index 0000000..57d1886
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396454.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396455.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396455.blp
new file mode 100644
index 0000000..74aa6f1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396455.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396456.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396456.blp
new file mode 100644
index 0000000..b687140
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396456.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396457.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396457.blp
new file mode 100644
index 0000000..6d7f0e2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396457.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396458.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396458.blp
new file mode 100644
index 0000000..3b9edc0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396458.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396459.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396459.blp
new file mode 100644
index 0000000..c235675
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396459.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396460.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396460.blp
new file mode 100644
index 0000000..b6ae207
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396460.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396461.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396461.blp
new file mode 100644
index 0000000..74aabec
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396461.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396462.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396462.blp
new file mode 100644
index 0000000..4ae267e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396462.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396463.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396463.blp
new file mode 100644
index 0000000..38767e3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396463.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396464.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396464.blp
new file mode 100644
index 0000000..b7ef487
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396464.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396465.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396465.blp
new file mode 100644
index 0000000..dc90e6b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396465.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396466.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396466.blp
new file mode 100644
index 0000000..462cc10
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396466.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396467.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396467.blp
new file mode 100644
index 0000000..9ed34c9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396467.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396468.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396468.blp
new file mode 100644
index 0000000..64f4b61
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396468.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396469.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396469.blp
new file mode 100644
index 0000000..1b7ec3a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396469.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396470.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396470.blp
new file mode 100644
index 0000000..b3a9f83
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/1396470.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522336.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522336.blp
new file mode 100644
index 0000000..3e6820b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522336.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522342.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522342.blp
new file mode 100644
index 0000000..1fe8265
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522342.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522347.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522347.blp
new file mode 100644
index 0000000..9cd423a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522347.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522348.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522348.blp
new file mode 100644
index 0000000..ac08519
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/522348.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608153.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608153.blp
new file mode 100644
index 0000000..c41a81d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608153.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608154.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608154.blp
new file mode 100644
index 0000000..84b1efc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608154.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608155.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608155.blp
new file mode 100644
index 0000000..58a835f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608155.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608156.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608156.blp
new file mode 100644
index 0000000..5cafa9a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608156.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608157.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608157.blp
new file mode 100644
index 0000000..06276c7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608157.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608158.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608158.blp
new file mode 100644
index 0000000..5a5f5e9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608158.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608159.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608159.blp
new file mode 100644
index 0000000..6318596
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608159.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608160.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608160.blp
new file mode 100644
index 0000000..333e2bf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608160.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608161.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608161.blp
new file mode 100644
index 0000000..8215bf7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608161.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608162.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608162.blp
new file mode 100644
index 0000000..c1ce12e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608162.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608163.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608163.blp
new file mode 100644
index 0000000..adcaa82
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608163.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608164.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608164.blp
new file mode 100644
index 0000000..02f0ce3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608164.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608165.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608165.blp
new file mode 100644
index 0000000..8406f52
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608165.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608166.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608166.blp
new file mode 100644
index 0000000..fab6aa6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608166.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608167.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608167.blp
new file mode 100644
index 0000000..7bf450e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608167.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608168.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608168.blp
new file mode 100644
index 0000000..09c8de1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608168.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608169.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608169.blp
new file mode 100644
index 0000000..f44c2fa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608169.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608170.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608170.blp
new file mode 100644
index 0000000..f9aafd4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608170.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608171.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608171.blp
new file mode 100644
index 0000000..3c6e598
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608171.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608172.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608172.blp
new file mode 100644
index 0000000..1f3dace
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608172.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608173.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608173.blp
new file mode 100644
index 0000000..0b266d6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608173.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608174.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608174.blp
new file mode 100644
index 0000000..e4e72f2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608174.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608175.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608175.blp
new file mode 100644
index 0000000..bc824f7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608175.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608176.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608176.blp
new file mode 100644
index 0000000..f893763
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608176.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608177.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608177.blp
new file mode 100644
index 0000000..41beb2e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608177.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608178.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608178.blp
new file mode 100644
index 0000000..2e913cb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608178.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608179.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608179.blp
new file mode 100644
index 0000000..450fc26
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608179.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608180.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608180.blp
new file mode 100644
index 0000000..ccb6088
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608180.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608181.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608181.blp
new file mode 100644
index 0000000..91d1e88
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608181.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608182.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608182.blp
new file mode 100644
index 0000000..297d2a7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608182.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608183.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608183.blp
new file mode 100644
index 0000000..c032937
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608183.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608184.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608184.blp
new file mode 100644
index 0000000..ad660ba
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608184.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608185.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608185.blp
new file mode 100644
index 0000000..450492d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608185.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608186.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608186.blp
new file mode 100644
index 0000000..d8c30f0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608186.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608187.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608187.blp
new file mode 100644
index 0000000..fc29ea3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608187.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608188.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608188.blp
new file mode 100644
index 0000000..eb4e4fb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608188.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608189.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608189.blp
new file mode 100644
index 0000000..10db5fa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608189.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608190.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608190.blp
new file mode 100644
index 0000000..2d42752
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608190.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608191.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608191.blp
new file mode 100644
index 0000000..5b5d850
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Backdrops/608191.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064175.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064175.blp
new file mode 100644
index 0000000..68ee400
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064175.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064178.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064178.blp
new file mode 100644
index 0000000..12bf301
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064178.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064179.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064179.blp
new file mode 100644
index 0000000..187563c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064179.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064180.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064180.blp
new file mode 100644
index 0000000..1776887
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064180.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064181.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064181.blp
new file mode 100644
index 0000000..c534419
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064181.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064182.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064182.blp
new file mode 100644
index 0000000..fe5afd2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064182.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064183.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064183.blp
new file mode 100644
index 0000000..a35b6b3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064183.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064184.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064184.blp
new file mode 100644
index 0000000..c7cc99d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1064184.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378964.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378964.blp
new file mode 100644
index 0000000..1324352
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378964.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378965.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378965.blp
new file mode 100644
index 0000000..2be44ba
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378965.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378966.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378966.blp
new file mode 100644
index 0000000..3a4f4a3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378966.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378967.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378967.blp
new file mode 100644
index 0000000..b4a45c1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378967.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378968.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378968.blp
new file mode 100644
index 0000000..4eae8fb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378968.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378969.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378969.blp
new file mode 100644
index 0000000..29c0c8d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378969.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378970.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378970.blp
new file mode 100644
index 0000000..e8f4e14
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378970.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378971.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378971.blp
new file mode 100644
index 0000000..f665ed6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378971.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378972.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378972.blp
new file mode 100644
index 0000000..5d1faba
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378972.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378973.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378973.blp
new file mode 100644
index 0000000..f665ed6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378973.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378974.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378974.blp
new file mode 100644
index 0000000..f665ed6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378974.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378975.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378975.blp
new file mode 100644
index 0000000..0ad2786
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378975.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378976.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378976.blp
new file mode 100644
index 0000000..dcf97ea
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378976.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378977.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378977.blp
new file mode 100644
index 0000000..84fe8f2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378977.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378978.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378978.blp
new file mode 100644
index 0000000..007054b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378978.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378979.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378979.blp
new file mode 100644
index 0000000..9aa1882
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378979.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378980.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378980.blp
new file mode 100644
index 0000000..2e96e17
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378980.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378981.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378981.blp
new file mode 100644
index 0000000..6168eae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378981.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378982.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378982.blp
new file mode 100644
index 0000000..def76a6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378982.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378983.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378983.blp
new file mode 100644
index 0000000..a129e57
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378983.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378984.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378984.blp
new file mode 100644
index 0000000..e37beb9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378984.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378985.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378985.blp
new file mode 100644
index 0000000..caf2847
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378985.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378986.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378986.blp
new file mode 100644
index 0000000..dad6cbd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378986.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378987.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378987.blp
new file mode 100644
index 0000000..e9d28f6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378987.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378988.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378988.blp
new file mode 100644
index 0000000..146aa2b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378988.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378989.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378989.blp
new file mode 100644
index 0000000..acd4c92
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378989.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378990.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378990.blp
new file mode 100644
index 0000000..bbd70e7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378990.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378991.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378991.blp
new file mode 100644
index 0000000..a4ffcbf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378991.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378992.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378992.blp
new file mode 100644
index 0000000..57a0e01
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378992.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378993.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378993.blp
new file mode 100644
index 0000000..ddfd114
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378993.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378994.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378994.blp
new file mode 100644
index 0000000..fe7e6f5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378994.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378995.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378995.blp
new file mode 100644
index 0000000..7d86cf0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378995.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378996.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378996.blp
new file mode 100644
index 0000000..6f43811
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378996.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378997.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378997.blp
new file mode 100644
index 0000000..fad5c6d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378997.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378998.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378998.blp
new file mode 100644
index 0000000..5324d29
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378998.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378999.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378999.blp
new file mode 100644
index 0000000..20f217b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1378999.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379000.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379000.blp
new file mode 100644
index 0000000..ce15d3e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379000.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379001.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379001.blp
new file mode 100644
index 0000000..ca7d91b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379001.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379002.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379002.blp
new file mode 100644
index 0000000..eb7effa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379002.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379004.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379004.blp
new file mode 100644
index 0000000..e8c3b7f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379004.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379005.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379005.blp
new file mode 100644
index 0000000..6d0063f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379005.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379006.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379006.blp
new file mode 100644
index 0000000..68191ba
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379006.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379007.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379007.blp
new file mode 100644
index 0000000..507a853
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379007.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379008.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379008.blp
new file mode 100644
index 0000000..cf75c54
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379008.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379009.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379009.blp
new file mode 100644
index 0000000..d809a9f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379009.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379010.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379010.blp
new file mode 100644
index 0000000..ff463d5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379010.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379011.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379011.blp
new file mode 100644
index 0000000..aa4e265
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379011.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379012.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379012.blp
new file mode 100644
index 0000000..21ce547
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379012.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379013.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379013.blp
new file mode 100644
index 0000000..ca7ffd1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379013.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379014.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379014.blp
new file mode 100644
index 0000000..70a7ce1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379014.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379015.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379015.blp
new file mode 100644
index 0000000..dcf97ea
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379015.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379016.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379016.blp
new file mode 100644
index 0000000..b635880
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379016.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379017.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379017.blp
new file mode 100644
index 0000000..9095ad1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379017.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379018.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379018.blp
new file mode 100644
index 0000000..4186970
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379018.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379019.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379019.blp
new file mode 100644
index 0000000..2810087
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379019.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379020.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379020.blp
new file mode 100644
index 0000000..98ba6e6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379020.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379021.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379021.blp
new file mode 100644
index 0000000..1a19dbf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379021.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379022.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379022.blp
new file mode 100644
index 0000000..666604e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379022.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379023.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379023.blp
new file mode 100644
index 0000000..01b9983
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379023.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379025.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379025.blp
new file mode 100644
index 0000000..ba18ba4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1379025.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385712.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385712.blp
new file mode 100644
index 0000000..a09ef8e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385712.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385713.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385713.blp
new file mode 100644
index 0000000..3d5270b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385713.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385714.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385714.blp
new file mode 100644
index 0000000..28a7b43
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385714.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385715.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385715.blp
new file mode 100644
index 0000000..e929db6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385715.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385716.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385716.blp
new file mode 100644
index 0000000..60e1aa9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385716.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385717.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385717.blp
new file mode 100644
index 0000000..aa3fadf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385717.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385718.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385718.blp
new file mode 100644
index 0000000..b74fff8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385718.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385719.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385719.blp
new file mode 100644
index 0000000..9234237
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385719.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385720.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385720.blp
new file mode 100644
index 0000000..52c5289
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385720.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385721.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385721.blp
new file mode 100644
index 0000000..235cbd3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385721.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385722.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385722.blp
new file mode 100644
index 0000000..f952416
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385722.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385723.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385723.blp
new file mode 100644
index 0000000..f05553b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385723.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385724.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385724.blp
new file mode 100644
index 0000000..301ecd4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385724.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385725.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385725.blp
new file mode 100644
index 0000000..78ed185
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385725.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385726.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385726.blp
new file mode 100644
index 0000000..1d00750
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385726.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385727.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385727.blp
new file mode 100644
index 0000000..ac5e967
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385727.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385728.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385728.blp
new file mode 100644
index 0000000..72bf82d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385728.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385729.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385729.blp
new file mode 100644
index 0000000..a309f30
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385729.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385730.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385730.blp
new file mode 100644
index 0000000..5aaad00
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385730.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385731.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385731.blp
new file mode 100644
index 0000000..ec52858
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385731.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385732.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385732.blp
new file mode 100644
index 0000000..5d0e5b9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385732.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385733.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385733.blp
new file mode 100644
index 0000000..a69b14c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385733.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385734.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385734.blp
new file mode 100644
index 0000000..92fe386
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385734.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385735.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385735.blp
new file mode 100644
index 0000000..c714a21
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385735.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385736.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385736.blp
new file mode 100644
index 0000000..086b94d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385736.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385737.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385737.blp
new file mode 100644
index 0000000..4b55563
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385737.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385738.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385738.blp
new file mode 100644
index 0000000..b880ce2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385738.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385739.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385739.blp
new file mode 100644
index 0000000..db16aa4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385739.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385740.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385740.blp
new file mode 100644
index 0000000..8ddbadd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385740.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385741.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385741.blp
new file mode 100644
index 0000000..325fa5c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385741.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385742.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385742.blp
new file mode 100644
index 0000000..7e1f968
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385742.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385743.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385743.blp
new file mode 100644
index 0000000..523dde1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385743.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385744.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385744.blp
new file mode 100644
index 0000000..5f97c70
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385744.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385745.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385745.blp
new file mode 100644
index 0000000..6fe6bc9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385745.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385746.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385746.blp
new file mode 100644
index 0000000..f02fe27
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385746.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385747.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385747.blp
new file mode 100644
index 0000000..a6aef9b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385747.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385748.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385748.blp
new file mode 100644
index 0000000..9979188
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385748.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385749.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385749.blp
new file mode 100644
index 0000000..779d4d7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385749.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385750.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385750.blp
new file mode 100644
index 0000000..9275460
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385750.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385751.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385751.blp
new file mode 100644
index 0000000..faf7288
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385751.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385752.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385752.blp
new file mode 100644
index 0000000..dabf2b9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385752.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385753.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385753.blp
new file mode 100644
index 0000000..fda79e0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385753.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385754.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385754.blp
new file mode 100644
index 0000000..65f3394
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385754.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385755.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385755.blp
new file mode 100644
index 0000000..e57c95c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385755.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385756.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385756.blp
new file mode 100644
index 0000000..e89e43f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385756.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385757.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385757.blp
new file mode 100644
index 0000000..fd284e9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385757.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385758.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385758.blp
new file mode 100644
index 0000000..b6ef3a2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385758.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385759.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385759.blp
new file mode 100644
index 0000000..f94ebf7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385759.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385760.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385760.blp
new file mode 100644
index 0000000..8a30eaf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385760.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385761.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385761.blp
new file mode 100644
index 0000000..813f4e8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385761.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385762.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385762.blp
new file mode 100644
index 0000000..58bcdd2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385762.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385763.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385763.blp
new file mode 100644
index 0000000..1ee6226
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385763.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385764.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385764.blp
new file mode 100644
index 0000000..67931c9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385764.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385765.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385765.blp
new file mode 100644
index 0000000..f6d8b4c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385765.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385766.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385766.blp
new file mode 100644
index 0000000..eb89fbe
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385766.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385767.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385767.blp
new file mode 100644
index 0000000..c305c3d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385767.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385768.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385768.blp
new file mode 100644
index 0000000..e96528b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385768.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385769.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385769.blp
new file mode 100644
index 0000000..fd4f897
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385769.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385770.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385770.blp
new file mode 100644
index 0000000..bb4782e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385770.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385771.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385771.blp
new file mode 100644
index 0000000..cb22de7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385771.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385772.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385772.blp
new file mode 100644
index 0000000..053c820
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385772.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385773.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385773.blp
new file mode 100644
index 0000000..8371150
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385773.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385774.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385774.blp
new file mode 100644
index 0000000..56bc4cc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1385774.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390436.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390436.blp
new file mode 100644
index 0000000..279e222
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390436.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390437.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390437.blp
new file mode 100644
index 0000000..809442e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390437.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390438.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390438.blp
new file mode 100644
index 0000000..f54f3d9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390438.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390439.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390439.blp
new file mode 100644
index 0000000..ac5e967
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390439.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390440.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390440.blp
new file mode 100644
index 0000000..66a5ab1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390440.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390443.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390443.blp
new file mode 100644
index 0000000..3df5457
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/1390443.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/4776138.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/4776138.blp
new file mode 100644
index 0000000..c1d9535
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/4776138.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522189.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522189.blp
new file mode 100644
index 0000000..1e6559f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522189.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522190.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522190.blp
new file mode 100644
index 0000000..8e74d5f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522190.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522205.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522205.blp
new file mode 100644
index 0000000..e6515bd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522205.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522206.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522206.blp
new file mode 100644
index 0000000..dd4bc48
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522206.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522209.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522209.blp
new file mode 100644
index 0000000..6a19d46
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522209.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522210.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522210.blp
new file mode 100644
index 0000000..a436773
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522210.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522213.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522213.blp
new file mode 100644
index 0000000..bf27a1a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522213.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522214.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522214.blp
new file mode 100644
index 0000000..de1f0fe
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522214.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522217.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522217.blp
new file mode 100644
index 0000000..20bfedc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522217.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522225.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522225.blp
new file mode 100644
index 0000000..629d1eb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522225.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522228.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522228.blp
new file mode 100644
index 0000000..9099751
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522228.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522230.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522230.blp
new file mode 100644
index 0000000..3bf7258
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522230.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522231.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522231.blp
new file mode 100644
index 0000000..da70da1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522231.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522233.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522233.blp
new file mode 100644
index 0000000..295f7b2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522233.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522234.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522234.blp
new file mode 100644
index 0000000..6587bdb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522234.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522235.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522235.blp
new file mode 100644
index 0000000..43d8bb9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522235.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522236.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522236.blp
new file mode 100644
index 0000000..78e7925
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522236.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522238.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522238.blp
new file mode 100644
index 0000000..cd11917
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522238.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522242.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522242.blp
new file mode 100644
index 0000000..17e032b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522242.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522243.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522243.blp
new file mode 100644
index 0000000..85d76de
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522243.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522247.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522247.blp
new file mode 100644
index 0000000..970f9a5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522247.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522249.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522249.blp
new file mode 100644
index 0000000..03cc91a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522249.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522251.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522251.blp
new file mode 100644
index 0000000..a983f74
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522251.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522254.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522254.blp
new file mode 100644
index 0000000..2aa67fe
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522254.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522261.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522261.blp
new file mode 100644
index 0000000..5381884
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522261.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522263.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522263.blp
new file mode 100644
index 0000000..a8f73f0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522263.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522278.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522278.blp
new file mode 100644
index 0000000..d3032ae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522278.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522279.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522279.blp
new file mode 100644
index 0000000..dde3dba
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522279.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522280.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522280.blp
new file mode 100644
index 0000000..2572fe7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/522280.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875505.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875505.blp
new file mode 100644
index 0000000..a2e80f0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875505.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875506.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875506.blp
new file mode 100644
index 0000000..76371b5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875506.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875507.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875507.blp
new file mode 100644
index 0000000..46d9d08
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875507.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875508.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875508.blp
new file mode 100644
index 0000000..6cbf51f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875508.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875509.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875509.blp
new file mode 100644
index 0000000..c43a73a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875509.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875510.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875510.blp
new file mode 100644
index 0000000..b6d6fc9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875510.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875511.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875511.blp
new file mode 100644
index 0000000..3cc98f6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875511.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875512.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875512.blp
new file mode 100644
index 0000000..0844691
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/5875512.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607529.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607529.blp
new file mode 100644
index 0000000..43cbbdf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607529.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607531.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607531.blp
new file mode 100644
index 0000000..1e82d7a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607531.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607532.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607532.blp
new file mode 100644
index 0000000..ca6d874
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607532.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607533.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607533.blp
new file mode 100644
index 0000000..9b5d0fe
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607533.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607534.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607534.blp
new file mode 100644
index 0000000..4ce0e9b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607534.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607535.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607535.blp
new file mode 100644
index 0000000..947b3ba
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607535.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607536.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607536.blp
new file mode 100644
index 0000000..a69f1a7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607536.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607537.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607537.blp
new file mode 100644
index 0000000..232c139
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607537.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607538.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607538.blp
new file mode 100644
index 0000000..80efdb4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607538.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607540.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607540.blp
new file mode 100644
index 0000000..0f71071
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607540.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607541.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607541.blp
new file mode 100644
index 0000000..2825ea4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607541.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607542.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607542.blp
new file mode 100644
index 0000000..a9dab7d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607542.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607544.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607544.blp
new file mode 100644
index 0000000..06d04af
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607544.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607546.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607546.blp
new file mode 100644
index 0000000..b9f6600
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607546.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607547.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607547.blp
new file mode 100644
index 0000000..7e0bf3c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607547.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607548.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607548.blp
new file mode 100644
index 0000000..c806ab2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607548.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607549.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607549.blp
new file mode 100644
index 0000000..3f1b819
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607549.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607550.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607550.blp
new file mode 100644
index 0000000..e52f694
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607550.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607551.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607551.blp
new file mode 100644
index 0000000..3565193
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607551.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607553.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607553.blp
new file mode 100644
index 0000000..9614983
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607553.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607555.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607555.blp
new file mode 100644
index 0000000..f5dc59c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607555.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607556.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607556.blp
new file mode 100644
index 0000000..8ff5813
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607556.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607558.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607558.blp
new file mode 100644
index 0000000..582a1a6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607558.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607559.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607559.blp
new file mode 100644
index 0000000..5de2394
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607559.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607560.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607560.blp
new file mode 100644
index 0000000..76db268
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607560.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607561.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607561.blp
new file mode 100644
index 0000000..4a46c8a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607561.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607562.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607562.blp
new file mode 100644
index 0000000..86f5029
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607562.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607563.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607563.blp
new file mode 100644
index 0000000..0c5791b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607563.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607564.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607564.blp
new file mode 100644
index 0000000..9bbcc8a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607564.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607565.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607565.blp
new file mode 100644
index 0000000..34f19a5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607565.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607566.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607566.blp
new file mode 100644
index 0000000..219af44
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607566.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607567.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607567.blp
new file mode 100644
index 0000000..74c91df
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607567.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607568.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607568.blp
new file mode 100644
index 0000000..44bf9de
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607568.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607569.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607569.blp
new file mode 100644
index 0000000..b9b294d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607569.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607570.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607570.blp
new file mode 100644
index 0000000..77d5aef
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607570.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607572.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607572.blp
new file mode 100644
index 0000000..4de625e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607572.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607573.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607573.blp
new file mode 100644
index 0000000..f5473a6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607573.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607574.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607574.blp
new file mode 100644
index 0000000..3192f30
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607574.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607582.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607582.blp
new file mode 100644
index 0000000..c1477c6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607582.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607583.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607583.blp
new file mode 100644
index 0000000..29757bd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607583.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607584.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607584.blp
new file mode 100644
index 0000000..ba32ed6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607584.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607585.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607585.blp
new file mode 100644
index 0000000..545d4be
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607585.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607587.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607587.blp
new file mode 100644
index 0000000..4153ce2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607587.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607589.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607589.blp
new file mode 100644
index 0000000..69239a3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607589.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607590.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607590.blp
new file mode 100644
index 0000000..e69e2fd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607590.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607591.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607591.blp
new file mode 100644
index 0000000..db1441a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607591.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607592.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607592.blp
new file mode 100644
index 0000000..924c91a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607592.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607593.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607593.blp
new file mode 100644
index 0000000..d992556
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607593.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607594.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607594.blp
new file mode 100644
index 0000000..02ec664
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607594.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607595.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607595.blp
new file mode 100644
index 0000000..823ebff
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607595.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607596.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607596.blp
new file mode 100644
index 0000000..854ab16
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607596.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607597.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607597.blp
new file mode 100644
index 0000000..d0bcc77
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607597.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607600.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607600.blp
new file mode 100644
index 0000000..5eaaec6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607600.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607601.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607601.blp
new file mode 100644
index 0000000..212a5c3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607601.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607602.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607602.blp
new file mode 100644
index 0000000..34b1788
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607602.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607603.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607603.blp
new file mode 100644
index 0000000..43c2cf3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607603.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607605.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607605.blp
new file mode 100644
index 0000000..5303a62
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607605.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607606.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607606.blp
new file mode 100644
index 0000000..9d97c18
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607606.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607610.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607610.blp
new file mode 100644
index 0000000..69042d8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607610.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607611.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607611.blp
new file mode 100644
index 0000000..c5f9b59
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607611.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607614.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607614.blp
new file mode 100644
index 0000000..da0ae33
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607614.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607615.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607615.blp
new file mode 100644
index 0000000..88503f4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607615.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607618.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607618.blp
new file mode 100644
index 0000000..a77cba1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607618.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607620.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607620.blp
new file mode 100644
index 0000000..9876bc7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607620.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607621.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607621.blp
new file mode 100644
index 0000000..c6eba94
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607621.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607622.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607622.blp
new file mode 100644
index 0000000..4de1673
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607622.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607623.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607623.blp
new file mode 100644
index 0000000..fb54887
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607623.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607624.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607624.blp
new file mode 100644
index 0000000..7375a6a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607624.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607625.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607625.blp
new file mode 100644
index 0000000..4e1f3ff
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607625.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607626.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607626.blp
new file mode 100644
index 0000000..0045f52
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607626.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607628.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607628.blp
new file mode 100644
index 0000000..0250286
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607628.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607629.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607629.blp
new file mode 100644
index 0000000..337cd17
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607629.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607630.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607630.blp
new file mode 100644
index 0000000..887dc34
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607630.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607631.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607631.blp
new file mode 100644
index 0000000..9c294b8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607631.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607633.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607633.blp
new file mode 100644
index 0000000..c0d1974
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607633.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607634.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607634.blp
new file mode 100644
index 0000000..24bfc2e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607634.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607635.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607635.blp
new file mode 100644
index 0000000..2bf5200
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607635.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607637.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607637.blp
new file mode 100644
index 0000000..112d53e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607637.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607639.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607639.blp
new file mode 100644
index 0000000..9683d8d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607639.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607641.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607641.blp
new file mode 100644
index 0000000..c86ff74
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607641.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607643.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607643.blp
new file mode 100644
index 0000000..75c1b73
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607643.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607644.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607644.blp
new file mode 100644
index 0000000..b900d36
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607644.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607645.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607645.blp
new file mode 100644
index 0000000..483b0b7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607645.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607647.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607647.blp
new file mode 100644
index 0000000..783c7f5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607647.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607649.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607649.blp
new file mode 100644
index 0000000..34644af
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607649.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607650.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607650.blp
new file mode 100644
index 0000000..e93a4e1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607650.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607651.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607651.blp
new file mode 100644
index 0000000..3523fd3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607651.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607652.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607652.blp
new file mode 100644
index 0000000..a43e37d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607652.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607653.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607653.blp
new file mode 100644
index 0000000..25e797c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607653.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607654.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607654.blp
new file mode 100644
index 0000000..5a7eab3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607654.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607655.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607655.blp
new file mode 100644
index 0000000..12fd6a6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607655.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607656.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607656.blp
new file mode 100644
index 0000000..094e745
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607656.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607657.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607657.blp
new file mode 100644
index 0000000..90633a0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607657.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607658.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607658.blp
new file mode 100644
index 0000000..6fac7f9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607658.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607659.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607659.blp
new file mode 100644
index 0000000..c70bbc8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607659.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607660.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607660.blp
new file mode 100644
index 0000000..32f8cd2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607660.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607663.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607663.blp
new file mode 100644
index 0000000..d5f3385
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607663.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607664.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607664.blp
new file mode 100644
index 0000000..6048720
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607664.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607665.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607665.blp
new file mode 100644
index 0000000..1087410
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607665.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607666.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607666.blp
new file mode 100644
index 0000000..4f487c0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607666.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607667.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607667.blp
new file mode 100644
index 0000000..d18831c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607667.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607669.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607669.blp
new file mode 100644
index 0000000..00a4c92
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607669.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607670.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607670.blp
new file mode 100644
index 0000000..87fac87
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607670.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607671.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607671.blp
new file mode 100644
index 0000000..ae1dd52
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607671.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607672.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607672.blp
new file mode 100644
index 0000000..87dd6cc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607672.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607673.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607673.blp
new file mode 100644
index 0000000..a34eed7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607673.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607674.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607674.blp
new file mode 100644
index 0000000..8f72d6b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607674.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607676.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607676.blp
new file mode 100644
index 0000000..bd10f6d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607676.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607678.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607678.blp
new file mode 100644
index 0000000..bcc4917
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607678.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607679.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607679.blp
new file mode 100644
index 0000000..7691936
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607679.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607680.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607680.blp
new file mode 100644
index 0000000..23808cb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607680.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607683.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607683.blp
new file mode 100644
index 0000000..8cbba41
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607683.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607684.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607684.blp
new file mode 100644
index 0000000..8b204b6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607684.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607685.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607685.blp
new file mode 100644
index 0000000..302e525
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607685.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607686.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607686.blp
new file mode 100644
index 0000000..a12e95b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607686.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607687.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607687.blp
new file mode 100644
index 0000000..42c2dd3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607687.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607688.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607688.blp
new file mode 100644
index 0000000..dd335d9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607688.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607689.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607689.blp
new file mode 100644
index 0000000..2217267
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607689.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607690.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607690.blp
new file mode 100644
index 0000000..8c6f9e0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607690.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607692.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607692.blp
new file mode 100644
index 0000000..1aa5d6c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607692.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607693.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607693.blp
new file mode 100644
index 0000000..dc5938f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607693.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607694.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607694.blp
new file mode 100644
index 0000000..892afda
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607694.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607695.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607695.blp
new file mode 100644
index 0000000..d8c89c1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607695.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607696.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607696.blp
new file mode 100644
index 0000000..afa69af
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607696.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607697.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607697.blp
new file mode 100644
index 0000000..a305d5a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607697.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607698.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607698.blp
new file mode 100644
index 0000000..350e95b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607698.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607699.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607699.blp
new file mode 100644
index 0000000..5bbf6ae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607699.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607702.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607702.blp
new file mode 100644
index 0000000..4b27f8f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607702.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607703.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607703.blp
new file mode 100644
index 0000000..34420ee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607703.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607705.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607705.blp
new file mode 100644
index 0000000..617d6a8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607705.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607706.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607706.blp
new file mode 100644
index 0000000..5906451
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607706.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607707.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607707.blp
new file mode 100644
index 0000000..7cbcbb8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607707.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607708.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607708.blp
new file mode 100644
index 0000000..922d46b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607708.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607710.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607710.blp
new file mode 100644
index 0000000..3c3d9eb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607710.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607711.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607711.blp
new file mode 100644
index 0000000..f965fb8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607711.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607712.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607712.blp
new file mode 100644
index 0000000..0934118
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607712.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607713.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607713.blp
new file mode 100644
index 0000000..fa50f74
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607713.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607714.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607714.blp
new file mode 100644
index 0000000..b974832
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607714.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607715.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607715.blp
new file mode 100644
index 0000000..b0bb4c6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607715.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607716.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607716.blp
new file mode 100644
index 0000000..09cc6d1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607716.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607717.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607717.blp
new file mode 100644
index 0000000..10a3dbc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607717.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607718.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607718.blp
new file mode 100644
index 0000000..4c28d32
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607718.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607719.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607719.blp
new file mode 100644
index 0000000..dffc4dc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607719.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607720.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607720.blp
new file mode 100644
index 0000000..e3faaaa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607720.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607721.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607721.blp
new file mode 100644
index 0000000..b8bbee9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607721.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607723.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607723.blp
new file mode 100644
index 0000000..04d6293
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607723.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607724.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607724.blp
new file mode 100644
index 0000000..c514ad6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607724.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607725.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607725.blp
new file mode 100644
index 0000000..d1243b7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607725.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607726.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607726.blp
new file mode 100644
index 0000000..9077b51
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607726.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607727.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607727.blp
new file mode 100644
index 0000000..2fae51a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607727.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607728.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607728.blp
new file mode 100644
index 0000000..d346808
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607728.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607729.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607729.blp
new file mode 100644
index 0000000..be09494
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607729.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607734.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607734.blp
new file mode 100644
index 0000000..dd53138
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607734.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607735.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607735.blp
new file mode 100644
index 0000000..4e3813e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607735.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607736.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607736.blp
new file mode 100644
index 0000000..6da5df3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607736.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607737.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607737.blp
new file mode 100644
index 0000000..ad6aa36
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607737.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607738.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607738.blp
new file mode 100644
index 0000000..1ee32a8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607738.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607739.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607739.blp
new file mode 100644
index 0000000..d1d9b96
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607739.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607740.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607740.blp
new file mode 100644
index 0000000..9eb3010
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607740.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607741.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607741.blp
new file mode 100644
index 0000000..a466054
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607741.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607742.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607742.blp
new file mode 100644
index 0000000..5cd2aae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607742.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607743.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607743.blp
new file mode 100644
index 0000000..d419eb2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607743.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607744.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607744.blp
new file mode 100644
index 0000000..cc61cb1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607744.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607745.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607745.blp
new file mode 100644
index 0000000..3257d08
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607745.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607747.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607747.blp
new file mode 100644
index 0000000..fbb59f4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607747.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607749.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607749.blp
new file mode 100644
index 0000000..4bd288b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607749.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607750.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607750.blp
new file mode 100644
index 0000000..633ce50
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607750.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607751.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607751.blp
new file mode 100644
index 0000000..dc70f4c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607751.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607752.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607752.blp
new file mode 100644
index 0000000..6938b9e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607752.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607753.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607753.blp
new file mode 100644
index 0000000..d4cffd3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607753.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607755.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607755.blp
new file mode 100644
index 0000000..40a6faa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607755.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607756.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607756.blp
new file mode 100644
index 0000000..213ea41
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607756.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607757.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607757.blp
new file mode 100644
index 0000000..b63480e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607757.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607759.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607759.blp
new file mode 100644
index 0000000..900d1be
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607759.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607760.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607760.blp
new file mode 100644
index 0000000..580ef84
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607760.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607761.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607761.blp
new file mode 100644
index 0000000..d3b97dc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607761.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607763.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607763.blp
new file mode 100644
index 0000000..ff3199e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607763.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607765.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607765.blp
new file mode 100644
index 0000000..f87d37f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607765.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607767.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607767.blp
new file mode 100644
index 0000000..277bc48
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607767.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607768.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607768.blp
new file mode 100644
index 0000000..5e42bc2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607768.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607769.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607769.blp
new file mode 100644
index 0000000..e71e6d9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607769.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607771.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607771.blp
new file mode 100644
index 0000000..269b9a8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607771.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607772.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607772.blp
new file mode 100644
index 0000000..eaa0dee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607772.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607773.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607773.blp
new file mode 100644
index 0000000..c815c4c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607773.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607774.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607774.blp
new file mode 100644
index 0000000..50edfca
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607774.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607775.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607775.blp
new file mode 100644
index 0000000..3169984
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607775.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607776.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607776.blp
new file mode 100644
index 0000000..636754a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607776.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607777.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607777.blp
new file mode 100644
index 0000000..4af0b76
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607777.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607778.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607778.blp
new file mode 100644
index 0000000..04868d6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607778.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607779.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607779.blp
new file mode 100644
index 0000000..2264fee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607779.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607780.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607780.blp
new file mode 100644
index 0000000..1d5d548
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607780.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607782.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607782.blp
new file mode 100644
index 0000000..790655f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607782.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607784.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607784.blp
new file mode 100644
index 0000000..151db00
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607784.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607785.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607785.blp
new file mode 100644
index 0000000..2838cce
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607785.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607787.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607787.blp
new file mode 100644
index 0000000..998f927
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607787.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607788.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607788.blp
new file mode 100644
index 0000000..4921598
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607788.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607789.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607789.blp
new file mode 100644
index 0000000..482a51d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607789.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607790.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607790.blp
new file mode 100644
index 0000000..147bf43
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607790.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607791.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607791.blp
new file mode 100644
index 0000000..5926836
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607791.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607792.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607792.blp
new file mode 100644
index 0000000..15eee82
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607792.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607793.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607793.blp
new file mode 100644
index 0000000..13da08c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607793.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607794.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607794.blp
new file mode 100644
index 0000000..a7ce828
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607794.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607795.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607795.blp
new file mode 100644
index 0000000..8fe743c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607795.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607796.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607796.blp
new file mode 100644
index 0000000..60349b9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607796.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607797.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607797.blp
new file mode 100644
index 0000000..bf1ab1f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607797.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607798.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607798.blp
new file mode 100644
index 0000000..0a1fd80
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607798.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607801.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607801.blp
new file mode 100644
index 0000000..4cf74c0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607801.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607802.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607802.blp
new file mode 100644
index 0000000..62691b0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607802.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607803.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607803.blp
new file mode 100644
index 0000000..3e01365
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607803.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607805.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607805.blp
new file mode 100644
index 0000000..87dec97
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607805.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607806.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607806.blp
new file mode 100644
index 0000000..b24fdb8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607806.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607808.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607808.blp
new file mode 100644
index 0000000..f68bac2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607808.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607809.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607809.blp
new file mode 100644
index 0000000..1326941
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607809.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607810.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607810.blp
new file mode 100644
index 0000000..fca8811
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607810.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607811.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607811.blp
new file mode 100644
index 0000000..c26ee41
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607811.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607812.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607812.blp
new file mode 100644
index 0000000..7ec101d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607812.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607814.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607814.blp
new file mode 100644
index 0000000..e15eed4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607814.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607815.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607815.blp
new file mode 100644
index 0000000..671c4b9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607815.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607816.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607816.blp
new file mode 100644
index 0000000..8384d66
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607816.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607817.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607817.blp
new file mode 100644
index 0000000..524f814
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607817.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607818.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607818.blp
new file mode 100644
index 0000000..555d380
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607818.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607819.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607819.blp
new file mode 100644
index 0000000..2998c43
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607819.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607820.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607820.blp
new file mode 100644
index 0000000..af405f2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607820.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607821.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607821.blp
new file mode 100644
index 0000000..927ff9c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607821.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607822.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607822.blp
new file mode 100644
index 0000000..d306d3c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607822.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607823.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607823.blp
new file mode 100644
index 0000000..e4daddd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607823.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607824.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607824.blp
new file mode 100644
index 0000000..9bd5654
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607824.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607825.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607825.blp
new file mode 100644
index 0000000..6900ae0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/607825.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608309.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608309.blp
new file mode 100644
index 0000000..23ec346
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608309.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608310.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608310.blp
new file mode 100644
index 0000000..5e0fdbf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608310.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608311.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608311.blp
new file mode 100644
index 0000000..9064106
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608311.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608314.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608314.blp
new file mode 100644
index 0000000..e84cf75
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608314.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608315.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608315.blp
new file mode 100644
index 0000000..c998d0f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/608315.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630818.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630818.blp
new file mode 100644
index 0000000..e2f34bc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630818.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630835.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630835.blp
new file mode 100644
index 0000000..784ec5d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630835.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630838.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630838.blp
new file mode 100644
index 0000000..bb77faa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630838.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630853.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630853.blp
new file mode 100644
index 0000000..fbf9fd7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/630853.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/69420.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/69420.blp
new file mode 100644
index 0000000..e55f6ae
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Bosses/69420.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10248.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10248.blp
new file mode 100644
index 0000000..8964468
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10248.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10433.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10433.blp
new file mode 100644
index 0000000..c220722
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10433.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10901.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10901.blp
new file mode 100644
index 0000000..cd0a3a7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/10901.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11069.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11069.blp
new file mode 100644
index 0000000..dc4bcb8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11069.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11070.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11070.blp
new file mode 100644
index 0000000..9c6c293
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11070.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11072.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11072.blp
new file mode 100644
index 0000000..d483421
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11072.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11073.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11073.blp
new file mode 100644
index 0000000..a8c5e70
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11073.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11179.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11179.blp
new file mode 100644
index 0000000..1ba1558
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11179.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11288.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11288.blp
new file mode 100644
index 0000000..35855a6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11288.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11311.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11311.blp
new file mode 100644
index 0000000..d02a6d5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11311.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11382.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11382.blp
new file mode 100644
index 0000000..fcddd71
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11382.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11396.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11396.blp
new file mode 100644
index 0000000..2c7d632
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11396.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11429.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11429.blp
new file mode 100644
index 0000000..e67981d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11429.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11492.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11492.blp
new file mode 100644
index 0000000..b5dcad7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11492.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11611.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11611.blp
new file mode 100644
index 0000000..25e5bc4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11611.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11835.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11835.blp
new file mode 100644
index 0000000..f7f4d80
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/11835.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/12073.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/12073.blp
new file mode 100644
index 0000000..6528e45
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/12073.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15216.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15216.blp
new file mode 100644
index 0000000..a2b2697
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15216.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15217.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15217.blp
new file mode 100644
index 0000000..8b2f84c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15217.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15218.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15218.blp
new file mode 100644
index 0000000..826966f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15218.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15219.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15219.blp
new file mode 100644
index 0000000..72b9f4a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15219.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15220.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15220.blp
new file mode 100644
index 0000000..7669823
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15220.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15267.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15267.blp
new file mode 100644
index 0000000..d703860
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15267.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15268.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15268.blp
new file mode 100644
index 0000000..7f00b0f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15268.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15269.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15269.blp
new file mode 100644
index 0000000..9cc702f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15269.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15288.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15288.blp
new file mode 100644
index 0000000..8489c4d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15288.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15295.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15295.blp
new file mode 100644
index 0000000..6ebc2ca
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/15295.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1621.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1621.blp
new file mode 100644
index 0000000..722b279
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1621.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1773.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1773.blp
new file mode 100644
index 0000000..7758059
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1773.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1816.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1816.blp
new file mode 100644
index 0000000..d923b4b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/1816.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2007.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2007.blp
new file mode 100644
index 0000000..18515a1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2007.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2040.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2040.blp
new file mode 100644
index 0000000..34a9e6d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2040.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2041.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2041.blp
new file mode 100644
index 0000000..147fd29
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2041.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2042.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2042.blp
new file mode 100644
index 0000000..6216876
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2042.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2043.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2043.blp
new file mode 100644
index 0000000..a68f8b9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2043.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2044.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2044.blp
new file mode 100644
index 0000000..ddefe4b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2044.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2142.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2142.blp
new file mode 100644
index 0000000..172aed4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2142.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2149.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2149.blp
new file mode 100644
index 0000000..14df186
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2149.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2352.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2352.blp
new file mode 100644
index 0000000..9369f82
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2352.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2353.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2353.blp
new file mode 100644
index 0000000..0d2cb2f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2353.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2450.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2450.blp
new file mode 100644
index 0000000..0c41211
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2450.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2605.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2605.blp
new file mode 100644
index 0000000..eda2b15
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2605.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2606.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2606.blp
new file mode 100644
index 0000000..6e59208
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2606.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2837.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2837.blp
new file mode 100644
index 0000000..3adfea9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/2837.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3222.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3222.blp
new file mode 100644
index 0000000..669f9db
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3222.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3223.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3223.blp
new file mode 100644
index 0000000..bcf3668
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3223.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3250.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3250.blp
new file mode 100644
index 0000000..dafb514
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/3250.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4642.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4642.blp
new file mode 100644
index 0000000..5f42286
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4642.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4644.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4644.blp
new file mode 100644
index 0000000..71bc621
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4644.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4652.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4652.blp
new file mode 100644
index 0000000..f65cde8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4652.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4939.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4939.blp
new file mode 100644
index 0000000..4421395
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4939.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4979.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4979.blp
new file mode 100644
index 0000000..000599f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/4979.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/5027.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/5027.blp
new file mode 100644
index 0000000..195fa33
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/5027.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/517.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/517.blp
new file mode 100644
index 0000000..e804ecc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/517.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/522.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/522.blp
new file mode 100644
index 0000000..62640ee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/522.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/524.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/524.blp
new file mode 100644
index 0000000..ef1d77e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/524.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/5266.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/5266.blp
new file mode 100644
index 0000000..b58c532
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/5266.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6097.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6097.blp
new file mode 100644
index 0000000..689e02e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6097.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6110.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6110.blp
new file mode 100644
index 0000000..23902f4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6110.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6124.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6124.blp
new file mode 100644
index 0000000..f052eab
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/6124.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7534.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7534.blp
new file mode 100644
index 0000000..b0a01d7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7534.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7845.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7845.blp
new file mode 100644
index 0000000..a7bdf9d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7845.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7864.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7864.blp
new file mode 100644
index 0000000..50492cb
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7864.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7919.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7919.blp
new file mode 100644
index 0000000..334d249
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7919.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7970.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7970.blp
new file mode 100644
index 0000000..076b532
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7970.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7971.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7971.blp
new file mode 100644
index 0000000..5e701f9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/7971.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/8055.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/8055.blp
new file mode 100644
index 0000000..dc94259
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/8055.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/825.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/825.blp
new file mode 100644
index 0000000..c663ee7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/825.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/8390.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/8390.blp
new file mode 100644
index 0000000..ce86cd2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/BossesGen/8390.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396498.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396498.blp
new file mode 100644
index 0000000..8c9bd39
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396498.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396499.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396499.blp
new file mode 100644
index 0000000..f9219b7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396499.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396500.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396500.blp
new file mode 100644
index 0000000..9988825
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396500.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396501.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396501.blp
new file mode 100644
index 0000000..d751c46
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396501.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396502.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396502.blp
new file mode 100644
index 0000000..41cb78b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396502.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396503.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396503.blp
new file mode 100644
index 0000000..df641cc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396503.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396504.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396504.blp
new file mode 100644
index 0000000..6d7733e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396504.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396505.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396505.blp
new file mode 100644
index 0000000..9cb023f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396505.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396506.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396506.blp
new file mode 100644
index 0000000..6095342
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396506.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396507.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396507.blp
new file mode 100644
index 0000000..eb36599
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396507.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396508.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396508.blp
new file mode 100644
index 0000000..ba54435
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396508.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396509.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396509.blp
new file mode 100644
index 0000000..86ae0b3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396509.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396510.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396510.blp
new file mode 100644
index 0000000..4e667bc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396510.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396511.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396511.blp
new file mode 100644
index 0000000..f5585ba
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396511.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396512.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396512.blp
new file mode 100644
index 0000000..7c1583e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396512.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396513.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396513.blp
new file mode 100644
index 0000000..f74acb6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396513.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396514.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396514.blp
new file mode 100644
index 0000000..048813f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396514.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396515.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396515.blp
new file mode 100644
index 0000000..9411248
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/1396515.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526404.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526404.blp
new file mode 100644
index 0000000..e053ec6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526404.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526410.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526410.blp
new file mode 100644
index 0000000..787ef37
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526410.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526415.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526415.blp
new file mode 100644
index 0000000..2f177fc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526415.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526416.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526416.blp
new file mode 100644
index 0000000..2886917
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/526416.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608231.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608231.blp
new file mode 100644
index 0000000..03b1182
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608231.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608232.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608232.blp
new file mode 100644
index 0000000..14d4e90
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608232.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608233.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608233.blp
new file mode 100644
index 0000000..7b8896e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608233.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608234.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608234.blp
new file mode 100644
index 0000000..e827317
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608234.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608235.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608235.blp
new file mode 100644
index 0000000..251e3bf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608235.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608236.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608236.blp
new file mode 100644
index 0000000..38ef8c3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608236.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608237.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608237.blp
new file mode 100644
index 0000000..685f74f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608237.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608238.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608238.blp
new file mode 100644
index 0000000..2cf0efe
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608238.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608239.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608239.blp
new file mode 100644
index 0000000..d4fe254
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608239.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608240.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608240.blp
new file mode 100644
index 0000000..c3c80ff
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608240.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608241.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608241.blp
new file mode 100644
index 0000000..e5bbe06
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608241.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608242.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608242.blp
new file mode 100644
index 0000000..5910d9d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608242.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608243.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608243.blp
new file mode 100644
index 0000000..0460167
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608243.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608244.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608244.blp
new file mode 100644
index 0000000..4b20e7f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608244.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608245.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608245.blp
new file mode 100644
index 0000000..0588254
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608245.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608246.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608246.blp
new file mode 100644
index 0000000..ca52512
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608246.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608247.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608247.blp
new file mode 100644
index 0000000..90af421
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608247.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608248.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608248.blp
new file mode 100644
index 0000000..2a5309b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608248.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608249.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608249.blp
new file mode 100644
index 0000000..45734b7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608249.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608250.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608250.blp
new file mode 100644
index 0000000..9d3d7c5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608250.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608251.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608251.blp
new file mode 100644
index 0000000..0b247f1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608251.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608252.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608252.blp
new file mode 100644
index 0000000..4f229a9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608252.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608253.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608253.blp
new file mode 100644
index 0000000..c2fdb31
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608253.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608254.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608254.blp
new file mode 100644
index 0000000..79d8147
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608254.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608255.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608255.blp
new file mode 100644
index 0000000..0cc1e4f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608255.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608256.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608256.blp
new file mode 100644
index 0000000..6fd863d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608256.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608257.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608257.blp
new file mode 100644
index 0000000..626db35
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608257.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608258.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608258.blp
new file mode 100644
index 0000000..36b2208
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608258.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608259.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608259.blp
new file mode 100644
index 0000000..5a3c30f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608259.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608260.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608260.blp
new file mode 100644
index 0000000..fc4477a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608260.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608261.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608261.blp
new file mode 100644
index 0000000..d69297c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608261.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608262.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608262.blp
new file mode 100644
index 0000000..f9999c9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608262.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608263.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608263.blp
new file mode 100644
index 0000000..be0388b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608263.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608264.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608264.blp
new file mode 100644
index 0000000..3cef4a3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608264.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608265.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608265.blp
new file mode 100644
index 0000000..72427dd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608265.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608266.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608266.blp
new file mode 100644
index 0000000..c88680d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608266.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608267.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608267.blp
new file mode 100644
index 0000000..6d58d15
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608267.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608312.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608312.blp
new file mode 100644
index 0000000..c73764a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608312.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608313.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608313.blp
new file mode 100644
index 0000000..7ca9bbf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/EncounterJournal/Lore/608313.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/1981967-communities.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/1981967-communities.blp
new file mode 100644
index 0000000..ffcf809
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/1981967-communities.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593917-bluemenu-goldborder-horiz.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593917-bluemenu-goldborder-horiz.blp
new file mode 100644
index 0000000..d9fdcb2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593917-bluemenu-goldborder-horiz.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593918-bluemenu-main.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593918-bluemenu-main.blp
new file mode 100644
index 0000000..beb0ff3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593918-bluemenu-main.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593919-bluemenu-vert.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593919-bluemenu-vert.blp
new file mode 100644
index 0000000..c4f7df1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/593919-bluemenu-vert.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/626421-battlenet-portrait.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/626421-battlenet-portrait.blp
new file mode 100644
index 0000000..849ad23
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Guild/626421-battlenet-portrait.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/4616456-groupfinder-eye.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/4616456-groupfinder-eye.blp
new file mode 100644
index 0000000..d9e71d2
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/4616456-groupfinder-eye.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/5171843-roleicons.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/5171843-roleicons.blp
new file mode 100644
index 0000000..17c17c9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/5171843-roleicons.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/985877-groupfinder.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/985877-groupfinder.blp
new file mode 100644
index 0000000..6bd6006
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LFG/985877-groupfinder.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LevelUp/LevelUpIcon-LFD.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LevelUp/LevelUpIcon-LFD.blp
new file mode 100644
index 0000000..78c56db
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LevelUp/LevelUpIcon-LFD.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LevelUp/LevelUpTex.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LevelUp/LevelUpTex.blp
new file mode 100644
index 0000000..2d8f34a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/LevelUp/LevelUpTex.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/3046538-auctionhouse-chrome.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/3046538-auctionhouse-chrome.blp
new file mode 100644
index 0000000..42c3c3c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/3046538-auctionhouse-chrome.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/383588-professions-book-left.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/383588-professions-book-left.blp
new file mode 100644
index 0000000..8e21134
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/383588-professions-book-left.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/383589-professions-book-right.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/383589-professions-book-right.blp
new file mode 100644
index 0000000..884ec82
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/383589-professions-book-right.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4417031-professions-chrome-sheet.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4417031-professions-chrome-sheet.blp
new file mode 100644
index 0000000..222c0cc
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4417031-professions-chrome-sheet.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620669-ui-profession-alchemy.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620669-ui-profession-alchemy.blp
new file mode 100644
index 0000000..d6939a1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620669-ui-profession-alchemy.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620670-ui-profession-blacksmithing.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620670-ui-profession-blacksmithing.blp
new file mode 100644
index 0000000..cffddb3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620670-ui-profession-blacksmithing.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620671-ui-profession-cooking.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620671-ui-profession-cooking.blp
new file mode 100644
index 0000000..4ff1c5a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620671-ui-profession-cooking.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620672-ui-profession-enchanting.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620672-ui-profession-enchanting.blp
new file mode 100644
index 0000000..9ed650c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620672-ui-profession-enchanting.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620673-ui-profession-engineering.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620673-ui-profession-engineering.blp
new file mode 100644
index 0000000..6e40bee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620673-ui-profession-engineering.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620674-ui-profession-fishing.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620674-ui-profession-fishing.blp
new file mode 100644
index 0000000..2c4e591
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620674-ui-profession-fishing.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620675-ui-profession-herbalism.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620675-ui-profession-herbalism.blp
new file mode 100644
index 0000000..86b9470
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620675-ui-profession-herbalism.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620676-ui-profession-inscription.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620676-ui-profession-inscription.blp
new file mode 100644
index 0000000..bac6404
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620676-ui-profession-inscription.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620677-ui-profession-jewelcrafting.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620677-ui-profession-jewelcrafting.blp
new file mode 100644
index 0000000..27da66a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620677-ui-profession-jewelcrafting.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620678-ui-profession-leatherworking.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620678-ui-profession-leatherworking.blp
new file mode 100644
index 0000000..40c00a1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620678-ui-profession-leatherworking.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620679-ui-profession-mining.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620679-ui-profession-mining.blp
new file mode 100644
index 0000000..a873b19
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620679-ui-profession-mining.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620680-ui-profession-skinning.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620680-ui-profession-skinning.blp
new file mode 100644
index 0000000..2b65d92
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620680-ui-profession-skinning.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620681-ui-profession-tailoring.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620681-ui-profession-tailoring.blp
new file mode 100644
index 0000000..bd3f63b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4620681-ui-profession-tailoring.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4625448-professions-recipe-background-blacksmithing.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4625448-professions-recipe-background-blacksmithing.blp
new file mode 100644
index 0000000..7752061
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4625448-professions-recipe-background-blacksmithing.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4625450-professions-recipe-background-alchemy.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4625450-professions-recipe-background-alchemy.blp
new file mode 100644
index 0000000..2e32e93
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4625450-professions-recipe-background-alchemy.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4626279-professions-skillbar-mask.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4626279-professions-skillbar-mask.blp
new file mode 100644
index 0000000..1e10002
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4626279-professions-skillbar-mask.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4627497-professions-recipe-background-tailoring.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4627497-professions-recipe-background-tailoring.blp
new file mode 100644
index 0000000..fe9241f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4627497-professions-recipe-background-tailoring.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4659666-professions-recipe-background.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4659666-professions-recipe-background.blp
new file mode 100644
index 0000000..359491e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4659666-professions-recipe-background.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4671747-professions-recipe-background-cooking.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4671747-professions-recipe-background-cooking.blp
new file mode 100644
index 0000000..bce3eaa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4671747-professions-recipe-background-cooking.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4683154-skillbar-fill-flipbook-blacksmithing.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4683154-skillbar-fill-flipbook-blacksmithing.blp
new file mode 100644
index 0000000..544bd6b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4683154-skillbar-fill-flipbook-blacksmithing.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693223-skillbar-fill-flipbook-enchanting.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693223-skillbar-fill-flipbook-enchanting.blp
new file mode 100644
index 0000000..899c7f8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693223-skillbar-fill-flipbook-enchanting.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693230-skillbar-fill-flipbook-tailoring.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693230-skillbar-fill-flipbook-tailoring.blp
new file mode 100644
index 0000000..2029731
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693230-skillbar-fill-flipbook-tailoring.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693237-skillbar-fill-flipbook-jewelcrafting.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693237-skillbar-fill-flipbook-jewelcrafting.blp
new file mode 100644
index 0000000..ea38c01
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4693237-skillbar-fill-flipbook-jewelcrafting.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4696956-skillbar-fill-flipbook-alchemy.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4696956-skillbar-fill-flipbook-alchemy.blp
new file mode 100644
index 0000000..f80129a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4696956-skillbar-fill-flipbook-alchemy.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4696971-skillbar-fill-flipbook-leatherworking.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4696971-skillbar-fill-flipbook-leatherworking.blp
new file mode 100644
index 0000000..22e96db
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4696971-skillbar-fill-flipbook-leatherworking.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4722478-professions-recipe-background-engineering.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4722478-professions-recipe-background-engineering.blp
new file mode 100644
index 0000000..405ff7e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4722478-professions-recipe-background-engineering.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723112-professions-recipe-background-jewelcrafting.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723112-professions-recipe-background-jewelcrafting.blp
new file mode 100644
index 0000000..8d64003
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723112-professions-recipe-background-jewelcrafting.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723119-professions-background-inscription.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723119-professions-background-inscription.blp
new file mode 100644
index 0000000..7887c5b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723119-professions-background-inscription.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723154-professions-recipe-background-leatherworking.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723154-professions-recipe-background-leatherworking.blp
new file mode 100644
index 0000000..3d28419
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723154-professions-recipe-background-leatherworking.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723159-professions-recipe-background-herbalism.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723159-professions-recipe-background-herbalism.blp
new file mode 100644
index 0000000..aec4baa
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723159-professions-recipe-background-herbalism.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723189-professions-recipe-background-mining.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723189-professions-recipe-background-mining.blp
new file mode 100644
index 0000000..9dea9f7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723189-professions-recipe-background-mining.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723308-professions-recipe-background-skinning.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723308-professions-recipe-background-skinning.blp
new file mode 100644
index 0000000..f041548
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723308-professions-recipe-background-skinning.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723316-professions-recipe-background-fishing.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723316-professions-recipe-background-fishing.blp
new file mode 100644
index 0000000..ca3e4a4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723316-professions-recipe-background-fishing.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723320-professions-recipe-background-enchanting.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723320-professions-recipe-background-enchanting.blp
new file mode 100644
index 0000000..e36d27e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4723320-professions-recipe-background-enchanting.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872261-skillbar-fill-flipbook-cooking.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872261-skillbar-fill-flipbook-cooking.blp
new file mode 100644
index 0000000..9a89097
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872261-skillbar-fill-flipbook-cooking.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872264-skillbar-fill-flipbook-inscription.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872264-skillbar-fill-flipbook-inscription.blp
new file mode 100644
index 0000000..a873cda
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872264-skillbar-fill-flipbook-inscription.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872267-skillbar-fill-flipbook-skinning.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872267-skillbar-fill-flipbook-skinning.blp
new file mode 100644
index 0000000..d8f76e3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4872267-skillbar-fill-flipbook-skinning.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4881558-skillbar-fill-flipbook-engineering.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4881558-skillbar-fill-flipbook-engineering.blp
new file mode 100644
index 0000000..de43db8
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Professions/4881558-skillbar-fill-flipbook-engineering.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4200162-spellbook-skilllinetab.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4200162-spellbook-skilllinetab.blp
new file mode 100644
index 0000000..30af066
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4200162-spellbook-skilllinetab.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4556093-talents.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4556093-talents.blp
new file mode 100644
index 0000000..55fdb19
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4556093-talents.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4633068-talents-circle-mask.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4633068-talents-circle-mask.blp
new file mode 100644
index 0000000..a08804e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4633068-talents-circle-mask.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4731579-talents-circle-sheenmask.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4731579-talents-circle-sheenmask.blp
new file mode 100644
index 0000000..9be7abd
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/4731579-talents-circle-sheenmask.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5506565-spellbook-items.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5506565-spellbook-items.blp
new file mode 100644
index 0000000..6963c0a
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5506565-spellbook-items.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5684744-questlog.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5684744-questlog.blp
new file mode 100644
index 0000000..308b999
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5684744-questlog.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5794906-spellbook-sheen-mask.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5794906-spellbook-sheen-mask.blp
new file mode 100644
index 0000000..509eacf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5794906-spellbook-sheen-mask.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-header.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-header.blp
new file mode 100644
index 0000000..044026d
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-header.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-left.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-left.blp
new file mode 100644
index 0000000..5a0c5f4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-left.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-ribbon.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-ribbon.blp
new file mode 100644
index 0000000..6d874a7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-ribbon.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-right.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-right.blp
new file mode 100644
index 0000000..8342d7f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-bg-right.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-spellbook-backgrounds.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-spellbook-backgrounds.blp
new file mode 100644
index 0000000..f4c744c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5834697-spellbook-backgrounds.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5899876-spellbook-spellicon-mask.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5899876-spellbook-spellicon-mask.blp
new file mode 100644
index 0000000..4bb8af5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5899876-spellbook-spellicon-mask.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5922242-spellbook-petautocast-mask.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5922242-spellbook-petautocast-mask.blp
new file mode 100644
index 0000000..0773141
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Spellbook/5922242-spellbook-petautocast-mask.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631290-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631290-talents-bg.blp
new file mode 100644
index 0000000..2fc6bea
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631290-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631293-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631293-talents-bg.blp
new file mode 100644
index 0000000..0ca7033
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631293-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631299-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631299-talents-bg.blp
new file mode 100644
index 0000000..946f5f5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631299-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631304-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631304-talents-bg.blp
new file mode 100644
index 0000000..bccf7ee
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631304-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631310-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631310-talents-bg.blp
new file mode 100644
index 0000000..a5c2111
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631310-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631313-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631313-talents-bg.blp
new file mode 100644
index 0000000..862067c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631313-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631318-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631318-talents-bg.blp
new file mode 100644
index 0000000..36ee3ed
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631318-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631321-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631321-talents-bg.blp
new file mode 100644
index 0000000..d979eb5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631321-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631337-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631337-talents-bg.blp
new file mode 100644
index 0000000..9854b04
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631337-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631340-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631340-talents-bg.blp
new file mode 100644
index 0000000..cd00d75
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631340-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631348-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631348-talents-bg.blp
new file mode 100644
index 0000000..8a6957f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631348-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631371-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631371-talents-bg.blp
new file mode 100644
index 0000000..25bad9c
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631371-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631374-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631374-talents-bg.blp
new file mode 100644
index 0000000..c08d6fe
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631374-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631377-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631377-talents-bg.blp
new file mode 100644
index 0000000..ecad0f0
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631377-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631381-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631381-talents-bg.blp
new file mode 100644
index 0000000..a2ffbf5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631381-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631383-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631383-talents-bg.blp
new file mode 100644
index 0000000..9dd85c6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631383-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631386-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631386-talents-bg.blp
new file mode 100644
index 0000000..ca7ad80
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631386-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631389-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631389-talents-bg.blp
new file mode 100644
index 0000000..4bb88e4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631389-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631392-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631392-talents-bg.blp
new file mode 100644
index 0000000..a489663
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631392-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631395-talents-bg.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631395-talents-bg.blp
new file mode 100644
index 0000000..4d688f6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/4631395-talents-bg.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/DeathKnight.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/DeathKnight.blp
new file mode 100644
index 0000000..8e3c0b4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/DeathKnight.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Druid.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Druid.blp
new file mode 100644
index 0000000..675cb20
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Druid.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Hunter.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Hunter.blp
new file mode 100644
index 0000000..75e16e3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Hunter.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Mage.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Mage.blp
new file mode 100644
index 0000000..8497f39
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Mage.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Paladin.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Paladin.blp
new file mode 100644
index 0000000..527ac8b
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Paladin.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Priest.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Priest.blp
new file mode 100644
index 0000000..ca20b94
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Priest.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Rogue.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Rogue.blp
new file mode 100644
index 0000000..e76e615
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Rogue.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Shaman.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Shaman.blp
new file mode 100644
index 0000000..75b9fe1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Shaman.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Warlock.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Warlock.blp
new file mode 100644
index 0000000..eb3a692
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Warlock.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Warrior.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Warrior.blp
new file mode 100644
index 0000000..879133f
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Artifact/Warrior.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Cunning.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Cunning.blp
new file mode 100644
index 0000000..dfa61b3
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Cunning.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Ferocity.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Ferocity.blp
new file mode 100644
index 0000000..e90c9c9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Ferocity.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Tenacity.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Tenacity.blp
new file mode 100644
index 0000000..255f7b9
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/Pet_Tenacity.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-health.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-health.blp
new file mode 100644
index 0000000..101cc8e
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-health.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-health.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-health.tga
new file mode 100644
index 0000000..b844962
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-health.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-mana.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-mana.blp
new file mode 100644
index 0000000..8b4c4bf
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-mana.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-mana.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-mana.tga
new file mode 100644
index 0000000..c69d227
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-globe-mana.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-orbgloss.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-orbgloss.blp
new file mode 100644
index 0000000..37ce2da
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-orbgloss.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-orbgloss.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-orbgloss.tga
new file mode 100644
index 0000000..6d0e3e7
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-orbgloss.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-desat.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-desat.blp
new file mode 100644
index 0000000..1997116
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-desat.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-desat.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-desat.tga
new file mode 100644
index 0000000..939a7b5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-desat.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-gold.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-gold.blp
new file mode 100644
index 0000000..72768c1
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-gold.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-gold.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-gold.tga
new file mode 100644
index 0000000..a11d4b5
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-ring-gold.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-shadow.blp b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-shadow.blp
new file mode 100644
index 0000000..80a6dd6
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-shadow.blp differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-shadow.tga b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-shadow.tga
new file mode 100644
index 0000000..bf925b4
Binary files /dev/null and b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/Textures/Talents/glyph-shadow.tga differ
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/bootstrap.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/bootstrap.lua
new file mode 100644
index 0000000..a923511
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/bootstrap.lua
@@ -0,0 +1,90 @@
+-- DragonUI_NewEra/bootstrap.lua
+-- Namespace + DragonUI handshake + SavedVariables. Loads before everything else.
+--
+-- CONVENTION (downport rule): every ported NewEra file begins with
+-- local NE = DragonUI_NewEra
+-- instead of NewEra's original `NE = NE or {}` global. This keeps NewEra's
+-- pervasive `NE.*` references working with a one-line change per file, while
+-- staying collision-safe (no global `NE` leak).
+
+local ADDON = ...
+
+DragonUI_NewEra = DragonUI_NewEra or {}
+local NE = DragonUI_NewEra
+NE.name = ADDON
+NE.version = "0.1.0-s0"
+
+-- Hard dependency on the base HUD addon. The TOC `## Dependencies: DragonUI`
+-- guarantees DragonUI's files have run by the time we parse.
+NE.dragon = _G.DragonUI
+if not NE.dragon then
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffff5555DragonUI_NewEra|r requires DragonUI; not loading.")
+ end
+ NE.disabled = true
+ return
+end
+
+-- Optional shim library. compat/ refines this and degrades gracefully if absent.
+NE.hasClassicAPI = _G["!!!ClassicAPI"] ~= nil
+
+-- Stable per-character key for per-char data inside the account-wide DB
+-- (equipment sets, talent prefs). Mirrors NewEra's NE.CharKey(). Only valid
+-- after PLAYER_LOGIN.
+function NE.CharKey()
+ return (UnitName("player") or "?") .. "-" .. (GetRealmName() or "")
+end
+
+-- Simple registry the QA harness reads: panels append { name=, frame=, open=, close= }.
+NE.qa = NE.qa or { modules = {} }
+
+-- Localization seam. Keys ARE the English text: NE.L["Retail bags"] returns a registered
+-- translation if a locale file set one, else the English key itself. All user-facing NewEra
+-- strings should route through NE.L (or, when WoW already ships a localized GLOBAL string like
+-- SEARCH/BACKPACK/CHARACTER, prefer that global with an English literal fallback). A future
+-- locale file, loaded after this, translates by assigning into it:
+-- local L = DragonUI_NewEra.L; if GetLocale() == "deDE" then L["Retail bags"] = "…" end
+NE.L = NE.L or setmetatable({}, { __index = function(_, k) return k end })
+
+-- SavedVariables. Our OWN db holds panel-internal per-char state (equipment sets, talent prefs)
+-- plus account-wide window positions (db.windowPos[key], see FrameUtil.PersistWindowPosition).
+-- Panel ENABLE flags live in DragonUI (DragonUIDB) via profile.newera for a unified UX.
+local CURRENT_SCHEMA = 3
+NE.migrations = NE.migrations or {}
+NE.migrations[1] = function(db)
+ db.equipmentSets = db.equipmentSets or {} -- [charKey] = sets
+ db.talents = db.talents or {} -- [charKey] = talent prefs
+end
+NE.migrations[2] = function(db)
+ db.companionFavorites = db.companionFavorites or {} -- [charKey][filterType..":"..creatureID] = true
+ db.guildChat = db.guildChat or {} -- [realm-guildName] = rolling chat log (modules/guild/Chat.lua)
+end
+NE.migrations[3] = function(db)
+ -- Level-up unlocks learned from the live server (modules/levelup/Harvest.lua). Keyed by REALM
+ -- first: one installed copy of this addon is expected to be used across several servers, and
+ -- what a private server teaches at level 40 must not leak into another's banner.
+ db.levelup = db.levelup or {}
+ db.levelup.realms = db.levelup.realms or {} -- [realm] = { classes, bgs, dungeons, talentTotals }
+end
+
+local function applyMigrations(db)
+ db.schema = db.schema or 0
+ while db.schema < CURRENT_SCHEMA do
+ local step = NE.migrations[db.schema + 1]
+ if not step then break end
+ step(db)
+ db.schema = db.schema + 1
+ end
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("ADDON_LOADED")
+boot:SetScript("OnEvent", function(self, _, name)
+ if name ~= ADDON then return end
+ DragonUI_NewEraDB = DragonUI_NewEraDB or {}
+ applyMigrations(DragonUI_NewEraDB)
+ NE.db = DragonUI_NewEraDB
+ NE.ready = true
+ if NE.OnReady then NE.OnReady() end -- integration/Register.lua sets this
+ self:UnregisterAllEvents()
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/COVERAGE.md b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/COVERAGE.md
new file mode 100644
index 0000000..de77ead
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/COVERAGE.md
@@ -0,0 +1,92 @@
+# compat/ — Modern-API Coverage Matrix
+
+`!!!ClassicAPI` is now a **HARD dependency** (`## Dependencies: DragonUI, !!!ClassicAPI`). Because of
+its `!!!` name prefix **and** the dependency ordering, ClassicAPI's files run **before** any
+`compat/` file. ClassicAPI therefore **owns** the modern C_* namespaces; `compat/` no longer vendors
+fallbacks for anything ClassicAPI provides — it ships **only** the handful of symbols ClassicAPI does
+not define.
+
+Each surviving `compat/` file still uses the `C_X = C_X or {}; if not C_X.Fn then C_X.Fn = … end`
+guard, so a real ClassicAPI impl is never clobbered; with ClassicAPI loaded first these guards are
+simply already satisfied for the symbols it owns.
+
+**Source legend**
+- `ClassicAPI` — provided by `!!!ClassicAPI` (loads first). **No compat/ shim shipped.**
+- `compat` — `compat/` defines it because ClassicAPI does NOT.
+- `stub` — `compat/` defines it but it CANNOT answer correctly on 3.3.5a (and ClassicAPI does not
+ emulate it); returns a safe value. Recorded at runtime in `NE.compat.stubs`.
+- `DragonUI` / `global` — satisfied by base DragonUI or a native 3.3.5a global.
+
+---
+
+## Owned by ClassicAPI — compat/ ships NOTHING
+
+| Namespace / symbol | Where ClassicAPI defines it |
+|---|---|
+| `C_Timer.After` / `NewTimer` / `NewTicker` | `Util/C_Timer.lua` (AnimationGroup pool). **`compat/C_Timer.lua` deleted.** |
+| `C_Texture.GetAtlasInfo` / `GetAtlasExists` / `RegisterAtlas` / `RegisterAtlasTable` / `AtlasData` | `Util/C_Texture.lua` (full atlas registry, identical return shape). **`compat/C_Texture.lua` deleted.** |
+| `Mixin` / `CreateFromMixins` / `CreateAndInitFromMixin` | `Util/Mixin.lua`. ClassicAPI defines all three (see note below re: `compat/Mixin.lua`). |
+| `C_Container.GetContainerItemInfo` (TABLE) / `GetContainerItemID` / `GetContainerItemLink` / `GetContainerNumSlots` / `GetContainerNumFreeSlots` / `UseContainerItem` / `PickupContainerItem` | `Util/C_Container.lua` |
+| `C_Item.GetItemInfo` / `GetItemInfoInstant` / `GetItemIconByID` / `GetItemCount` / ItemLocation family | `Util/C_Item.lua` |
+| `C_Spell.GetSpellInfo` / `GetSpellName` / `GetSpellSubtext` / `GetSpellTexture` / `GetSpellCooldown` / `GetSpellDescription` / `GetSpellLink` | `Util/C_Spell.lua` |
+| `C_SpellBook.GetSpellBookItemName` (+ global) / `PickupSpellBookItem` (+ global) / `GetSpellLinkFromSpellID` / `HasPetSpells` | `Util/C_SpellBook.lua` |
+| `C_Map.GetBestMapForUnit` / `IsWorldMap` / `WorldMap` | `Util/C_Map.lua` |
+| `C_NewItems.*` | `Util/C_NewItems.lua` |
+| `GetItemInfoInstant`, `GetPhysicalScreenSize`, `SearchBoxTemplate` (XML) | `Util/C_Item.lua`, `Util/PixelUtil.lua`, `Templates/UIPanelTemplates.*` |
+| `C_UIDropDownMenu_Initialize` / `_AddButton` / `_CreateInfo` / `C_UIDropDownMenuTemplate` | `Templates/C_UIDropDownMenu.{lua,xml}`, reached via `Load.xml` → `Templates/Load.xml`. Note for any menu deeper than two levels: this grows `C_UIDROPDOWNMENU_MAXLEVELS` on demand (`:124-130`), which the 3.3.5a global does not. **Consumer: `core/Menu.lua`** — it prefers these over the native `UIDropDownMenu_*` for exactly that reason (the Cooldown Manager's ready-sound menu is three levels), and falls back to the native two-level API if ClassicAPI is ever absent. |
+
+`C_GetContainerItemInfo` table fields ClassicAPI returns
+(`iconFileID/stackCount/isLocked/quality/isReadable/hasLoot/hyperlink/isFiltered/hasNoValue/itemID/isBound`)
+**match** the fields v1 reads (`.iconFileID/.stackCount/.isLocked/.quality/.hyperlink/.itemID`). No adapter needed.
+
+---
+
+## Shipped by compat/ — symbols ClassicAPI does NOT provide
+
+| Symbol | Source | compat file | Notes |
+|---|---|---|---|
+| `Mixin` / `CreateFromMixins` / `CreateAndInitFromMixin` | compat (redundant — see note) | `Mixin.lua` | Retained pending review; ClassicAPI's `Util/Mixin.lua` already defines these and loads first, so the bodies here are dead at runtime. |
+| `C_Container.GetContainerItemCooldown` | compat | `C_Container.lua` | Forward to 3.3.5 global. ClassicAPI marks this INCOMPLETE (it ships `GetItemCooldown`, not the container variant). Used by `core/ItemGrid.lua` (cooldown swipe). |
+| `C_Container.GetContainerItemQuestInfo` | compat (partial) | `C_Container.lua` | Positional → table `{isQuestItem,questID/questId,isActive}`. ClassicAPI marks this INCOMPLETE. Used by `core/ItemGrid.lua` (quest-item border). If the 3.3.5 global is absent, returns empty — **partial**, recorded. |
+| `C_Item.GetItemSpell` | compat | `C_Item.lua` | Forward to 3.3.5 global. ClassicAPI's `C_Item` has no `GetItemSpell`. Used by `core/ItemGrid.lua`. |
+| `C_Item.GetItemQualityColor` | compat | `C_Item.lua` | Forward, or fallback to `ITEM_QUALITY_COLORS`. ClassicAPI's `C_Item` has no `GetItemQualityColor`. |
+| `C_Spell.GetSpellPowerCost` | **stub** | `C_Spell.lua` | 3.3.5 has no per-spell power-cost API and ClassicAPI does not emulate one → returns `{}`. Source does `… or {}` then `pairs()`, so the cost overlay shows nothing. Recorded. |
+| `C_Map.GetMapInfo` | **stub** | `C_Map.lua` | No uiMapID→metadata on 3.3.5; ClassicAPI doesn't define it. Returns nil. Recorded. v1 never calls C_Map. |
+| `C_Map.GetPlayerMapPosition` | **stub (partial)** | `C_Map.lua` | Wraps legacy `GetPlayerMapPosition` in a `:GetXY()` object; ignores uiMapID. ClassicAPI doesn't define it. Recorded. |
+| `GetSpellBookItemTexture` (global) | compat | `SpellBook.lua` | Maps to 3.3.5 `GetSpellTexture(slot,bookType)`. ClassicAPI's `C_SpellBook` does not alias this. Used by `modules/spellbook`. |
+| `GetSpellBookItemInfo` (global) | compat | `SpellBook.lua` | Returns `"SPELL", spellID` (spellID parsed from the hyperlink). No 3.3.5 / ClassicAPI equivalent. Used by `modules/spellbook`. |
+| `GameTooltip:SetSpellBookItem` | compat | `SpellBook.lua` | Aliased to `GameTooltip:SetSpell`. No 3.3.5 / ClassicAPI equivalent. Used by `modules/spellbook`. |
+| `C_EquipmentSet.*` | compat (custom backend gate) | `C_EquipmentSet.lua` | ClassicAPI ships NO `C_EquipmentSet`. CharacterPanel uses the ItemRack-model custom backend in its own SavedVariables; this file publishes a safe namespace so stray `C_EquipmentSet.*` refs don't nil-error. |
+
+> **Mixin note (for human review):** the task brief listed `compat/Mixin.lua` under "ClassicAPI does
+> NOT provide", but `!!!ClassicAPI/Util/Mixin.lua` **does** define `Mixin`, `CreateFromMixins`, and
+> `CreateAndInitFromMixin`, and loads first. The file is therefore redundant. It was **kept** to
+> respect the explicit brief; it can be deleted (behavior-preserving) once confirmed.
+
+---
+
+## Out of scope (not shimmed; owned elsewhere or by ClassicAPI)
+
+`C_SpecializationInfo.*` (Talents-owned), `C_NewItems.*` (ClassicAPI), `C_MerchantFrame.*`
+(MerchantFrame-owned), `C_CVar.*` / `C_AddOns.*` (native globals / ClassicAPI), `C_ActionBar.*`,
+`C_Reputation.*`, `C_UnitAuras.*`, `C_FriendList.*` (ClassicAPI), `C_GuildInfo.*`, `C_ChatBubbles.*`
+— all **not-needed-v1**.
+
+---
+
+## Runtime self-report
+
+`NE.compat` exposes:
+- capability bools: `classicAPI, mixin, container, item, spell, map`
+- `NE.compat.stubs` — array of `{ sym, why }` for every symbol that could only be stubbed
+ (`C_Spell.GetSpellPowerCost`, `C_Map.GetMapInfo`, `C_Map.GetPlayerMapPosition`, and the no-native
+ fallback branch of `C_Container.GetContainerItemQuestInfo`). QA's `/dnetest` can dump this.
+
+## Headline
+
+- `!!!ClassicAPI` is a **hard dependency** and owns `C_Timer`, `C_Texture`, `Mixin`, the bulk of
+ `C_Container` / `C_Item` / `C_Spell` / `C_SpellBook`, and `C_Map.GetBestMapForUnit`.
+- `compat/C_Timer.lua` and `compat/C_Texture.lua` were **deleted** (fully ClassicAPI-owned).
+- `compat/` now ships only ClassicAPI gaps: two `C_Container` fns, two `C_Item` fns, one `C_Spell`
+ stub, two `C_Map` stubs, three spellbook globals, and the `C_EquipmentSet` custom-backend gate
+ (plus the redundant `Mixin.lua`, retained pending review).
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Container.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Container.lua
new file mode 100644
index 0000000..815e552
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Container.lua
@@ -0,0 +1,50 @@
+-- DragonUI_NewEra/compat/C_Container.lua
+-- Fills the C_Container.* symbols !!!ClassicAPI does NOT define:
+-- GetContainerItemCooldown, GetContainerItemQuestInfo.
+--
+-- DOWNPORT: Classic 1.15 namespaced the container API under C_Container and changed
+-- GetContainerItemInfo to return a TABLE. !!!ClassicAPI is a HARD dependency and loads
+-- first, providing the table-returning GetContainerItemInfo plus GetContainerItemID /
+-- GetContainerItemLink / GetContainerNumSlots / GetContainerNumFreeSlots /
+-- UseContainerItem / PickupContainerItem. It marks GetContainerItemCooldown and
+-- GetContainerItemQuestInfo as INCOMPLETE (it exposes GetItemCooldown, not the
+-- container variant, and no quest-info accessor), so we fill those two — both of which
+-- v1's ItemGrid uses (cooldown swipe + quest-item border).
+
+local NE = DragonUI_NewEra
+if not NE or NE.disabled then return end
+
+C_Container = C_Container or {}
+local C = C_Container
+
+-- GetContainerItemCooldown(bag, slot) -> start, duration, enable. ClassicAPI does not
+-- define this (it ships GetItemCooldown instead); forward to the 3.3.5a global.
+if not C.GetContainerItemCooldown then
+ C.GetContainerItemCooldown = GetContainerItemCooldown
+end
+
+-- GetContainerItemQuestInfo(bag, slot) -> table (retail shape). ClassicAPI does not
+-- define this. 3.3.5: GetContainerItemQuestInfo returns isQuestItem, questId, isActive
+-- (positional). v1 only reads .isQuestItem / .questId, so adapt positional -> table; if
+-- the global is absent, return a safe empty table (no quest item) and record the partial.
+if not C.GetContainerItemQuestInfo then
+ if type(GetContainerItemQuestInfo) == "function" then
+ function C.GetContainerItemQuestInfo(bag, slot)
+ local isQuestItem, questId, isActive = GetContainerItemQuestInfo(bag, slot)
+ return {
+ isQuestItem = isQuestItem,
+ questID = questId,
+ questId = questId, -- tolerate both casings
+ isActive = isActive,
+ }
+ end
+ else
+ function C.GetContainerItemQuestInfo(bag, slot)
+ return { isQuestItem = nil, questID = nil, questId = nil, isActive = nil }
+ end
+ NE.compat.RecordStub("C_Container.GetContainerItemQuestInfo",
+ "no native GetContainerItemQuestInfo on this client; returns empty (no quest item)")
+ end
+end
+
+NE.compat.container = true
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_EquipmentSet.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_EquipmentSet.lua
new file mode 100644
index 0000000..cab7758
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_EquipmentSet.lua
@@ -0,0 +1,294 @@
+-- DragonUI_NewEra/compat/C_EquipmentSet.lua — map the retail C_EquipmentSet
+-- namespace (used by NewEra's Equipment Manager source) onto 3.3.5a's NATIVE
+-- equipment-manager, which lives as a GLOBAL function family, NOT under
+-- C_EquipmentSet (CONTRACT_S1 §A.4):
+--
+-- GetNumEquipmentSets() -> number
+-- GetEquipmentSetInfo(index) -> name, texture (index is 1-based ORDINAL)
+-- GetEquipmentSetInfoByName(name) -> texture, index, isEquipped, numItems
+-- UseEquipmentSet(name) -- equip the set
+-- SaveEquipmentSet(name, iconIndex) -- create/overwrite (iconIndex, NOT a path!)
+-- DeleteEquipmentSet(name)
+-- GetEquipmentSetItemIDs(name) -> { [slot]=itemID }
+-- GetEquipmentSetLocations(name) -> { [slot]=location }
+-- EquipmentSetContainsLockedItems(name) -> bool
+-- GetEquipmentSetIconInfo(index) -> texture, realIndex
+--
+-- THE IMPEDANCE MISMATCH this shim resolves:
+-- * retail keys sets by an opaque integer `setID`; 3.3.5 keys by NAME + a
+-- volatile 1-based ordinal (the ordinal renumbers when a set is deleted).
+-- * retail's icon argument is a texture PATH; 3.3.5's SaveEquipmentSet takes an
+-- icon INDEX into the macro-icon list (GetMacroIconInfo).
+-- So we synthesise a STABLE integer setID per name (id↔name maps, never reused
+-- within a session) and convert texture-path icons to the nearest macro index.
+--
+-- This file is a thin namespace so any NewEra code that literally calls
+-- C_EquipmentSet.* still works; the actual backend decision (native vs custom
+-- ItemRack-model) lives in EquipmentSets.lua, which calls the same globals.
+--
+-- 3.3.5 LAW (project memory): native equipment API is FLAKY on private servers.
+-- We PROBE GetNumEquipmentSets at load and set NE.cap.equipmentSets; if the probe
+-- fails or the family is absent, EquipmentSets.lua falls back to a custom backend
+-- and this shim's calls are simply never exercised. We NEVER clobber a real
+-- C_EquipmentSet (ClassicAPI or a future server) — define only what's missing.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+NE.cap = NE.cap or {}
+
+local function log(msg)
+ if NE.charpanel and NE.charpanel._log then NE.charpanel._log(msg); return end
+ if NE.Log then NE.Log("EQUIPCOMPAT", msg) end
+end
+
+-- ----------------------------------------------------------------------------
+-- Capability probe. The native family is present AND callable?
+-- ----------------------------------------------------------------------------
+local function probeNative()
+ -- DOWNPORT/REPORT: CLIENT-SIDE ONLY (deliberate). The native equipment-manager API
+ -- (GetNumEquipmentSets / SaveEquipmentSet / UseEquipmentSet) depends on SERVER support — on servers
+ -- that lack it, or where it's flaky or DISABLED (CVar equipmentManager=0 → "The Equipment Manager is
+ -- disabled"), sets silently break. We use the self-contained client-side backend instead: sets live
+ -- in our SavedVariables (DragonUI_NewEraDB) and equipping is a physical item swap, so it works on ANY
+ -- server. Native is intentionally never used.
+ return false
+end
+
+local NATIVE = probeNative()
+NE.cap.equipmentSets = NATIVE
+
+-- If the native family isn't usable, there's nothing to map onto — the custom
+-- backend (EquipmentSets.lua) handles everything in Lua. Still publish an empty
+-- namespace so a stray C_EquipmentSet.* reference doesn't nil-error; each method
+-- degrades to a safe value.
+if not NATIVE then
+ if not _G.C_EquipmentSet then
+ _G.C_EquipmentSet = {
+ CanUseEquipmentSets = function() return false end,
+ GetNumEquipmentSets = function() return 0 end,
+ GetEquipmentSetIDs = function() return {} end,
+ GetEquipmentSetID = function() return nil end,
+ GetEquipmentSetInfo = function() return nil end,
+ GetItemIDs = function() return {} end,
+ GetIgnoredSlots = function() return {} end,
+ CreateEquipmentSet = function() end,
+ SaveEquipmentSet = function() end,
+ ModifyEquipmentSet = function() end,
+ DeleteEquipmentSet = function() end,
+ UseEquipmentSet = function() return false end,
+ PickupEquipmentSet = function() end,
+ EquipmentSetContainsLockedItems = function() return false end,
+ IgnoreSlotForSave = function() end,
+ UnignoreSlotForSave = function() end,
+ IsSlotIgnoredForSave = function() return false end,
+ ClearIgnoredSlotsForSave = function() end,
+ }
+ NE.compat = NE.compat or {}
+ NE.compat.stubs = NE.compat.stubs or {}
+ NE.compat.stubs["C_EquipmentSet"] = "no native engine; custom backend live"
+ end
+ return
+end
+
+-- ----------------------------------------------------------------------------
+-- NATIVE path: build the C_EquipmentSet.* adapter onto the 3.3.5 globals.
+-- Never clobber an existing impl (ClassicAPI / future server / hydrate path).
+-- ----------------------------------------------------------------------------
+if _G.C_EquipmentSet and _G.C_EquipmentSet.GetNumEquipmentSets then
+ -- Something already provides a real namespace; trust it.
+ return
+end
+
+local C = _G.C_EquipmentSet or {}
+_G.C_EquipmentSet = C
+
+-- Stable setID <-> name synthesis. 3.3.5 names are the real identity; we mint a
+-- monotonically-increasing integer id per name we see, and never reuse an id for
+-- a different name in a session, so the pane's selectedSetID stays valid even as
+-- the native ordinal renumbers under deletes.
+local nameToID, idToName = {}, {}
+local nextID = 1
+local function idFor(name)
+ if not name then return nil end
+ local id = nameToID[name]
+ if not id then
+ id = nextID; nextID = nextID + 1
+ nameToID[name] = id
+ idToName[id] = name
+ end
+ return id
+end
+local function nameFor(id)
+ return idToName[id]
+end
+
+-- Ordinal (native 1-based index) for a name — needed because some native getters
+-- take the ordinal, not the name. Rebuilt each call (cheap; <=20 sets).
+local function ordinalFor(name)
+ local n = _G.GetNumEquipmentSets() or 0
+ for i = 1, n do
+ local iname = _G.GetEquipmentSetInfo(i)
+ if iname == name then return i end
+ end
+ return nil
+end
+
+-- texture path -> macro-icon INDEX for SaveEquipmentSet. Scan the macro-icon list
+-- for a matching path; fall back to index 1 ("question mark") if not found. Most
+-- equip-set icons ARE equipped-item textures, which RefreshEquipmentSetIconInfo
+-- prepends as NEGATIVE indices — SaveEquipmentSet accepts those too, so we first
+-- try GetEquipmentSetIconInfo's negative range.
+local function iconPathToIndex(path)
+ if not path then return 1 end
+ if type(path) == "number" then return path end -- already an index
+ if type(_G.GetEquipmentSetIconInfo) == "function" and type(_G.GetNumMacroIcons) == "function" then
+ -- RefreshEquipmentSetIconInfo populates the equipped-item prefix; calling
+ -- GetEquipmentSetInfoByName / any set getter keeps it fresh enough. Scan a
+ -- bounded window (equipped items are few; macro icons are the bulk).
+ local total = (_G.GetNumMacroIcons() or 0) + 19
+ for i = 1, total do
+ local tex = _G.GetEquipmentSetIconInfo(i)
+ if tex == path then
+ local _, realIndex = _G.GetEquipmentSetIconInfo(i)
+ return realIndex or i
+ end
+ end
+ end
+ return 1
+end
+
+function C.CanUseEquipmentSets() return true end
+
+function C.GetNumEquipmentSets()
+ return _G.GetNumEquipmentSets() or 0
+end
+
+-- Array of synthesised setIDs, display order = native ordinal order.
+function C.GetEquipmentSetIDs()
+ local out = {}
+ local n = _G.GetNumEquipmentSets() or 0
+ for i = 1, n do
+ local name = _G.GetEquipmentSetInfo(i)
+ if name then out[#out + 1] = idFor(name) end
+ end
+ return out
+end
+
+function C.GetEquipmentSetID(name)
+ if not name then return nil end
+ local texture = _G.GetEquipmentSetInfoByName(name)
+ if texture == nil and not ordinalFor(name) then return nil end
+ return idFor(name)
+end
+
+-- retail shape: name, icon, setID, isEquipped, numItems, numEquipped,
+-- numInInventory, numLost, numIgnored.
+function C.GetEquipmentSetInfo(setID)
+ local name = nameFor(setID)
+ if not name then return nil end
+ local texture, index, isEquipped, numItems = _G.GetEquipmentSetInfoByName(name)
+ if texture == nil and index == nil then return nil end -- gone
+ -- 3.3.5 reports total + (implicitly) equipped via isEquipped only; we surface a
+ -- best-effort split: when equipped, all items are equipped. numInInventory/Lost
+ -- aren't exposed natively, so report 0 (the pane degrades the missing-item red
+ -- colouring, which is cosmetic).
+ numItems = numItems or 0
+ local numEquipped = isEquipped and numItems or 0
+ return name, texture, setID, isEquipped and true or false, numItems, numEquipped, 0, 0, 0
+end
+
+function C.GetItemIDs(setID)
+ local name = nameFor(setID)
+ if not name then return {} end
+ if type(_G.GetEquipmentSetItemIDs) == "function" then
+ return _G.GetEquipmentSetItemIDs(name) or {}
+ end
+ return {}
+end
+
+function C.GetIgnoredSlots(setID)
+ -- 3.3.5 has GetEquipmentSetLocations(name) -> { [slot]=location }; a location of
+ -- 1 (ITEM_INVENTORY_LOCATION_PLAYER-ish "ignored" sentinel) isn't exposed cleanly,
+ -- so report none ignored (cosmetic; shirt/tabard default-ignore is handled in the
+ -- save flow by the custom path, and native SaveEquipmentSet saves all slots).
+ return {}
+end
+
+function C.CreateEquipmentSet(name, icon)
+ if not name then return end
+ _G.SaveEquipmentSet(name, iconPathToIndex(icon))
+ idFor(name)
+end
+
+function C.SaveEquipmentSet(setID, icon)
+ local name = nameFor(setID)
+ if not name then return end
+ if icon ~= nil then
+ _G.SaveEquipmentSet(name, iconPathToIndex(icon))
+ else
+ -- Re-save keeping the current icon: read it back, convert, re-save.
+ local tex = _G.GetEquipmentSetInfoByName(name)
+ _G.SaveEquipmentSet(name, iconPathToIndex(tex))
+ end
+end
+
+function C.ModifyEquipmentSet(setID, newName, newIcon)
+ local name = nameFor(setID)
+ if not name then return end
+ if type(_G.RenameEquipmentSet) == "function" and newName and newName ~= name then
+ _G.RenameEquipmentSet(name, newName)
+ -- rebind the synthesised id to the new name
+ nameToID[name] = nil
+ nameToID[newName] = setID
+ idToName[setID] = newName
+ name = newName
+ end
+ if newIcon ~= nil then
+ _G.SaveEquipmentSet(name, iconPathToIndex(newIcon))
+ end
+end
+
+function C.DeleteEquipmentSet(setID)
+ local name = nameFor(setID)
+ if not name then return end
+ _G.DeleteEquipmentSet(name)
+ nameToID[name] = nil
+ idToName[setID] = nil
+end
+
+function C.UseEquipmentSet(setID)
+ local name = nameFor(setID)
+ if not name then return false end
+ if type(_G.EquipmentManager_EquipSet) == "function" then
+ _G.EquipmentManager_EquipSet(name) -- guards locked items + casting
+ else
+ _G.UseEquipmentSet(name)
+ end
+ return true
+end
+
+function C.PickupEquipmentSet(setID)
+ local name = nameFor(setID)
+ if name and type(_G.PickupEquipmentSet) == "function" then
+ _G.PickupEquipmentSet(name)
+ end
+end
+
+function C.EquipmentSetContainsLockedItems(setID)
+ local name = nameFor(setID)
+ if name and type(_G.EquipmentSetContainsLockedItems) == "function" then
+ return _G.EquipmentSetContainsLockedItems(name) and true or false
+ end
+ return false
+end
+
+-- Ignored-slot scratch state: 3.3.5's SaveEquipmentSet has no per-save ignore API,
+-- so these are no-ops on the native path (shirt/tabard get saved like any slot).
+function C.IgnoreSlotForSave() end
+function C.UnignoreSlotForSave() end
+function C.IsSlotIgnoredForSave() return false end
+function C.ClearIgnoredSlotsForSave() end
+
+NE.compat = NE.compat or {}
+NE.compat.equipmentSetBackend = "native"
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Item.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Item.lua
new file mode 100644
index 0000000..a764558
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Item.lua
@@ -0,0 +1,38 @@
+-- DragonUI_NewEra/compat/C_Item.lua
+-- Fills the C_Item.* symbols !!!ClassicAPI does NOT define: GetItemSpell, GetItemQualityColor.
+--
+-- DOWNPORT: Classic 1.15 namespaced item queries under C_Item. !!!ClassicAPI is a
+-- HARD dependency and loads first, providing GetItemInfo / GetItemInfoInstant /
+-- GetItemIconByID / GetItemCount and the ItemLocation family. It does NOT, however,
+-- define C_Item.GetItemSpell or C_Item.GetItemQualityColor — both of which v1 uses
+-- (ItemGrid reads GetItemSpell; quality colouring reads GetItemQualityColor). We
+-- forward those two to the 3.3.5a globals.
+
+local NE = DragonUI_NewEra
+if not NE or NE.disabled then return end
+
+C_Item = C_Item or {}
+local C = C_Item
+
+-- GetItemSpell(item) -> spellName, spellID (same on 3.3.5; not provided by ClassicAPI).
+if not C.GetItemSpell then
+ C.GetItemSpell = GetItemSpell
+end
+
+-- GetItemQualityColor(quality) -> r, g, b, hex (same on 3.3.5; not provided by ClassicAPI).
+if not C.GetItemQualityColor then
+ if type(GetItemQualityColor) == "function" then
+ C.GetItemQualityColor = GetItemQualityColor
+ else
+ -- Fallback from the global ITEM_QUALITY_COLORS table (always present on 3.3.5).
+ function C.GetItemQualityColor(quality)
+ local q = ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality]
+ if q then
+ return q.r, q.g, q.b, q.hex
+ end
+ return 1, 1, 1, "|cffffffff"
+ end
+ end
+end
+
+NE.compat.item = true
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Map.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Map.lua
new file mode 100644
index 0000000..859b53a
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Map.lua
@@ -0,0 +1,58 @@
+-- DragonUI_NewEra/compat/C_Map.lua
+-- Best-effort fillers for the handful of C_Map.* symbols !!!ClassicAPI does NOT define.
+--
+-- DOWNPORT: Classic 1.15's C_Map is a uiMapID-based world-map model that 3.3.5a
+-- simply does not have (3.3.5 uses SetMapToCurrentZone / GetCurrentMapAreaID /
+-- GetPlayerMapPosition with a totally different id space). The v1 modules
+-- (CharacterPanel, Spellbook, Talents, QuestFrame, MerchantFrame, MailFrame) do NOT
+-- call C_Map — it's referenced only by NewEra's WorldMap/Minimap/Quest map providers
+-- which are out of scope for v1. CONTRACTS §1 still asks for a best-effort shim so
+-- anything that incidentally probes C_Map gets safe nils rather than a nil-index
+-- error.
+--
+-- !!!ClassicAPI is a HARD dependency and loads first; it already provides
+-- C_Map.GetBestMapForUnit / C_Map.IsWorldMap / C_Map.WorldMap, so we DON'T touch
+-- those. It does NOT provide GetMapInfo or GetPlayerMapPosition — we fill those two
+-- as recorded stubs (and only if still missing).
+
+local NE = DragonUI_NewEra
+if not NE or NE.disabled then return end
+
+local compat = NE.compat
+
+C_Map = C_Map or {}
+local C = C_Map
+
+-- GetMapInfo(uiMapID) -> table { mapID, name, mapType, parentMapID }. 3.3.5 cannot
+-- map a retail uiMapID; return nil. Recorded. (ClassicAPI does not provide this.)
+if not C.GetMapInfo then
+ function C.GetMapInfo(uiMapID)
+ return nil
+ end
+ compat.RecordStub("C_Map.GetMapInfo",
+ "no uiMapID->map metadata on 3.3.5; returns nil")
+end
+
+-- GetPlayerMapPosition(uiMapID, unit) -> position table with :GetXY(). 3.3.5's
+-- GetPlayerMapPosition returns positional x, y for the CURRENT map. We wrap it in a
+-- minimal position object exposing GetXY() to match the retail return shape. The
+-- uiMapID argument is ignored (we use whatever map is currently selected).
+-- (ClassicAPI does not provide this.)
+if not C.GetPlayerMapPosition then
+ local posMethods = {}
+ posMethods.__index = posMethods
+ function posMethods:GetXY()
+ return self.x, self.y
+ end
+ function C.GetPlayerMapPosition(uiMapID, unit)
+ unit = unit or "player"
+ if type(GetPlayerMapPosition) ~= "function" then return nil end
+ local x, y = GetPlayerMapPosition(unit)
+ if not x or (x == 0 and y == 0) then return nil end
+ return setmetatable({ x = x, y = y }, posMethods)
+ end
+ compat.RecordStub("C_Map.GetPlayerMapPosition",
+ "wraps legacy GetPlayerMapPosition; ignores uiMapID arg, uses current map")
+end
+
+NE.compat.map = true
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Spell.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Spell.lua
new file mode 100644
index 0000000..3761f3a
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/C_Spell.lua
@@ -0,0 +1,31 @@
+-- DragonUI_NewEra/compat/C_Spell.lua
+-- Fills the ONE C_Spell.* symbol !!!ClassicAPI does NOT define: GetSpellPowerCost.
+--
+-- DOWNPORT: Classic 1.15 namespaced spell queries under C_Spell. !!!ClassicAPI is a
+-- HARD dependency and loads first, providing the bulk of C_Spell (GetSpellInfo /
+-- GetSpellName / GetSpellSubtext / GetSpellTexture / GetSpellCooldown /
+-- GetSpellDescription / GetSpellLink / ...). 3.3.5a has NO concept of a per-spell
+-- power-cost table and ClassicAPI does not emulate one, so we provide that stub.
+
+local NE = DragonUI_NewEra
+if not NE or NE.disabled then return end
+
+local compat = NE.compat
+
+C_Spell = C_Spell or {}
+local C = C_Spell
+
+-- GetSpellPowerCost(spellID) -> array of { type=, cost=, name=, ... }
+-- 3.3.5 has NO per-spell power-cost API and ClassicAPI does not emulate one. NewEra
+-- reads costInfo.type / costInfo.cost and matches against a power-bar's powerType. We
+-- cannot answer accurately, so return an empty list (the source already does `... or {}`
+-- and the pairs() loop no-ops). Recorded as a stub.
+if not C.GetSpellPowerCost then
+ function C.GetSpellPowerCost(spellID)
+ return {}
+ end
+ compat.RecordStub("C_Spell.GetSpellPowerCost",
+ "3.3.5 has no per-spell power-cost API; returns empty list (cost overlay shows nothing)")
+end
+
+NE.compat.spell = true
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Compat.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Compat.lua
new file mode 100644
index 0000000..e0b0b07
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Compat.lua
@@ -0,0 +1,52 @@
+-- DragonUI_NewEra/compat/Compat.lua
+-- Modern-API shim LOADER. Loads first in the compat/ block (after bootstrap).
+--
+-- Purpose: confirm !!!ClassicAPI is present, expose NE.compat capability booleans,
+-- and own the stub registry (NE.compat.RecordStub / NE.compat.stubs) that the
+-- per-symbol shims reuse to record symbols 3.3.5a genuinely can't answer. This file
+-- does NOT define C_* globals itself — each sibling file (C_Item.lua, C_Spell.lua,
+-- ...) ensures its own symbols, only when missing, in TOC order.
+--
+-- DOWNPORT: !!!ClassicAPI is now a HARD dependency (## Dependencies) and loads first
+-- (its `!!!` prefix + the dependency ordering), so it owns the modern C_* namespaces
+-- (C_Timer/C_Texture/C_Container/C_Item/C_Spell/C_SpellBook/C_Map/Mixin/...). The
+-- sibling files only fill the handful of symbols ClassicAPI does NOT provide.
+
+local NE = DragonUI_NewEra
+if not NE or NE.disabled then return end
+
+-- ---------------------------------------------------------------------------
+-- ClassicAPI detection. bootstrap.lua already set NE.hasClassicAPI from the
+-- "!!!ClassicAPI" load name; re-evaluate here in case load order surprised us.
+-- (It is a hard dependency, so this should always be true.)
+-- ---------------------------------------------------------------------------
+local hasClassicAPI = _G["!!!ClassicAPI"] ~= nil
+NE.hasClassicAPI = hasClassicAPI
+
+-- ---------------------------------------------------------------------------
+-- compat namespace + capability table. Each shim flips its bool true once it has
+-- guaranteed its symbols. Consumers may read NE.compat. to branch, but the
+-- v1 contract is simply: after compat/ loads, the modern globals exist.
+-- ---------------------------------------------------------------------------
+NE.compat = NE.compat or {}
+local compat = NE.compat
+compat.classicAPI = hasClassicAPI
+
+-- Capability booleans (set by sibling files; default false here so reads are safe
+-- even if a file failed to load). C_Timer/C_Texture are now owned entirely by
+-- ClassicAPI, so they have no compat/ sibling and no bool.
+compat.mixin = compat.mixin or false -- Mixin/CreateFromMixins/CreateAndInitFromMixin
+compat.container = compat.container or false -- C_Container.* gap-fills
+compat.item = compat.item or false -- C_Item.* gap-fills
+compat.spell = compat.spell or false -- C_Spell.* gap-fills
+compat.map = compat.map or false -- C_Map.* (best-effort; stubs)
+
+-- Record of symbols we could only STUB (3.3.5 genuinely can't answer, and ClassicAPI
+-- doesn't emulate). Sibling files append { sym=, why= }; COVERAGE.md documents these.
+-- Lets QA enumerate at runtime.
+compat.stubs = compat.stubs or {}
+function compat.RecordStub(sym, why)
+ compat.stubs[#compat.stubs + 1] = { sym = sym, why = why }
+end
+
+-- print nothing on success (per contract).
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Events.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Events.lua
new file mode 100644
index 0000000..fdbafbe
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Events.lua
@@ -0,0 +1,102 @@
+-- DragonUI_NewEra/compat/Events.lua — event-surface gap-fills.
+--
+-- (1) Frame:RegisterUnitEvent — added in 5.0 (MoP). NewEra uses it throughout the CooldownViewer
+-- to engine-filter UNIT_AURA / UNIT_SPELLCAST_* down to "player" (and "target"), which matters
+-- for more than tidiness: an unfiltered UNIT_AURA delivers every raid/party/nameplate unit's
+-- aura churn to the handler, per plate per tick in big pulls.
+--
+-- !!!ClassicAPI DOES implement the semantics (Util/EventHandler.lua:189) but only inside its
+-- `Private` namespace — `Private.EventHandler` is never exported to _G, so we cannot delegate.
+-- We therefore implement it ourselves on the Frame metatable: RegisterEvent + a per-frame,
+-- per-event allowed-unit set, enforced by wrapping that frame's OnEvent handler.
+--
+-- Only the frames that actually call RegisterUnitEvent pay the wrapper cost — the metatable
+-- gains the method, but the SetScript interception is installed per-instance, on demand.
+--
+-- (2) NE.EV_LEARNED_SPELL — NewEra's flavour-correct learn-a-spell event name.
+
+local NE = DragonUI_NewEra
+if not NE or NE.disabled then return end
+
+-- ---------------------------------------------------------------------------
+-- (1) Frame:RegisterUnitEvent
+-- ---------------------------------------------------------------------------
+
+-- The shared Frame method table. Adding here gives every frame the method without a per-frame cost.
+local frameProto
+do
+ local probe = CreateFrame("Frame")
+ local mt = getmetatable(probe)
+ frameProto = mt and mt.__index
+end
+
+if frameProto and not frameProto.RegisterUnitEvent then
+
+ -- Wrap an OnEvent handler so a filtered event whose unit isn't in the allow-set is dropped before
+ -- the handler sees it. The filter table is read at DISPATCH time (not capture time) so registering
+ -- more filtered events later works without re-wrapping.
+ local function filtering(inner)
+ return function(self, event, ...)
+ local allowed = self._neUnitFilter and self._neUnitFilter[event]
+ if allowed then
+ local unit = ...
+ if not (unit and allowed[unit]) then return end
+ end
+ return inner(self, event, ...)
+ end
+ end
+
+ -- Install the interception on one frame. Idempotent. Handles both orders: a handler already set
+ -- via SetScript (re-wrapped now) and one set later (wrapped by the shadowing SetScript).
+ local function ensureFilter(self)
+ if self._neUnitFilterHooked then return end
+ self._neUnitFilterHooked = true
+ self._neUnitFilter = {}
+
+ local rawSetScript = frameProto.SetScript
+ local existing = self:GetScript("OnEvent")
+ if existing then rawSetScript(self, "OnEvent", filtering(existing)) end
+
+ -- Instance field shadows the metatable method for this frame only.
+ self.SetScript = function(s, script, handler)
+ if script == "OnEvent" and handler then handler = filtering(handler) end
+ return rawSetScript(s, script, handler)
+ end
+ end
+
+ -- RegisterUnitEvent(event [, unit1, unit2, ...]). With no units this degrades to RegisterEvent,
+ -- matching the real API.
+ function frameProto.RegisterUnitEvent(self, event, ...)
+ if not event then return end
+ ensureFilter(self)
+ local n = select("#", ...)
+ if n > 0 then
+ local set = {}
+ for i = 1, n do
+ local u = select(i, ...)
+ if u then set[u] = true end
+ end
+ self._neUnitFilter[event] = set
+ else
+ self._neUnitFilter[event] = nil
+ end
+ return self:RegisterEvent(event)
+ end
+
+ -- Unregistering must drop the filter too, or a later plain RegisterEvent on the same event would
+ -- silently inherit the old unit restriction.
+ local rawUnregister = frameProto.UnregisterEvent
+ function frameProto.UnregisterEvent(self, event)
+ if self._neUnitFilter then self._neUnitFilter[event] = nil end
+ return rawUnregister(self, event)
+ end
+
+ if NE.compat then NE.compat.unitEvents = true end
+end
+
+-- ---------------------------------------------------------------------------
+-- (2) Learn-a-spell event
+-- ---------------------------------------------------------------------------
+-- NewEra picks this per flavour (Era/TBC fire LEARNED_SPELL_IN_TAB). 3.3.5a fires
+-- LEARNED_SPELL_IN_TAB as well; the modern SPELLS_CHANGED-adjacent names don't exist here.
+NE.EV_LEARNED_SPELL = "LEARNED_SPELL_IN_TAB"
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Locale.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Locale.lua
new file mode 100644
index 0000000..0fe6951
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/Locale.lua
@@ -0,0 +1,7 @@
+DragonUI_NewEra = DragonUI_NewEra or {}
+local NE = DragonUI_NewEra
+
+function NE:GetLocale()
+ local Ace = LibStub and LibStub("AceLocale-3.0", true)
+ return Ace and Ace:GetLocale("DragonUI_NewEra", true)
+end
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/SpellBook.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/SpellBook.lua
new file mode 100644
index 0000000..935730c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/compat/SpellBook.lua
@@ -0,0 +1,39 @@
+-- compat/SpellBook.lua — Cataclysm-era spellbook API shims !!!ClassicAPI does NOT cover.
+--
+-- The retail/Classic GetSpellBookItem* family (and GameTooltip:SetSpellBookItem) was added in
+-- Cataclysm (4.0). NewEra (Classic Era 1.15, rebased on a modern client) uses them; real 3.3.5a
+-- (WotLK) does NOT — it has the older INDEX+bookType API (GetSpellName / GetSpellTexture /
+-- GetSpellLink / PickupSpell / GameTooltip:SetSpell).
+--
+-- !!!ClassicAPI is a HARD dependency and loads first; its C_SpellBook already aliases the GLOBALS
+-- GetSpellBookItemName (-> GetSpellName) and PickupSpellBookItem (-> PickupSpell). It does NOT
+-- provide GetSpellBookItemTexture, GetSpellBookItemInfo, or GameTooltip:SetSpellBookItem, so we map
+-- those three onto the 3.3.5a functions. Each is defined ONLY when absent, so this is a no-op on a
+-- client that already provides them.
+
+-- GetSpellBookItemTexture(slot, bookType) -> texture path
+if not _G.GetSpellBookItemTexture and _G.GetSpellTexture then
+ function GetSpellBookItemTexture(slot, bookType)
+ return GetSpellTexture(slot, bookType)
+ end
+end
+
+-- GetSpellBookItemInfo(slot, bookType) -> slotType, spellID
+-- 3.3.5a has no slotType/spellID accessor; every spellbook slot is a castable SPELL. Derive the
+-- spellID from the hyperlink when available (used for tooltips + icon fallback). Returns nil spellID
+-- if the link can't be parsed — callers must tolerate that (cast/tooltip fall back to the name/slot).
+if not _G.GetSpellBookItemInfo then
+ function GetSpellBookItemInfo(slot, bookType)
+ local spellID
+ if _G.GetSpellLink then
+ local link = GetSpellLink(slot, bookType)
+ if link then spellID = tonumber(link:match("spell:(%d+)")) end
+ end
+ return "SPELL", spellID
+ end
+end
+
+-- GameTooltip:SetSpellBookItem(slot, bookType) -> 3.3.5a GameTooltip:SetSpell(slot, bookType)
+if _G.GameTooltip and not GameTooltip.SetSpellBookItem and GameTooltip.SetSpell then
+ GameTooltip.SetSpellBookItem = GameTooltip.SetSpell
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/AuraSnapshot.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/AuraSnapshot.lua
new file mode 100644
index 0000000..9754ac1
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/AuraSnapshot.lua
@@ -0,0 +1,106 @@
+-- DragonUI_NewEra/core/AuraSnapshot.lua — one aura scan per unit per frame (NE.aura).
+--
+-- DOWNPORT of NewEra's Core/Aura.lua. Purpose is unchanged: consumers that need "is aura X on unit
+-- Y right now" would each run their own 1..40 UnitBuff loop, and during aura bursts UNIT_AURA can
+-- fire many times per frame — so the same list gets walked over and over. This walks it ONCE per
+-- (unit, filter) per frame and hands out a shared read-only view.
+--
+-- PUBLIC:
+-- NE.aura.GetSnapshot(unit, filter) -> { byName = {[name]=row}, list = {row,...}, n = }
+-- filter: "HELPFUL" (default) or "HARMFUL"
+-- row: { name, icon, count, duration, expiration, dispelType, spellID, index }
+--
+-- The returned tables are POOLED and rewritten on the next frame's scan — read within the frame,
+-- never retain. (NewEra documents the same contract.)
+--
+-- !! 3.3.5a ARG-POSITION HAZARD (CONTRACTS §0: "some Blizzard returns are at shifted arg positions")
+-- UnitAura on 3.3.5a returns `rank` as the SECOND value, which was removed in MoP:
+-- 3.3.5a : name, RANK, icon, count, dispelType, duration, expirationTime, caster, ..., spellID
+-- modern : name, icon, count, dispelType, duration, expirationTime, caster, ..., spellID
+-- Every index after the first is shifted by one versus the NewEra source this is ported from. The
+-- source's own comment (CooldownViewer.lua ScanTargetTrackedAuras) documents the MODERN layout —
+-- do not copy those indices over.
+
+local NE = DragonUI_NewEra
+NE.aura = NE.aura or {}
+
+local UnitBuff, UnitDebuff, GetTime = UnitBuff, UnitDebuff, GetTime
+
+-- Per (unit .. filter) cached snapshot: { stamp = , byName = {}, list = {}, n = }
+-- GetTime() is constant for the whole frame in WoW, so it is a correct and cheap frame key.
+local cache = {}
+
+local MAX_AURAS = 40
+
+local function scan(snapshot, unit, filter)
+ local fn = (filter == "HARMFUL") and UnitDebuff or UnitBuff
+
+ local byName, list = snapshot.byName, snapshot.list
+ for k in pairs(byName) do byName[k] = nil end
+
+ local n = 0
+ for i = 1, MAX_AURAS do
+ -- 3.3.5a layout — see the hazard note in the file header.
+ local name, _rank, icon, count, dispelType, duration, expiration, _caster,
+ _isStealable, _shouldConsolidate, spellID = fn(unit, i)
+ if not name then break end
+
+ n = n + 1
+ local row = list[n]
+ if not row then row = {}; list[n] = row end
+
+ row.name = name
+ row.icon = icon
+ row.count = count or 0
+ row.dispelType = dispelType
+ row.duration = duration or 0
+ row.expiration = expiration or 0
+ row.spellID = spellID
+ row.index = i
+
+ -- First occurrence wins: with two ranks of the same buff present, the lower index is the one
+ -- the UI conventionally shows.
+ if byName[name] == nil then byName[name] = row end
+ end
+
+ snapshot.n = n
+ return snapshot
+end
+
+function NE.aura.GetSnapshot(unit, filter)
+ if not unit then return nil end
+ filter = filter or "HELPFUL"
+
+ -- A unit that isn't present yields an empty (but valid) snapshot, so callers never nil-check.
+ local key = unit .. filter
+ local snapshot = cache[key]
+ if not snapshot then
+ snapshot = { byName = {}, list = {}, n = 0, stamp = -1 }
+ cache[key] = snapshot
+ end
+
+ local now = GetTime()
+ if snapshot.stamp == now then return snapshot end
+ snapshot.stamp = now
+
+ if not UnitExists(unit) then
+ for k in pairs(snapshot.byName) do snapshot.byName[k] = nil end
+ snapshot.n = 0
+ return snapshot
+ end
+
+ return scan(snapshot, unit, filter)
+end
+
+-- Convenience: the row for one aura name, or nil. Buff-preferred, then debuff — the order the
+-- CooldownViewer's findPlayerAuraDataByName relies on.
+function NE.aura.FindByName(unit, name)
+ if not (unit and name) then return nil end
+ local snap = NE.aura.GetSnapshot(unit, "HELPFUL")
+ local row = snap and snap.byName[name]
+ if row then return row, false end
+ snap = NE.aura.GetSnapshot(unit, "HARMFUL")
+ row = snap and snap.byName[name]
+ if row then return row, true end
+ return nil
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ButtonSkin.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ButtonSkin.lua
new file mode 100644
index 0000000..2803b56
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ButtonSkin.lua
@@ -0,0 +1,259 @@
+-- DragonUI_NewEra/core/ButtonSkin.lua — shared modern button skin (NE.buttonskin.Skin).
+--
+-- DOWNPORT: NewEra Core/ButtonSkin.lua → 3.3.5a. Reskins a Button to retail's
+-- BigRedThreeSliceButton (the red 3-slice ESC-menu/dialog button). NewEra called the NATIVE
+-- Texture:SetAtlas (a retail/Era method) and read atlas dimensions via C_Texture.GetAtlasInfo.
+-- On 3.3.5a there is no Texture:SetAtlas, so every piece-atlas swap routes through NE.tex.SetAtlas
+-- (our coord registry), and dimensions come from NE.tex._atlasEntry. C_Texture.GetAtlasInfo is
+-- the compat shim — present, but only answers for atlases the Asset agent registered, so the
+-- whole reskin is FAIL-SAFE: missing 128-RedButton art → native button art kept (returns false).
+--
+-- §2 CONTRACT: exposed as NE.buttonskin.* (and NE.button.* kept for NewEra-name parity).
+-- Taint-safe: adds BACKGROUND textures + hooks visual-state scripts only.
+
+local NE = DragonUI_NewEra
+NE.buttonskin = NE.buttonskin or {}
+NE.button = NE.buttonskin -- DOWNPORT: NewEra used NE.button; alias to the contract name
+
+local ATLAS = "128-RedButton"
+
+local function pieceNames(postfix)
+ return ATLAS .. "-Left" .. postfix, "_" .. ATLAS .. "-Center" .. postfix, ATLAS .. "-Right" .. postfix
+end
+
+-- Port of ThreeSliceButtonMixin:UpdateScale.
+--
+-- DOWNPORT: retail's mixin does `self.Left:SetScale(scale)`. **Texture has no SetScale on 3.3.5a** —
+-- ClassicAPI has to synthesise even GetEffectiveScale for a Region by delegating to its parent
+-- (Util/WidgetAPI.lua:205), and adds no SetScale at all. So the caps are SIZED here instead, in real
+-- pixels, and the trim branch below no longer divides back out by a scale that was never applied.
+--
+-- This never ran until the 128-RedButton sheet was shipped: Skin returns false before reaching here
+-- when the atlas is missing, and every call site wraps it in pcall — so the error would have been
+-- swallowed, leaving a button with three blank textures over its hidden native art.
+local function updateScale(btn)
+ local d = btn._neThreeSlice
+ if not d or not d.leftInfo or not d.rightInfo then return end
+ local buttonH, buttonW = btn:GetHeight(), btn:GetWidth()
+ if buttonH <= 0 or d.leftInfo.height <= 0 then return end
+ local scale = buttonH / d.leftInfo.height
+ d.Left:SetHeight(buttonH); d.Right:SetHeight(buttonH)
+
+ local leftW, rightW = d.leftInfo.width * scale, d.rightInfo.width * scale
+ local both = leftW + rightW
+ if both > buttonW then
+ local extra = both - buttonW
+ local newLeftW, newRightW = leftW, rightW
+ if (leftW - extra) > rightW then
+ newLeftW = leftW - extra
+ elseif (rightW - extra) > leftW then
+ newRightW = rightW - extra
+ else
+ if leftW ~= rightW then
+ local uneven = math.abs(leftW - rightW)
+ extra = extra - uneven
+ newLeftW = math.min(leftW, rightW); newRightW = newLeftW
+ end
+ local half = extra / 2
+ newLeftW = newLeftW - half; newRightW = newRightW - half
+ end
+ -- DOWNPORT: the texcoord trim composes the atlas sub-rect AFTER NE.tex.SetAtlas has set it.
+ -- We re-resolve the atlas entry to compose the trim within the element's own rect.
+ local le = NE.tex._atlasEntry(pieceNames(d.postfix or ""))
+ d.Left:SetWidth(newLeftW)
+ d.Right:SetWidth(newRightW)
+ if le then
+ d.Left:SetTexCoord(le.left, le.left + (le.right - le.left) * (newLeftW / leftW), le.top, le.bottom)
+ end
+ else
+ NE.tex.SetAtlas(d.Left, pieceNames(d.postfix or ""), false)
+ d.Left:SetWidth(leftW)
+ d.Right:SetWidth(rightW)
+ end
+end
+
+-- Faithful port of ThreeSliceButtonMixin:UpdateButton — swap the 3 atlases per state.
+local function updateButton(btn, state)
+ local d = btn._neThreeSlice
+ if not d then return end
+ state = state or (btn.GetButtonState and btn:GetButtonState()) or "NORMAL"
+ if btn.IsEnabled and not btn:IsEnabled() then state = "DISABLED" end
+ local postfix = (state == "DISABLED" and "-Disabled") or (state == "PUSHED" and "-Pressed") or ""
+ d.postfix = postfix
+ local l, c, r = pieceNames(postfix)
+ -- DOWNPORT: native SetAtlas → NE.tex.SetAtlas.
+ NE.tex.SetAtlas(d.Left, l, true)
+ NE.tex.SetAtlas(d.Center, c)
+ NE.tex.SetAtlas(d.Right, r, true)
+ updateScale(btn)
+end
+
+-- Hide the button's native art so the 3-slice shows through.
+local function hideNativeArt(btn)
+ local name = btn.GetName and btn:GetName()
+ for _, key in ipairs({ "Left", "Middle", "Right" }) do
+ local t = btn[key] or (name and _G[name .. key])
+ if t and t.SetTexture then t:SetTexture(nil); if t.Hide then t:Hide() end end
+ end
+ for _, getter in ipairs({ "GetNormalTexture", "GetPushedTexture", "GetDisabledTexture" }) do
+ local t = btn[getter] and btn[getter](btn)
+ if t and t.SetTexture then t:SetTexture(nil); if t.Hide then t:Hide() end end
+ end
+ local h = btn.GetHighlightTexture and btn:GetHighlightTexture()
+ if h and h.SetTexture then h:SetTexture(nil) end
+end
+
+-- Reskin `btn`. Idempotent. opts.atlas overrides the family. Returns true on success.
+function NE.buttonskin.Skin(btn, opts)
+ if not btn or btn._neThreeSlice then return btn and btn._neThreeSlice ~= nil end
+ -- DOWNPORT: prefer our registry (NE.tex._atlasEntry) for dimensions; fall back to the
+ -- C_Texture shim. If neither knows the atlas, fail-safe (native art kept).
+ local leftInfo = NE.tex._atlasEntry(ATLAS .. "-Left")
+ local rightInfo = NE.tex._atlasEntry(ATLAS .. "-Right")
+ if not (leftInfo and rightInfo) and C_Texture and C_Texture.GetAtlasInfo then
+ leftInfo = leftInfo or C_Texture.GetAtlasInfo(ATLAS .. "-Left")
+ rightInfo = rightInfo or C_Texture.GetAtlasInfo(ATLAS .. "-Right")
+ end
+ if not (leftInfo and rightInfo and leftInfo.width and rightInfo.width) then return false end
+
+ hideNativeArt(btn)
+
+ local d = { leftInfo = leftInfo, rightInfo = rightInfo }
+ d.Left = btn:CreateTexture(nil, "BACKGROUND")
+ d.Left:SetPoint("TOPLEFT")
+ d.Right = btn:CreateTexture(nil, "BACKGROUND")
+ d.Right:SetPoint("TOPRIGHT")
+ d.Center = btn:CreateTexture(nil, "BACKGROUND")
+ d.Center:SetHorizTile(true)
+ d.Center:SetPoint("TOPLEFT", d.Left, "TOPRIGHT")
+ d.Center:SetPoint("BOTTOMRIGHT", d.Right, "BOTTOMLEFT")
+ btn._neThreeSlice = d
+
+ -- Highlight: a 3-slice matching the body, shown on hover.
+ local hi = NE.tex._atlasEntry(ATLAS .. "-Highlight")
+ if hi and hi.file then
+ local hiFile = NE.tex.localFiles[hi.file] or hi.file
+ local L, R = hi.left or 0, hi.right or 1
+ local T, B = hi.top or 0, hi.bottom or 1
+ local span = R - L
+ local lFrac = (leftInfo.width / (hi.width or 441)) * span
+ local rFrac = (rightInfo.width / (hi.width or 441)) * span
+ local function hlPiece(over, x0, x1)
+ local t = btn:CreateTexture(nil, "ARTWORK", nil, 2)
+ t:SetTexture(hiFile)
+ t:SetTexCoord(x0, x1, T, B)
+ t:SetAllPoints(over)
+ t:SetBlendMode("ADD")
+ t:Hide()
+ return t
+ end
+ d.hl = {
+ hlPiece(d.Left, L, L + lFrac),
+ hlPiece(d.Center, L + lFrac, R - rFrac),
+ hlPiece(d.Right, R - rFrac, R),
+ }
+ btn:HookScript("OnEnter", function() for _, t in ipairs(d.hl) do t:Show() end end)
+ btn:HookScript("OnLeave", function() for _, t in ipairs(d.hl) do t:Hide() end end)
+ end
+
+ updateButton(btn, "NORMAL")
+
+ btn:HookScript("OnMouseDown", function(self) updateButton(self, "PUSHED") end)
+ btn:HookScript("OnMouseUp", function(self) updateButton(self, "NORMAL") end)
+ btn:HookScript("OnEnable", function(self) updateButton(self) end)
+ btn:HookScript("OnDisable", function(self) updateButton(self) end)
+ btn:HookScript("OnSizeChanged", function(self) updateScale(self) end)
+ return true
+end
+
+-- ── The addon-wide sweep ────────────────────────────────────────────────────────────────────────
+--
+-- The red 3-slice is the addon's STANDARD button now, and this is what makes that true without
+-- rewriting sixty CreateFrame calls. Nearly every button NewEra builds is a plain
+-- UIPanelButtonTemplate, and on 3.3.5a that template's whole identity is one texture path: it
+-- SetNormalTextures "Interface\Buttons\UI-Panel-Button-Up". So the predicate reads the art rather
+-- than guessing at the template, which is the one thing a running frame can actually be asked.
+--
+-- Self-limiting by construction: Skin() clears that normal texture, so a skinned button no longer
+-- matches, and `_neThreeSlice` short-circuits a re-skin anyway.
+--
+-- OPT-OUT is `_nePlain` (already the codebase's marker for "this is a plain button standing in for a
+-- tab" — those live in a tab strip and would read as loose buttons in red) and `_neNoSkin` for
+-- anything else that has to stay stock.
+
+local PANEL_ART = "ui-panel-button-up"
+local MAX_DEPTH = 12
+local HOOK_DEPTH = 2 -- how deep Watch hooks OnShow; see below
+
+local function isPanelButton(f)
+ if not f or f._neThreeSlice or f._nePlain or f._neNoSkin then return false end
+ if not (f.GetObjectType and f:GetObjectType() == "Button") then return false end
+ local nt = f.GetNormalTexture and f:GetNormalTexture()
+ local path = nt and nt.GetTexture and nt:GetTexture()
+ if type(path) ~= "string" then return false end
+ return string.find(string.lower(path), PANEL_ART, 1, true) ~= nil
+end
+
+NE.buttonskin._isPanelButton = isPanelButton -- test seam
+
+-- Skin every panel button under `root`. Returns how many it skinned this pass.
+function NE.buttonskin.SkinPanelButtons(root, depth)
+ if not (root and root.GetChildren) then return 0 end
+ depth = (depth or 0) + 1
+ if depth > MAX_DEPTH then return 0 end
+ local n = 0
+ local kids = { root:GetChildren() }
+ for i = 1, #kids do
+ local c = kids[i]
+ if c and not c._neNoSkin then
+ if isPanelButton(c) and NE.buttonskin.Skin(c) then n = n + 1 end
+ n = n + NE.buttonskin.SkinPanelButtons(c, depth)
+ end
+ end
+ return n
+end
+
+-- Keep `root` skinned as it grows. A window is registered before its contents exist, and panes are
+-- built lazily on the tab that first needs them, so ONE sweep at registration would miss most of the
+-- addon. Three passes cover it: now, next frame (after the caller finishes building), and on show.
+--
+-- OnShow is also hooked on frames near the top of the tree — that is where tab panes live, and a pane
+-- built on first selection is shown immediately after, which is the moment its buttons appear. The
+-- depth cap keeps that to a couple of dozen hooks per window instead of one per frame.
+function NE.buttonskin.Watch(root)
+ if not root or root._neSkinWatched then return end
+ root._neSkinWatched = true
+
+ local function sweep()
+ NE.buttonskin.SkinPanelButtons(root)
+ NE.buttonskin._HookShown(root, 0)
+ end
+ root._neSkinSweep = sweep
+
+ sweep()
+ if C_Timer and C_Timer.After then C_Timer.After(0, sweep) end
+ if root.HookScript then
+ root:HookScript("OnShow", function()
+ sweep()
+ if C_Timer and C_Timer.After then C_Timer.After(0, sweep) end
+ end)
+ end
+end
+
+function NE.buttonskin._HookShown(frame, depth)
+ if depth >= HOOK_DEPTH or not (frame and frame.GetChildren) then return end
+ local kids = { frame:GetChildren() }
+ for i = 1, #kids do
+ local c = kids[i]
+ if c and c.HookScript and not c._neSkinShowHooked then
+ c._neSkinShowHooked = true
+ c:HookScript("OnShow", function(self)
+ NE.buttonskin.SkinPanelButtons(self)
+ if C_Timer and C_Timer.After then
+ C_Timer.After(0, function() NE.buttonskin.SkinPanelButtons(self) end)
+ end
+ end)
+ end
+ NE.buttonskin._HookShown(c, depth + 1)
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/CooldownNumbers.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/CooldownNumbers.lua
new file mode 100644
index 0000000..9c89ded
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/CooldownNumbers.lua
@@ -0,0 +1,180 @@
+-- DragonUI_NewEra/core/CooldownNumbers.lua — countdown text on Cooldown frames (NE.cd).
+--
+-- DOWNPORT: NewEra's Core/Cooldown.lua drives retail's BUILT-IN cooldown numbers — the engine draws
+-- them and `NE.cd.ApplyNumbers` just configures font / abbreviation threshold / minimum duration via
+-- Cooldown:SetCountdownFont / SetHideCountdownNumbers. 3.3.5a's Cooldown widget has none of that and
+-- draws no text at all, so this file OWNS the text: a FontString parented to the cooldown plus a
+-- throttled OnUpdate. Same public surface, so ported call sites are unchanged.
+--
+-- PUBLIC:
+-- NE.cd.FONT -- named font objects per surface
+-- NE.cd.ApplyNumbers(cooldown, opts) -- opts = { font = , show = }
+--
+-- opts fields are INDEPENDENT and sticky: ApplyNumbers{font=...} at OnLoad then ApplyNumbers{show=...}
+-- from SetTimerShown must not clear the font. Only keys present in `opts` are applied.
+
+local NE = DragonUI_NewEra
+NE.cd = NE.cd or {}
+
+-- Retail uses GameFontHighlightHugeOutline / ...Outline for the viewer surfaces. 3.3.5a has no
+-- "Huge" outlined game font, but the NumberFont family is outlined and is what Blizzard uses for
+-- action-button cooldown text, so it is the closest match.
+--
+-- Each entry is a FALLBACK CHAIN, not a single name: NumberFontNormalHuge is present in some 3.3.5a
+-- builds and not others (unlike NumberFontNormalLarge/Normal/Small, which are used by shipped
+-- addons in this install and are therefore known-good). Resolution walks the chain and takes the
+-- first font object that actually exists, so a missing name costs nothing and logs nothing.
+NE.cd.FONT = {
+ viewerEssential = { "NumberFontNormalHuge", "NumberFontNormalLarge", "NumberFontNormal" },
+ viewerUtility = { "NumberFontNormalLarge", "NumberFontNormal" },
+ viewerAura = { "NumberFontNormal" },
+ viewerBar = { "NumberFontNormalSmall", "NumberFontNormal" },
+}
+
+-- Resolve a font spec (a name, or a chain of names) to a live font object.
+local function resolveFont(spec)
+ if type(spec) == "string" then
+ return _G[spec], spec
+ end
+ if type(spec) == "table" then
+ for _, name in ipairs(spec) do
+ local obj = _G[name]
+ if obj then return obj, name end
+ end
+ return nil, table.concat(spec, "/")
+ end
+ return nil, tostring(spec)
+end
+
+-- Retail constants (Blizzard_CooldownViewer): abbreviate to minutes at 120s, and suppress the
+-- countdown entirely for cooldowns shorter than 2s (GCD-length noise).
+local ABBREV_THRESHOLD = 120
+local MIN_DURATION = 2.0
+local UPDATE_INTERVAL = 0.1
+
+local function formatTime(t)
+ if t >= 3600 then
+ return math.ceil(t / 3600) .. "h"
+ elseif t >= ABBREV_THRESHOLD then
+ return math.ceil(t / 60) .. "m"
+ elseif t >= 1 then
+ return tostring(math.floor(t))
+ end
+ return string.format("%.1f", t)
+end
+
+-- Paint the current remaining time. Returns false once the cooldown is over (or shouldn't show),
+-- so callers can tear the ticker down.
+local function paint(cd)
+ local text = cd._neText
+ if not text then return false end
+
+ local start, duration = cd._neStart, cd._neDuration
+ if not (cd._neShow and start and duration and duration >= MIN_DURATION) then
+ text:SetText("")
+ return false
+ end
+
+ local remaining = (start + duration) - GetTime()
+ if remaining <= 0 then
+ text:SetText("")
+ return false
+ end
+ text:SetText(formatTime(remaining))
+ return true
+end
+
+local function tick(cd, elapsed)
+ cd._neAccum = (cd._neAccum or 0) + elapsed
+ if cd._neAccum < UPDATE_INTERVAL then return end
+ cd._neAccum = 0
+ if not paint(cd) then cd:SetScript("OnUpdate", nil) end
+end
+
+-- Begin/stop the ticker based on the current stashed cooldown + show flag.
+local function refresh(cd)
+ local start, duration = cd._neStart, cd._neDuration
+ local active = cd._neShow
+ and start and start > 0
+ and duration and duration >= MIN_DURATION
+ and (start + duration) > GetTime()
+
+ if active then
+ -- Paint NOW rather than waiting for the first OnUpdate — otherwise the number is blank for up
+ -- to UPDATE_INTERVAL at the exact moment the cooldown starts, which reads as a flicker.
+ paint(cd)
+ cd._neAccum = 0
+ cd:SetScript("OnUpdate", tick)
+ else
+ cd:SetScript("OnUpdate", nil)
+ if cd._neText then cd._neText:SetText("") end
+ end
+end
+
+-- Intercept SetCooldown/Hide so we learn start+duration without the caller changing.
+-- 3.3.5a's Cooldown has SetCooldown and Hide but NOT Clear (added in a later expansion); we add a
+-- Clear that matches ClassicAPI's CooldownFrame_Clear semantics (hide + forget) so ported call
+-- sites can use either.
+local function instrument(cd)
+ if cd._neInstrumented then return end
+ cd._neInstrumented = true
+
+ local rawSetCooldown = cd.SetCooldown
+ cd.SetCooldown = function(self, start, duration, ...)
+ self._neStart, self._neDuration = start, duration
+ refresh(self)
+ return rawSetCooldown(self, start, duration, ...)
+ end
+
+ local rawHide = cd.Hide
+ cd.Hide = function(self, ...)
+ self._neStart, self._neDuration = nil, nil
+ refresh(self)
+ return rawHide(self, ...)
+ end
+
+ if not cd.Clear then
+ cd.Clear = function(self)
+ self._neStart, self._neDuration = nil, nil
+ refresh(self)
+ self:Hide()
+ end
+ end
+end
+
+-- opts = { font = , show = }. Only keys present are applied.
+function NE.cd.ApplyNumbers(cooldown, opts)
+ if not cooldown then return end
+ opts = opts or {}
+ instrument(cooldown)
+
+ if not cooldown._neText then
+ -- OVERLAY so the number sits above the swipe. Parented to the cooldown itself so it inherits
+ -- the cooldown's show/hide and frame level.
+ local fs = cooldown:CreateFontString(nil, "OVERLAY")
+ fs:SetPoint("CENTER", cooldown, "CENTER", 0, 0)
+ cooldown._neText = fs
+ cooldown._neShow = true
+ end
+
+ if opts.font then
+ local fontObject, tried = resolveFont(opts.font)
+ if fontObject then
+ cooldown._neText:SetFontObject(fontObject)
+ elseif NE.Log and not NE.cd._warnedFont then
+ -- Once per session, not once per icon: the whole chain missing is a single fact about the
+ -- client, and the text still renders in the FontString's inherited default.
+ NE.cd._warnedFont = true
+ NE.Log("CD", "no font object resolved from " .. tostring(tried))
+ end
+ end
+
+ if opts.show ~= nil then
+ cooldown._neShow = opts.show and true or false
+ end
+
+ refresh(cooldown)
+end
+
+-- Expose the formatter — the BuffBar duration label wants the same abbreviation rules.
+NE.cd.FormatTime = formatTime
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/FrameUtil.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/FrameUtil.lua
new file mode 100644
index 0000000..1ca56d9
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/FrameUtil.lua
@@ -0,0 +1,524 @@
+-- DragonUI_NewEra/core/FrameUtil.lua — shared frame utilities. The canonical pixel-perfect pin.
+--
+-- DOWNPORT: NewEra Core/FrameUtil.lua → 3.3.5a. Body is mostly raw WoW API and ports almost
+-- verbatim. Adaptations are marked `-- DOWNPORT:` inline. Top changed from `local NE = NE` to
+-- `local NE = DragonUI_NewEra` per the addon convention (CONTRACTS §0).
+--
+-- ONE PinPixelPerfect for the whole addon. Pins a frame to the 768/physicalHeight pixel
+-- scale (same crispness as every other UI frame). `userScale` is the per-frame multiplier
+-- (default 1.0). NewEra also exposes this on NE.frameutil (the §2 public surface) — we alias
+-- both NE.FrameUtil (NewEra's internal name) and NE.frameutil (the contract name).
+
+local NE = DragonUI_NewEra
+
+NE.FrameUtil = NE.FrameUtil or {}
+NE.frameutil = NE.FrameUtil -- DOWNPORT: §2 contract exposes NE.frameutil.*; alias to the same table
+
+-- THE shared "do it when combat ends" deferral. Runs fn immediately when not in
+-- lockdown; otherwise queues it for the next PLAYER_REGEN_ENABLED (one shared watcher
+-- frame for the whole addon).
+local regenJobs, regenWatcher
+function NE.FrameUtil.AfterCombat(fn)
+ if not (InCombatLockdown and InCombatLockdown()) then
+ fn()
+ return
+ end
+ regenJobs = regenJobs or {}
+ regenJobs[#regenJobs + 1] = fn
+ if not regenWatcher then
+ regenWatcher = CreateFrame("Frame")
+ regenWatcher:RegisterEvent("PLAYER_REGEN_ENABLED")
+ regenWatcher:SetScript("OnEvent", function()
+ local jobs = regenJobs
+ regenJobs = nil
+ if jobs then
+ for _, job in ipairs(jobs) do job() end
+ end
+ end)
+ end
+end
+
+-- SetScale on a PROTECTED frame raises in combat — defer those pins to PLAYER_REGEN_ENABLED.
+-- Pins coalesce by frame (only the last requested userScale applies).
+local deferredPins
+local function flushDeferredPins()
+ local pins = deferredPins
+ deferredPins = nil
+ if not pins then return end
+ for frame, userScale in pairs(pins) do
+ NE.FrameUtil.PinPixelPerfect(frame, userScale)
+ end
+end
+
+-- TRUE physical screen height in PIXELS, the stock-3.3.5a way (see Blizzard UIParent.lua:1170):
+-- parse GetScreenResolutions()[GetCurrentResolution()] ("WxH"), with the gxResolution CVar as a
+-- fallback. We do NOT use GetPhysicalScreenSize() — that is a retail-only function; ClassicAPI's
+-- shim returns GetScreenWidth/Height, which are LOGICAL UIParent units (physicalH / UIParent scale),
+-- not physical pixels. Feeding logical units into the math below makes the integer multiplier wrong
+-- (e.g. m=2 instead of 1) whenever UIParent's effective scale ~= 1 → frames pinned 2x too large.
+local function physicalScreenHeight()
+ local h
+ if GetScreenResolutions and GetCurrentResolution then
+ local cur = (({ GetScreenResolutions() })[GetCurrentResolution()]) or ""
+ local _, hh = string.match(cur, "(%d+).-(%d+)")
+ h = tonumber(hh)
+ end
+ if (not h or h <= 0) and GetCVar then
+ local _, hh = string.match(GetCVar("gxResolution") or "", "(%d+).-(%d+)")
+ h = tonumber(hh)
+ end
+ return h
+end
+NE.FrameUtil.PhysicalScreenHeight = physicalScreenHeight
+
+-- The canonical pixel base: (768/physicalHeight) × the automatic integer multiplier — or NIL
+-- when the user ticked Blizzard's UI Scale (useUiScale CVar).
+function NE.FrameUtil.PixelBaseScale()
+ if GetCVarBool and GetCVarBool("useUiScale") then return nil end
+ local ph = physicalScreenHeight()
+ if not ph or ph <= 0 then return nil end
+ return (768 / ph) * math.max(1, math.floor(ph / 1080 + 0.5))
+end
+
+-- Registry of every pinned frame → its last userScale, so a UI-scale / resolution change re-pins
+-- ALL of them live.
+local pinnedFrames = setmetatable({}, { __mode = "k" }) -- weak keys: don't keep dead frames alive
+
+function NE.FrameUtil.PinPixelPerfect(frame, userScale)
+ if not frame then return end
+ pinnedFrames[frame] = userScale or 1.0
+ if InCombatLockdown and InCombatLockdown() and frame.IsProtected and frame:IsProtected() then
+ local schedule = deferredPins == nil
+ deferredPins = deferredPins or {}
+ deferredPins[frame] = userScale or 1.0
+ if schedule then NE.FrameUtil.AfterCombat(flushDeferredPins) end
+ return
+ end
+ if GetCVarBool and GetCVarBool("useUiScale") then
+ frame:SetScale(userScale or 1.0)
+ return
+ end
+ local ph = physicalScreenHeight()
+ if not ph or ph <= 0 then return end
+ local m = math.max(1, math.floor(ph / 1080 + 0.5))
+ local target = (768 / ph) * m * (userScale or 1.0)
+ local parent = frame:GetParent() or UIParent
+ local parentScale = parent:GetEffectiveScale()
+ if parentScale and parentScale > 0 then
+ frame:SetScale(target / parentScale)
+ end
+end
+
+-- Drop a frame back OUT of the re-pin registry. Needed by core/Scale.lua: its "ui"/"custom" modes
+-- are a plain SetScale, but a window that was previously pinned ("No scaling" mode, or a module's
+-- own build-time pin) stays in pinnedFrames forever, so the next UI-scale/resolution change would
+-- silently re-pin it and throw the user's chosen scale away. Unpin on every non-pixel-perfect apply.
+function NE.FrameUtil.Unpin(frame)
+ if frame then pinnedFrames[frame] = nil end
+end
+
+-- Central re-pin: when the user changes UI scale / resolution, re-pin every registered frame.
+local function repinAllFrames()
+ for frame, us in pairs(pinnedFrames) do
+ if frame.GetObjectType then NE.FrameUtil.PinPixelPerfect(frame, us) end
+ end
+end
+
+-- Re-pin DEFERRED one frame, coalesced.
+local repinPending
+local function scheduleRepin()
+ if repinPending then return end
+ repinPending = true
+ local function run() repinPending = nil; repinAllFrames() end
+ if C_Timer and C_Timer.After then C_Timer.After(0, run) else run() end
+end
+
+-- DOWNPORT: 3.3.5a has UI_SCALE_CHANGED + CVAR_UPDATE; DISPLAY_SIZE_CHANGED also exists.
+-- The CVar-cache flip detection (un-ticking "UI Scale" without a scale event) ports as-is.
+local lastUseUi = GetCVarBool and GetCVarBool("useUiScale")
+local lastUiScale = GetCVar and GetCVar("uiScale")
+
+local pinScaleWatcher = CreateFrame("Frame")
+pinScaleWatcher:RegisterEvent("UI_SCALE_CHANGED")
+pinScaleWatcher:RegisterEvent("DISPLAY_SIZE_CHANGED")
+pinScaleWatcher:RegisterEvent("CVAR_UPDATE")
+pinScaleWatcher:SetScript("OnEvent", function(_, event)
+ if event == "CVAR_UPDATE" then
+ local function check()
+ local u = GetCVarBool and GetCVarBool("useUiScale")
+ local s = GetCVar and GetCVar("uiScale")
+ if u ~= lastUseUi or s ~= lastUiScale then
+ lastUseUi, lastUiScale = u, s
+ repinAllFrames()
+ end
+ end
+ if C_Timer and C_Timer.After then C_Timer.After(0, check) else check() end
+ return
+ end
+ lastUseUi = GetCVarBool and GetCVarBool("useUiScale")
+ lastUiScale = GetCVar and GetCVar("uiScale")
+ scheduleRepin()
+end)
+
+-- Font constants + setter. One home for the client font paths.
+-- DOWNPORT: NewEra hardcoded "Fonts\\FRIZQT__.TTF"; on 3.3.5a we prefer DragonUI's locale-aware
+-- font (NE.dragon.Fonts.PRIMARY) so CJK/Cyrillic clients don't render "???". Falls back to the
+-- literal path if DragonUI's font table isn't present.
+NE.font = NE.font or {}
+NE.font.FRIZ = (NE.dragon and NE.dragon.Fonts and NE.dragon.Fonts.PRIMARY) or "Fonts\\FRIZQT__.TTF"
+NE.font.MORPHEUS = "Fonts\\MORPHEUS.ttf"
+function NE.font.Set(fs, path, size, flags, fallbackObject)
+ if not fs:SetFont(path, size, flags or "") and fallbackObject then
+ fs:SetFontObject(fallbackObject)
+ end
+end
+
+-- ESC-close registration: add the frame (by global name) to UISpecialFrames, once.
+function NE.FrameUtil.EscClose(frame)
+ local name = type(frame) == "string" and frame
+ or (frame and frame.GetName and frame:GetName())
+ if not name then return end
+ for _, n in ipairs(UISpecialFrames) do
+ if n == name then return end
+ end
+ tinsert(UISpecialFrames, name)
+end
+
+-- Wire an item tooltip that keeps refreshing while the mouse sits still, so pressing SHIFT
+-- mid-hover pops the comparison tooltip (and releasing it drops the comparison again).
+--
+-- Stock GameTooltip_OnUpdate (GameTooltip.lua) does this for anyone who opts in:
+-- local owner = self:GetOwner();
+-- if ( owner and owner.UpdateTooltip ) then owner:UpdateTooltip(); end
+-- ...called every TOOLTIP_UPDATE_TIME (0.2s) while the tooltip is up. Bag buttons opt in at the
+-- end of their own OnEnter (ContainerFrame.lua: `self.UpdateTooltip =
+-- ContainerFrameItemButton_OnEnter`), which is the entire reason shift-compare works there.
+--
+-- A frame that just sets a tooltip in OnEnter and stops has no such refresh, so its comparison
+-- only ever appears if SHIFT happened to be held already when the mouse arrived. Route those
+-- through here instead of hand-rolling a modifier watcher: this is the stock contract, it costs
+-- nothing when no tooltip is showing, and it picks up ALT/CTRL-driven tooltip content for free.
+--
+-- `enter` is the existing OnEnter body. `leave` is optional; the default hides the tooltip.
+function NE.FrameUtil.WireLiveTooltip(frame, enter, leave)
+ if not (frame and enter) then return end
+ local function onEnter(self, ...)
+ enter(self, ...)
+ -- Re-armed on every pass: the refresh calls this same function, and an OnLeave in between
+ -- clears it, so the frame is only ever registered while genuinely hovered.
+ self.UpdateTooltip = onEnter
+ end
+ frame:SetScript("OnEnter", onEnter)
+ frame:SetScript("OnLeave", function(self, ...)
+ self.UpdateTooltip = nil
+ if leave then
+ leave(self, ...)
+ elseif GameTooltip then
+ GameTooltip:Hide()
+ end
+ end)
+ return onEnter
+end
+
+-- Keep a frame on screen after its SIZE changes.
+function NE.FrameUtil.KeepOnScreen(frame)
+ if not (frame and frame.SetClampedToScreen) then return end
+ frame:SetClampedToScreen(true)
+ local p1, rel, p2, x, y = frame:GetPoint(1)
+ if p1 then frame:SetPoint(p1, rel, p2, x or 0, y or 0) end
+end
+
+-- Disable-path fallback for XML-built frames: at PLAYER_LOGIN, if the module did NOT boot,
+-- unhook any unit watch and hide each named frame (combat-deferred).
+function NE.FrameUtil.HideWhenModuleOff(moduleName, ...)
+ local names = { ... }
+ local w = CreateFrame("Frame")
+ w:RegisterEvent("PLAYER_LOGIN")
+ w:SetScript("OnEvent", function()
+ if not NE.modules or NE.modules.IsBooted(moduleName) then return end
+ NE.FrameUtil.AfterCombat(function()
+ for _, n in ipairs(names) do
+ local f = _G[n]
+ if f then
+ if UnregisterUnitWatch then pcall(UnregisterUnitWatch, f) end
+ f:Hide()
+ end
+ end
+ end)
+ end)
+end
+
+-- Panel open/close sounds via an invisible CHILD frame (never HookScript on protected windows).
+-- DOWNPORT: 3.3.5a PlaySound returns (willPlay, soundHandle) for known kits; for unknown kit
+-- IDs it can hard-error, so wrap in pcall and fall back to the vanilla-native kit.
+function NE.FrameUtil.WirePanelSounds(frame, openKit, closeKit, fallbackOpen, fallbackClose)
+ if not frame or frame._neSoundWatcher then return end
+ local function play(kit, fallback)
+ if kit then
+ local ok, willPlay = pcall(PlaySound, kit)
+ if ok and willPlay then return end
+ end
+ if fallback then pcall(PlaySound, fallback) end
+ end
+ local w = CreateFrame("Frame", nil, frame)
+ w:SetScript("OnShow", function() play(openKit, fallbackOpen) end)
+ w:SetScript("OnHide", function() play(closeKit, fallbackClose) end)
+ frame._neSoundWatcher = w
+end
+
+-- Registry of persisted windows: { { frame, key, default }, ... }. Used by the resolution watcher.
+NE.FrameUtil._persisted = NE.FrameUtil._persisted or {}
+
+-- A signature for the current screen resolution. A real resolution change moves this; a normal
+-- login does not (so we never wipe saved positions just because the player logged in).
+local function currentResSig()
+ local r = (GetCVar and GetCVar("gxResolution")) or ""
+ if r == "" then
+ r = tostring(math.floor((GetScreenWidth() or 0) + 0.5)) .. "x"
+ .. tostring(math.floor((GetScreenHeight() or 0) + 0.5))
+ end
+ return r
+end
+
+-- Forget every saved position and snap each window back to its default anchor.
+local function resetAllToDefault()
+ local db = NE.db
+ for _, e in ipairs(NE.FrameUtil._persisted) do
+ if db and db.windowPos then db.windowPos[e.key] = nil end
+ NE.FrameUtil.RestoreWindowPosition(e.frame, e.key, e.default) -- no saved entry -> uses default
+ end
+end
+
+-- One shared watcher: when the screen RESOLUTION actually changes, reset all persisted windows to
+-- their defaults. A saved spot from a larger/wider resolution could otherwise land off-screen and
+-- become unreachable, so we deliberately drop it rather than try to nudge it back.
+local function ensureResetWatcher()
+ if NE.FrameUtil._resetWatcher then return end
+ local w = CreateFrame("Frame")
+ w:RegisterEvent("DISPLAY_SIZE_CHANGED") -- fires on a resolution change (also UIParent resize)
+ w:SetScript("OnEvent", function()
+ local db = NE.db
+ if not db then return end
+ local sig = currentResSig()
+ if db.lastRes and db.lastRes ~= sig then
+ resetAllToDefault()
+ end
+ db.lastRes = sig
+ end)
+ NE.FrameUtil._resetWatcher = w
+end
+
+-- Drag-to-move WITH persistence. Makes `frame` movable and remembers where the user drops it,
+-- account-wide in NE.db.windowPos[key] = { point, relPoint, x, y }, restoring it next session.
+-- `default` (optional) = { point, relPoint, x, y } applied when nothing is saved yet AND the spot
+-- the windows reset to when the screen resolution changes.
+-- `dragHandle` (optional) = the region that initiates the drag (a title band etc.); defaults to the
+-- frame itself. Either way it is `frame` that moves and whose position is saved.
+-- Anchors are always relative to UIParent (these are top-level windows), so a saved position is
+-- stable regardless of what the frame happened to be anchored to at drag time.
+function NE.FrameUtil.PersistWindowPosition(frame, key, default, dragHandle)
+ if not frame or not key then return end
+ local handle = dragHandle or frame
+
+ local function store()
+ local db = NE.db
+ if not db then return nil end
+ db.windowPos = db.windowPos or {}
+ db.windowPos[key] = db.windowPos[key] or {}
+ return db.windowPos[key]
+ end
+
+ frame:SetMovable(true)
+ frame:SetClampedToScreen(true)
+ handle:EnableMouse(true)
+ handle:RegisterForDrag("LeftButton")
+ handle:SetScript("OnDragStart", function() frame:StartMoving() end)
+ handle:SetScript("OnDragStop", function()
+ frame:StopMovingOrSizing()
+ local t = store()
+ if not t then return end
+ local p, _, rp, x, y = frame:GetPoint(1)
+ if p then
+ t.point, t.relPoint, t.x, t.y = p, rp or p, x or 0, y or 0
+ end
+ end)
+
+ table.insert(NE.FrameUtil._persisted, { frame = frame, key = key, default = default })
+
+ -- Seed / check the resolution baseline. Covers a resolution change made BEFORE this window was
+ -- first opened this session (the live watcher only catches changes after a window exists): if the
+ -- resolution differs from what we last saw, drop saved spots so nothing restores off-screen.
+ if NE.db then
+ local sig = currentResSig()
+ if NE.db.lastRes == nil then
+ NE.db.lastRes = sig
+ elseif NE.db.lastRes ~= sig then
+ resetAllToDefault()
+ NE.db.lastRes = sig
+ end
+ end
+ ensureResetWatcher()
+
+ -- Apply the saved position now (or the supplied default).
+ NE.FrameUtil.RestoreWindowPosition(frame, key, default)
+end
+
+-- Re-anchor `frame` to its saved position (set via PersistWindowPosition), falling back to
+-- `default` when nothing is stored. Safe to call repeatedly (e.g. on each OnShow).
+-- Returns true if a SAVED position was applied (false if it used the default / did nothing).
+function NE.FrameUtil.RestoreWindowPosition(frame, key, default)
+ if not frame or not key then return false end
+ local db = NE.db
+ local t = db and db.windowPos and db.windowPos[key]
+ if t and t.point then
+ frame:ClearAllPoints()
+ frame:SetPoint(t.point, UIParent, t.relPoint or t.point, t.x or 0, t.y or 0)
+ return true
+ end
+ if default and default.point then
+ frame:ClearAllPoints()
+ frame:SetPoint(default.point, UIParent, default.relPoint or default.point, default.x or 0, default.y or 0)
+ end
+ return false
+end
+
+-- Money text — the gold/silver/copper coin-icon string for a copper amount.
+NE.money = NE.money or {}
+function NE.money.Text(copper, empty)
+ if copper and copper > 0 then
+ return (GetCoinTextureString and GetCoinTextureString(copper)) or tostring(copper)
+ end
+ return empty or "—"
+end
+
+-- Shared 5-tier difficulty ladder (creature/quest level vs the player).
+function NE.difficultyTier(level)
+ local diff = (level or 0) - (UnitLevel("player") or 0)
+ if diff >= 5 then return "impossible"
+ elseif diff >= 3 then return "verydifficult"
+ elseif diff >= -2 then return "difficult"
+ elseif GetQuestGreenRange and (-diff) <= (GetQuestGreenRange("player") or 0) then return "standard"
+ else return "trivial" end
+end
+
+NE.color = NE.color or {}
+-- "ffrrggbb" hex (floor rounding) for a {r,g,b} colour table (0-1). No "|c" prefix.
+function NE.color.ToHex(color)
+ color = color or { r = 1, g = 1, b = 1 }
+ return string.format("ff%02x%02x%02x",
+ math.floor((color.r or 1) * 255), math.floor((color.g or 1) * 255), math.floor((color.b or 1) * 255))
+end
+-- Wrap `text` in a class colour code: |cffRRGGBB|r.
+function NE.color.WrapClass(classFile, text)
+ local c = (classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile]) or { r = 1, g = 1, b = 1 }
+ return "|c" .. NE.color.ToHex(c) .. (text or "") .. "|r"
+end
+-- Class colour as raw r,g,b (0-1). Honours CUSTOM_CLASS_COLORS, then RAID_CLASS_COLORS, then white.
+function NE.color.ClassRGB(classFile)
+ local pool = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
+ local c = classFile and pool and pool[classFile]
+ if not c then return 1, 1, 1 end
+ return c.r, c.g, c.b
+end
+-- Debuff-type (dispel) colour as raw r,g,b (0-1) from the global DebuffTypeColor table.
+function NE.color.DispelRGB(dtype)
+ local pool = _G.DebuffTypeColor
+ local c = pool and (pool[dtype or "none"] or pool.none)
+ if not c then return 1, 1, 1 end
+ return c.r, c.g, c.b
+end
+
+-- Region walking — call fn(region) for each child region whose object type == `kind` and draw
+-- layer == `layer`. Returns the count visited.
+function NE.FrameUtil.ForEachRegion(frame, kind, layer, fn)
+ if not (frame and frame.GetRegions and fn) then return 0 end
+ local regions = { frame:GetRegions() }
+ local visited = 0
+ for i = 1, #regions do
+ local r = regions[i]
+ if r and (not kind or (r.GetObjectType and r:GetObjectType() == kind))
+ and (not layer or (r.GetDrawLayer and (r:GetDrawLayer()) == layer)) then
+ fn(r)
+ visited = visited + 1
+ end
+ end
+ return visited
+end
+
+-- FindRegion(frame, kind, predicate): the first matching child region, else nil.
+function NE.FrameUtil.FindRegion(frame, kind, predicate)
+ if not (frame and frame.GetRegions and predicate) then return nil end
+ local regions = { frame:GetRegions() }
+ for i = 1, #regions do
+ local r = regions[i]
+ if r and (not kind or (r.GetObjectType and r:GetObjectType() == kind)) and predicate(r) then
+ return r
+ end
+ end
+ return nil
+end
+
+-- Copyable dump dialog — a movable DIALOG-strata frame with a selectable multiline EditBox.
+-- DOWNPORT: 3.3.5a has no BackdropTemplate (it's a Cata+ template). CreateFrame with that
+-- template name silently returns a plain frame whose :SetBackdrop still exists natively on
+-- 3.3.5a frames, so we call SetBackdrop directly and drop the template argument.
+function NE.FrameUtil.CopyBox(opts)
+ opts = opts or {}
+ local w, h = opts.w or 760, opts.h or 500
+ local f = CreateFrame("Frame", opts.name, UIParent) -- DOWNPORT: no "BackdropTemplate" on 3.3.5a
+ f:SetSize(w, h)
+ f:SetPoint("CENTER")
+ f:SetFrameStrata("DIALOG")
+ if f.SetBackdrop then
+ f:SetBackdrop({
+ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
+ edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
+ edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 },
+ })
+ end
+ f:EnableMouse(true)
+ f:SetMovable(true)
+ f:RegisterForDrag("LeftButton")
+ f:SetScript("OnDragStart", f.StartMoving)
+ f:SetScript("OnDragStop", f.StopMovingOrSizing)
+ f:Hide()
+
+ local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ title:SetPoint("TOP", 0, -10)
+ title:SetText(opts.title or "Copy — Ctrl+A then Ctrl+C")
+
+ local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
+ close:SetPoint("TOPRIGHT", -4, -4)
+
+ local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
+ scroll:SetPoint("TOPLEFT", 12, -34)
+ scroll:SetPoint("BOTTOMRIGHT", -30, 38)
+
+ local edit = CreateFrame("EditBox", nil, scroll)
+ edit:SetMultiLine(true)
+ edit:SetAutoFocus(false)
+ edit:SetFontObject(ChatFontNormal)
+ edit:SetWidth(w - 50)
+ edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
+ scroll:SetScrollChild(edit)
+
+ local selectAll = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
+ selectAll:SetSize(90, 20)
+ selectAll:SetPoint("BOTTOMLEFT", 12, 10)
+ selectAll:SetText("Select All")
+ selectAll:SetScript("OnClick", function() edit:SetFocus(); edit:HighlightText() end)
+
+ f.edit = edit
+ function f:SetText(t) edit:SetText(t or "") end
+ function f:ShowText(t) edit:SetText(t or ""); self:Show(); edit:SetFocus(); edit:HighlightText() end
+ return f
+end
+
+-- IsAddOnLoaded shim. DOWNPORT: 3.3.5a has the GLOBAL IsAddOnLoaded (no C_AddOns namespace),
+-- so this prefers the compat C_AddOns if present, else the global. Returns a plain bool.
+function NE.IsAddOnLoaded(name)
+ local fn = (C_AddOns and C_AddOns.IsAddOnLoaded) or _G.IsAddOnLoaded
+ if not fn then return false end
+ local ok, loaded = pcall(fn, name)
+ return (ok and loaded) and true or false
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/GridLayout.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/GridLayout.lua
new file mode 100644
index 0000000..26b3326
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/GridLayout.lua
@@ -0,0 +1,213 @@
+-- DragonUI_NewEra/core/GridLayout.lua — Lua reimplementation of retail's GridLayoutFrame.
+--
+-- DOWNPORT: retail FrameXML ships LayoutFrame.{lua,xml} providing the `GridLayoutFrame` virtual
+-- template. NewEra's CooldownViewer.xml inherits it on all four viewer frames and drives it through
+-- `layoutIndex` / `stride` / `childXPadding` / `layoutFramesGoingRight` / `:Layout()`. 3.3.5a has
+-- none of it, and !!!ClassicAPI ships only the AnchorUtil.GridLayout *helpers* (Util/AnchorUtil.lua)
+-- — not the template. So we provide the same surface as a mixin applied in Lua at frame creation.
+--
+-- PUBLIC:
+-- NE.gridlayout.Apply(frame) -- installs the mixin; frame:Layout() then works
+--
+-- FIELDS the owner sets (all read fresh on every Layout, same as retail):
+-- frame.isHorizontal -- true: fill rows first. false: fill columns first.
+-- frame.stride -- items per row (horizontal) / per column (vertical)
+-- frame.childXPadding -- px between columns (MAY BE NEGATIVE — the viewers use -2)
+-- frame.childYPadding -- px between rows
+-- frame.layoutFramesGoingRight -- false mirrors the grid horizontally
+-- frame.layoutFramesGoingUp -- true anchors from the BOTTOM edge and stacks upward
+-- frame.centerPartialLines -- centre a line that doesn't fill `stride` (see Layout)
+-- child.layoutIndex -- sort key; children without one are skipped
+-- child.ignoreInLayout -- skip this child (retired pool slots set this)
+-- child.includeAsLayoutChildWhenHidden -- a hidden child still occupies its grid cell
+--
+-- Retail resizes the frame to the laid-out bounds every pass when `alwaysUpdateLayout` is set; the
+-- viewers always set it, so we unconditionally resize. That is what keeps a mover/drag handle
+-- tracking the live size as icons are learned.
+
+local NE = DragonUI_NewEra
+NE.gridlayout = NE.gridlayout or {}
+
+local GridLayoutMixin = {}
+
+-- Collect the children that participate, ordered by layoutIndex.
+--
+-- DOWNPORT: retail's LayoutMixin:GetLayoutChildren walks `self.layoutChildren` maintained by the
+-- XML template. We have no such bookkeeping, so we walk GetChildren() and filter — the viewers keep
+-- their items in `self.items` but ALSO parent them to the frame, and going through GetChildren()
+-- means this mixin works for any caller, not just the cooldown viewers.
+local function layoutChildren(self)
+ local out = {}
+ local n = 0
+ for i = 1, select("#", self:GetChildren()) do
+ local child = select(i, self:GetChildren())
+ -- `ignoreInLayout` is the authoritative skip (retired pool slots set it). A merely HIDDEN child
+ -- is skipped too, unless it opts into holding its cell via includeAsLayoutChildWhenHidden —
+ -- retail's LayoutFrame key, which the Essential/Utility item templates set so a row doesn't
+ -- reflow as individual icons come and go.
+ local participates = child and child.layoutIndex and not child.ignoreInLayout
+ and (child:IsShown() or child.includeAsLayoutChildWhenHidden)
+ if participates then
+ n = n + 1
+ out[n] = child
+ end
+ end
+ table.sort(out, function(a, b) return a.layoutIndex < b.layoutIndex end)
+ return out, n
+end
+
+-- A child's size in the PARENT's coordinate units.
+--
+-- 3.3.5a gotcha (the one that silently breaks every scaled grid): a frame's SetPoint offsets are
+-- interpreted in the CHILD's own scale units, while GetWidth/GetHeight also return child units. The
+-- viewers call item:SetScale(iconScale) per item (CooldownViewer.lua RefreshLayout), so a 50px icon
+-- at scale 1.5 occupies 75px of the parent. We convert to parent units for the arithmetic, then
+-- convert the final offsets back to child units at SetPoint time.
+local function childScale(child)
+ local s = child.GetScale and child:GetScale() or 1
+ if not s or s <= 0 then return 1 end
+ return s
+end
+
+local function childExtent(child)
+ local s = childScale(child)
+ return (child:GetWidth() or 0) * s, (child:GetHeight() or 0) * s
+end
+
+function GridLayoutMixin:Layout()
+ local children, count = layoutChildren(self)
+ if count == 0 then
+ -- Nothing to show. Collapse to a 1x1 so the frame has a legal (non-zero) size — SetSize(0,0)
+ -- is rejected by the 3.3.5a widget API and leaves the previous size stuck.
+ self:SetSize(1, 1)
+ return
+ end
+
+ local horizontal = self.isHorizontal ~= false
+ local stride = self.stride
+ if not stride or stride < 1 then stride = count end
+ local xPad = self.childXPadding or 0
+ local yPad = self.childYPadding or 0
+
+ -- Grid coordinates per child. `stride` counts along the fill axis: rows fill first when
+ -- horizontal, columns fill first when vertical.
+ local rowOf, colOf = {}, {}
+ local numRows, numCols = 0, 0
+ for i = 1, count do
+ local major = math.floor((i - 1) / stride) -- which row (horizontal) / column (vertical)
+ local minor = (i - 1) % stride -- position along the fill axis
+ local r, c
+ if horizontal then r, c = major, minor else r, c = minor, major end
+ rowOf[i], colOf[i] = r, c
+ if r + 1 > numRows then numRows = r + 1 end
+ if c + 1 > numCols then numCols = c + 1 end
+ end
+
+ -- Column widths / row heights as maxima, so mixed-size children (the BuffBar's wide bars vs the
+ -- icon viewers' square tiles) still line up instead of overlapping.
+ local colW, rowH = {}, {}
+ for i = 1, count do
+ local w, h = childExtent(children[i])
+ local c, r = colOf[i], rowOf[i]
+ if not colW[c] or w > colW[c] then colW[c] = w end
+ if not rowH[r] or h > rowH[r] then rowH[r] = h end
+ end
+
+ -- Running offsets to the start of each column/row.
+ local colX, rowY = {}, {}
+ local acc = 0
+ for c = 0, numCols - 1 do
+ colX[c] = acc
+ acc = acc + (colW[c] or 0) + xPad
+ end
+ local totalW = acc - xPad
+ acc = 0
+ for r = 0, numRows - 1 do
+ rowY[r] = acc
+ acc = acc + (rowH[r] or 0) + yPad
+ end
+ local totalH = acc - yPad
+
+ -- Negative padding (the viewers default to iconPadding 2 + additionalPaddingOffset -4 = -2) can
+ -- drive a single-item axis negative. Clamp so SetSize never receives <= 0.
+ if totalW < 1 then totalW = 1 end
+ if totalH < 1 then totalH = 1 end
+
+ -- Centre a SHORT line. 12 icons at a stride of 9 wrap to 9 + 3, and the trailing 3 would otherwise
+ -- sit flush against the start edge, visibly off-centre under the full row above. `centerPartialLines`
+ -- shifts each line by half its shortfall against the laid-out bounds.
+ --
+ -- A line here is a row when horizontal and a column when vertical — the same axis `stride` counts
+ -- along. A line that IS full measures the full extent and so gets an offset of 0, which is what
+ -- keeps the columns of a multi-row grid aligned with each other.
+ local lineOffset = {}
+ if self.centerPartialLines then
+ -- The line's own extent, taken as the furthest child edge rather than a sum of cell sizes, so a
+ -- line of mixed-width children (the BuffBar's bars) measures what it actually occupies.
+ local extent = {}
+ for i = 1, count do
+ local w, h = childExtent(children[i])
+ local line = horizontal and rowOf[i] or colOf[i]
+ local e = horizontal and ((colX[colOf[i]] or 0) + w) or ((rowY[rowOf[i]] or 0) + h)
+ if not extent[line] or e > extent[line] then extent[line] = e end
+ end
+ local total = horizontal and totalW or totalH
+ for line, e in pairs(extent) do
+ lineOffset[line] = (total - e) / 2
+ end
+ end
+
+ local goingRight = self.layoutFramesGoingRight ~= false
+ local goingUp = self.layoutFramesGoingUp and true or false
+ local anchor = goingUp and "BOTTOMLEFT" or "TOPLEFT"
+
+ for i = 1, count do
+ local child = children[i]
+ local w, h = childExtent(child)
+ local s = childScale(child)
+
+ local x = colX[colOf[i]] or 0
+ local y = rowY[rowOf[i]] or 0
+
+ -- Centring offset, applied BEFORE the mirror below. Mirroring reflects the line about the
+ -- frame's centre and the centring offset is symmetric about that same centre, so folding it in
+ -- first is exactly what flips its sign for a leftward grid — no separate case needed.
+ local off = lineOffset[horizontal and rowOf[i] or colOf[i]]
+ if off then
+ if horizontal then x = x + off else y = y + off end
+ end
+
+ -- Mirror horizontally: the row grows leftward from the right edge. Uses this child's own width
+ -- so a mixed-width row stays flush against the correct edge.
+ if not goingRight then x = totalW - x - w end
+
+ child:ClearAllPoints()
+ -- Offsets back into child units (see childScale above). Y is negative when anchoring from the
+ -- top (grid grows downward) and positive from the bottom (grows upward).
+ if goingUp then
+ child:SetPoint(anchor, self, anchor, x / s, y / s)
+ else
+ child:SetPoint(anchor, self, anchor, x / s, -y / s)
+ end
+ end
+
+ self:SetSize(totalW, totalH)
+end
+
+-- Retail name for "resize me to my laid-out contents". Our Layout always resizes, so this is an
+-- alias kept so ported call sites don't need editing.
+function GridLayoutMixin:ResizeLayout()
+ self:Layout()
+end
+
+-- Install onto a frame. Safe to call twice.
+function NE.gridlayout.Apply(frame)
+ if not frame or frame._neGridLayout then return frame end
+ for k, v in pairs(GridLayoutMixin) do
+ frame[k] = v
+ end
+ frame._neGridLayout = true
+ return frame
+end
+
+NE.gridlayout.Mixin = GridLayoutMixin
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ItemButton.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ItemButton.lua
new file mode 100644
index 0000000..b6821a5
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ItemButton.lua
@@ -0,0 +1,76 @@
+-- DragonUI_NewEra/core/ItemButton.lua — shared item-rarity (quality) border coloring.
+--
+-- DOWNPORT: NewEra Core/ItemButton.lua → 3.3.5a. NewEra resurrected retail's neutered
+-- SetItemButtonQuality via a post-hook re-coloring the IconBorder texture. On 3.3.5a:
+-- * SetItemButtonQuality EXISTS as a global and (unlike Era) already colours item buttons that
+-- have a named $parentIconBorder/.IconBorder — but many 3.3.5a item buttons predate IconBorder,
+-- so the post-hook is still the robust way to guarantee a colored border everywhere.
+-- * BAG_ITEM_QUALITY_COLORS / ITEM_QUALITY_COLORS exist on 3.3.5a (Constants), so the colour
+-- policy ports unchanged.
+-- * Enum.ItemQuality may not exist on 3.3.5a → the Poor constant is feature-gated to 0.
+-- * The shipped WhiteIconFrame BLP path is repointed to DragonUI_NewEra's Textures dir; if the
+-- Asset agent hasn't shipped it, we fall back to the 3.3.5a client path so the border still
+-- paints.
+--
+-- §2 CONTRACT: exposed as NE.itembutton.* (and NE.itembtn.* kept for NewEra-name parity).
+
+local NE = DragonUI_NewEra
+NE.itembutton = NE.itembutton or {}
+NE.itembtn = NE.itembutton -- DOWNPORT: NewEra used NE.itembtn; alias to contract name
+local M = NE.itembutton
+
+-- DOWNPORT: prefer a shipped modern WhiteIconFrame; fall back to the 3.3.5a client asset.
+local WHITE_FRAME = [[Interface\AddOns\DragonUI_NewEra\Textures\Common\651080-whiteiconframe.blp]]
+local WHITE_FRAME_FALLBACK = [[Interface\Common\WhiteIconFrame]]
+
+local function colorFor(quality)
+ if not quality then return nil end
+ return BAG_ITEM_QUALITY_COLORS and BAG_ITEM_QUALITY_COLORS[quality]
+end
+
+-- Resolve the IconBorder texture once and cache whether the shipped art is usable.
+local function borderTex(button)
+ local b = button.IconBorder
+ if not b and button.GetName and button:GetName() then
+ b = _G[button:GetName() .. "IconBorder"]
+ end
+ return b
+end
+
+-- Apply the quality border to one button's IconBorder.
+function M.ApplyQuality(button, quality)
+ if not button then return end
+ local b = borderTex(button)
+ if not b then return end
+
+ local c = colorFor(quality)
+ if c then
+ -- DOWNPORT: try the shipped modern frame; SetTexture returns false on a missing path, so fall
+ -- back to the 3.3.5a client WhiteIconFrame.
+ if b:SetTexture(WHITE_FRAME) == false then b:SetTexture(WHITE_FRAME_FALLBACK) end
+ b:SetVertexColor(c.r, c.g, c.b)
+ b:Show()
+ else
+ b:Hide()
+ end
+end
+
+-- Resurrect/augment the global. 3.3.5a's Blizzard FrameXML defines SetItemButtonQuality at parse
+-- time, so it exists when this addon loads; the post-hook runs after the native body.
+if SetItemButtonQuality then
+ hooksecurefunc("SetItemButtonQuality", function(button, quality)
+ M.ApplyQuality(button, quality)
+ end)
+end
+
+-- Quality TEXT coloring — reads ITEM_QUALITY_COLORS (the brighter table with .hex).
+function M.TextColor(quality)
+ return quality and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality] or nil
+end
+
+-- Wrap an item name in its quality's hex color escape.
+function M.WrapTextByQuality(name, quality)
+ local c = M.TextColor(quality)
+ if c and c.hex then return c.hex .. (name or "") .. "|r" end
+ return name or ""
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ItemGrid.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ItemGrid.lua
new file mode 100644
index 0000000..b1bc2fe
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ItemGrid.lua
@@ -0,0 +1,528 @@
+-- DragonUI_NewEra/core/ItemGrid.lua — reusable combined item-grid ENGINE (NE.itemgrid).
+--
+-- DOWNPORT: NewEra Core/ItemGrid.lua → 3.3.5a. The engine (pooled ContainerFrameItemButton-
+-- Template buttons re-parented to per-container proxy frames so Era's native handlers resolve the
+-- bag via GetParent():GetID()) is structurally portable. 3.3.5a adaptations:
+-- * C_Container.* / C_Item.* come from the compat shim (CONTRACTS §1); we still feature-gate the
+-- lowest-level calls so a missing shim can't hard-error.
+-- * C_NewItems / NEW_ITEM_ATLAS_BY_QUALITY do NOT exist on 3.3.5a → the new-item glow path is
+-- feature-gated (no-op).
+-- * Enum.ItemQuality may be absent → Poor constant feature-gated to 0.
+-- * SetShown → Show/Hide (HighlightContainer/ClearHighlight/JunkIcon).
+-- * NE.slot.* (a sibling Core helper not in this sprint's deliverables) is feature-gated; when
+-- absent we paint empty slots inline so the grid still works standalone.
+-- * SetItemButtonQuality is the 4-arg-on-Era form; 3.3.5a's takes (button, quality) — we call
+-- it pcall-safe with the extra arg, which 3.3.5a ignores.
+--
+-- §2 CONTRACT: NE.itemgrid.* preserved (NE.itemgrid.New, instance :Refresh, etc.).
+
+local NE = DragonUI_NewEra
+NE.itemgrid = NE.itemgrid or {}
+local G = NE.itemgrid
+
+NE.containerframe = NE.containerframe or {}
+local M = NE.containerframe
+
+-- DOWNPORT: SetShown shim.
+local function setShown(obj, on)
+ if not obj then return end
+ if on then obj:Show() else obj:Hide() end
+end
+
+-- Does this item have an on-use effect? Cached by itemID.
+local onUseCache = {}
+local function itemHasOnUse(info)
+ if not info then return false end
+ local id = info.itemID
+ if id and onUseCache[id] ~= nil then return onUseCache[id] end
+ local key = info.hyperlink or id
+ if not (key and C_Item and C_Item.GetItemSpell) then return false end
+ local has = C_Item.GetItemSpell(key) ~= nil
+ if id then onUseCache[id] = has end
+ return has
+end
+
+-- Shared overlay hydration.
+local OVERLAY_KEYS = {
+ "BagStaticTop", "BagStaticBottom",
+ "UpgradeIcon",
+ "BattlepayItemTexture",
+ "ExtendedSlot", "ExtendedOverlay", "ExtendedOverlay2",
+}
+
+local function silenceOverlays(btn)
+ for _, key in ipairs(OVERLAY_KEYS) do
+ local tex = btn[key]
+ if tex and tex.Hide then tex:Hide() end
+ end
+ if btn.searchOverlay then btn.searchOverlay:Hide() end
+end
+
+-- Quest-STARTER detection (the "!" bang) via a hidden scanning tooltip, cached per itemID.
+local _questScanTip
+local _questStarterCache = {}
+local QUEST_STARTS_TEXT = (type(ITEM_STARTS_QUEST) == "string" and ITEM_STARTS_QUEST) or "Begins a Quest"
+local function itemStartsQuest(bag, slot, itemID)
+ if itemID == nil then return false end
+ local cached = _questStarterCache[itemID]
+ if cached ~= nil then return cached end
+ if not _questScanTip then
+ _questScanTip = CreateFrame("GameTooltip", "NE_QuestScanTip", UIParent, "GameTooltipTemplate")
+ _questScanTip:SetOwner(UIParent, "ANCHOR_NONE")
+ end
+ _questScanTip:ClearLines()
+ _questScanTip:SetBagItem(bag, slot)
+ local found = false
+ for i = 1, _questScanTip:NumLines() do
+ local fs = _G["NE_QuestScanTipTextLeft" .. i]
+ local t = fs and fs:GetText()
+ if t and t:find(QUEST_STARTS_TEXT, 1, true) then found = true; break end
+ end
+ _questStarterCache[itemID] = found
+ return found
+end
+
+-- DOWNPORT: container quest-info shim — 3.3.5a has the global GetContainerItemQuestInfo
+-- (returns isQuestItem, questId, isActive); C_Container.GetContainerItemQuestInfo is a compat
+-- wrapper if present.
+local function containerQuestInfo(bag, slot)
+ if C_Container and C_Container.GetContainerItemQuestInfo then
+ local qi = C_Container.GetContainerItemQuestInfo(bag, slot)
+ if type(qi) == "table" then return qi.isQuestItem, qi.questID, qi.isActive end
+ end
+ if GetContainerItemQuestInfo then
+ return GetContainerItemQuestInfo(bag, slot)
+ end
+ return nil
+end
+
+-- Per-item conditional overlays — quest border, junk coin, new-item glow.
+local function updateItemOverlays(btn, bag, slot, info)
+ local quality = info and info.quality
+ local noValue = info and info.hasNoValue
+
+ local qt = btn.IconQuestTexture or (btn.GetName and _G[btn:GetName() .. "IconQuestTexture"])
+ if qt then
+ local isQuestItem, questID, isActive
+ if info then
+ isQuestItem, questID, isActive = containerQuestInfo(bag, slot)
+ end
+ local startsQuest = (isQuestItem or questID) and (not questID)
+ and info and itemStartsQuest(bag, slot, info.itemID)
+ if (questID and not isActive) or startsQuest then
+ -- Item that STARTS a quest → the native "!" bang (a meaningful icon, not a rarity-style glow).
+ -- Reset to native blend/geometry in case this pooled button last held a quest OBJECTIVE (below).
+ if TEXTURE_ITEM_QUEST_BANG then qt:SetTexture(TEXTURE_ITEM_QUEST_BANG) end
+ qt:SetBlendMode("BLEND"); qt:SetVertexColor(1, 1, 1)
+ qt:ClearAllPoints(); qt:SetAllPoints(btn)
+ qt:Show()
+ elseif questID or isQuestItem then
+ -- Quest OBJECTIVE item → OUR glow (UI-ActionButton-Border, ADD-blended) tinted quest-gold, sized
+ -- like the rarity ring, instead of the oversized default quest border. Matches the bag's look.
+ qt:SetTexture((NE.bagskin and NE.bagskin.QUALITY_GLOW_PATH) or "Interface\\Buttons\\UI-ActionButton-Border")
+ qt:SetBlendMode("ADD"); qt:SetVertexColor(1, 0.82, 0)
+ local w = (btn.GetWidth and btn:GetWidth()) or 34
+ local over = math.max(8, math.floor(w * 0.35 + 0.5))
+ qt:ClearAllPoints()
+ qt:SetPoint("TOPLEFT", btn, "TOPLEFT", -over, over)
+ qt:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", over, -over)
+ qt:Show()
+ else
+ qt:Hide()
+ end
+ end
+
+ if btn.JunkIcon then
+ local POOR = (Enum and Enum.ItemQuality and Enum.ItemQuality.Poor) or 0
+ local atMerchant = MerchantFrame and MerchantFrame:IsShown()
+ setShown(btn.JunkIcon, (quality == POOR and not noValue and atMerchant) and true or false)
+ end
+
+ -- DOWNPORT: C_NewItems doesn't exist on 3.3.5a → the new-item glow is a no-op (feature-gated).
+ local newTex = btn.NewItemTexture
+ if newTex and C_NewItems and C_NewItems.IsNewItem then
+ local isNew = info and C_NewItems.IsNewItem(bag, slot)
+ if isNew then
+ local atlas = (quality and NEW_ITEM_ATLAS_BY_QUALITY and NEW_ITEM_ATLAS_BY_QUALITY[quality])
+ or "bags-glow-white"
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(newTex, atlas) end
+ newTex:Show()
+ if btn.flashAnim and not btn.flashAnim:IsPlaying() then btn.flashAnim:Play() end
+ if btn.newitemglowAnim and not btn.newitemglowAnim:IsPlaying() then btn.newitemglowAnim:Play() end
+ else
+ newTex:Hide()
+ if btn.flashAnim and btn.flashAnim:IsPlaying() then btn.flashAnim:Stop() end
+ if btn.newitemglowAnim and btn.newitemglowAnim:IsPlaying() then btn.newitemglowAnim:Stop() end
+ end
+ elseif newTex then
+ newTex:Hide()
+ end
+end
+
+-- Clear the new-item glow on hover. DOWNPORT: C_NewItems feature-gated.
+local function hookNewItemClear(btn)
+ if not btn or btn._neGlowClearHook then return end
+ btn._neGlowClearHook = true
+ btn:HookScript("OnEnter", function(self)
+ local parent = self.GetParent and self:GetParent()
+ local bag = self._bagID or (parent and parent.GetID and parent:GetID())
+ local slot = self._slotID or (self.GetID and self:GetID())
+ if bag and slot and C_NewItems and C_NewItems.RemoveNewItem then
+ C_NewItems.RemoveNewItem(bag, slot)
+ end
+ if self.NewItemTexture then self.NewItemTexture:Hide() end
+ if self.flashAnim and self.flashAnim:IsPlaying() then self.flashAnim:Stop() end
+ if self.newitemglowAnim and self.newitemglowAnim:IsPlaying() then self.newitemglowAnim:Stop() end
+ end)
+end
+
+-- Back-compat exports.
+M.UpdateItemOverlays = updateItemOverlays
+G.ItemStartsQuest = itemStartsQuest
+M.HookNewItemClear = hookNewItemClear
+M.SilenceOverlays = silenceOverlays
+
+-- Authenticator "locked" slot — shared visual. DOWNPORT: IsAccountSecured feature-gated.
+local LOCK_ATLAS = "bags-padlock-authenticator"
+
+function M.WantLockedSlots()
+ if M._hideLocked then return false end
+ return not (IsAccountSecured and IsAccountSecured())
+end
+
+local function lockOnEnter(self)
+ GameTooltip:SetOwner(self, "ANCHOR_NONE")
+ if ContainerFrameItemButton_CalculateItemTooltipAnchors then
+ ContainerFrameItemButton_CalculateItemTooltipAnchors(self, GameTooltip)
+ else
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ end
+ GameTooltip:SetText(BACKPACK_AUTHENTICATOR_INCREASE_SIZE or "", 1, 1, 1, 1, true)
+ GameTooltip:Show()
+end
+
+function M.ApplyLockedSlot(btn, locked)
+ if locked then
+ if not btn._neLock then
+ local lock = CreateFrame("Frame", nil, btn)
+ lock:SetAllPoints(btn)
+ lock:EnableMouse(true)
+ local tex = lock:CreateTexture(nil, "OVERLAY")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(tex, LOCK_ATLAS) end
+ tex:SetSize(41, 49)
+ tex:SetPoint("CENTER")
+ lock.tex = tex
+ lock:SetScript("OnEnter", lockOnEnter)
+ lock:SetScript("OnLeave", GameTooltip_Hide)
+ btn._neLock = lock
+ end
+ btn._neLock:SetFrameLevel(btn:GetFrameLevel() + 5)
+ btn._neLock:Show()
+ btn:EnableMouse(false)
+ elseif btn._neLock then
+ btn._neLock:Hide()
+ btn:EnableMouse(true)
+ end
+end
+
+-- DOWNPORT: NE.slot.* (a sibling Core helper, not ported this sprint) is feature-gated. When
+-- absent these inline helpers keep the grid self-sufficient.
+local function slotWireTooltip(b)
+ if NE.slot and NE.slot.WireTooltip then NE.slot.WireTooltip(b); return end
+ -- Inline fallback: standard container item tooltip.
+ if b._neTooltipWired then return end
+ b._neTooltipWired = true
+ -- Set the item tooltip + cursor feedback for this slot. Factored out of OnEnter so the OnUpdate
+ -- ticker below can re-run it while the cursor stays put: re-calling SetBagItem re-evaluates the
+ -- COMPAREITEMS modifier, which is what makes pressing shift *after* mousing over an item show the
+ -- comparison tooltip (issue #19). Stock bags get this from ContainerFrameItemButton_OnUpdate
+ -- re-firing _OnEnter every TOOLTIP_UPDATE_TIME; the custom grid overrode OnEnter without it, so
+ -- the tooltip was set once and frozen — shift had to be held *before* hovering to compare.
+ local function showTip(self)
+ local bag, slot = self._bagID, self._slotID
+ if not (bag and slot) then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ if self.UpdateTooltip then self.UpdateTooltip = nil end
+ GameTooltip:SetBagItem(bag, slot)
+ GameTooltip:Show()
+ -- Cursor feedback, mirroring the stock ContainerFrameItemButton_OnEnter: show the merchant
+ -- "sell item" (coin) cursor while a vendor is open, the inspect cursor for dress-up/readable
+ -- items, else the plain cursor. Without this the grid's custom OnEnter suppressed the native
+ -- sell-cursor the default bags give you at a merchant.
+ if IsModifiedClick and IsModifiedClick("DRESSUP") and self.hasItem then
+ if ShowInspectCursor then ShowInspectCursor() end
+ elseif MerchantFrame and MerchantFrame:IsShown() and MerchantFrame.selectedTab == 1
+ and ShowContainerSellCursor then
+ ShowContainerSellCursor(bag, slot)
+ elseif self.readable then
+ if ShowInspectCursor then ShowInspectCursor() end
+ elseif ResetCursor then
+ ResetCursor()
+ end
+ end
+ local REFRESH = TOOLTIP_UPDATE_TIME or 0.2
+ b:SetScript("OnEnter", function(self)
+ self._neTipTimer = REFRESH
+ showTip(self)
+ end)
+ b:SetScript("OnUpdate", function(self, elapsed)
+ -- Only the hovered slot has a live timer; every other pooled/shown button early-returns here.
+ if not self._neTipTimer then return end
+ self._neTipTimer = self._neTipTimer - (elapsed or 0)
+ if self._neTipTimer > 0 then return end
+ self._neTipTimer = REFRESH
+ if GameTooltip:IsOwned(self) then
+ showTip(self)
+ else
+ self._neTipTimer = nil -- tooltip re-owned elsewhere (e.g. left without OnLeave firing); stop ticking
+ end
+ end)
+ b:SetScript("OnLeave", function(self)
+ self._neTipTimer = nil
+ GameTooltip:Hide()
+ if ResetCursor then ResetCursor() end
+ end)
+end
+local function slotMarkEmpty(b) if NE.slot and NE.slot.MarkEmpty then NE.slot.MarkEmpty(b) end end
+local function slotApplyEmpty(b)
+ if NE.slot and NE.slot.ApplyEmpty then NE.slot.ApplyEmpty(b); return end
+ if SetItemButtonTexture then SetItemButtonTexture(b, nil) end
+end
+
+-- Grid instance factory.
+function G.New(opts)
+ local self = {
+ host = opts.host,
+ columns = opts.columns or 10,
+ itemSize = opts.itemSize or 37,
+ spacingX = opts.spacingX or 5,
+ spacingY = opts.spacingY or 5,
+ originX = opts.originX or 8,
+ originY = opts.originY or -66,
+ direction = opts.direction or "TLBR",
+ slotDescending = opts.slotDescending or false,
+ slotCount = opts.slotCount,
+ cellPos = opts.cellPos,
+ isSlotLocked = opts.isSlotLocked,
+ sortExtendedLast = opts.sortExtendedLast,
+ namePrefix = opts.namePrefix or "NE_GridItem",
+ _containers = opts.containers,
+ pool = {},
+ proxies = {},
+ }
+
+ local function containerList()
+ if type(self._containers) == "function" then return self._containers() end
+ return self._containers
+ end
+
+ local function getProxy(container)
+ local p = self.proxies[container]
+ if not p then
+ p = CreateFrame("Frame", self.namePrefix .. "Proxy" .. (container < 0 and "m" .. -container or container), self.host)
+ p:SetID(container)
+ p:SetAllPoints(self.host)
+ self.proxies[container] = p
+ end
+ return p
+ end
+
+ local function getButton(index)
+ local b = self.pool[index]
+ if b then return b end
+ b = CreateFrame("Button", self.namePrefix .. index, self.host, "ContainerFrameItemButtonTemplate")
+ b:SetSize(self.itemSize, self.itemSize)
+ slotWireTooltip(b)
+ slotMarkEmpty(b)
+ silenceOverlays(b)
+ hookNewItemClear(b)
+ if not b.BagIndicator then
+ local ind = b:CreateTexture(nil, "OVERLAY", nil, 2)
+ ind:SetTexture("Interface\\Store\\store-item-highlight")
+ ind:SetPoint("CENTER")
+ ind:Hide()
+ b.BagIndicator = ind
+ end
+ self.pool[index] = b
+ return b
+ end
+
+ local function cellXY(index, totalRows)
+ local cell = index - 1
+ local col, row
+ if self.direction == "BRTL" then
+ local colFromRight = cell % self.columns
+ local rowFromBottom = math.floor(cell / self.columns)
+ col = (self.columns - 1) - colFromRight
+ row = (totalRows - 1) - rowFromBottom
+ else
+ col = cell % self.columns
+ row = math.floor(cell / self.columns)
+ end
+ local x = self.originX + col * (self.itemSize + self.spacingX)
+ local y = self.originY - row * (self.itemSize + self.spacingY)
+ return x, y
+ end
+
+ local function numSlots(c)
+ if self.slotCount then return self.slotCount(c) or 0 end
+ return (C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerNumSlots(c)) or 0
+ end
+
+ function self:Refresh()
+ local containers = containerList()
+ self._maxRight, self._maxDown = 0, 0
+ local total = 0
+ for _, c in ipairs(containers) do
+ total = total + numSlots(c)
+ end
+ local totalRows = math.ceil(total / self.columns)
+
+ local entries = {}
+ for _, bag in ipairs(containers) do
+ local n = numSlots(bag)
+ local sFrom, sTo, sStep = 1, n, 1
+ if self.slotDescending then sFrom, sTo, sStep = n, 1, -1 end
+ for slot = sFrom, sTo, sStep do
+ local locked = self.isSlotLocked and self.isSlotLocked(bag, slot) or false
+ entries[#entries + 1] = { bag = bag, slot = slot, locked = locked }
+ end
+ end
+ if self.sortExtendedLast then
+ local anyLocked = false
+ for _, e in ipairs(entries) do if e.locked then anyLocked = true; break end end
+ if anyLocked then
+ local reordered, locked = {}, {}
+ for _, e in ipairs(entries) do
+ if e.locked then locked[#locked + 1] = e else reordered[#reordered + 1] = e end
+ end
+ for _, e in ipairs(locked) do reordered[#reordered + 1] = e end
+ entries = reordered
+ end
+ end
+
+ self._lastLocked = nil
+ do
+ for index = 1, #entries do
+ local e = entries[index]
+ local bag, slot = e.bag, e.slot
+ local btn = getButton(index)
+
+ local proxy = getProxy(bag)
+ if btn:GetParent() ~= proxy then
+ if InCombatLockdown() then
+ btn._pendingParent = bag
+ else
+ btn:SetParent(proxy)
+ btn._pendingParent = nil
+ end
+ end
+ btn:SetID(slot)
+ btn:SetFrameLevel((opts.frameLevel and opts.frameLevel()) or (self.host:GetFrameLevel() + 5))
+ btn._bagID, btn._slotID = bag, slot
+ local info = C_Container and C_Container.GetContainerItemInfo and C_Container.GetContainerItemInfo(bag, slot)
+
+ local cooldown = _G[btn:GetName() .. "Cooldown"] or btn.Cooldown
+ if cooldown and C_Container and C_Container.GetContainerItemCooldown then
+ if itemHasOnUse(info) then
+ local cdStart, cdDuration, cdEnable = C_Container.GetContainerItemCooldown(bag, slot)
+ if CooldownFrame_Set then CooldownFrame_Set(cooldown, cdStart, cdDuration, cdEnable) end
+ local g = (cdDuration and cdDuration > 0 and cdEnable == 0) and 0.4 or 1
+ if SetItemButtonTextureVertexColor then SetItemButtonTextureVertexColor(btn, g, g, g) end
+ else
+ if CooldownFrame_Set then CooldownFrame_Set(cooldown, 0, 0, 0) end
+ if SetItemButtonTextureVertexColor then SetItemButtonTextureVertexColor(btn, 1, 1, 1) end
+ end
+ end
+
+ local x, y
+ if self.cellPos then
+ x, y = self.cellPos(index - 1)
+ else
+ x, y = cellXY(index, totalRows)
+ end
+ btn:ClearAllPoints()
+ btn:SetPoint("TOPLEFT", self.host, "TOPLEFT", x, y)
+ if x + self.itemSize > self._maxRight then self._maxRight = x + self.itemSize end
+ if -y + self.itemSize > self._maxDown then self._maxDown = -y + self.itemSize end
+
+ silenceOverlays(btn)
+ if info then
+ if SetItemButtonTexture then SetItemButtonTexture(btn, info.iconFileID) end
+ if SetItemButtonCount then SetItemButtonCount(btn, info.stackCount) end
+ if SetItemButtonQuality then pcall(SetItemButtonQuality, btn, info.quality, info.itemID) end
+ if SetItemButtonDesaturated then SetItemButtonDesaturated(btn, info.isLocked) end
+ local icon = btn.icon or _G[btn:GetName() .. "IconTexture"]
+ if icon then
+ icon:SetTexCoord(0, 1, 0, 1)
+ icon:ClearAllPoints()
+ icon:SetAllPoints(btn)
+ end
+ if btn.searchOverlay then
+ setShown(btn.searchOverlay, info.isFiltered and true or false)
+ end
+ else
+ slotApplyEmpty(btn)
+ if SetItemButtonCount then SetItemButtonCount(btn, 0) end
+ if SetItemButtonDesaturated then SetItemButtonDesaturated(btn, false) end
+ if btn.searchOverlay then btn.searchOverlay:Hide() end
+ end
+ updateItemOverlays(btn, bag, slot, info)
+
+ local locked = e.locked
+ M.ApplyLockedSlot(btn, locked)
+ if locked then self._lastLocked = btn end
+
+ btn:Show()
+ end
+ end
+
+ for i = #entries + 1, #self.pool do
+ self.pool[i]:Hide()
+ self.pool[i]._bagID = nil
+ self.pool[i]._slotID = nil
+ end
+
+ local contentW, contentH
+ if self.cellPos then
+ contentW, contentH = self._maxRight, self._maxDown
+ else
+ contentW = self.columns * self.itemSize + (self.columns - 1) * self.spacingX
+ contentH = totalRows * self.itemSize + math.max(totalRows - 1, 0) * self.spacingY
+ end
+ return total, totalRows, contentW, contentH
+ end
+
+ function self:GetLastLockedButton()
+ return self._lastLocked
+ end
+
+ function self:FlushPendingParents()
+ for _, btn in ipairs(self.pool) do
+ if btn._pendingParent ~= nil then
+ btn:SetParent(getProxy(btn._pendingParent))
+ btn._pendingParent = nil
+ end
+ end
+ end
+
+ function self:ForEachButton(fn)
+ for _, btn in ipairs(self.pool) do
+ if btn:IsShown() then fn(btn) end
+ end
+ end
+
+ function self:HighlightContainer(bagID)
+ for _, btn in ipairs(self.pool) do
+ if btn.BagIndicator then setShown(btn.BagIndicator, btn:IsShown() and btn._bagID == bagID) end
+ end
+ end
+
+ function self:ClearHighlight()
+ for _, btn in ipairs(self.pool) do
+ if btn.BagIndicator then btn.BagIndicator:Hide() end
+ end
+ end
+
+ return self
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Menu.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Menu.lua
new file mode 100644
index 0000000..30c0f73
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Menu.lua
@@ -0,0 +1,456 @@
+-- DragonUI_NewEra/core/Menu.lua — a MenuUtil-shaped context-menu builder over 3.3.5a's
+-- UIDropDownMenu.
+--
+-- WHY THIS EXISTS. Every menu in the NewEra source is written against retail's MenuUtil builder
+-- API: a generator function receives a root description and calls `root:CreateTitle`,
+-- `CreateButton`, `CreateRadio`, `CreateCheckbox`, `CreateDivider`, nesting freely by adding
+-- children to a returned description. None of that exists here. Reimplementing the *API* once, in
+-- ~200 lines, lets each of those generators port close to verbatim instead of being hand-rewritten
+-- into UIDropDownMenu's init-callback idiom — which is both more code overall and a fresh chance to
+-- get the level plumbing wrong at every site.
+--
+-- WHICH BACKEND. Prefer ClassicAPI's `C_UIDropDownMenu_*`. The native 3.3.5a one hard-caps at
+-- C_UIDROPDOWNMENU_MAXLEVELS = 2, and the Cooldown Manager's ready-sound menu is three deep
+-- (item -> Ready Sound -> category -> entry). ClassicAPI's copy grows the cap on demand inside
+-- C_UIDropDownMenu_CreateFrames, which is exactly the constraint that decides this. The native
+-- functions are kept as a fallback so the shim still works (two levels deep) if ClassicAPI is
+-- absent; the two APIs are otherwise the same shape, differing only in the list-frame name prefix.
+--
+-- THE TREE IS SEPARATE FROM THE RENDER. `NE.menu.BuildRoot` runs a generator and returns a plain
+-- node tree with no widget touched. That is what makes menu CONTENT testable offline: a test can
+-- build the tree, walk it, and invoke a leaf's callback to assert the wiring, without stubbing any
+-- of UIDropDownMenu.
+--
+-- SUBMENU PARENTS ARE `notClickable`. A row with children gets hasArrow + notClickable, not a
+-- no-op func. UIDropDownMenu's OnClick toggles the row's Check texture *before* it looks at func,
+-- so a clickable-but-inert parent paints a stray checkmark on itself. What that costs, and what
+-- `openOnHover` below pays back, is the row's ability to see the mouse at all: notClickable means
+-- disabled, and a disabled row neither fires the OnEnter that opens the submenu nor keeps the
+-- invisible button — whose OnEnter CLOSES the submenu — out of the way.
+--
+-- RADIOS REFRESH THEMSELVES. C_UIDropDownMenu_Refresh keys off frame.selectedName/ID/Value, which
+-- says nothing about function-valued `checked`, so it cannot be used here. Instead a radio's click
+-- re-evaluates every sibling's predicate and repaints the check textures directly.
+--
+-- AND `info.checked` IS ALWAYS A BOOLEAN, NEVER A FUNCTION. UIDropDownMenu accepts a predicate
+-- there, but it reads the result through the classic `and`/`or` idiom:
+--
+-- local checked = type(info.checked) == "function" and info.checked() or info.checked
+--
+-- which collapses when the predicate returns FALSE: `(true and false)` is false, so the `or` falls
+-- through to `info.checked` — the function itself, which is truthy. Every function-valued radio
+-- therefore renders as selected, and a menu where everything is ticked shows nothing at all. We
+-- snapshot the predicate to a boolean at build time instead. Nothing is lost: the menu is rebuilt
+-- from the generator on every open, and refreshChecks re-reads the predicate on every click.
+
+local NE = DragonUI_NewEra
+NE.menu = NE.menu or {}
+
+-- ── node tree ───────────────────────────────────────────────────────────────────────────────────
+
+local Node = {}
+Node.__index = Node
+
+local function newNode(kind, text)
+ return setmetatable({ kind = kind, text = text, children = {} }, Node)
+end
+
+function Node:Add(node)
+ self.children[#self.children + 1] = node
+ return node
+end
+
+function Node:CreateTitle(text)
+ return self:Add(newNode("title", text))
+end
+
+function Node:CreateDivider()
+ return self:Add(newNode("divider"))
+end
+
+-- A button with children becomes a submenu; the callback is then ignored, matching MenuUtil.
+function Node:CreateButton(text, onClick, data)
+ local n = newNode("button", text)
+ n.onClick, n.data = onClick, data
+ return self:Add(n)
+end
+
+function Node:CreateRadio(text, isSelected, onClick, data)
+ local n = newNode("radio", text)
+ n.isSelected, n.onClick, n.data = isSelected, onClick, data
+ return self:Add(n)
+end
+
+function Node:CreateCheckbox(text, isSelected, onClick, data)
+ local n = newNode("checkbox", text)
+ n.isSelected, n.onClick, n.data = isSelected, onClick, data
+ return self:Add(n)
+end
+
+-- Hover text for one row. UIDropDownMenu shows this from the row's own OnEnter, and shows it on
+-- disabled rows too when tooltipWhileDisabled is set — which is what lets an entry explain why it
+-- cannot do anything rather than just sitting there inert.
+function Node:SetTooltip(title, text)
+ self.tipTitle, self.tipText = title, text
+ return self
+end
+
+-- First child with this exact text. For callers that want to drive a built menu (tests, mostly)
+-- without counting indices.
+function Node:Child(text)
+ for _, c in ipairs(self.children) do
+ if c.text == text then return c end
+ end
+ return nil
+end
+
+function Node:Invoke()
+ if self.onClick then self.onClick(self.data) end
+end
+
+NE.menu.Node = Node
+
+-- generator(owner, root, ...) — MenuUtil's signature. Our generators ignore `owner`.
+function NE.menu.BuildRoot(generator, owner, ...)
+ if type(generator) ~= "function" then return nil end
+ local root = newNode("root")
+ generator(owner, root, ...)
+ NE.menu._lastRoot = root -- test seam
+ return root
+end
+
+-- ── backend ─────────────────────────────────────────────────────────────────────────────────────
+
+local B -- resolved on first use; the globals do not exist until ClassicAPI has loaded
+
+local function backend()
+ if B ~= nil then return B or nil end
+ if _G.C_UIDropDownMenu_AddButton and _G.C_ToggleDropDownMenu then
+ B = {
+ prefix = "C_DropDownList",
+ template = "C_UIDropDownMenuTemplate",
+ openVar = "C_UIDROPDOWNMENU_OPEN_MENU",
+ CreateInfo = _G.C_UIDropDownMenu_CreateInfo,
+ AddButton = _G.C_UIDropDownMenu_AddButton,
+ Initialize = _G.C_UIDropDownMenu_Initialize,
+ Toggle = _G.C_ToggleDropDownMenu,
+ CloseAll = _G.C_CloseDropDownMenus,
+ }
+ elseif _G.UIDropDownMenu_AddButton and _G.ToggleDropDownMenu then
+ -- Two levels only. Enough for a flat menu, not for the sound picker.
+ B = {
+ prefix = "DropDownList",
+ template = "UIDropDownMenuTemplate",
+ openVar = "UIDROPDOWNMENU_OPEN_MENU",
+ CreateInfo = _G.UIDropDownMenu_CreateInfo,
+ AddButton = _G.UIDropDownMenu_AddButton,
+ Initialize = _G.UIDropDownMenu_Initialize,
+ Toggle = _G.ToggleDropDownMenu,
+ CloseAll = _G.CloseDropDownMenus,
+ }
+ else
+ B = false
+ end
+ return B or nil
+end
+
+local function listButton(level, index)
+ local b = backend()
+ return b and _G[b.prefix .. level .. "Button" .. index] or nil
+end
+
+-- Repaint the check/radio marks for one open level from their predicates. Called after a radio or
+-- checkbox fires, because the built-in OnClick only knows how to toggle the row you clicked.
+local function refreshChecks(level)
+ local b = backend()
+ local list = b and _G[b.prefix .. level]
+ if not list then return end
+ for i = 1, (list.numButtons or 0) do
+ local btn = listButton(level, i)
+ local node = btn and btn._neNode
+ if node and node.isSelected then
+ local on = node.isSelected(node.data) and true or false
+ local check = _G[btn:GetName() .. "Check"]
+ if check then
+ if node.kind == "radio" then
+ check:SetTexture("Interface\\Buttons\\UI-RadioButton")
+ check:SetTexCoord(on and 0.25 or 0, on and 0.5 or 0.25, 0, 1)
+ if check.SetDesaturated then check:SetDesaturated(not on) end
+ check:SetAlpha(on and 1 or 0.25)
+ check:Show()
+ elseif on then
+ check:Show()
+ else
+ check:Hide()
+ end
+ end
+ if on then btn:LockHighlight() else btn:UnlockHighlight() end
+ btn.checked = on -- keeps the built-in OnClick's next toggle pointing the right way
+ end
+ end
+end
+
+-- ── render ──────────────────────────────────────────────────────────────────────────────────────
+
+-- Let a submenu row take the mouse across its whole width, instead of only on its 16px arrow.
+--
+-- `notClickable` (see the header) makes the row a DISABLED button — C_UIDropDownMenu.lua:172 turns it
+-- into `info.disabled`. Two things follow from that on this client, and both of them fight the player:
+--
+-- 1. A disabled Button fires no OnEnter unless it is told to. The row's own OnEnter is the thing
+-- that opens a `hasArrow` submenu, so it never ran.
+-- 2. Disabling also SHOWS `$parentInvisibleButton`, which covers the row and whose OnEnter calls
+-- CloseDropDownMenus(level + 1) — so hovering the row body actively shut the submenu again.
+--
+-- That left `$parentExpandArrow` as the only way in, and any approach to it that crossed the row
+-- first slammed the door. Reported as "I have to specifically mouse over the arrow which makes
+-- navigating annoying", on the alert menu's FX Style submenu.
+--
+-- The tooltip the invisible button existed to serve is on the row's own OnEnter too, so nothing is
+-- lost by taking it down; AddButton re-shows it on the next build anyway.
+--
+-- SET ON EVERY ROW, not only the submenu ones. These list buttons are shared with every other
+-- C_UIDropDownMenu in the game, so a row left motion-enabled is one that some later menu's disabled
+-- title could hover-highlight. Passing `false` back is what keeps the leak from spreading.
+--
+-- …and then put the submenu where the row is, because letting the row open it exposed a second fault
+-- underneath the first. ClassicAPI picks a level-2+ list's anchor like this (C_UIDropDownMenu.lua:397):
+--
+-- local anchorFrame = (strsub(button:GetParent():GetName(), 1, 12) == listFramePrefix)
+-- and button or button:GetParent()
+--
+-- Blizzard's original compared against the LITERAL "DropDownList" — exactly 12 characters. ClassicAPI
+-- renamed the prefix to "C_DropDownList", which is 14, and left the 12 alone, so the test can never be
+-- true and the anchor is always `button:GetParent()`. That stayed invisible while the arrow was the
+-- only way in: an arrow's parent IS its row, which is the right answer by accident. A row's parent is
+-- the whole LIST — so the submenu appeared pinned to the top of the parent menu, and only dropped into
+-- place once the mouse crossed the arrow and re-opened it from there.
+--
+-- We re-anchor to the ARROW, not to the row that was actually hovered. The arrow is a 16x16 child
+-- centred in a 16px row, so its TOPRIGHT is the row's TOPRIGHT and the placement is the same either
+-- way — but the arrow's own OnEnter re-opens the menu unless it finds ITSELF as the anchor already
+-- (C_UIDropDownMenu.xml:94), and anchoring to the row would rebuild the whole submenu every time the
+-- mouse crossed the arrow on its way there.
+local function anchorSubmenu(list, arrow)
+ list:ClearAllPoints()
+ list:SetPoint("TOPLEFT", arrow, "TOPRIGHT", 0, 0)
+
+ -- The screen-bounds correction from ToggleDropDownMenu's own tail (line 429). It runs there against
+ -- the wrong anchorFrame, so it has to run again here against the right one.
+ local x, y = list:GetCenter()
+ if not (x and y) then return end
+ local offY = (y - list:GetHeight() / 2) < 0
+ local offX = list:GetRight() > GetScreenWidth()
+ if offY or offX then
+ list:ClearAllPoints()
+ list:SetPoint(offY and "BOTTOMRIGHT" or "TOPRIGHT", arrow, offY and "BOTTOMLEFT" or "TOPLEFT",
+ offX and -11 or 0, offY and -14 or 14)
+ end
+end
+
+local function openOnHover(btn, level, wanted)
+ if btn.SetMotionScriptsWhileDisabled then
+ btn:SetMotionScriptsWhileDisabled(wanted and true or false)
+ end
+ if not wanted then return end
+
+ local name = btn.GetName and btn:GetName()
+ local inv = name and _G[name .. "InvisibleButton"]
+ if inv then inv:Hide() end
+
+ -- Hooked once and never removed — HookScript has no inverse — so the hook itself has to ask whose
+ -- menu is open before it moves anything. The level is closed over rather than read back off the
+ -- parent: a given button belongs to exactly one list forever, its name says which, and asking would
+ -- mean depending on GetID for a number we already have.
+ if name and not btn._neSubAnchor then
+ btn._neSubAnchor = true
+ btn:HookScript("OnEnter", function(self)
+ local b = backend()
+ if not (b and _G[b.openVar] == NE.menu._frame) then return end
+ local list, arrow = _G[b.prefix .. (level + 1)], _G[self:GetName() .. "ExpandArrow"]
+ if list and arrow and list:IsShown() and arrow:IsShown() then anchorSubmenu(list, arrow) end
+ end)
+ end
+end
+
+-- UIDropDownMenu calls this once per open level, handing back whatever we stashed in info.menuList
+-- for the parent row. Stashing the node itself is what gives us arbitrary nesting for free.
+local function initLevel(frame, level, menuList)
+ local b = backend()
+ if not b then return end
+ level = level or 1
+ local node = menuList or frame._neRoot
+ if not node then return end
+
+ for _, child in ipairs(node.children) do
+ local info = b.CreateInfo()
+ local kind = child.kind
+ local hasKids = #child.children > 0
+
+ if kind == "divider" then
+ -- No divider primitive on this client. A disabled blank row reads as one.
+ info.text, info.isTitle, info.notCheckable, info.disabled = " ", true, true, 1
+ elseif kind == "title" then
+ info.text, info.isTitle, info.notCheckable, info.disabled = child.text, true, true, 1
+ elseif hasKids then
+ info.text = child.text
+ info.notCheckable = true
+ info.hasArrow = true
+ info.notClickable = true -- see header: keeps OnClick from painting a stray check
+ info.menuList = child
+ elseif child.isSelected then
+ info.text = child.text
+ info.isRadio = (kind == "radio") or nil
+ info.isNotRadio = (kind ~= "radio") or nil
+ info.checked = child.isSelected(child.data) and true or false -- boolean, see header
+ info.keepShownOnClick = true
+ info.func = function()
+ if child.onClick then child.onClick(child.data) end
+ refreshChecks(level)
+ end
+ else
+ info.text = child.text
+ info.notCheckable = true
+ info.func = function() if child.onClick then child.onClick(child.data) end end
+ end
+
+ if child.tipTitle then
+ info.tooltipTitle = child.tipTitle
+ info.tooltipText = child.tipText
+ info.tooltipOnButton = true
+ info.tooltipWhileDisabled = true
+ end
+
+ b.AddButton(info, level)
+
+ -- AddButton bumps numButtons; that is the index of the row it just wrote.
+ local list = _G[b.prefix .. level]
+ local btn = list and listButton(level, list.numButtons or 0)
+ if btn then
+ btn._neNode = child
+ openOnHover(btn, level, hasKids)
+ end
+ end
+end
+
+-- One shared, never-shown anchor frame. UIDropDownMenu needs a "dropdown" object to hang the
+-- initialize function and the open state off; it is not the thing the player sees.
+local function anchorFrame()
+ local b = backend()
+ if not b then return nil end
+ if NE.menu._frame then return NE.menu._frame end
+ local f = CreateFrame("Frame", "NE_ContextMenu", UIParent, b.template)
+ f:Hide()
+ NE.menu._frame = f
+ return f
+end
+
+function NE.menu.Close(level)
+ local b = backend()
+ if b and b.CloseAll then b.CloseAll(level or 1) end
+end
+
+-- ── Click-away to dismiss ───────────────────────────────────────────────────────────────────────
+--
+-- An open menu had exactly two ways out: pick a row, or press Escape. Clicking anywhere else left it
+-- hanging, which is not how any other menu in the game behaves — Blizzard's own dropdowns are
+-- dismissed by the click that lands outside them.
+--
+-- Nothing in the dropdown backend does this for us. ClassicAPI's C_DropDownList frames sit at
+-- FULLSCREEN_DIALOG with SetToplevel, and the only auto-close they carry is the mouse-off timer that
+-- applies to menus opened in MENU mode from a parent that is itself hovered — not to a menu opened by
+-- clicking a button. So the dismiss is a full-screen mouse catcher, parked ONE frame level under the
+-- open list: the list still takes its own clicks, and everything else on screen hits the catcher.
+--
+-- The click is CONSUMED, which is deliberate and is what Blizzard's dropdowns do — clicking away from
+-- an open menu should close it, not close it and also press whatever was underneath.
+
+local catcher
+
+local function ensureCatcher()
+ if catcher then return catcher end
+ local f = CreateFrame("Frame", "NE_MenuClickCatcher", UIParent)
+ f:SetAllPoints(UIParent)
+ f:EnableMouse(true)
+ f:Hide()
+ f:SetScript("OnMouseDown", function(self)
+ self:Hide()
+ NE.menu.Close(1)
+ end)
+ catcher = f
+ return f
+end
+
+-- Arm the catcher for whatever is open now. Called after a toggle, so it also has to notice that the
+-- toggle CLOSED the menu — ToggleAnchored is a toggle, and arming a catcher over a screen with no
+-- menu on it would eat one click for nothing.
+local function armCatcher()
+ local b = backend()
+ local list = b and _G[b.prefix .. "1"]
+ if not (list and list.IsShown and list:IsShown()) then
+ if catcher then catcher:Hide() end
+ return
+ end
+ local f = ensureCatcher()
+ -- STRICTLY below the list, which means clamping the LIST up rather than the catcher down: at equal
+ -- frame levels the click resolves by creation order, and a catcher that ties with the menu it is
+ -- protecting would sometimes swallow the row the player was aiming at.
+ local lvl = list:GetFrameLevel() or 2
+ if lvl < 1 then list:SetFrameLevel(1); lvl = 1 end
+ f:SetFrameStrata(list:GetFrameStrata())
+ f:SetFrameLevel(lvl - 1)
+ f:Show()
+ -- The menu can close without going through us — a row picked, Escape, CloseAll from elsewhere — and
+ -- a catcher left up would silently eat the next click anywhere on screen.
+ if not list._neCatcherHooked then
+ list._neCatcherHooked = true
+ list:HookScript("OnHide", function() if catcher then catcher:Hide() end end)
+ end
+end
+
+NE.menu._catcher = function() return catcher end -- test seam
+
+-- Open a cursor-anchored context menu. Always opens: closing first means right-clicking a second
+-- item while the first item's menu is up switches to it instead of just dismissing.
+function NE.menu.OpenContext(generator, owner, ...)
+ local b, f = backend(), anchorFrame()
+ if not (b and f) then return nil end
+ f._neRoot = NE.menu.BuildRoot(generator, owner, ...)
+ if not f._neRoot then return nil end
+ f._neAnchor = nil
+ b.CloseAll(1)
+ b.Initialize(f, initLevel, "MENU")
+ b.Toggle(1, nil, f, "cursor", 0, 0)
+ armCatcher()
+ return f
+end
+
+-- Open anchored to a frame, with toggle semantics — the cog-button behaviour, where clicking the
+-- same button again dismisses. `spec` = { point, relativePoint, x, y }.
+function NE.menu.ToggleAnchored(generator, anchor, spec, owner, ...)
+ local b, f = backend(), anchorFrame()
+ if not (b and f and anchor) then return nil end
+ spec = spec or {}
+
+ -- Reusing one anchor frame means "is a menu open?" is global. If the open one belongs to some
+ -- other trigger, this click should switch to us, not dismiss theirs.
+ if f._neAnchor ~= anchor then b.CloseAll(1) end
+ f._neAnchor = anchor
+
+ f._neRoot = NE.menu.BuildRoot(generator, owner, ...)
+ if not f._neRoot then return nil end
+
+ -- C_ToggleDropDownMenu reads these off the dropdown frame in preference to its arguments.
+ f.point = spec.point or "TOPRIGHT"
+ f.relativePoint = spec.relativePoint or "BOTTOMRIGHT"
+ f.relativeTo = nil
+ f.xOffset = spec.x or 0
+ f.yOffset = spec.y or -2
+
+ b.Initialize(f, initLevel, "MENU")
+ b.Toggle(1, nil, f, anchor, f.xOffset, f.yOffset)
+ armCatcher()
+ return f
+end
+
+function NE.menu.IsAvailable() return backend() ~= nil end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Modules.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Modules.lua
new file mode 100644
index 0000000..3855599
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Modules.lua
@@ -0,0 +1,263 @@
+-- DragonUI_NewEra/core/Modules.lua — central module registry + reload-gated boot dispatcher.
+--
+-- DOWNPORT: NewEra Core/Modules.lua → 3.3.5a, re-homed onto DragonUI's DB. NewEra read enable
+-- flags from its own SV (NE.db.modules.enabled). Per CONTRACTS §2 + §4, the DragonUI-facing toggle
+-- lives in NE.dragon.db.profile.newera with shape `modules..enabled`. So IsEnabled/SetEnabled
+-- read/write THAT location first (coordinated with the Integration agent), falling back to NewEra's
+-- own SV and the registered default. The dispatcher, requires-chains, RIVALS/conflict logic, and
+-- sub-boots port unchanged (all plain Lua + WoW events available on 3.3.5a).
+--
+-- §2 CONTRACT:
+-- NE.modules.Register{ name=, default=, onBoot=fn } (also accepts NewEra's Register(name, opts))
+-- reload-gated boot dispatcher reading NE.dragon.db.profile.newera.modules..enabled.
+--
+-- THE RELOAD-GATED CONTRACT: disabling only writes the flag; a disabled module's onBoot is never
+-- invoked on the next /reload. "Disabled" == "absence of boot", not teardown.
+
+local NE = DragonUI_NewEra
+NE.modules = NE.modules or {}
+local Mods = NE.modules
+
+Mods.registry = Mods.registry or {} -- name -> opts
+Mods.order = Mods.order or {} -- registration order
+Mods.booted = Mods.booted or {} -- name -> true once booted this session
+
+-- DOWNPORT: the DragonUI-backed enable store. Returns the `modules` table inside
+-- NE.dragon.db.profile.newera, creating the chain lazily. nil if DragonUI's DB isn't ready yet.
+local function dragonModulesTable(create)
+ local dragon = NE.dragon
+ local db = dragon and dragon.db
+ local profile = db and db.profile
+ if not profile then return nil end
+ if not profile.newera then
+ if not create then return nil end
+ profile.newera = { enabled = true }
+ end
+ if not profile.newera.modules then
+ if not create then return nil end
+ profile.newera.modules = {}
+ end
+ return profile.newera.modules
+end
+Mods._dragonModulesTable = dragonModulesTable -- exposed for the Integration agent / options
+
+-- Enable state. Reads DragonUI's profile first, then NewEra's own SV, then the registered default.
+function Mods.IsEnabled(name)
+ local opts = Mods.registry[name]
+ local default = not (opts and opts.default == false)
+ -- (1) DragonUI profile: modules..enabled
+ local dm = dragonModulesTable(false)
+ if dm and dm[name] and dm[name].enabled ~= nil then
+ return dm[name].enabled and true or false
+ end
+ -- (2) NewEra's own SV (legacy / standalone).
+ if NE.db and NE.db.modules and NE.db.modules.enabled then
+ local v = NE.db.modules.enabled[name]
+ if v ~= nil then return v and true or false end
+ end
+ return default
+end
+
+-- Write the flag only. Reload-gated. Writes BOTH stores so either reader is consistent.
+function Mods.SetEnabled(name, on)
+ on = on and true or false
+ -- DragonUI profile (the canonical store).
+ local dm = dragonModulesTable(true)
+ if dm then
+ dm[name] = dm[name] or {}
+ dm[name].enabled = on
+ end
+ -- NewEra SV mirror (so a standalone-loaded build still honours it).
+ if NE.db then
+ NE.db.modules = NE.db.modules or {}
+ NE.db.modules.enabled = NE.db.modules.enabled or {}
+ NE.db.modules.enabled[name] = on
+ end
+ local booted = Mods.booted[name] and true or false
+ Mods.reloadPending = (booted ~= on) or Mods.reloadPending
+ if Mods.onReloadPending then pcall(Mods.onReloadPending) end
+end
+
+Mods.blockedBy = Mods.blockedBy or {}
+Mods.missingAddOn = Mods.missingAddOn or {}
+
+function Mods.AddOnSatisfied(name)
+ local opts = Mods.registry[name]
+ local dep = opts and opts.requiresAddOn
+ if not dep or type(dep.present) ~= "function" then return true end
+ local ok, present = pcall(dep.present)
+ return (ok and present) and true or false
+end
+
+-- Rival-addon yield ("play ball").
+Mods.RIVALS = {
+ BAGS = { "Bagnon", "Bagshui", "ArkInventory", "AdiBags", "Baganator",
+ "Combuctor", "OneBag3", "tdBag2", "Sorted" },
+ NAMEPLATES = { "Plater", "TidyPlates_ThreatPlates", "TidyPlates", "NeatPlates",
+ "Kui_Nameplates", "BetterBlizzPlates" },
+ AUCTION = { "Auctionator", "TradeSkillMaster", "aux-addon", "Auc-Advanced",
+ "AuctionLite" },
+ ACTIONBARS = { "Bartender4", "Dominos" },
+ CHAT = { "Prat-3.0", "Chattynator" },
+ TOOLTIP = { "TipTac" },
+}
+
+Mods.conflictCache = Mods.conflictCache or {}
+
+-- DOWNPORT: NewEra used C_AddOns.IsAddOnLoaded; on 3.3.5a use NE.IsAddOnLoaded (the FrameUtil
+-- shim that prefers the global IsAddOnLoaded).
+function Mods.ConflictedBy(name)
+ local cached = Mods.conflictCache[name]
+ if cached ~= nil then return cached or nil end
+ local opts = Mods.registry[name]
+ local rivals = opts and opts.conflictsWith
+ local found = false
+ if rivals then
+ for _, addon in ipairs(rivals) do
+ if NE.IsAddOnLoaded and NE.IsAddOnLoaded(addon) then found = addon; break end
+ end
+ end
+ Mods.conflictCache[name] = found
+ return found or nil
+end
+
+function Mods.ConflictOverride(name)
+ local dm = dragonModulesTable(false)
+ if dm and dm[name] and dm[name].conflictOverride ~= nil then
+ return dm[name].conflictOverride and true or false
+ end
+ local t = NE.db and NE.db.modules and NE.db.modules.conflictOverride
+ return (t and t[name]) and true or false
+end
+
+function Mods.SetConflictOverride(name, on)
+ on = on and true or nil
+ local dm = dragonModulesTable(true)
+ if dm then
+ dm[name] = dm[name] or {}
+ dm[name].conflictOverride = on
+ end
+ if NE.db then
+ NE.db.modules = NE.db.modules or {}
+ NE.db.modules.conflictOverride = NE.db.modules.conflictOverride or {}
+ NE.db.modules.conflictOverride[name] = on
+ end
+ Mods.reloadPending = true
+ if Mods.onReloadPending then pcall(Mods.onReloadPending) end
+end
+
+function Mods.EffectivelyEnabled(name)
+ if not Mods.IsEnabled(name) then return false end
+ local opts = Mods.registry[name]
+ if opts and opts.requiresAddOn and not Mods.AddOnSatisfied(name) then return false end
+ if opts and opts.conflictsWith and not Mods.ConflictOverride(name) and Mods.ConflictedBy(name) then
+ return false
+ end
+ return true
+end
+
+-- Dispatcher. One shared event frame.
+local dispatcher = CreateFrame("Frame")
+local wanted = {}
+
+dispatcher:RegisterEvent("PLAYER_LOGIN")
+dispatcher:RegisterEvent("PLAYER_ENTERING_WORLD")
+
+local function shouldBoot(name)
+ if not Mods.IsEnabled(name) then return false end
+ local opts = Mods.registry[name]
+ if opts and opts.requires then
+ for _, dep in ipairs(opts.requires) do
+ if not Mods.EffectivelyEnabled(dep) then
+ if Mods.blockedBy[name] ~= dep and NE.Log then
+ NE.Log("MODULES", name.." skipped: requires "..dep.." (disabled)")
+ end
+ Mods.blockedBy[name] = dep
+ return false
+ end
+ end
+ end
+ if opts and opts.requiresAddOn and not Mods.AddOnSatisfied(name) then
+ Mods.missingAddOn[name] = opts.requiresAddOn.label or "(addon)"
+ return false
+ end
+ if opts and opts.conflictsWith and not Mods.ConflictOverride(name) and Mods.ConflictedBy(name) then
+ return false
+ end
+ return true
+end
+
+Mods.subBoots = Mods.subBoots or {}
+
+dispatcher:SetScript("OnEvent", function(_, event, arg1, ...)
+ local list = wanted[event]
+ if not list then return end
+ for _, name in ipairs(list) do
+ local opts = Mods.registry[name]
+ if opts and opts.onBoot and shouldBoot(name) then
+ Mods.booted[name] = true
+ local ok, err = pcall(opts.onBoot, event, arg1, ...)
+ if not ok and NE.Log then NE.Log("MODULES", "onBoot error in "..name..": "..tostring(err)) end
+ local subs = Mods.subBoots[name]
+ if subs then
+ for _, e in ipairs(subs) do
+ if e.every or not e.ran then
+ e.ran = true
+ local ok2, err2 = pcall(e.fn, event, arg1, ...)
+ if not ok2 and NE.Log then NE.Log("MODULES", "sub-boot error in "..name..": "..tostring(err2)) end
+ end
+ end
+ end
+ end
+ end
+end)
+
+function Mods.OnBoot(family, fn, opts)
+ local entry = { fn = fn, every = (opts and opts.everyFire) and true or false }
+ if Mods.booted[family] then
+ entry.ran = true
+ local ok, err = pcall(fn)
+ if not ok and NE.Log then NE.Log("MODULES", "sub-boot error in "..family..": "..tostring(err)) end
+ if not entry.every then return end
+ end
+ local list = Mods.subBoots[family]
+ if not list then list = {}; Mods.subBoots[family] = list end
+ list[#list + 1] = entry
+end
+
+-- Register. DOWNPORT: accepts EITHER NewEra's positional Register(name, opts) OR the §2 table form
+-- Register{ name=, default=, onBoot=, events=/event=, ... }.
+function Mods.Register(arg1, arg2)
+ local name, opts
+ if type(arg1) == "table" then
+ opts = arg1
+ name = opts.name
+ else
+ name = arg1
+ opts = arg2 or {}
+ end
+ if not name then
+ if NE.Log then NE.Log("MODULES", "Register called without a name") end
+ return
+ end
+ if Mods.registry[name] then
+ if NE.Log then NE.Log("MODULES", "duplicate module registration: "..tostring(name)) end
+ return
+ end
+ Mods.registry[name] = opts
+ Mods.order[#Mods.order + 1] = name
+ -- DOWNPORT: default events. A module that supplies onBoot but no events still wants to boot —
+ -- default it to PLAYER_LOGIN (the §2 onBoot intent), where DragonUI's DB is guaranteed ready.
+ local evs = opts.events or (opts.event and { opts.event }) or (opts.onBoot and { "PLAYER_LOGIN" }) or nil
+ if evs and opts.onBoot then
+ for _, ev in ipairs(evs) do
+ wanted[ev] = wanted[ev] or {}
+ wanted[ev][#wanted[ev] + 1] = name
+ dispatcher:RegisterEvent(ev)
+ end
+ end
+end
+
+function Mods.GetAll() return Mods.order, Mods.registry end
+function Mods.Get(name) return Mods.registry[name] end
+function Mods.IsBooted(name) return Mods.booted[name] and true or false end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/NineSlice.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/NineSlice.lua
new file mode 100644
index 0000000..07bbca7
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/NineSlice.lua
@@ -0,0 +1,217 @@
+-- DragonUI_NewEra/core/NineSlice.lua — reusable retail-style nineslice walker.
+--
+-- DOWNPORT: NewEra Core/NineSlice.lua → 3.3.5a. NewEra mirrored retail's NineSlice.lua walker
+-- and routed each piece's atlas through NE.tex.SetAtlas (which reads the generated NE_ATLAS). We
+-- keep the walker verbatim but route atlas-entry lookups through NE.tex._atlasEntry (our owned
+-- coord registry, per the architect decision) instead of the raw NE_ATLAS global. The structure
+-- (corners→edges→center, SetPoint anchoring, SetHorizTile/SetVertTile) is all 3.3.5a-legal.
+--
+-- The §2 contract exposes NE.nineslice.Apply(frame, layoutName[, opts]); NewEra's name is
+-- ApplyLayout. We expose BOTH (Apply is the contract alias).
+--
+-- USAGE:
+-- NE.nineslice.AddLayout("ButtonFrameTemplate", { ... })
+-- NE.nineslice.Apply(frame, "PortraitFrameTemplate")
+
+local NE = DragonUI_NewEra
+NE.nineslice = NE.nineslice or {}
+NE.nineslice.layouts = NE.nineslice.layouts or {}
+
+function NE.nineslice.AddLayout(name, layout)
+ NE.nineslice.layouts[name] = layout
+end
+
+-- Look up or create the piece texture on the container (retail GetNineSlicePiece).
+-- DOWNPORT: 3.3.5a frames don't have GetNineSlicePiece (a retail method); the feature-gate falls
+-- straight through to CreateTexture, which is exactly what we want for ported chrome.
+local function getPiece(container, pieceName)
+ local p = container[pieceName]
+ if p then return p, true end
+ if container.GetNineSlicePiece then
+ p = container:GetNineSlicePiece(pieceName)
+ if p then
+ container[pieceName] = p
+ return p, true
+ end
+ end
+ p = container:CreateTexture()
+ container[pieceName] = p
+ return p, false
+end
+
+-- Tile flags from the WoW atlas naming convention: `_` = horiz-tile, `!` = vert-tile.
+local function tileFlagsFromAtlasName(atlasName)
+ if not atlasName or atlasName == "" then return false, false end
+ local prefix = atlasName:sub(1, 1)
+ return prefix == "_", prefix == "!"
+end
+
+-- After SetAtlas, optionally re-flip texcoord for mirrored pieces.
+-- DOWNPORT: read the atlas entry through NE.tex._atlasEntry (our registry) not NE_ATLAS.
+local function applyPostAtlasMirror(piece, setupInfo, pieceLayout, userLayout)
+ local pieceMirrored = pieceLayout.mirrorLayout
+ if pieceMirrored == nil then pieceMirrored = userLayout and userLayout.mirrorLayout end
+ if not pieceMirrored then return end
+ local entry = NE.tex._atlasEntry and NE.tex._atlasEntry(pieceLayout.atlas)
+ if not entry then return end
+ local L, R, T, B = entry.left, entry.right, entry.top, entry.bottom
+ if setupInfo.mirrorHorizontal then L, R = R, L end
+ if setupInfo.mirrorVertical then T, B = B, T end
+ piece:SetTexCoord(L, R, T, B)
+end
+
+local function setupCorner(container, piece, setupInfo, pieceLayout)
+ piece:ClearAllPoints()
+ piece:SetPoint(pieceLayout.point or setupInfo.point, container,
+ pieceLayout.relativePoint or setupInfo.point,
+ pieceLayout.x or 0, pieceLayout.y or 0)
+end
+
+local function setupEdge(container, piece, setupInfo, pieceLayout)
+ piece:ClearAllPoints()
+ local p1 = getPiece(container, setupInfo.relativePieces[1])
+ local p2 = getPiece(container, setupInfo.relativePieces[2])
+ piece:SetPoint(setupInfo.point, p1, setupInfo.relativePoint,
+ pieceLayout.x or 0, pieceLayout.y or 0)
+ piece:SetPoint(setupInfo.relativePoint, p2, setupInfo.point,
+ pieceLayout.x1 or 0, pieceLayout.y1 or 0)
+end
+
+local function setupCenter(container, piece, setupInfo, pieceLayout)
+ piece:ClearAllPoints()
+ local tl = getPiece(container, "TopLeftCorner")
+ local br = getPiece(container, "BottomRightCorner")
+ piece:SetPoint("TOPLEFT", tl, "BOTTOMRIGHT", pieceLayout.x or 0, pieceLayout.y or 0)
+ piece:SetPoint("BOTTOMRIGHT", br, "TOPLEFT", pieceLayout.x1 or 0, pieceLayout.y1 or 0)
+end
+
+-- Ordered piece list. Corners first so edges can anchor to them; center last.
+local nineSliceSetup = {
+ { pieceName = "TopLeftCorner", point = "TOPLEFT", fn = setupCorner },
+ { pieceName = "TopRightCorner", point = "TOPRIGHT", mirrorHorizontal = true, fn = setupCorner },
+ { pieceName = "BottomLeftCorner", point = "BOTTOMLEFT", mirrorVertical = true, fn = setupCorner },
+ { pieceName = "BottomRightCorner", point = "BOTTOMRIGHT", mirrorHorizontal = true, mirrorVertical = true, fn = setupCorner },
+ { pieceName = "TopEdge", point = "TOPLEFT", relativePoint = "TOPRIGHT",
+ relativePieces = { "TopLeftCorner", "TopRightCorner" }, fn = setupEdge },
+ { pieceName = "BottomEdge", point = "BOTTOMLEFT", relativePoint = "BOTTOMRIGHT",
+ relativePieces = { "BottomLeftCorner", "BottomRightCorner" }, mirrorVertical = true, fn = setupEdge },
+ { pieceName = "LeftEdge", point = "TOPLEFT", relativePoint = "BOTTOMLEFT",
+ relativePieces = { "TopLeftCorner", "BottomLeftCorner" }, fn = setupEdge },
+ { pieceName = "RightEdge", point = "TOPRIGHT", relativePoint = "BOTTOMRIGHT",
+ relativePieces = { "TopRightCorner", "BottomRightCorner" }, mirrorHorizontal = true, fn = setupEdge },
+ { pieceName = "Center", fn = setupCenter },
+}
+
+function NE.nineslice.ApplyLayout(container, layoutName)
+ if not container then return false end
+ local layout = NE.nineslice.layouts[layoutName]
+ if not layout then
+ if NE.Log then NE.Log("NINESLICE", "MISS layout " .. tostring(layoutName)) end
+ return false
+ end
+
+ local anyApplied = false
+ for _, setup in ipairs(nineSliceSetup) do
+ local pieceLayout = layout[setup.pieceName]
+ if pieceLayout then
+ local piece, existed = getPiece(container, setup.pieceName)
+ if not existed then
+ local layer = container.layoutTextureLayer or pieceLayout.layer or "BORDER"
+ local subLevel = container.layoutTextureSubLevel or pieceLayout.subLevel
+ piece:SetDrawLayer(layer, subLevel)
+ end
+ setup.fn(container, piece, setup, pieceLayout)
+ if NE.tex.SetAtlas(piece, pieceLayout.atlas, false) then anyApplied = true end
+ -- Tile flags and mirror must run POST-atlas (SetAtlas resets wrap + overwrites texcoord).
+ local h, v = tileFlagsFromAtlasName(pieceLayout.atlas)
+ piece:SetHorizTile(h)
+ piece:SetVertTile(v)
+ applyPostAtlasMirror(piece, setup, pieceLayout, layout)
+ if pieceLayout.w then piece:SetWidth(pieceLayout.w) end
+ if pieceLayout.h then piece:SetHeight(pieceLayout.h) end
+ piece:Show()
+ end
+ end
+
+ -- DOWNPORT: disableSharpening drove SetTexelSnappingBias/SetSnapToPixelGrid (retail-only) —
+ -- feature-gated so it no-ops on 3.3.5a.
+ if layout.disableSharpening then
+ for _, setup in ipairs(nineSliceSetup) do
+ local piece = container[setup.pieceName]
+ if piece and piece.SetTexelSnappingBias then
+ piece:SetTexelSnappingBias(0)
+ if piece.SetSnapToPixelGrid then piece:SetSnapToPixelGrid(false) end
+ end
+ end
+ end
+ return anyApplied
+end
+
+-- §2 CONTRACT alias. opts is accepted for forward-compat (unused in v1 layouts).
+function NE.nineslice.Apply(frame, layoutName, opts)
+ return NE.nineslice.ApplyLayout(frame, layoutName, opts)
+end
+
+-- AttachInset — the recurring "recessed inner border" idiom: a mouse-transparent child Frame
+-- anchored TOPLEFT/BOTTOMRIGHT into `parent` with the InsetFrameTemplate nineslice layout walked
+-- onto it. Returns the child so the caller can stash it (e.g. parent.NineSlice = ns). Offsets
+-- default to 0, so an all-points inset is AttachInset(parent).
+-- DOWNPORT: NewEra Core/Chrome/NineSlice.lua — ported verbatim, was missing from this file.
+function NE.nineslice.AttachInset(parent, tlx, tly, brx, bry)
+ if not parent then return end
+ local ns = CreateFrame("Frame", nil, parent)
+ ns:SetPoint("TOPLEFT", parent, "TOPLEFT", tlx or 0, tly or 0)
+ ns:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", brx or 0, bry or 0)
+ ns:EnableMouse(false)
+ NE.nineslice.ApplyLayout(ns, "InsetFrameTemplate")
+ return ns
+end
+
+function NE.nineslice.HideLayout(container)
+ if not container then return end
+ for _, setup in ipairs(nineSliceSetup) do
+ local p = container[setup.pieceName]
+ if p then p:Hide() end
+ end
+end
+
+-- TopTileStreaks — the horizontally-tiled decorative streak band under the title bar.
+-- DOWNPORT: SetShown → Show/Hide; reads our atlas registry via NE.tex._atlasEntry.
+function NE.nineslice.ApplyTopTileStreaks(frame, opts)
+ if not frame then return end
+ opts = opts or {}
+ local host = opts.parent or frame
+ local t = frame._neTopTileStreaks
+ if not t then
+ t = host:CreateTexture(nil, opts.layer or "BORDER", nil, opts.subLevel)
+ frame._neTopTileStreaks = t
+ end
+ t:ClearAllPoints()
+ t:SetPoint("TOPLEFT", host, "TOPLEFT", opts.xL or 6, opts.y or -21)
+ t:SetPoint("TOPRIGHT", host, "TOPRIGHT", opts.xR or -2, opts.y or -21)
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(t, "_ui-frame-toptilestreaks", false)) then
+ -- Hiding and giving up here is why the band sometimes never appeared: whatever made the atlas
+ -- unresolvable at build time (registry or BLP not ready yet) is usually transient, but nothing
+ -- ever asked again, so the panel stayed bandless for the session. Retry on a timer instead.
+ t:Hide()
+ if C_Timer and C_Timer.After and (frame._neStreakTries or 0) < 5 then
+ frame._neStreakTries = (frame._neStreakTries or 0) + 1
+ C_Timer.After(1, function() NE.nineslice.ApplyTopTileStreaks(frame, opts) end)
+ end
+ return t
+ end
+ frame._neStreakTries = nil
+ t:SetHorizTile(true)
+ local entry = NE.tex._atlasEntry and NE.tex._atlasEntry("_ui-frame-toptilestreaks")
+ t:SetHeight(opts.h or (entry and entry.height) or 43)
+ t:Show()
+ return t
+end
+
+-- Hide any pre-existing nineslice pieces (a frame's .NineSlice child + direct pieces).
+function NE.nineslice.HideClassicChrome(frame)
+ if not frame then return end
+ local nineSlice = frame.NineSlice
+ if nineSlice then NE.nineslice.HideLayout(nineSlice) end
+ NE.nineslice.HideLayout(frame)
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/NineSliceLayouts.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/NineSliceLayouts.lua
new file mode 100644
index 0000000..dee3d3f
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/NineSliceLayouts.lua
@@ -0,0 +1,258 @@
+-- DragonUI_NewEra/core/NineSliceLayouts.lua — retail nineslice layout data + the atlas COORD
+-- tables the Core agent owns.
+--
+-- DOWNPORT: NewEra Core/NineSliceLayouts.lua → 3.3.5a. NewEra's layouts referenced retail atlas
+-- nicknames that resolved through the GENERATED NE_ATLAS global. Per the ARCHITECT DECISION
+-- (CONTRACTS §2): the Core agent OWNS the atlas COORD tables. So this file does two jobs:
+-- (1) RegisterAtlas the coord rects for every atlas these layouts use (transcribed from
+-- NewEra's Generated/AtlasData.lua — the exact left/right/top/bottom + width/height).
+-- (2) AddLayout the nineslice piece tables (verbatim from NewEra, minus retail-only fields).
+-- The Asset agent (Textures/Assets.lua) supplies fdid→BLP path via NE.tex.RegisterLocal; we
+-- supply atlas-name→{fdid, coords}. We do NOT call NE.tex.RegisterLocal here (that's §3's job).
+--
+-- Coord entries are { file=, left=, right=, top=, bottom=, width=, height= }, identical to
+-- NewEra's NE_ATLAS rows so the texcoords are pixel-correct against the same BLP sheets.
+
+local NE = DragonUI_NewEra
+NE.nineslice = NE.nineslice or {}
+NE.nineslice.layouts = NE.nineslice.layouts or {}
+
+-- ============================================================================
+-- ATLAS COORD TABLES (owned by Core; transcribed from NewEra Generated/AtlasData.lua)
+-- ============================================================================
+-- UI-Frame-Metal family (corners FDID 2406979, vert edges 2406984, horiz edges 2406987),
+-- the PortraitMetal corner, the InsetFrameTemplate inner-border (1723831/1723832/1723833),
+-- and the TopTileStreaks band (1723833). These are the Sprint-0 minimum atlas set (§3).
+NE.tex.RegisterAtlases({
+ -- Metal corners (sheet 2406979).
+ ["ui-frame-metal-cornertopleft-2x"] = { file = 2406979, left = 0.001953, right = 0.294922, top = 0.001953, bottom = 0.294922, width = 150, height = 150 },
+ ["ui-frame-metal-cornertopright-2x"] = { file = 2406979, left = 0.298828, right = 0.591797, top = 0.001953, bottom = 0.294922, width = 150, height = 150 },
+ ["ui-frame-metal-cornertoprightdouble-2x"] = { file = 2406979, left = 0.595703, right = 0.888672, top = 0.001953, bottom = 0.294922, width = 150, height = 150 },
+ ["ui-frame-metal-cornerbottomleft-2x"] = { file = 2406979, left = 0.298828, right = 0.423828, top = 0.298828, bottom = 0.423828, width = 64, height = 64 },
+ ["ui-frame-metal-cornerbottomright-2x"] = { file = 2406979, left = 0.427734, right = 0.552734, top = 0.298828, bottom = 0.423828, width = 64, height = 64 },
+ ["ui-frame-portraitmetal-cornertopleft-2x"]= { file = 2406979, left = 0.001953, right = 0.294922, top = 0.298828, bottom = 0.591797, width = 150, height = 150 },
+ -- Metal edges (vert-tile sheet 2406984, horiz-tile sheet 2406987).
+ ["!ui-frame-metal-edgeleft-2x"] = { file = 2406984, left = 0.001953, right = 0.294922, top = 0.000000, bottom = 1.000000, width = 150, height = 32 },
+ ["!ui-frame-metal-edgeright-2x"] = { file = 2406984, left = 0.298828, right = 0.591797, top = 0.000000, bottom = 1.000000, width = 150, height = 32 },
+ ["_ui-frame-metal-edgetop-2x"] = { file = 2406987, left = 0.000000, right = 1.000000, top = 0.003906, bottom = 0.589844, width = 64, height = 150 },
+ ["_ui-frame-metal-edgebottom-2x"] = { file = 2406987, left = 0.000000, right = 0.500000, top = 0.597656, bottom = 0.847656, width = 32, height = 64 },
+ -- TopTileStreaks band (sheet 1723833).
+ ["_ui-frame-toptilestreaks"] = { file = 1723833, left = 0.000000, right = 1.000000, top = 0.007812, bottom = 0.343750, width = 256, height = 43 },
+ -- InsetFrameTemplate inner border: corners (1723831), L/R tiles (1723832), T/B tiles (1723833).
+ ["ui-frame-innertopleft"] = { file = 1723831, left = 0.757812, right = 0.804688, top = 0.554688, bottom = 0.601562, width = 6, height = 6 },
+ ["ui-frame-innertopright"] = { file = 1723831, left = 0.820312, right = 0.867188, top = 0.554688, bottom = 0.601562, width = 6, height = 6 },
+ ["ui-frame-innerbotleftcorner"] = { file = 1723831, left = 0.632812, right = 0.679688, top = 0.554688, bottom = 0.601562, width = 6, height = 6 },
+ ["ui-frame-innerbotright"] = { file = 1723831, left = 0.695312, right = 0.742188, top = 0.554688, bottom = 0.601562, width = 6, height = 6 },
+ ["!ui-frame-innerlefttile"] = { file = 1723832, left = 0.484375, right = 0.531250, top = 0.000000, bottom = 1.000000, width = 3, height = 256 },
+ ["!ui-frame-innerrighttile"] = { file = 1723832, left = 0.562500, right = 0.609375, top = 0.000000, bottom = 1.000000, width = 3, height = 256 },
+ ["_ui-frame-innertoptile"] = { file = 1723833, left = 0.000000, right = 1.000000, top = 0.906250, bottom = 0.929688, width = 256, height = 3 },
+ ["_ui-frame-innerbottile"] = { file = 1723833, left = 0.000000, right = 1.000000, top = 0.867188, bottom = 0.890625, width = 256, height = 3 },
+})
+
+-- RedButton close-button family (sheet 4698972) — used by PanelChrome.ModernizeCloseButton.
+NE.tex.RegisterAtlases({
+ ["redbutton-exit-2x"] = { file = 4698972, left = 0.152344, right = 0.292969, top = 0.007812, bottom = 0.304688, width = 36, height = 38 },
+ ["redbutton-exit-disabled-2x"] = { file = 4698972, left = 0.152344, right = 0.292969, top = 0.320312, bottom = 0.617188, width = 36, height = 38 },
+ ["redbutton-exit-pressed-2x"] = { file = 4698972, left = 0.152344, right = 0.292969, top = 0.632812, bottom = 0.929688, width = 36, height = 38 },
+ ["redbutton-highlight-2x"] = { file = 4698972, left = 0.449219, right = 0.589844, top = 0.007812, bottom = 0.304688, width = 36, height = 38 },
+})
+
+-- Panel-tab family (sheet 4707839) — used by Tabs.lua.
+NE.tex.RegisterAtlases({
+ ["uiframe-tab-left"] = { file = 4707839, left = 0.015625, right = 0.562500, top = 0.816406, bottom = 0.957031, width = 35, height = 36 },
+ ["uiframe-tab-right"] = { file = 4707839, left = 0.015625, right = 0.593750, top = 0.667969, bottom = 0.808594, width = 37, height = 36 },
+ ["_uiframe-tab-center"] = { file = 4707839, left = 0.000000, right = 0.015625, top = 0.175781, bottom = 0.316406, width = 1, height = 36 },
+ ["uiframe-activetab-left"] = { file = 4707839, left = 0.015625, right = 0.562500, top = 0.496094, bottom = 0.660156, width = 35, height = 42 },
+ ["uiframe-activetab-right"] = { file = 4707839, left = 0.015625, right = 0.593750, top = 0.324219, bottom = 0.488281, width = 37, height = 42 },
+ ["_uiframe-activetab-center"] = { file = 4707839, left = 0.000000, right = 0.015625, top = 0.003906, bottom = 0.167969, width = 1, height = 42 },
+})
+
+-- Minimal-scrollbar family (track/arrows sheet 4331838, thumb caps 5142787, thumb middle
+-- 5142784) — used by ScrollbarReskin.lua. These sheets are NOT in the Sprint-0 minimum set, so
+-- the scrollbar reskin degrades gracefully (native art kept) until §3 ships them; the coords are
+-- registered now so it lights up automatically once the BLPs land.
+NE.tex.RegisterAtlases({
+ ["minimal-scrollbar-track-top"] = { file = 4331838, left = 0.164062, right = 0.226562, top = 0.609375, bottom = 0.734375, width = 8, height = 8 },
+ ["minimal-scrollbar-track-bottom"] = { file = 4331838, left = 0.085938, right = 0.148438, top = 0.765625, bottom = 0.890625, width = 8, height = 8 },
+ ["!minimal-scrollbar-track-middle"]= { file = 4332072, left = 0.015625, right = 0.140625, top = 0.000000, bottom = 0.000977, width = 8, height = 1 },
+ ["minimal-scrollbar-arrow-top"] = { file = 4331838, left = 0.687500, right = 0.820312, top = 0.015625, bottom = 0.187500, width = 17, height = 11 },
+ ["minimal-scrollbar-arrow-top-over"] = { file = 4331838, left = 0.390625, right = 0.523438, top = 0.218750, bottom = 0.390625, width = 17, height = 11 },
+ ["minimal-scrollbar-arrow-top-down"] = { file = 4331838, left = 0.835938, right = 0.968750, top = 0.015625, bottom = 0.187500, width = 17, height = 11 },
+ ["minimal-scrollbar-arrow-bottom"] = { file = 4331838, left = 0.242188, right = 0.375000, top = 0.812500, bottom = 0.984375, width = 17, height = 11 },
+ ["minimal-scrollbar-arrow-bottom-over"] = { file = 4331838, left = 0.539062, right = 0.671875, top = 0.015625, bottom = 0.187500, width = 17, height = 11 },
+ ["minimal-scrollbar-arrow-bottom-down"] = { file = 4331838, left = 0.390625, right = 0.523438, top = 0.015625, bottom = 0.187500, width = 17, height = 11 },
+ ["minimal-scrollbar-small-thumb-middle"] = { file = 5142784, left = 0.484375, right = 0.609375, top = 0.000977, bottom = 0.699219, width = 8, height = 715 },
+ ["minimal-scrollbar-small-thumb-middle-over"] = { file = 5142784, left = 0.328125, right = 0.453125, top = 0.000977, bottom = 0.699219, width = 8, height = 715 },
+ ["minimal-scrollbar-small-thumb-middle-down"] = { file = 5142784, left = 0.171875, right = 0.296875, top = 0.000977, bottom = 0.699219, width = 8, height = 715 },
+ ["minimal-scrollbar-small-thumb-top"] = { file = 5142787, left = 0.312500, right = 0.437500, top = 0.843750, bottom = 0.968750, width = 8, height = 8 },
+ ["minimal-scrollbar-small-thumb-top-over"] = { file = 5142787, left = 0.468750, right = 0.593750, top = 0.843750, bottom = 0.968750, width = 8, height = 8 },
+ ["minimal-scrollbar-small-thumb-top-down"] = { file = 5142787, left = 0.468750, right = 0.593750, top = 0.687500, bottom = 0.812500, width = 8, height = 8 },
+ ["minimal-scrollbar-small-thumb-bottom"] = { file = 5142787, left = 0.609375, right = 0.734375, top = 0.484375, bottom = 0.609375, width = 8, height = 8 },
+ ["minimal-scrollbar-small-thumb-bottom-over"] = { file = 5142787, left = 0.312500, right = 0.437500, top = 0.687500, bottom = 0.812500, width = 8, height = 8 },
+ ["minimal-scrollbar-small-thumb-bottom-down"] = { file = 5142787, left = 0.765625, right = 0.890625, top = 0.484375, bottom = 0.609375, width = 8, height = 8 },
+
+ -- MinimalSliderWithSteppers (sheet 4567914, 32x128) — the HORIZONTAL slider, natively. Retail's
+ -- own rects, verbatim; the thumb is the little diamond. Two things to know about this family:
+ -- * The run is ONE PIXEL wide (`_` prefix = tiles horizontally), so the track is cap + run + cap
+ -- and only the middle stretches, the same shape as every bar in this file.
+ -- * There are NO hover or pressed variants. Retail's minimal slider does not change art on state,
+ -- and the sheet has nothing to swap to — so the skin does not invent any.
+ ["minimal_sliderbar_left"] = { file = 4567914, left = 0.437500, right = 0.781250, top = 0.320312, bottom = 0.453125, width = 11, height = 17 },
+ ["minimal_sliderbar_right"] = { file = 4567914, left = 0.031250, right = 0.375000, top = 0.484375, bottom = 0.617188, width = 11, height = 17 },
+ ["_minimal_sliderbar_middle"] = { file = 4567914, left = 0.000000, right = 0.031250, top = 0.007812, bottom = 0.140625, width = 1, height = 17 },
+ ["minimal_sliderbar_button"] = { file = 4567914, left = 0.031250, right = 0.656250, top = 0.156250, bottom = 0.304688, width = 20, height = 19 },
+ ["minimal_sliderbar_button_left"] = { file = 4567914, left = 0.031250, right = 0.375000, top = 0.320312, bottom = 0.468750, width = 11, height = 19 },
+ ["minimal_sliderbar_button_right"] = { file = 4567914, left = 0.031250, right = 0.312500, top = 0.632812, bottom = 0.773438, width = 9, height = 18 },
+
+ -- DiamondMetal (frame sheet 3056750, side edges 3056755) — retail's "Dialog" border, the clean
+ -- thin frame the ESC menu and every modern dialog wear. Distinct from the Metal family above,
+ -- which is the heavy panel chrome with the portrait ring.
+ ["ui-frame-diamondmetal-cornertopleft-2x"] = { file = 3056750, left = 0.003906, right = 0.503906, top = 0.508789, bottom = 0.633789, width = 64, height = 64 },
+ ["ui-frame-diamondmetal-cornertopright-2x"] = { file = 3056750, left = 0.003906, right = 0.503906, top = 0.635742, bottom = 0.760742, width = 64, height = 64 },
+ ["ui-frame-diamondmetal-cornerbottomleft-2x"] = { file = 3056750, left = 0.003906, right = 0.503906, top = 0.254883, bottom = 0.379883, width = 64, height = 64 },
+ ["ui-frame-diamondmetal-cornerbottomright-2x"] = { file = 3056750, left = 0.003906, right = 0.503906, top = 0.381836, bottom = 0.506836, width = 64, height = 64 },
+ ["_ui-frame-diamondmetal-edgetop-2x"] = { file = 3056750, left = 0.000000, right = 0.500000, top = 0.127930, bottom = 0.252930, width = 64, height = 64 },
+ ["_ui-frame-diamondmetal-edgebottom-2x"] = { file = 3056750, left = 0.000000, right = 0.500000, top = 0.000977, bottom = 0.125977, width = 64, height = 64 },
+ ["!ui-frame-diamondmetal-edgeleft-2x"] = { file = 3056755, left = 0.001953, right = 0.251953, top = 0.000000, bottom = 1.000000, width = 64, height = 64 },
+ ["!ui-frame-diamondmetal-edgeright-2x"] = { file = 3056755, left = 0.255859, right = 0.505859, top = 0.000000, bottom = 1.000000, width = 64, height = 64 },
+
+ -- BigRedThreeSliceButton (sheet 1536801) — the red dialog button retail uses for confirm/action
+ -- rows, and what NE.buttonskin.Skin is written against. Left and Right carry the caps at their own
+ -- native widths (114 and 292 at 2x); Center is the `_`-prefixed tile between them. Three states,
+ -- plus a highlight that is one wide strip the skin re-slices itself.
+ ["128-redbutton-left"] = { file = 1536801, left = 0.763672, right = 0.986328, top = 0.444824, bottom = 0.507324, width = 114, height = 128 },
+ ["128-redbutton-left-pressed"] = { file = 1536801, left = 0.763672, right = 0.986328, top = 0.571777, bottom = 0.634277, width = 114, height = 128 },
+ ["128-redbutton-left-disabled"] = { file = 1536801, left = 0.763672, right = 0.986328, top = 0.508301, bottom = 0.570801, width = 114, height = 128 },
+ ["_128-redbutton-center"] = { file = 1536801, left = 0.000000, right = 0.125000, top = 0.000488, bottom = 0.062988, width = 64, height = 128 },
+ ["_128-redbutton-center-pressed"] = { file = 1536801, left = 0.000000, right = 0.125000, top = 0.127441, bottom = 0.189941, width = 64, height = 128 },
+ ["_128-redbutton-center-disabled"] = { file = 1536801, left = 0.000000, right = 0.125000, top = 0.063965, bottom = 0.126465, width = 64, height = 128 },
+ ["128-redbutton-right"] = { file = 1536801, left = 0.001953, right = 0.572266, top = 0.254395, bottom = 0.316895, width = 292, height = 128 },
+ ["128-redbutton-right-pressed"] = { file = 1536801, left = 0.001953, right = 0.572266, top = 0.381348, bottom = 0.443848, width = 292, height = 128 },
+ ["128-redbutton-right-disabled"] = { file = 1536801, left = 0.001953, right = 0.572266, top = 0.317871, bottom = 0.380371, width = 292, height = 128 },
+ ["128-redbutton-highlight"] = { file = 1536801, left = 0.001953, right = 0.863281, top = 0.190918, bottom = 0.253418, width = 441, height = 128 },
+
+ -- Modern dropdown trigger (sheet 5390329). `textholder` is the static body; the GRADIENT lives
+ -- on the a-button arrow, which is why the states below are arrow art and the body has none.
+ --
+ -- THE BODY IS A 3-SLICE, NOT A STRETCH. The source is a 54x41 rounded rect with a ~12px bevelled
+ -- corner and a soft outer glow — stretching that whole rect to a 200x26 row smears the corners into
+ -- horizontal blobs and squashes the bevel to a blur. So it is sliced: the two caps keep the corners
+ -- at their own aspect (scaled by the row's height), and only the flat middle run stretches, which is
+ -- a plain vertical border and so stretches invisibly. Columns 13..42 of the source are uniform, so
+ -- the 18px cut below lands well inside the flat region on both sides.
+ ["common-dropdown-textholder"] = { file = 5390329, left = 0.001953, right = 0.107422, top = 0.636719, bottom = 0.796875, width = 54, height = 41 },
+ ["common-dropdown-textholder-left"] = { file = 5390329, left = 0.001953, right = 0.037109, top = 0.636719, bottom = 0.796875, width = 18, height = 41 },
+ ["common-dropdown-textholder-center"] = { file = 5390329, left = 0.037109, right = 0.072266, top = 0.636719, bottom = 0.796875, width = 18, height = 41 },
+ ["common-dropdown-textholder-right"] = { file = 5390329, left = 0.072266, right = 0.107422, top = 0.636719, bottom = 0.796875, width = 18, height = 41 },
+ ["common-dropdown-a-button"] = { file = 5390329, left = 0.111328, right = 0.164062, top = 0.636719, bottom = 0.742188, width = 27, height = 27 },
+ ["common-dropdown-a-button-hover"] = { file = 5390329, left = 0.341797, right = 0.394531, top = 0.863281, bottom = 0.968750, width = 27, height = 27 },
+ ["common-dropdown-a-button-pressed"] = { file = 5390329, left = 0.501953, right = 0.554688, top = 0.335938, bottom = 0.441406, width = 27, height = 27 },
+ ["common-dropdown-a-button-pressedhover"] = { file = 5390329, left = 0.501953, right = 0.554688, top = 0.562500, bottom = 0.667969, width = 27, height = 27 },
+ ["common-dropdown-a-button-open"] = { file = 5390329, left = 0.421875, right = 0.474609, top = 0.816406, bottom = 0.921875, width = 27, height = 27 },
+ ["common-dropdown-a-button-disabled"] = { file = 5390329, left = 0.181641, right = 0.234375, top = 0.863281, bottom = 0.968750, width = 27, height = 27 },
+})
+
+-- ============================================================================
+-- LAYOUT TABLES (verbatim from NewEra, retail-only fields kept — they're harmless data)
+-- ============================================================================
+
+-- The mainline panel chrome (no portrait). Used by non-portrait frames.
+NE.nineslice.AddLayout("ButtonFrameTemplateNoPortrait", {
+ disableSharpening = true,
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopLeft-2x", w = 75, h = 75, x = -8, y = 16 },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRight-2x", w = 75, h = 75, x = 4, y = 16 },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft-2x", w = 32, h = 32, x = -8, y = -3 },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight-2x", w = 32, h = 32, x = 4, y = -3 },
+ TopEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop-2x", h = 75 },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom-2x", h = 32 },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft-2x", w = 75 },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight-2x", w = 75 },
+})
+-- §2 contract requires "ButtonFrameTemplate" by name — alias it to the no-portrait metal chrome.
+NE.nineslice.AddLayout("ButtonFrameTemplate", NE.nineslice.layouts["ButtonFrameTemplateNoPortrait"])
+
+-- Dialog — retail's "clean diamond" border (DialogBorderTemplate: the ESC menu and every modern
+-- dialog). The DiamondMetal frame, NOT the heavy Metal panel chrome above: a small floating dialog
+-- wearing panel chrome reads as a window that lost its contents. Verbatim from NewEra
+-- Core/Chrome/NineSliceLayouts.lua, including its D — the corner square and the edge thickness must
+-- match or the corners and edges do not line up.
+local D = 32
+NE.nineslice.AddLayout("Dialog", {
+ -- No disableSharpening: retail's Dialog layout leaves the chrome at the crisp engine default,
+ -- and NewEra's note records that softening it here read blurrier than retail.
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-DiamondMetal-CornerTopLeft-2x", w = D, h = D },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-DiamondMetal-CornerTopRight-2x", w = D, h = D },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-DiamondMetal-CornerBottomLeft-2x", w = D, h = D },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-DiamondMetal-CornerBottomRight-2x", w = D, h = D },
+ TopEdge = { layer = "OVERLAY", atlas = "_UI-Frame-DiamondMetal-EdgeTop-2x", h = D },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-DiamondMetal-EdgeBottom-2x", h = D },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-DiamondMetal-EdgeLeft-2x", w = D },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-DiamondMetal-EdgeRight-2x", w = D },
+})
+
+-- InsetFrameTemplate — the THIN GOLD inner border that wraps an inset's content area.
+NE.nineslice.AddLayout("InsetFrameTemplate", {
+ TopLeftCorner = { layer = "BORDER", subLevel = -5, atlas = "UI-Frame-InnerTopLeft", w = 6, h = 6 },
+ TopRightCorner = { layer = "BORDER", subLevel = -5, atlas = "UI-Frame-InnerTopRight", w = 6, h = 6 },
+ BottomLeftCorner = { layer = "BORDER", subLevel = -5, atlas = "UI-Frame-InnerBotLeftCorner", w = 6, h = 6, x = 0, y = -1 },
+ BottomRightCorner = { layer = "BORDER", subLevel = -5, atlas = "UI-Frame-InnerBotRight", w = 6, h = 6, x = 0, y = -1 },
+ TopEdge = { layer = "BORDER", subLevel = -5, atlas = "_UI-Frame-InnerTopTile", h = 3 },
+ BottomEdge = { layer = "BORDER", subLevel = -5, atlas = "_UI-Frame-InnerBotTile", h = 3 },
+ LeftEdge = { layer = "BORDER", subLevel = -5, atlas = "!UI-Frame-InnerLeftTile", w = 3 },
+ RightEdge = { layer = "BORDER", subLevel = -5, atlas = "!UI-Frame-InnerRightTile", w = 3 },
+})
+
+-- PortraitFrameTemplate — same metal family, with the circular-cutout top-left corner.
+NE.nineslice.AddLayout("PortraitFrameTemplate", {
+ disableSharpening = true,
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-PortraitMetal-CornerTopLeft-2x", w = 75, h = 75, x = -13, y = 16 },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRight-2x", w = 75, h = 75, x = 4, y = 16 },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft-2x", w = 32, h = 32, x = -13, y = -3 },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight-2x", w = 32, h = 32, x = 4, y = -3 },
+ TopEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop-2x", h = 75 },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom-2x", h = 32 },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft-2x", w = 75 },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight-2x", w = 75 },
+})
+
+-- PortraitFrameTemplateMinimizable — "Double" right corner (max/min button room).
+NE.nineslice.AddLayout("PortraitFrameTemplateMinimizable", {
+ disableSharpening = true,
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-PortraitMetal-CornerTopLeft-2x", w = 75, h = 75, x = -13, y = 16 },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRightDouble-2x", w = 75, h = 75, x = 4, y = 16 },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft-2x", w = 32, h = 32, x = -13, y = -3 },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight-2x", w = 32, h = 32, x = 4, y = -3 },
+ TopEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop-2x", h = 75 },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom-2x", h = 32 },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft-2x", w = 75 },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight-2x", w = 75 },
+})
+
+-- ButtonFrameTemplateNoPortraitMinimizable — plain metal frame WITH the Double top-right corner.
+NE.nineslice.AddLayout("ButtonFrameTemplateNoPortraitMinimizable", {
+ disableSharpening = true,
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopLeft-2x", w = 75, h = 75, x = -12, y = 16 },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRightDouble-2x", w = 75, h = 75, x = 4, y = 16 },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft-2x", w = 32, h = 32, x = -12, y = -3 },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight-2x", w = 32, h = 32, x = 4, y = -3 },
+ TopEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop-2x", h = 75 },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom-2x", h = 32 },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft-2x", w = 75 },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight-2x", w = 75 },
+})
+
+-- HeldBagLayout — PortraitFrameTemplate with the SMALL portrait corner (36x36 portrait).
+-- DOWNPORT: NewEra used "UI-Frame-PortraitMetal-CornerTopLeftSmall-2x"; that small-cutout corner
+-- isn't in the Sprint-0 sheet set, so we reuse the standard portrait corner. The piece falls back
+-- gracefully (SetAtlas returns false → piece hidden) if the small variant isn't registered.
+NE.nineslice.AddLayout("HeldBagLayout", {
+ disableSharpening = true,
+ TopLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-PortraitMetal-CornerTopLeft-2x", w = 75, h = 75, x = -13, y = 16 },
+ TopRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerTopRight-2x", w = 75, h = 75, x = 4, y = 16 },
+ BottomLeftCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomLeft-2x", w = 32, h = 32, x = -13, y = -3 },
+ BottomRightCorner = { layer = "OVERLAY", atlas = "UI-Frame-Metal-CornerBottomRight-2x", w = 32, h = 32, x = 4, y = -3 },
+ TopEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeTop-2x", h = 75 },
+ BottomEdge = { layer = "OVERLAY", atlas = "_UI-Frame-Metal-EdgeBottom-2x", h = 32 },
+ LeftEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeLeft-2x", w = 75 },
+ RightEdge = { layer = "OVERLAY", atlas = "!UI-Frame-Metal-EdgeRight-2x", w = 75 },
+})
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/PanelChrome.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/PanelChrome.lua
new file mode 100644
index 0000000..44a2ba0
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/PanelChrome.lua
@@ -0,0 +1,342 @@
+-- DragonUI_NewEra/core/PanelChrome.lua — shared "modern portrait-frame chrome".
+--
+-- DOWNPORT: NewEra Core/PanelChrome.lua → 3.3.5a. NewEra's PC.Apply RESKINNED an existing Era
+-- ButtonFrameTemplate frame (hide classic border → modern NineSlice on f.NineSlice + rock Bg +
+-- TopTileStreaks + modern CloseButton). On 3.3.5a our panels are frequently BUILT FROM SCRATCH
+-- (the Hello demo, and Sprint-1 NE_ frames), so this version BUILDS the chrome pieces it needs
+-- (NineSlice child, Bg, portrait, title, close button) when they're absent, then reskins. It
+-- still reskins in-place when the pieces already exist.
+--
+-- §2 CONTRACT: NE.chrome.Apply(frame, opts) — nineslice + portrait + title + close-button reskin.
+-- We expose BOTH NE.chrome (the contract name) and NE.panelchrome (NewEra's name) on one table.
+--
+-- All ops are texture Show/Hide + SetTexture/SetAtlas + SetFrameLevel — never protected — so
+-- callers may re-assert in combat. DEGRADES GRACEFULLY: if an atlas/art isn't registered yet, the
+-- nineslice pieces hide themselves and we fall back to a solid-colour backdrop (Hello-demo proof).
+
+local NE = DragonUI_NewEra
+NE.panelchrome = NE.panelchrome or {}
+NE.chrome = NE.panelchrome -- DOWNPORT: §2 contract name; same table as NewEra's
+local PC = NE.panelchrome
+
+local ROCK_FDID = 374155 -- UI-Background-Rock (retail ButtonFrameTemplate default fill)
+
+-- ButtonFrameTemplate / PortraitFrameTemplate border pieces to hide so our modern NineSlice
+-- shows through (only relevant when reskinning an existing Blizzard/Era frame).
+local BORDER_KEYS = {
+ "PortraitFrame", "TopRightCorner", "TopBorder", "TopLeftCorner",
+ "BotLeftCorner", "BotRightCorner", "BottomBorder", "LeftBorder",
+ "RightBorder", "TitleBg",
+}
+
+-- Register a frame-owned BACKGROUND texture to preserve it from HideClassicChrome's walk.
+function PC.Keep(f, tex)
+ if not f or not tex then return end
+ f._nePanelKeep = f._nePanelKeep or {}
+ f._nePanelKeep[tex] = true
+end
+
+-- Thin alias to the canonical helper (FrameUtil.lua).
+function PC.PinPixelPerfect(f, userScale)
+ NE.FrameUtil.PinPixelPerfect(f, userScale)
+end
+
+-- Canonical panel title TEXT styling (GameFontNormal gold, centered, no wrap).
+function PC.SetTitle(frame, text, fs, anchorTo)
+ if not frame then return end
+ local tc = frame.TitleContainer
+ fs = fs or (tc and tc.TitleText) or frame.Title
+ if not fs then return end
+ fs:SetFontObject(GameFontNormal)
+ if fs.SetWordWrap then fs:SetWordWrap(false) end
+ fs:SetJustifyH("CENTER")
+ anchorTo = anchorTo or tc or frame
+ fs:ClearAllPoints()
+ fs:SetPoint("TOP", anchorTo, "TOP", 0, -5)
+ fs:SetPoint("LEFT", anchorTo, "LEFT")
+ fs:SetPoint("RIGHT", anchorTo, "RIGHT")
+ if text ~= nil then fs:SetText(text) end
+ frame._neTitle = fs
+ return fs
+end
+
+-- The retail title BAND for rehosted titles. Idempotent per frame.
+function PC.TitleBand(f)
+ if f._neTitleBand then return f._neTitleBand end
+ local tc = CreateFrame("Frame", nil, f)
+ tc:SetPoint("TOPLEFT", f, "TOPLEFT", 58, -1)
+ tc:SetPoint("TOPRIGHT", f, "TOPRIGHT", -24, -1)
+ tc:SetHeight(20)
+ tc:EnableMouse(false)
+ local nsLevel = (f.NineSlice and f.NineSlice.GetFrameLevel and f.NineSlice:GetFrameLevel())
+ or f:GetFrameLevel() or 1
+ tc:SetFrameLevel(nsLevel + 10)
+ f._neTitleBand = tc
+ return tc
+end
+
+-- Hide the classic chrome (only when reskinning a frame that already carries Blizzard/Era chrome).
+function PC.HideClassicChrome(f)
+ if not f then return end
+ for _, key in ipairs(BORDER_KEYS) do
+ local r = f[key]
+ if r and r.Hide then r:Hide() end
+ end
+ local keep = f._nePanelKeep
+ NE.FrameUtil.ForEachRegion(f, "Texture", "BACKGROUND", function(r)
+ if r ~= f.Bg and not (keep and keep[r]) then r:Hide() end
+ end)
+end
+
+-- DOWNPORT: ensure the structural pieces a portrait-frame needs exist. NewEra assumed the Era
+-- template already provided f.NineSlice / f.Bg / f.CloseButton / portrait; on a bare 3.3.5a
+-- CreateFrame they don't, so build them. Each is idempotent.
+local function ensureBg(f)
+ if f.Bg then return f.Bg end
+ local bg = f:CreateTexture(nil, "BACKGROUND")
+ -- DOWNPORT: the metal border's inner edge sits closer to the frame edge than a 4px inset, so a
+ -- 4px inset left a see-through gap (≈3-4px L/R, ≈2px bottom). Tighten the fill to meet the border.
+ bg:SetPoint("TOPLEFT", 1, -4)
+ bg:SetPoint("BOTTOMRIGHT", -1, 2)
+ f.Bg = bg
+ return bg
+end
+
+local function ensureNineSlice(f)
+ if f.NineSlice then return f.NineSlice end
+ local ns = CreateFrame("Frame", nil, f)
+ ns:SetAllPoints(f)
+ ns:SetFrameLevel((f:GetFrameLevel() or 1) + 1)
+ f.NineSlice = ns
+ return ns
+end
+
+local function ensureCloseButton(f)
+ if f.CloseButton then return f.CloseButton end
+ local cb = CreateFrame("Button", nil, f, "UIPanelCloseButton")
+ cb:SetPoint("TOPRIGHT", 1, 0)
+ -- DOWNPORT: the chrome stack paints over this spot — the nineslice top-right metal corner
+ -- (f:level+1) AND the title band (f:level+11) both sit at higher frame levels than a default
+ -- child button, occluding the button's Normal (X) texture while the HIGHLIGHT layer bleeds
+ -- through on hover. Lift the button clearly above the entire chrome stack so the X is visible.
+ local baseLvl = (f.GetFrameLevel and f:GetFrameLevel()) or 1
+ cb:SetFrameLevel(baseLvl + 20)
+ f.CloseButton = cb
+ return cb
+end
+
+local function ensureTitle(f, text)
+ if f.TitleContainer and f.TitleContainer.TitleText then return f.TitleContainer.TitleText end
+ if f.Title then return f.Title end
+ local band = PC.TitleBand(f)
+ local fs = band:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ f.Title = fs
+ PC.SetTitle(f, text, fs, band)
+ return fs
+end
+
+-- Public wrapper over ensureTitle: give the frame a standard title band + FontString if it has no
+-- title of its own yet, then style it. PC.SetTitle deliberately no-ops when it can't find a
+-- FontString (frame.TitleContainer.TitleText / frame.Title / one passed in), which is an easy trap
+-- to fall into -- a window that builds its own chrome and never made one renders a blank title bar
+-- with no error to explain it. Call this once at chrome-build time; SetTitle(f, text) works
+-- normally afterwards, so per-tab title updates need no changes.
+function PC.EnsureTitle(f, text)
+ if not f then return end
+ return ensureTitle(f, text)
+end
+
+local function ensurePortrait(f)
+ if f.portrait then return f.portrait end
+ if f.PortraitTexture then return f.PortraitTexture end
+ local p = f:CreateTexture(nil, "ARTWORK")
+ f.portrait = p
+ return p
+end
+
+-- DOWNPORT: solid-colour fallback backdrop so a chrome'd frame is never invisible/hard-erroring
+-- when the metal atlas art isn't registered yet (the Hello-demo graceful-degrade requirement).
+local function applyFallbackBackdrop(f)
+ if not f.Bg then ensureBg(f) end
+ f.Bg:SetTexture(0.07, 0.07, 0.09, 0.95) -- 3.3.5a accepts colour args to SetTexture
+ f.Bg:Show()
+ if not f._neFallbackBorder and f.SetBackdrop then
+ f:SetBackdrop({
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ edgeSize = 14, insets = { left = 4, right = 4, top = 4, bottom = 4 },
+ })
+ f:SetBackdropBorderColor(0.6, 0.5, 0.3, 1)
+ f._neFallbackBorder = true
+ end
+end
+
+-- DOWNPORT: the raw UI-Background-Rock sheet renders mid-brown on first paint (retail darkens it via
+-- panel composition). This explicit dark tint is what makes every NewEra window read dark immediately
+-- instead of only after a /reload settles it. It's the SHARED body tone for the whole window set —
+-- retune it here and the character panel, bags and every other chrome'd window move together.
+PC.BODY_TINT = { 0.32, 0.32, 0.32 }
+
+-- Paint f.Bg with the standard dark rock body fill. Returns true when the rock art was available,
+-- false when it degraded to the solid near-black fill — callers that want to know if real art landed
+-- can branch on it, but both outcomes are dark and safe to leave as-is.
+function PC.ApplyBodyFill(f)
+ if not f then return false end
+ ensureBg(f)
+ local localPath = NE.tex.localFiles and NE.tex.localFiles[ROCK_FDID]
+ if not localPath then
+ -- DOWNPORT: rock sheet not registered → solid fill (graceful degrade).
+ f.Bg:SetTexture(0.07, 0.07, 0.09, 0.95)
+ f.Bg:Show()
+ return false
+ end
+ f.Bg:SetTexture(localPath, "REPEAT", "REPEAT")
+ f.Bg:SetHorizTile(true)
+ f.Bg:SetVertTile(true)
+ f.Bg:SetTexCoord(0, 1, 0, 1) -- reset: a caller may have stretched a non-tiling fill in here
+ f.Bg:SetVertexColor(PC.BODY_TINT[1], PC.BODY_TINT[2], PC.BODY_TINT[3])
+ f.Bg:Show()
+ return true
+end
+
+-- layout (optional): which registered NineSlice layout to apply. Default "PortraitFrameTemplate".
+function PC.ApplyModernChrome(f, layout)
+ if not f then return end
+ layout = layout or "PortraitFrameTemplate"
+
+ -- Bg → tiled UI-Background-Rock (retail default), if the art is shipped; else a solid fill.
+ PC.ApplyBodyFill(f)
+
+ -- Modern nineslice on f.NineSlice. If NONE of the pieces resolve, fall back to a flat border.
+ local ns = ensureNineSlice(f)
+ ns:Show()
+ local applied = NE.nineslice and NE.nineslice.ApplyLayout and NE.nineslice.ApplyLayout(ns, layout)
+ if not applied then
+ applyFallbackBackdrop(f)
+ if NE.Log then NE.Log("CHROME", "nineslice art missing for "..tostring(layout)..", used fallback") end
+ end
+
+ -- TopTileStreaks under the title.
+ if NE.nineslice and NE.nineslice.ApplyTopTileStreaks then
+ NE.nineslice.ApplyTopTileStreaks(f)
+ end
+
+ -- Uniform title styling (no-op if the frame has no title region).
+ PC.SetTitle(f)
+end
+
+-- Modernize a close button to retail's RedButton-Exit family. Idempotent. Degrades to the native
+-- UIPanelCloseButton art if the atlas isn't registered.
+function PC.ModernizeCloseButton(target, opts)
+ local cb = target
+ if cb and not cb.SetNormalTexture and cb.CloseButton then cb = cb.CloseButton end
+ if not cb or not cb.SetNormalTexture or cb._neCloseModernized then return end
+ if not (NE.tex and NE.tex.SetAtlas) then return end
+ opts = opts or {}
+
+ local size = opts.size or 24
+ cb:SetSize(size, size)
+
+ if opts.anchor ~= false then
+ local a = opts.anchor or { "TOPRIGHT", "TOPRIGHT", 1, 0 }
+ cb:ClearAllPoints()
+ cb:SetPoint(a[1], cb:GetParent(), a[2] or a[1], a[3] or 0, a[4] or 0)
+ end
+ if opts.frameLevelBump then
+ local parent = cb:GetParent()
+ local base = (parent and parent.GetFrameLevel and parent:GetFrameLevel()) or 0
+ cb:SetFrameLevel(base + opts.frameLevelBump)
+ end
+
+ -- DOWNPORT: NewEra set state textures by passing a CreateTexture OBJECT to SetNormalTexture.
+ -- On 3.3.5a, Button:SetNormalTexture/SetPushedTexture/etc. take a FILE-PATH STRING, not a texture
+ -- object (passing an object silently no-ops → blank button — the bug we chased). Use the canonical
+ -- 3.3.5a pattern: SetXTexture(path) then SetTexCoord on the returned GetXTexture(). Art comes from
+ -- the shipped local BLP (NE.tex.Local(fdid)); if it isn't shipped, keep the native button art.
+ local function applyState(setter, getter, atlas, blend)
+ local entry = NE.tex._atlasEntry and NE.tex._atlasEntry(atlas)
+ local path = entry and NE.tex.Local and NE.tex.Local(entry.file)
+ if not (entry and path) then return false end
+ cb[setter](cb, path)
+ local t = cb[getter] and cb[getter](cb)
+ if t then
+ t:SetTexCoord(entry.left, entry.right, entry.top, entry.bottom)
+ if blend then t:SetBlendMode(blend) end
+ end
+ return true
+ end
+
+ local okN = applyState("SetNormalTexture", "GetNormalTexture", "redbutton-exit-2x")
+ applyState("SetPushedTexture", "GetPushedTexture", "redbutton-exit-pressed-2x")
+ applyState("SetHighlightTexture", "GetHighlightTexture", "redbutton-highlight-2x", "ADD")
+ if opts.disabled ~= false then
+ applyState("SetDisabledTexture", "GetDisabledTexture", "redbutton-exit-disabled-2x")
+ end
+
+ -- Only mark "modernized" if the normal state took; else leave native art (never blank).
+ if okN then cb._neCloseModernized = true end
+end
+
+-- §2 CONTRACT entry. opts (all optional):
+-- layout registered nineslice layout name (default "PortraitFrameTemplate")
+-- title title text
+-- portrait texture path / FDID for the portrait cutout
+-- noPortrait true → skip the portrait
+-- noCloseButton true → skip building/reskinning a close button
+-- pixelPerfect true → PinPixelPerfect the frame
+-- DOWNPORT: NewEra's Apply only did HideClassicChrome + ApplyModernChrome (reskin path). Ours
+-- additionally BUILDS portrait/title/close-button on bare frames and wires the §2 opts.
+function PC.Apply(f, opts)
+ if not f then return end
+ opts = opts or {}
+
+ PC.HideClassicChrome(f)
+ PC.ApplyModernChrome(f, opts.layout)
+
+ -- Title.
+ if opts.title ~= nil then
+ local fs = ensureTitle(f, opts.title)
+ if fs then fs:SetText(opts.title) end
+ end
+
+ -- Portrait cutout.
+ if not opts.noPortrait then
+ local p = ensurePortrait(f)
+ if p then
+ if opts.portrait then
+ NE.tex.Set(p, opts.portrait)
+ elseif SetPortraitTexture then
+ -- Default: the player portrait. DOWNPORT: SetPortraitTexture yields an EMPTY portrait when
+ -- the player model isn't ready yet (first open after login — populated only after a /reload
+ -- caches the model). Re-apply on the next frame, on every Show, and on portrait-update events
+ -- so it fills in without needing a reload.
+ local function refreshPortrait() pcall(SetPortraitTexture, p, "player") end
+ refreshPortrait()
+ if C_Timer and C_Timer.After then C_Timer.After(0, refreshPortrait) end
+ f:HookScript("OnShow", refreshPortrait)
+ if not f._nePortraitWatcher then
+ local w = CreateFrame("Frame", nil, f)
+ w:RegisterEvent("UNIT_PORTRAIT_UPDATE")
+ w:RegisterEvent("PLAYER_ENTERING_WORLD")
+ w:SetScript("OnEvent", function(_, _, unit)
+ if (not unit) or unit == "player" then refreshPortrait() end
+ end)
+ f._nePortraitWatcher = w
+ end
+ else
+ NE.tex.Set(p, 134400) -- INV_Misc_QuestionMark fallback FDID-ish path id
+ end
+ if NE.portrait and NE.portrait.ApplyCutout then
+ NE.portrait.ApplyCutout(p, f, opts.portraitOpts)
+ end
+ end
+ end
+
+ -- Close button.
+ if not opts.noCloseButton then
+ local cb = ensureCloseButton(f)
+ PC.ModernizeCloseButton(cb)
+ end
+
+ if opts.pixelPerfect then PC.PinPixelPerfect(f, opts.userScale) end
+ return f
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Portrait.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Portrait.lua
new file mode 100644
index 0000000..ee840ee
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Portrait.lua
@@ -0,0 +1,75 @@
+-- DragonUI_NewEra/core/Portrait.lua — shared masked portrait cutout for PortraitFrameTemplate
+-- panels. Drops a square portrait texture into the nineslice corner cutout.
+--
+-- DOWNPORT: NewEra Core/Portrait.lua → 3.3.5a. NewEra circular-clipped the portrait with
+-- Frame:CreateMaskTexture + Texture:AddMaskTexture (retail/Era API). Neither exists on 3.3.5a
+-- (and SetMask doesn't exist either — see project memory). So the circular mask is FEATURE-GATED
+-- and simply no-ops on 3.3.5a: the portrait renders as a square cutout framed by the metal
+-- corner's own circular ring (which already reads as round). For player/unit portraits, callers
+-- should use the global SetPortraitTexture(tex, unit) — natively circular on 3.3.5a (memory:
+-- reference_3355_circular_portrait) — and NOT SetTexCoord-zoom it.
+--
+-- §2 CONTRACT: NE.portrait.* surface preserved.
+--
+-- USAGE
+-- NE.portrait.ApplyCutout(CharacterFramePortrait, CharacterFrame)
+--
+-- opts (all optional): size(62), anchor({"TOPLEFT",-5,7}), layer("ARTWORK"), sublevel,
+-- mask(true — no-op on 3.3.5a), maskInset({2,0,-2,4}).
+-- Idempotent (tex._neCutout). The mask, if created, is exposed as tex._neMask.
+
+local NE = DragonUI_NewEra
+NE.portrait = NE.portrait or {}
+
+local MASK_TEX = "Interface\\CharacterFrame\\TempPortraitAlphaMask"
+
+function NE.portrait.ApplyCutout(tex, parent, opts)
+ if not tex or not parent then return end
+ if tex._neCutout then return end
+ opts = opts or {}
+
+ -- DOWNPORT: NewEra's size 62 / y 7 assumed a circular MASK clipping the portrait to the ring.
+ -- On 3.3.5a the mask is feature-gated off, so the raw (natively-circular) portrait was a touch
+ -- large and low — the chin poked below the metal ring. Shrink 2px and lift 3px to seat it inside.
+ local size = opts.size or 60
+ local layer = opts.layer or "ARTWORK"
+ local anchor = opts.anchor or { "TOPLEFT", -5, 8 }
+ local point = anchor[1]
+ local mi = opts.maskInset or { 2, 0, -2, 4 }
+
+ tex:ClearAllPoints()
+ tex:SetPoint(point, parent, point, anchor[2], anchor[3])
+ tex:SetSize(size, size)
+ tex:SetDrawLayer(layer, opts.sublevel)
+
+ -- DOWNPORT: circular mask via CreateMaskTexture/AddMaskTexture is retail-only. Feature-gate it
+ -- so it lights up IF a compat shim ever provides those methods; otherwise the portrait is a
+ -- square cutout inside the metal ring (acceptable on 3.3.5a — the ring reads as round).
+ -- DOWNPORT: the method-existence guard is NOT enough — this 3.3.5a client EXPOSES
+ -- CreateMaskTexture but it RETURNS NIL (masks are non-functional here). Indexing that nil
+ -- aborted the whole chrome Apply (Portrait.lua:50). Guard the actual return value: if the mask
+ -- didn't materialise, skip it (the native-circular portrait + metal ring already read as round).
+ if opts.mask ~= false and tex.AddMaskTexture and parent.CreateMaskTexture and not tex._neMask then
+ local ok, mask = pcall(parent.CreateMaskTexture, parent)
+ if ok and mask and mask.SetTexture then
+ mask:SetTexture(MASK_TEX, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
+ mask:SetPoint("TOPLEFT", tex, "TOPLEFT", mi[1], mi[2])
+ mask:SetPoint("BOTTOMRIGHT", tex, "BOTTOMRIGHT", mi[3], mi[4])
+ tex:AddMaskTexture(mask)
+ tex._neMask = mask
+ end
+ end
+
+ tex._neCutout = true
+end
+
+-- Convenience: set a unit portrait into the cutout the 3.3.5a-native (circular) way.
+-- DOWNPORT: new helper — wraps the global SetPortraitTexture (natively circular on 3.3.5a) so
+-- panels don't reach for the missing mask API. Falls back to a plain texture path/FDID.
+function NE.portrait.SetUnit(tex, parent, unit, opts)
+ if not (tex and parent) then return end
+ NE.portrait.ApplyCutout(tex, parent, opts)
+ if unit and SetPortraitTexture then
+ pcall(SetPortraitTexture, tex, unit)
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Scale.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Scale.lua
new file mode 100644
index 0000000..37e18e6
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Scale.lua
@@ -0,0 +1,144 @@
+-- DragonUI_NewEra/core/Scale.lua — per-window scaling for the standalone panels.
+--
+-- Three modes per window:
+-- "ui" follow the game's UI Scale slider (the frame inherits UIParent → SetScale(1)).
+-- "none" no scaling: pixel-perfect, independent of the UI Scale slider (PinPixelPerfect).
+-- "custom" a fixed multiplier in [MIN, MAX].
+-- Settings persist account-wide in NE.db.scale[window] = { mode, custom }. The DragonUI options
+-- "New Era" tab exposes a dropdown (mode) + slider (custom) per window (see integration/Options.lua),
+-- and changes apply IMMEDIATELY to the live frame via S.Apply.
+
+local NE = DragonUI_NewEra
+NE.scale = NE.scale or {}
+local S = NE.scale
+
+S.MIN, S.MAX = 0.5, 1.5
+
+-- Per-window defaults preserve each window's prior look: spellbook/talents were a fixed 0.8; the
+-- character panel had no custom scale (= followed the UI scale).
+-- BUG FIX (owner report 2026-07-17: "the default scaling is massive" after social/guild switched
+-- from PinPixelPerfect(f, 1.21) to NE.scale's plain "custom" mode SetScale(c)): those aren't the
+-- same computation -- PinPixelPerfect normalizes against physical screen height and the parent's
+-- effective scale first, so its 1.21 wasn't a literal 1.21x of anything on-screen, while "custom"
+-- mode is a bare f:SetScale(c). 1.21 there rendered much bigger than intended. Owner steer: "make
+-- it overall 30% smaller as the addon default, this should show as 1.0" -- 1.0 (plain, unscaled)
+-- IS the addon default now; still fully user-adjustable from here same as every other window.
+--
+-- Windows added 2026-08-09 (auctionhouse / cooldownmanager / encounterjournal / combinedbag): each
+-- default reproduces EXACTLY what that window did before it had a setting, so nobody's UI moves on
+-- upgrade. The AH and combined bag never scaled themselves at all -> "ui" (a plain SetScale(1.0),
+-- i.e. inherit UIParent). The Cooldown Manager window and the Adventure Guide both built themselves
+-- with a hard-coded PinPixelPerfect(f, N) -> "none" (which IS PinPixelPerfect) with that same N
+-- carried over as their BASE_SCALE below.
+local DEFAULTS = {
+ character = { mode = "ui", custom = 1.0 },
+ spellbook = { mode = "custom", custom = 0.8 },
+ talents = { mode = "custom", custom = 0.8 },
+ social = { mode = "custom", custom = 1.0 },
+ guild = { mode = "custom", custom = 1.0 },
+ -- Explicit rather than left to S.Get's fallback: LFG already registered itself with SetFrame but
+ -- had no row in the options list, so this is the first release where anyone can change it. "ui" is
+ -- what the fallback was already giving it, so its shipped size is unchanged.
+ lfg = { mode = "ui", custom = 1.0 },
+ auctionhouse = { mode = "ui", custom = 1.0 },
+ cooldownmanager = { mode = "none", custom = 1.0 },
+ encounterjournal = { mode = "none", custom = 1.0 },
+ combinedbag = { mode = "ui", custom = 1.0 },
+}
+
+-- BUG FIX (owner report 2026-07-17, round two: "the default is still massive" after DEFAULTS.social/
+-- guild's custom value dropped to 1.0). Setting custom=1.0 only made S.Apply's "custom" branch do a
+-- literal f:SetScale(1.0) -- genuinely unscaled/native. Turns out social/guild's raw pixel footprint
+-- (465x560, unlike the smaller character/spellbook/talents/professions windows) IS just "massive" at
+-- native size on this owner's setup -- 1.0 was never the right target scale, it just happened to be
+-- what "not 1.21" meant. Owner steer: "make it overall 30% smaller ... this should show as 1.0" --
+-- i.e. the SLIDER should keep reading a clean "1.0" as its default/center point, but the ACTUAL
+-- applied scale at that position needs to be 30% smaller than plain SetScale(1.0). A fixed per-window
+-- baseline multiplier gets us both: the user-facing custom/mode values stay simple round numbers
+-- (slider still shows 1.0, still ranges MIN..MAX same as every other window), while the real
+-- on-screen scale is BASE_SCALE[window] * (whatever S.Get's mode/custom computation yields). Windows
+-- not listed here default to 1.0 (no change from prior behavior).
+--
+-- The Cooldown Manager window and the Adventure Guide use BASE_SCALE for a different reason than
+-- social/guild do: both were BUILT at a hard-coded oversize pin (the CDM's "owner's +30%" and the
+-- Adventure Guide's 1.5), applied as a scale rather than by growing their layouts. Folding those
+-- numbers in here keeps their out-of-the-box look identical AND keeps their sliders reading a clean
+-- 1.0 at that look, so "1.0" means "how it shipped" on every window in the list.
+local BASE_SCALE = {
+ social = 0.7,
+ guild = 0.7,
+ cooldownmanager = 1.3, -- was SettingsPanel.lua's PANEL_SCALE
+ encounterjournal = 1.5, -- was EncounterJournal.lua's WINDOW_USER_SCALE
+}
+
+S._frames = S._frames or {} -- window -> live Frame
+
+local function store()
+ local db = NE.db
+ if not db then return nil end
+ db.scale = db.scale or {}
+ return db.scale
+end
+
+-- mode, custom for a window (falls back to defaults; never nil).
+function S.Get(window)
+ local d = DEFAULTS[window] or { mode = "ui", custom = 1.0 }
+ local st = store()
+ local s = st and st[window]
+ if not s then return d.mode, d.custom end
+ return s.mode or d.mode, s.custom or d.custom
+end
+
+-- Register a window's live frame so Apply/Set can rescale it on demand.
+function S.SetFrame(window, frame)
+ if window and frame then S._frames[window] = frame end
+end
+
+-- Leaving pixel-perfect mode has to also leave FrameUtil's re-pin registry, or the next UI-scale or
+-- resolution change re-pins the frame and silently discards the mode the user just picked. Matters
+-- for any window whose module pinned it at build time (the Cooldown Manager, the Adventure Guide)
+-- as much as for one the user switched off "No scaling".
+local function unpin(f)
+ if NE.FrameUtil and NE.FrameUtil.Unpin then NE.FrameUtil.Unpin(f) end
+end
+
+-- Apply the current setting to the window's registered frame (no-op if not registered yet).
+function S.Apply(window)
+ local f = S._frames[window]
+ if not f or not f.SetScale then return end
+ local mode, custom = S.Get(window)
+ local base = BASE_SCALE[window] or 1.0
+ if mode == "none" then
+ if NE.FrameUtil and NE.FrameUtil.PinPixelPerfect then
+ NE.FrameUtil.PinPixelPerfect(f, base) -- 1 logical px = 1 physical; ignores the UI scale slider
+ else
+ f:SetScale(base)
+ end
+ elseif mode == "custom" then
+ local c = tonumber(custom) or 0.8
+ if c < S.MIN then c = S.MIN elseif c > S.MAX then c = S.MAX end
+ unpin(f)
+ f:SetScale(c * base)
+ else -- "ui": inherit UIParent so the window tracks the game UI Scale slider
+ unpin(f)
+ f:SetScale(1.0 * base)
+ end
+end
+
+-- Persist a new mode + apply live.
+function S.SetMode(window, mode)
+ if mode ~= "ui" and mode ~= "none" and mode ~= "custom" then return end
+ local st = store()
+ if st then st[window] = st[window] or {}; st[window].mode = mode end
+ S.Apply(window)
+end
+
+-- Persist a new custom value (clamped) + apply live.
+function S.SetCustom(window, value)
+ local v = tonumber(value)
+ if not v then return end
+ if v < S.MIN then v = S.MIN elseif v > S.MAX then v = S.MAX end
+ local st = store()
+ if st then st[window] = st[window] or {}; st[window].custom = v end
+ S.Apply(window)
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ScrollbarReskin.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ScrollbarReskin.lua
new file mode 100644
index 0000000..d7711c8
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/ScrollbarReskin.lua
@@ -0,0 +1,1348 @@
+-- DragonUI_NewEra/core/ScrollbarReskin.lua — SHARED scrollbar reskin for any UIPanel ScrollFrame.
+-- Visual port of retail's MinimalScrollBar over the CLASSIC Slider-based UIPanelScrollBar.
+--
+-- DOWNPORT: NewEra Core/ScrollbarReskin.lua → 3.3.5a.
+-- * NE.scrollbar.Reskin (the Slider/UIPanelScrollBar path) PORTS DIRECTLY — UIPanelScrollFrame
+-- Template + Slider + named ScrollUpButton/ScrollDownButton all exist on 3.3.5a. The only
+-- change is SetShown → Show/Hide and routing setAtlas through NE.tex.SetAtlas.
+-- * NE.scrollbar.AttachMinimal + AttachBottomShadow depend on RETAIL-ONLY infrastructure
+-- (WowScrollBox, MinimalScrollBar template, EventFrame, ScrollUtil, BaseScrollBoxEvents,
+-- RegisterCallback). None exist on 3.3.5a, so they are FEATURE-GATED v1 STUBS: they detect
+-- the missing infra and return nil (no error). When/if a compat layer ships WowScrollBox they
+-- light up unchanged. v1 panels use Reskin (the Slider path).
+--
+-- §2 CONTRACT: NE.scrollbar.* preserved.
+--
+-- The minimal-scrollbar atlas sheets are NOT in the Sprint-0 set, so even Reskin's art swaps
+-- degrade gracefully (NE.tex.SetAtlas returns false → piece untextured) until §3 ships them; the
+-- reposition/sizing/wheel-enable still work.
+
+local NE = DragonUI_NewEra
+NE.scrollbar = NE.scrollbar or {}
+
+-- AttachMinimal — DOWNPORT STUB. Needs WowScrollBox + MinimalScrollBar (retail-only). No-op on
+-- 3.3.5a; returns nil. Guarded so a caller that probes for modern infra degrades cleanly.
+function NE.scrollbar.AttachMinimal(scrollBox, opts)
+ if not scrollBox then return end
+ if not (ScrollUtil and CreateFrame) then return end
+ -- DOWNPORT: "EventFrame" frame type + "MinimalScrollBar" template don't exist on 3.3.5a;
+ -- pcall the create so a missing template can't error. If it fails, bail (v1 stub).
+ opts = opts or {}
+ local parent = opts.parent or (scrollBox.GetParent and scrollBox:GetParent())
+ local ok, bar = pcall(CreateFrame, "EventFrame", opts.name, parent, "MinimalScrollBar")
+ if not ok or not bar then return end
+ local x = opts.x or 0
+ bar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", x, opts.top or 0)
+ bar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", x, opts.bottom or 0)
+ if opts.hideIfUnscrollable ~= nil then bar.hideIfUnscrollable = opts.hideIfUnscrollable end
+ if ScrollUtil and ScrollUtil.InitScrollBoxWithScrollBar then
+ if opts.list and ScrollUtil.InitScrollBoxListWithScrollBar then
+ ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, bar, opts.view)
+ else
+ local view = opts.view or (CreateScrollBoxLinearView and CreateScrollBoxLinearView(0, 0, 0, 0, 0))
+ ScrollUtil.InitScrollBoxWithScrollBar(scrollBox, bar, view)
+ end
+ end
+ return bar
+end
+
+local SLIDER_WIDTH = 8
+local THUMB_WIDTH = 8
+local THUMB_HEIGHT = 60
+local ARROW_W = 17
+local ARROW_H = 11
+local CAP_H = 8
+local VISIBLE_GAP = 6
+local ARROW_GAP = CAP_H + VISIBLE_GAP -- 14
+local SLIDER_Y_INSET = ARROW_H + ARROW_GAP -- 25
+
+local function setAtlas(tex, name, useAtlasSize)
+ if tex and NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(tex, name, useAtlasSize)
+ end
+end
+
+local function reskinArrowButton(button, normalAtlas, overAtlas, downAtlas, anchor)
+ if not button then return end
+ button:ClearAllPoints()
+ local sb = button:GetParent()
+ if anchor == "TOP" then
+ button:SetPoint("BOTTOM", sb, "TOP", 0, ARROW_GAP)
+ else
+ button:SetPoint("TOP", sb, "BOTTOM", 0, -ARROW_GAP)
+ end
+ local n = button:GetNormalTexture()
+ local p = button:GetPushedTexture()
+ local d = button:GetDisabledTexture()
+ local h = button:GetHighlightTexture()
+ if n then setAtlas(n, normalAtlas, false) end
+ if p then setAtlas(p, downAtlas, false) end
+ if d then setAtlas(d, normalAtlas, false); d:SetDesaturated(true) end
+ if h then
+ setAtlas(h, overAtlas, false)
+ h:SetBlendMode("ADD")
+ end
+ button:SetSize(ARROW_W, ARROW_H)
+end
+
+local function buildTrack(sb)
+ if sb._neTrackBegin then return end
+
+ local begin = sb:CreateTexture(nil, "BACKGROUND")
+ setAtlas(begin, "minimal-scrollbar-track-top", true)
+ begin:SetPoint("TOP", sb, "TOP", 0, 0)
+
+ local endTex = sb:CreateTexture(nil, "BACKGROUND")
+ setAtlas(endTex, "minimal-scrollbar-track-bottom", true)
+ endTex:SetPoint("BOTTOM", sb, "BOTTOM", 0, 0)
+
+ local middle = sb:CreateTexture(nil, "BACKGROUND")
+ setAtlas(middle, "!minimal-scrollbar-track-middle", false)
+ middle:SetPoint("TOPLEFT", begin, "BOTTOMLEFT")
+ middle:SetPoint("BOTTOMRIGHT", endTex, "TOPRIGHT")
+
+ sb._neTrackBegin, sb._neTrackEnd, sb._neTrackMiddle = begin, endTex, middle
+end
+
+local function buildThumb(sb)
+ if not sb.SetThumbTexture then return end
+ local thumb = sb:GetThumbTexture()
+ if not thumb then return end
+
+ setAtlas(thumb, "minimal-scrollbar-small-thumb-middle", false)
+ thumb:SetSize(THUMB_WIDTH, THUMB_HEIGHT)
+
+ if not sb._neThumbCapHost then
+ local host = CreateFrame("Frame", nil, sb)
+ host:SetFrameLevel((sb:GetFrameLevel() or 1) + 5)
+ host:SetPoint("TOPLEFT", thumb, "TOPLEFT", 0, 0)
+ host:SetPoint("BOTTOMRIGHT", thumb, "BOTTOMRIGHT", 0, 0)
+ sb._neThumbCapHost = host
+ end
+ if not sb._neThumbCapTop then
+ local capTop = sb._neThumbCapHost:CreateTexture(nil, "OVERLAY")
+ setAtlas(capTop, "minimal-scrollbar-small-thumb-top", true)
+ capTop:SetPoint("TOP", sb._neThumbCapHost, "TOP", 0, 0)
+ sb._neThumbCapTop = capTop
+ end
+ if not sb._neThumbCapBot then
+ local capBot = sb._neThumbCapHost:CreateTexture(nil, "OVERLAY")
+ setAtlas(capBot, "minimal-scrollbar-small-thumb-bottom", true)
+ capBot:SetPoint("BOTTOM", sb._neThumbCapHost, "BOTTOM", 0, 0)
+ sb._neThumbCapBot = capBot
+ end
+
+ if not sb._neThumbHoverHooked then
+ sb._neThumbHoverHooked = true
+ local function applyState(suffix)
+ setAtlas(thumb, "minimal-scrollbar-small-thumb-middle" .. suffix, false)
+ setAtlas(sb._neThumbCapTop, "minimal-scrollbar-small-thumb-top" .. suffix, true)
+ setAtlas(sb._neThumbCapBot, "minimal-scrollbar-small-thumb-bottom" .. suffix, true)
+ end
+ sb:HookScript("OnEnter", function() applyState("-over") end)
+ sb:HookScript("OnLeave", function() applyState("") end)
+ sb:HookScript("OnMouseDown", function() applyState("-down") end)
+ sb:HookScript("OnMouseUp", function() applyState("-over") end)
+ end
+end
+
+-- DOWNPORT helper: SetShown is absent on 3.3.5a — Show/Hide by a boolean.
+local function setShown(obj, on)
+ if not obj then return end
+ if on then obj:Show() else obj:Hide() end
+end
+
+-- Public entry — the classic Slider/UIPanelScrollBar reskin. Works on 3.3.5a.
+function NE.scrollbar.Reskin(scroll, opts)
+ if not scroll then return end
+ local sb = scroll.ScrollBar or scroll.scrollBar
+ if not sb then return end
+ if not sb.ScrollUpButton then
+ local sbName = sb.GetName and sb:GetName()
+ if sbName then
+ sb.ScrollUpButton = _G[sbName .. "ScrollUpButton"]
+ sb.ScrollDownButton = _G[sbName .. "ScrollDownButton"]
+ end
+ end
+
+ opts = opts or {}
+ local xOffset = opts.x or 7
+ local strataHigh = opts.strataHigh ~= false
+ local hideIfUnscrollable = opts.hideIfUnscrollable == true
+
+ local CLASSIC_TRACK = { "Top", "Bottom", "Middle", "Background", "trackBG", "Track" }
+ local function hideClassicTrack()
+ for _, key in ipairs(CLASSIC_TRACK) do
+ local r = sb[key]
+ if r and r ~= sb._neTrackBegin and r ~= sb._neTrackEnd and r ~= sb._neTrackMiddle
+ and r.Hide then r:Hide() end
+ end
+ end
+ hideClassicTrack()
+ if not sb._neTrackHideHooked then
+ sb._neTrackHideHooked = true
+ sb:HookScript("OnShow", hideClassicTrack)
+ end
+
+ scroll:EnableMouseWheel(true)
+
+ sb:ClearAllPoints()
+ sb:SetPoint("TOPLEFT", scroll, "TOPRIGHT", xOffset, -SLIDER_Y_INSET)
+ sb:SetPoint("BOTTOMLEFT", scroll, "BOTTOMRIGHT", xOffset, SLIDER_Y_INSET)
+ sb:SetWidth(SLIDER_WIDTH)
+ sb:SetHitRectInsets(-2, -2, 0, 0)
+
+ if strataHigh then
+ sb:SetFrameStrata("HIGH")
+ if sb.ScrollUpButton then sb.ScrollUpButton:SetFrameStrata("HIGH") end
+ if sb.ScrollDownButton then sb.ScrollDownButton:SetFrameStrata("HIGH") end
+ end
+ sb:EnableMouse(true)
+
+ reskinArrowButton(sb.ScrollUpButton,
+ "minimal-scrollbar-arrow-top", "minimal-scrollbar-arrow-top-over",
+ "minimal-scrollbar-arrow-top-down", "TOP")
+ reskinArrowButton(sb.ScrollDownButton,
+ "minimal-scrollbar-arrow-bottom", "minimal-scrollbar-arrow-bottom-over",
+ "minimal-scrollbar-arrow-bottom-down", "BOTTOM")
+ if sb.ScrollUpButton then sb.ScrollUpButton:EnableMouse(true) end
+ if sb.ScrollDownButton then sb.ScrollDownButton:EnableMouse(true) end
+
+ buildTrack(sb)
+ buildThumb(sb)
+
+ local function refreshThumb()
+ local yr = (scroll.GetVerticalScrollRange and scroll:GetVerticalScrollRange()) or 0
+ local show = yr and yr > 1
+ local thumb = sb.GetThumbTexture and sb:GetThumbTexture()
+ setShown(thumb, show) -- DOWNPORT: SetShown → setShown helper
+ setShown(sb._neThumbCapHost, show)
+ end
+ scroll:HookScript("OnScrollRangeChanged", refreshThumb)
+ if C_Timer and C_Timer.After then C_Timer.After(0, refreshThumb) end
+
+ if hideIfUnscrollable then
+ scroll.scrollBarHideable = true
+ local function applyVisibility()
+ local yrange = scroll.GetVerticalScrollRange and scroll:GetVerticalScrollRange() or 0
+ local visible = yrange > 0
+ setShown(sb.ScrollUpButton, visible)
+ setShown(sb.ScrollDownButton, visible)
+ setShown(sb, visible)
+ end
+ scroll:HookScript("OnScrollRangeChanged", applyVisibility)
+ if C_Timer and C_Timer.After then C_Timer.After(0, applyVisibility) end
+ else
+ scroll.scrollBarHideable = nil
+ if sb.ScrollUpButton then sb.ScrollUpButton:Show() end
+ if sb.ScrollDownButton then sb.ScrollDownButton:Show() end
+ sb:Show()
+ scroll:HookScript("OnScrollRangeChanged", function()
+ if sb.ScrollUpButton then sb.ScrollUpButton:Show() end
+ if sb.ScrollDownButton then sb.ScrollDownButton:Show() end
+ sb:Show()
+ end)
+ end
+end
+
+-- ============================================================================
+-- NE.scrollbar.BuildCustom — hand-built minimal scrollbar for a named
+-- FauxScrollFrameTemplate. (DOWNPORT: built FROM SCRATCH because Reskin's
+-- in-place re-skin of the stock UIPanelScrollBar Slider was not rendering — the
+-- user still saw the default Blizzard bar. This widget OWNS its own track+thumb
+-- frames and merely DRIVES the FauxScrollFrame's hidden internal slider so all
+-- the existing FauxScrollFrame_Update / FauxScrollFrame_GetOffset row logic keeps
+-- working untouched.)
+--
+-- How it syncs: a FauxScrollFrameTemplate contains a hidden Slider named
+-- "ScrollBar". FauxScrollFrame_Update() sets that slider's min/max and
+-- FauxScrollFrame_GetOffset() reads slider:GetValue()/step. So we:
+-- * Hide the stock slider + its two arrow buttons (Hide, not remove — Faux
+-- still reads/writes the slider's value).
+-- * Poll the slider's GetMinMaxValues / GetValue (cheap, OnUpdate-throttled +
+-- OnVerticalScroll/OnScrollRangeChanged hooks) to size & place our thumb.
+-- * On thumb drag, map the pixel position back to a slider value and
+-- slider:SetValue() — which fires the scrollframe's OnVerticalScroll, i.e.
+-- the SAME path the wheel uses. No row logic is duplicated.
+-- * Hide the whole bar when the content fits (max <= min).
+-- Idempotent via scrollFrame._neCustomBar.
+-- ----------------------------------------------------------------------------
+
+local CB_WIDTH = 8 -- track + thumb width
+local CB_CAP_H = 8 -- top/bottom cap height (track + thumb)
+local CB_MIN_THUMB = 24 -- never let the thumb shrink below this
+local CB_X_INSET = -2 -- bar x relative to the pane's right edge
+
+local function cbSetThumbState(bar, suffix)
+ setAtlas(bar._thumbMid, "minimal-scrollbar-small-thumb-middle" .. suffix, false)
+ setAtlas(bar._thumbTop, "minimal-scrollbar-small-thumb-top" .. suffix, true)
+ setAtlas(bar._thumbBot, "minimal-scrollbar-small-thumb-bottom" .. suffix, true)
+end
+
+-- Read the FauxScrollFrame's hidden slider, size+place the thumb, toggle the bar.
+local function cbSync(bar)
+ local slider = bar._slider
+ if not slider then return end
+ local minV, maxV = slider:GetMinMaxValues()
+ minV = minV or 0; maxV = maxV or 0
+ local range = maxV - minV
+ -- Content fits → normally hide the whole custom bar (and keep the stock bits hidden).
+ -- opts.alwaysShow keeps the TRACK (+ arrows, if present) always visible even with nothing to
+ -- scroll yet -- some lists (e.g. the Auctions tab) want the scrollbar chrome present from the
+ -- first render, not popping in once content overflows. Matches the reference's own Reskin()
+ -- behavior verbatim: "the arrows and track stay ... [only] the thumb hides when content fits
+ -- the viewport" -- arrows are NOT disabled in that state, just the thumb disappears. An earlier
+ -- attempt stretched the thumb to fill the whole track instead, which just looked like a solid
+ -- highlighted bar rather than an empty groove.
+ if range <= 0 then
+ if not bar._alwaysShow then
+ -- The arrow buttons are reparented OFF the slider onto scrollFrame's parent (see BuildCustom)
+ -- so they can outlive the slider's own Hide() -- but that also makes them SIBLINGS of bar,
+ -- not children of it, so hiding bar alone doesn't hide them. Have to hide them explicitly or
+ -- they're left showing over an otherwise-fully-hidden scrollbar (e.g. the Honor tab when its
+ -- list fits the viewport).
+ bar:Hide()
+ if bar._upBtn then bar._upBtn:Hide() end
+ if bar._downBtn then bar._downBtn:Hide() end
+ return
+ end
+ bar:Show()
+ if bar._upBtn then bar._upBtn:Show() end
+ if bar._downBtn then bar._downBtn:Show() end
+ if bar._thumb then bar._thumb:Hide() end
+ return
+ end
+ bar:Show()
+ if bar._upBtn then bar._upBtn:Show() end
+ if bar._downBtn then bar._downBtn:Show() end
+ if bar._thumb then bar._thumb:Show() end
+
+ local trackH = bar:GetHeight() or 0
+ if trackH <= 0 then return end
+
+ -- Thumb height ~ proportion of visible:total, but we only know the value
+ -- range (in steps). Use a fixed-ish thumb that still shrinks on long lists.
+ local thumbH = trackH * 0.40
+ if range > 0 then
+ -- shrink as the scrollable range grows; clamp to a sane band.
+ thumbH = trackH * math.max(0.18, math.min(0.6, 1 / (1 + range / (trackH))))
+ end
+ if thumbH < CB_MIN_THUMB then thumbH = CB_MIN_THUMB end
+ if thumbH > trackH then thumbH = trackH end
+ bar._thumb:SetHeight(thumbH)
+
+ local val = slider:GetValue() or minV
+ local frac = (val - minV) / range
+ if frac < 0 then frac = 0 elseif frac > 1 then frac = 1 end
+ local travel = trackH - thumbH
+ bar._thumb:ClearAllPoints()
+ bar._thumb:SetPoint("TOP", bar, "TOP", 0, -(frac * travel))
+end
+
+-- BUG FIX (owner report 2026-07-17, TWICE — the global hook below wasn't enough on its own: "bars
+-- still don't hide after not being needed"). cbSync is driven by OnVerticalScroll/
+-- OnScrollRangeChanged hooks (which a FauxScrollFrame — a fake/manual pagination widget, not a
+-- real scrolled child — may never actually fire), the slider's OnValueChanged (doesn't fire for a
+-- pure min/max SHRINK when the current value stays clamped at the same spot, e.g. already scrolled
+-- to top going from scrollable to fully-fits), and a 100ms-throttled OnUpdate poll. None of those
+-- proved reliable enough in practice. NE.scrollbar.SyncCustom is the deterministic fix: callers
+-- that already know exactly when their list's item count changed (every Refresh* function calls
+-- FauxScrollFrame_Update with the fresh total right there) call this immediately afterward,
+-- synchronously, in the SAME call stack as the data change — no hook/event/poll timing to trust.
+-- Optional total/numToDisplay (owner report 2026-07-17, persisted through the alwaysShow switch:
+-- "the thumb of the bar stays" after a Show Offline-style toggle shrinks the list back to fitting).
+-- Root cause: FauxScrollFrame_Update doesn't reliably reset its hidden slider's min/max to 0 on
+-- this client when a list shrinks from scrollable to fitting — stale drag/value state can leave
+-- GetMinMaxValues() reporting a nonzero range, so cbSync's range<=0 check never trips and the thumb
+-- (sized/positioned off that stale range) never hides. This mirrors the defensive clamp the
+-- Professions recipe list and Auction House Browse tab already use for the same reason: when the
+-- caller tells us the list fits, force the slider to (0, 0) directly before syncing rather than
+-- trusting FauxScrollFrame_Update's own write to have taken effect.
+function NE.scrollbar.SyncCustom(scrollFrame, total, numToDisplay)
+ if not scrollFrame then return end
+ if total ~= nil and numToDisplay ~= nil and total <= numToDisplay then
+ local bar = scrollFrame._neCustomBar
+ local slider = bar and bar._slider
+ if slider then
+ if slider.SetMinMaxValues then slider:SetMinMaxValues(0, 0) end
+ if slider.SetValue then slider:SetValue(0) end
+ end
+ end
+ if scrollFrame._neCustomBar then
+ cbSync(scrollFrame._neCustomBar)
+ end
+end
+
+-- Kept as a redundant safety net for any caller that doesn't (or can't) call SyncCustom directly.
+if not NE.scrollbar._neFauxUpdateHooked and _G.FauxScrollFrame_Update and hooksecurefunc then
+ NE.scrollbar._neFauxUpdateHooked = true
+ hooksecurefunc("FauxScrollFrame_Update", function(scrollFrame)
+ NE.scrollbar.SyncCustom(scrollFrame)
+ end)
+end
+
+-- Drag handler: convert the thumb's top Y (relative to the track) into a slider
+-- value and write it back (which scrolls the rows via OnVerticalScroll).
+local function cbOnThumbUpdate(thumb)
+ local bar = thumb._bar
+ local slider = bar and bar._slider
+ if not slider then return end
+ local minV, maxV = slider:GetMinMaxValues()
+ minV = minV or 0; maxV = maxV or 0
+ local range = maxV - minV
+ if range <= 0 then return end
+
+ local trackTop = bar:GetTop()
+ local thumbTop = thumb:GetTop()
+ local trackH = bar:GetHeight() or 0
+ local thumbH = thumb:GetHeight() or 0
+ local travel = trackH - thumbH
+ if not (trackTop and thumbTop) or travel <= 0 then return end
+
+ local frac = (trackTop - thumbTop) / travel
+ if frac < 0 then frac = 0 elseif frac > 1 then frac = 1 end
+ local newVal = minV + frac * range
+ if math.abs(newVal - (slider:GetValue() or minV)) >= 0.5 then
+ slider:SetValue(newVal) -- fires scrollFrame OnVerticalScroll → rows update
+ end
+end
+
+-- Reskin+reposition a FauxScrollFrame's native arrow button in place, anchored off OUR bar frame
+-- (not the stock slider) -- used only when opts.arrows requests the reference's Auctions-tab style
+-- (the Buy/Sell tabs' scrollbars have no arrows and don't pass this option, so they're unaffected).
+local CB_ARROW_W = 17
+local CB_ARROW_H = 11
+-- Reference's ARROW_GAP = thumb cap height (CB_CAP_H, 8) + a 6px visible buffer = 14 -- the gap
+-- between the track's cap and the arrow has to clear the cap's own height or the thumb's rounded
+-- corner visually pokes out past the arrow at full scroll. NOT just the 6px buffer alone.
+local CB_ARROW_GAP = CB_CAP_H + 6
+local function cbReskinArrow(button, normalAtlas, overAtlas, downAtlas, bar, anchor)
+ if not button then return end
+ local n = button:GetNormalTexture()
+ local p = button:GetPushedTexture()
+ local d = button:GetDisabledTexture()
+ local h = button:GetHighlightTexture()
+ if n then setAtlas(n, normalAtlas, false) end
+ if p then setAtlas(p, downAtlas, false) end
+ if d then setAtlas(d, normalAtlas, false); d:SetDesaturated(true) end
+ if h then setAtlas(h, overAtlas, false); h:SetBlendMode("ADD") end
+ button:SetSize(CB_ARROW_W, CB_ARROW_H)
+ button:ClearAllPoints()
+ if anchor == "TOP" then
+ button:SetPoint("BOTTOM", bar, "TOP", 0, CB_ARROW_GAP)
+ else
+ button:SetPoint("TOP", bar, "BOTTOM", 0, -CB_ARROW_GAP)
+ end
+ button:EnableMouse(true)
+ button:Show()
+end
+
+function NE.scrollbar.BuildCustom(scrollFrame, opts)
+ if not scrollFrame then return end
+ if scrollFrame._neCustomBar then return scrollFrame._neCustomBar end
+
+ local name = scrollFrame.GetName and scrollFrame:GetName()
+ -- Locate the FauxScrollFrame's hidden slider + arrow buttons.
+ local slider = (name and _G[name .. "ScrollBar"]) or scrollFrame.ScrollBar or scrollFrame.scrollBar
+ if not slider then return end
+ local sName = slider.GetName and slider:GetName()
+ local upBtn = (sName and _G[sName .. "ScrollUpButton"])
+ local downBtn = (sName and _G[sName .. "ScrollDownButton"])
+
+ opts = opts or {}
+ local xInset = opts.x ~= nil and -opts.x or CB_X_INSET
+ -- The reference addon's minimal scrollbar always has arrow buttons -- that's now the DEFAULT
+ -- for every list using this widget, not a per-call opt-in. The vertical clearance the arrows
+ -- need is reserved by insetting OUR OWN bar within scrollFrame's existing bounds (below), so
+ -- this doesn't require auditing every caller's layout. Pass arrows = false to opt a specific
+ -- list out if it genuinely needs the old bare-track look. (Whether the TRACK stays visible with
+ -- nothing to scroll, vs. hiding entirely, is the separate opts.alwaysShow -- opt-in, see below.)
+ local wantArrows = opts.arrows ~= false
+
+ -- upBtn/downBtn are children of the STOCK SLIDER in FauxScrollFrameTemplate's own hierarchy --
+ -- if we want to keep showing them (wantArrows) while hiding the slider itself (its track+thumb
+ -- art we never want), they have to be reparented OFF the slider first. A hidden parent hides its
+ -- children regardless of the child's own Show()/Hide() state, so leaving them parented to the
+ -- now-hidden slider would make them invisible no matter what we do below.
+ if wantArrows then
+ local newParent = scrollFrame:GetParent() or scrollFrame
+ if upBtn then upBtn:SetParent(newParent) end
+ if downBtn then downBtn:SetParent(newParent) end
+
+ -- ...but reparenting BREAKS their click handler, so we have to replace it. FauxScrollFrameTemplate
+ -- instantiates the arrows with an inline OnClick (UIPanelTemplates.xml) hardcoded to its own
+ -- parent, which it assumes IS the slider:
+ -- local parent = self:GetParent();
+ -- parent:SetValue(parent:GetValue() - (parent:GetHeight() / 2));
+ -- Once the button hangs off the scroll frame's parent instead, GetValue is nil there and every
+ -- arrow click threw "attempt to call method 'GetValue' (a nil value)" (reported against the
+ -- character Skills list, but it affected every list built by this function). Drive the slider we
+ -- already captured above instead of whatever the button happens to be parented to.
+ --
+ -- Step by ONE row rather than stock's half-a-bar-height: FauxScrollFrame_Update calls
+ -- slider:SetValueStep(valueStep) with the caller's row height, so GetValueStep() is exactly one
+ -- row -- which also matches what the callers' own OnMouseWheel handlers scroll by.
+ local function arrowClick(dir)
+ return function()
+ if not (slider and slider.GetValue) then return end
+ local mn, mx = slider:GetMinMaxValues()
+ local step = slider.GetValueStep and slider:GetValueStep() or nil
+ if not step or step <= 0 then step = (slider:GetHeight() or 32) / 2 end
+ local v = slider:GetValue() + dir * step
+ if v < mn then v = mn elseif v > mx then v = mx end
+ slider:SetValue(v)
+ if PlaySound then pcall(PlaySound, "UChatScrollButton") end
+ end
+ end
+ if upBtn then upBtn:SetScript("OnClick", arrowClick(-1)) end
+ if downBtn then downBtn:SetScript("OnClick", arrowClick(1)) end
+ end
+
+ -- Hide the stock scrollbar (do NOT remove — Faux still drives the slider value). If arrows are
+ -- wanted, leave upBtn/downBtn alone here -- they're reskinned+repositioned below instead.
+ -- Re-hide the slider on any attempt to show it.
+ local function hideStock()
+ if slider then slider:Hide() end
+ if not wantArrows then
+ if upBtn then upBtn:Hide() end
+ if downBtn then downBtn:Hide() end
+ end
+ end
+ hideStock()
+ if slider.HookScript then slider:HookScript("OnShow", function(s) s:Hide() end) end
+ if not wantArrows then
+ if upBtn and upBtn.HookScript then upBtn:HookScript("OnShow", function(s) s:Hide() end) end
+ if downBtn and downBtn.HookScript then downBtn:HookScript("OnShow", function(s) s:Hide() end) end
+ end
+
+ -- ---- the custom bar frame (= the track) ----------------------------------
+ -- Parented to the scrollFrame's OWN PARENT, not the scrollFrame itself -- a ScrollFrame widget
+ -- clips ALL its children to its own rectangle (not just a designated scroll child), and this
+ -- bar is deliberately anchored OUTSIDE the scrollFrame's bounds (to its right). Parented to the
+ -- scrollFrame directly, it would render fully clipped/invisible regardless of Show()/Hide()
+ -- state -- anchors still resolve fine against a non-parent frame, so this changes nothing else.
+ local bar = CreateFrame("Frame", nil, scrollFrame:GetParent() or scrollFrame)
+ bar:SetWidth(CB_WIDTH)
+ -- Anchor to the right edge of the scroll pane, inset a little. Full height normally; when arrows
+ -- are wanted, inset top/bottom by the same amount the arrow buttons occupy (CB_ARROW_H +
+ -- CB_ARROW_GAP) so the track+arrows stay entirely within scrollFrame's own bounds instead of
+ -- needing every caller to carve out extra vertical clearance in its own layout.
+ local barYInset = wantArrows and (CB_ARROW_H + CB_ARROW_GAP) or 0
+ bar:ClearAllPoints()
+ bar:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", xInset, -barYInset)
+ bar:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", xInset, barYInset)
+ bar:SetFrameStrata("HIGH")
+ bar:SetFrameLevel((scrollFrame:GetFrameLevel() or 1) + 6)
+ bar._slider = slider
+ -- Opt-in, NOT a default -- always-visible-track (even with nothing to scroll) is an Auction
+ -- House convention (Browse/Auctions tabs pass this explicitly); other panels keep the normal
+ -- "hide entirely when content fits" behavior unless they ask for otherwise.
+ bar._alwaysShow = opts.alwaysShow and true or false
+
+ if wantArrows then
+ cbReskinArrow(upBtn, "minimal-scrollbar-arrow-top", "minimal-scrollbar-arrow-top-over", "minimal-scrollbar-arrow-top-down", bar, "TOP")
+ cbReskinArrow(downBtn, "minimal-scrollbar-arrow-bottom", "minimal-scrollbar-arrow-bottom-over", "minimal-scrollbar-arrow-bottom-down", bar, "BOTTOM")
+ if upBtn then
+ upBtn:SetFrameStrata(bar:GetFrameStrata())
+ upBtn:SetFrameLevel(bar:GetFrameLevel() + 1)
+ end
+ if downBtn then
+ downBtn:SetFrameStrata(bar:GetFrameStrata())
+ downBtn:SetFrameLevel(bar:GetFrameLevel() + 1)
+ end
+ bar._upBtn, bar._downBtn = upBtn, downBtn
+
+ -- BUG FIX (owner report 2026-07-17, persisted through two prior fix attempts: "bars still
+ -- don't hide"). The stock slider gets a PERMANENT re-hide guard above (line ~466) regardless
+ -- of wantArrows, but upBtn/downBtn only got one when wantArrows was FALSE — every list in this
+ -- addon wants arrows, so neither button ever got that safety net. They're reparented off the
+ -- slider (onto scrollFrame's parent) specifically so they can survive the slider's own Hide(),
+ -- which means something in the stock FauxScrollFrame/UIPanelScrollBar machinery re-Show()-ing
+ -- them independently (e.g. on hover, or Blizzard's own internal update pass) was never caught
+ -- by cbSync afterward — explaining the exact asymmetry reported: showing worked (our own
+ -- cbSync Show() call), hiding didn't stick (something else's Show() call happened after ours).
+ -- Range-aware, not a blanket re-hide like the slider's: re-checks the CURRENT range on every
+ -- Show() and only hides if there's genuinely nothing to scroll.
+ local function guardArrow(btn)
+ if not (btn and btn.HookScript) then return end
+ btn:HookScript("OnShow", function(self)
+ local slider = bar._slider
+ if not slider then return end
+ local minV, maxV = slider:GetMinMaxValues()
+ minV = minV or 0; maxV = maxV or 0
+ if (maxV - minV) <= 0 and not bar._alwaysShow then self:Hide() end
+ end)
+ end
+ guardArrow(upBtn)
+ guardArrow(downBtn)
+ end
+
+ -- track: top cap + bottom cap + tiled middle
+ local tTop = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tTop, "minimal-scrollbar-track-top", true)
+ tTop:SetWidth(CB_WIDTH); tTop:SetHeight(CB_CAP_H)
+ tTop:SetPoint("TOP", bar, "TOP", 0, 0)
+
+ local tBot = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tBot, "minimal-scrollbar-track-bottom", true)
+ tBot:SetWidth(CB_WIDTH); tBot:SetHeight(CB_CAP_H)
+ tBot:SetPoint("BOTTOM", bar, "BOTTOM", 0, 0)
+
+ local tMid = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tMid, "!minimal-scrollbar-track-middle", false)
+ tMid:SetPoint("TOPLEFT", tTop, "BOTTOMLEFT", 0, 0)
+ tMid:SetPoint("BOTTOMRIGHT", tBot, "TOPRIGHT", 0, 0)
+
+ -- ---- the thumb -----------------------------------------------------------
+ local thumb = CreateFrame("Frame", nil, bar)
+ thumb:SetWidth(CB_WIDTH)
+ thumb:SetHeight(CB_MIN_THUMB)
+ thumb:SetPoint("TOP", bar, "TOP", 0, 0)
+ thumb:SetFrameLevel((bar:GetFrameLevel() or 1) + 2)
+ thumb:EnableMouse(true)
+ thumb._bar = bar
+ bar._thumb = thumb
+
+ local thMid = thumb:CreateTexture(nil, "ARTWORK")
+ setAtlas(thMid, "minimal-scrollbar-small-thumb-middle", false)
+ thMid:SetPoint("TOPLEFT", thumb, "TOPLEFT", 0, -CB_CAP_H)
+ thMid:SetPoint("BOTTOMRIGHT", thumb, "BOTTOMRIGHT", 0, CB_CAP_H)
+ bar._thumbMid = thMid
+
+ local thTop = thumb:CreateTexture(nil, "OVERLAY")
+ setAtlas(thTop, "minimal-scrollbar-small-thumb-top", true)
+ thTop:SetWidth(CB_WIDTH); thTop:SetHeight(CB_CAP_H)
+ thTop:SetPoint("TOP", thumb, "TOP", 0, 0)
+ bar._thumbTop = thTop
+
+ local thBot = thumb:CreateTexture(nil, "OVERLAY")
+ setAtlas(thBot, "minimal-scrollbar-small-thumb-bottom", true)
+ thBot:SetWidth(CB_WIDTH); thBot:SetHeight(CB_CAP_H)
+ thBot:SetPoint("BOTTOM", thumb, "BOTTOM", 0, 0)
+ bar._thumbBot = thBot
+
+ -- drag: while held, follow the cursor (clamped) and write the slider value.
+ thumb:SetScript("OnMouseDown", function(self)
+ cbSetThumbState(bar, "-down")
+ self._dragging = true
+ self:SetScript("OnUpdate", function(s)
+ -- Follow the cursor: place the thumb under the mouse Y, clamped to track.
+ local trackTop = bar:GetTop()
+ local trackH = bar:GetHeight() or 0
+ local thumbH = s:GetHeight() or 0
+ if not trackTop then return end
+ local _, cursorY = GetCursorPosition()
+ local scale = bar:GetEffectiveScale() or 1
+ cursorY = cursorY / scale
+ -- desired thumb-top so the grab point stays under cursor (center grab is fine)
+ local desiredTop = cursorY + (thumbH / 2)
+ local maxTop = trackTop
+ local minTop = trackTop - (trackH - thumbH)
+ if desiredTop > maxTop then desiredTop = maxTop end
+ if desiredTop < minTop then desiredTop = minTop end
+ s:ClearAllPoints()
+ s:SetPoint("TOP", bar, "TOP", 0, -(maxTop - desiredTop))
+ cbOnThumbUpdate(s)
+ end)
+ end)
+ thumb:SetScript("OnMouseUp", function(self)
+ self._dragging = false
+ self:SetScript("OnUpdate", nil)
+ cbSetThumbState(bar, "")
+ cbSync(bar) -- snap the thumb to the canonical slider position
+ end)
+ thumb:SetScript("OnEnter", function() if not thumb._dragging then cbSetThumbState(bar, "-over") end end)
+ thumb:SetScript("OnLeave", function() if not thumb._dragging then cbSetThumbState(bar, "") end end)
+
+ -- keep the wheel working (FauxScrollFrames usually wire it; ensure enabled).
+ scrollFrame:EnableMouseWheel(true)
+
+ -- ---- syncing -------------------------------------------------------------
+ local function sync() cbSync(bar) end
+ scrollFrame:HookScript("OnVerticalScroll", sync)
+ scrollFrame:HookScript("OnScrollRangeChanged", sync)
+ -- The slider value is set by FauxScrollFrame_Update (which the row refreshers
+ -- call) and by the wheel; mirror those via the slider's own value change.
+ if slider.HookScript then slider:HookScript("OnValueChanged", sync) end
+
+ -- Throttled OnUpdate fallback so the thumb tracks (and the bar re-appears when
+ -- content grows) even if a refresher updates the slider without firing a hook
+ -- we caught. DOWNPORT: driven off the scrollFrame (always shown while the panel
+ -- is open) NOT the bar — a hidden frame fires no OnUpdate, so a bar that hid
+ -- itself when content fit could never re-show itself. (cheap; ~10/sec)
+ local accum = 0
+ scrollFrame:HookScript("OnUpdate", function(self, elapsed)
+ accum = accum + (elapsed or 0)
+ if accum < 0.1 then return end
+ accum = 0
+ if not thumb._dragging then cbSync(bar) end
+ end)
+
+ scrollFrame._neCustomBar = bar
+ -- Sync once immediately (bar:GetHeight() etc. may not be resolved on this very first pass
+ -- through a long anchor chain -- e.g. this bar's own scrollFrame anchors off a panel that
+ -- anchors off a tab whose width comes from GetWidth() on its own text) AND once more deferred
+ -- a frame later once layout has fully settled. Without the deferred pass an alwaysShow bar can
+ -- get stuck invisible if this first call sees a zero-height track.
+ sync()
+ if C_Timer and C_Timer.After then C_Timer.After(0, sync) end
+ return bar
+end
+
+-- ============================================================================
+-- NE.scrollbar.CenterIfNoBar(scroll, needsBar) — the secondary-tab faux scroll frames are inset 10
+-- on the left and 24 on the right (the scrollbar gutter). When the list fits and NO scrollbar shows,
+-- that asymmetry leaves dead space on the right and the content reads left-biased. Balance the L/R
+-- insets to their average (17) so the content is centered; restore the gutter when a bar is needed.
+-- Same total width either way (10+24 == 17+17), so the rows keep their width and just shift. Host =
+-- scroll:GetParent(). NOTE: assumes the shared 10/-12 / -24/+10 inset values the secondary panes use.
+-- ============================================================================
+function NE.scrollbar.CenterIfNoBar(scroll, needsBar)
+ if not scroll then return end
+ local host = scroll:GetParent()
+ if not host then return end
+ local l, r = 10, 24
+ if not needsBar then local a = (l + r) / 2; l, r = a, a end
+ scroll:ClearAllPoints()
+ scroll:SetPoint("TOPLEFT", host, "TOPLEFT", l, -12)
+ scroll:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -r, 10)
+end
+
+-- ============================================================================
+-- NE.scrollbar.BuildCustomPixel — DOWNPORT/REPORT: a PIXEL-SCROLL variant of BuildCustom for a
+-- plain ScrollFrame (NOT a FauxScrollFrame). The faithful-NewEra stats sidebar builds ALL rows at
+-- cumulative Y in one tall content frame and scrolls by PIXELS (NewEra used a retail WowScrollBox,
+-- absent on 3.3.5a). This widget OWNS its own track+thumb and drives the ScrollFrame's
+-- SetVerticalScroll directly, reading GetVerticalScrollRange()/GetVerticalScroll() for the thumb.
+--
+-- Reuses the SAME track/thumb art + drag feel as BuildCustom; only the value source/sink differ:
+-- * range = scrollFrame:GetVerticalScrollRange()
+-- * value = scrollFrame:GetVerticalScroll()
+-- * set = scrollFrame:SetVerticalScroll(v)
+-- Idempotent via scrollFrame._neCustomBar.
+-- ----------------------------------------------------------------------------
+
+-- Read the ScrollFrame's pixel scroll range/value, size+place the thumb, toggle the bar.
+local function cbpSync(bar)
+ local sf = bar._scrollFrame
+ if not sf then return end
+ local range = (sf.GetVerticalScrollRange and sf:GetVerticalScrollRange()) or 0
+ if range <= 0 then
+ bar:Hide()
+ -- The arrows are reparented OFF the stock slider (see BuildCustomPixel) so they can outlive its
+ -- Hide(), which also makes them SIBLINGS of bar rather than children — hiding bar alone leaves
+ -- them floating over nothing. Same trap, same fix as cbSync's.
+ if bar._upBtn then bar._upBtn:Hide() end
+ if bar._downBtn then bar._downBtn:Hide() end
+ return
+ end
+ bar:Show()
+ if bar._upBtn then bar._upBtn:Show() end
+ if bar._downBtn then bar._downBtn:Show() end
+
+ local trackH = bar:GetHeight() or 0
+ if trackH <= 0 then return end
+
+ -- Thumb height ~ proportion of visible:total. visible = trackH-ish; total = visible + range.
+ local visible = (sf.GetHeight and sf:GetHeight()) or trackH
+ local total = visible + range
+ local thumbH = trackH * (visible / total)
+ if thumbH < CB_MIN_THUMB then thumbH = CB_MIN_THUMB end
+ if thumbH > trackH then thumbH = trackH end
+ bar._thumb:SetHeight(thumbH)
+
+ local val = (sf.GetVerticalScroll and sf:GetVerticalScroll()) or 0
+ local frac = val / range
+ if frac < 0 then frac = 0 elseif frac > 1 then frac = 1 end
+ local travel = trackH - thumbH
+ bar._thumb:ClearAllPoints()
+ bar._thumb:SetPoint("TOP", bar, "TOP", 0, -(frac * travel))
+end
+
+-- Drag handler: convert the thumb's top Y into a pixel scroll value and write it back.
+local function cbpOnThumbUpdate(thumb)
+ local bar = thumb._bar
+ local sf = bar and bar._scrollFrame
+ if not sf then return end
+ local range = (sf.GetVerticalScrollRange and sf:GetVerticalScrollRange()) or 0
+ if range <= 0 then return end
+
+ local trackTop = bar:GetTop()
+ local thumbTop = thumb:GetTop()
+ local trackH = bar:GetHeight() or 0
+ local thumbH = thumb:GetHeight() or 0
+ local travel = trackH - thumbH
+ if not (trackTop and thumbTop) or travel <= 0 then return end
+
+ local frac = (trackTop - thumbTop) / travel
+ if frac < 0 then frac = 0 elseif frac > 1 then frac = 1 end
+ sf:SetVerticalScroll(frac * range)
+end
+
+function NE.scrollbar.BuildCustomPixel(scrollFrame, opts)
+ if not scrollFrame then return end
+ if scrollFrame._neCustomBar then return scrollFrame._neCustomBar end
+
+ opts = opts or {}
+ local xInset = opts.x ~= nil and -opts.x or CB_X_INSET
+
+ -- ---- the STOCK bar, if this ScrollFrame came from a template -------------
+ -- BuildCustomPixel was written for a BARE CreateFrame("ScrollFrame") (the character stats
+ -- sidebar), which has no stock scrollbar to get out of the way of — so it never dealt with one.
+ -- A UIPanelScrollFrameTemplate ScrollFrame does have one, complete with arrow buttons, and
+ -- without this the player gets BOTH bars side by side. BuildCustom has always done this for the
+ -- Faux case; this is the same handling for the pixel case.
+ --
+ -- Resolved by GLOBAL NAME first: 3.3.5a's UIPanelScrollFrameTemplate declares its slider as
+ -- `$parentScrollBar` with no parentKey, so `scrollFrame.ScrollBar` is nil on this client and any
+ -- `if scrollFrame.ScrollBar` guard silently finds nothing. That is the same shape as the
+ -- PortraitFrameTemplate `$parentBg` trap, and it is why NE.scrollbar.Reskin — which checks only
+ -- the parentKey — quietly did nothing at all on a frame like this.
+ local sfName = scrollFrame.GetName and scrollFrame:GetName()
+ local stock = (sfName and _G[sfName .. "ScrollBar"]) or scrollFrame.ScrollBar or scrollFrame.scrollBar
+ local stockName = stock and stock.GetName and stock:GetName()
+ local upBtn = stock and ((stockName and _G[stockName .. "ScrollUpButton"]) or stock.ScrollUpButton)
+ local downBtn = stock and ((stockName and _G[stockName .. "ScrollDownButton"]) or stock.ScrollDownButton)
+ local wantArrows = opts.arrows ~= false and upBtn and downBtn
+
+ if stock then
+ -- Hidden, not removed: the template's own OnScrollRangeChanged still writes to it, and reading
+ -- it back is harmless. The re-hide guard is what makes it stay gone.
+ stock:Hide()
+ if stock.HookScript then stock:HookScript("OnShow", function(s) s:Hide() end) end
+ end
+ if wantArrows then
+ -- The arrows are children of the stock slider, and a hidden parent hides its children whatever
+ -- their own state. Reparent them so they survive the Hide above.
+ local newParent = scrollFrame:GetParent() or scrollFrame
+ upBtn:SetParent(newParent)
+ downBtn:SetParent(newParent)
+ elseif stock then
+ if upBtn then upBtn:Hide() end
+ if downBtn then downBtn:Hide() end
+ end
+
+ -- ---- the custom bar frame (= the track) ----------------------------------
+ -- Parented to the scrollFrame's own parent, not the scrollFrame itself -- see the matching
+ -- note in BuildCustom above (a ScrollFrame clips all its children to its own rect, and this bar
+ -- is deliberately anchored outside it).
+ local bar = CreateFrame("Frame", nil, scrollFrame:GetParent() or scrollFrame)
+ bar:SetWidth(CB_WIDTH)
+ -- With arrows, inset the track by exactly what they occupy so track + arrows together still fit
+ -- the scrollFrame's own bounds — no caller has to carve out extra room. Same arithmetic as
+ -- BuildCustom's barYInset.
+ local barYInset = wantArrows and (CB_ARROW_H + CB_ARROW_GAP) or 0
+ bar:ClearAllPoints()
+ bar:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", xInset, -barYInset)
+ bar:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", xInset, barYInset)
+ bar:SetFrameStrata("HIGH")
+ bar:SetFrameLevel((scrollFrame:GetFrameLevel() or 1) + 6)
+ bar._scrollFrame = scrollFrame
+
+ if wantArrows then
+ cbReskinArrow(upBtn, "minimal-scrollbar-arrow-top", "minimal-scrollbar-arrow-top-over", "minimal-scrollbar-arrow-top-down", bar, "TOP")
+ cbReskinArrow(downBtn, "minimal-scrollbar-arrow-bottom", "minimal-scrollbar-arrow-bottom-over", "minimal-scrollbar-arrow-bottom-down", bar, "BOTTOM")
+ for _, b in ipairs({ upBtn, downBtn }) do
+ b:SetFrameStrata(bar:GetFrameStrata())
+ b:SetFrameLevel(bar:GetFrameLevel() + 1)
+ end
+ bar._upBtn, bar._downBtn = upBtn, downBtn
+
+ -- Reparenting breaks the template's inline OnClick, which drives `self:GetParent()` and assumes
+ -- that is the slider. Once the button hangs off the panel instead, GetValue is nil there and
+ -- every click errors. Scroll the frame directly — the same sink the wheel and the thumb use.
+ local function arrowClick(dir)
+ return function()
+ local range = (scrollFrame.GetVerticalScrollRange and scrollFrame:GetVerticalScrollRange()) or 0
+ if range <= 0 then return end
+ local cur = (scrollFrame.GetVerticalScroll and scrollFrame:GetVerticalScroll()) or 0
+ local step = opts.wheelStep or 30
+ local v = cur + dir * step
+ if v < 0 then v = 0 elseif v > range then v = range end
+ scrollFrame:SetVerticalScroll(v)
+ if PlaySound then pcall(PlaySound, "UChatScrollButton") end
+ end
+ end
+ upBtn:SetScript("OnClick", arrowClick(-1))
+ downBtn:SetScript("OnClick", arrowClick(1))
+
+ -- Range-aware re-hide, not a blanket one: something in the stock machinery can Show() these
+ -- back independently of cbpSync, which is the asymmetry BuildCustom hit twice (shown worked,
+ -- hidden did not stick).
+ for _, b in ipairs({ upBtn, downBtn }) do
+ if b.HookScript then
+ b:HookScript("OnShow", function(self)
+ local range = (scrollFrame.GetVerticalScrollRange and scrollFrame:GetVerticalScrollRange()) or 0
+ if range <= 0 then self:Hide() end
+ end)
+ end
+ end
+ end
+
+ -- track: top cap + bottom cap + tiled middle
+ local tTop = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tTop, "minimal-scrollbar-track-top", true)
+ tTop:SetWidth(CB_WIDTH); tTop:SetHeight(CB_CAP_H)
+ tTop:SetPoint("TOP", bar, "TOP", 0, 0)
+
+ local tBot = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tBot, "minimal-scrollbar-track-bottom", true)
+ tBot:SetWidth(CB_WIDTH); tBot:SetHeight(CB_CAP_H)
+ tBot:SetPoint("BOTTOM", bar, "BOTTOM", 0, 0)
+
+ local tMid = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tMid, "!minimal-scrollbar-track-middle", false)
+ tMid:SetPoint("TOPLEFT", tTop, "BOTTOMLEFT", 0, 0)
+ tMid:SetPoint("BOTTOMRIGHT", tBot, "TOPRIGHT", 0, 0)
+
+ -- ---- the thumb -----------------------------------------------------------
+ local thumb = CreateFrame("Frame", nil, bar)
+ thumb:SetWidth(CB_WIDTH)
+ thumb:SetHeight(CB_MIN_THUMB)
+ thumb:SetPoint("TOP", bar, "TOP", 0, 0)
+ thumb:SetFrameLevel((bar:GetFrameLevel() or 1) + 2)
+ thumb:EnableMouse(true)
+ thumb._bar = bar
+ bar._thumb = thumb
+
+ local thMid = thumb:CreateTexture(nil, "ARTWORK")
+ setAtlas(thMid, "minimal-scrollbar-small-thumb-middle", false)
+ thMid:SetPoint("TOPLEFT", thumb, "TOPLEFT", 0, -CB_CAP_H)
+ thMid:SetPoint("BOTTOMRIGHT", thumb, "BOTTOMRIGHT", 0, CB_CAP_H)
+ bar._thumbMid = thMid
+
+ local thTop = thumb:CreateTexture(nil, "OVERLAY")
+ setAtlas(thTop, "minimal-scrollbar-small-thumb-top", true)
+ thTop:SetWidth(CB_WIDTH); thTop:SetHeight(CB_CAP_H)
+ thTop:SetPoint("TOP", thumb, "TOP", 0, 0)
+ bar._thumbTop = thTop
+
+ local thBot = thumb:CreateTexture(nil, "OVERLAY")
+ setAtlas(thBot, "minimal-scrollbar-small-thumb-bottom", true)
+ thBot:SetWidth(CB_WIDTH); thBot:SetHeight(CB_CAP_H)
+ thBot:SetPoint("BOTTOM", thumb, "BOTTOM", 0, 0)
+ bar._thumbBot = thBot
+
+ -- drag: while held, follow the cursor (clamped) and write the scroll value.
+ thumb:SetScript("OnMouseDown", function(self)
+ cbSetThumbState(bar, "-down")
+ self._dragging = true
+ self:SetScript("OnUpdate", function(s)
+ local trackTop = bar:GetTop()
+ local trackH = bar:GetHeight() or 0
+ local thumbH = s:GetHeight() or 0
+ if not trackTop then return end
+ local _, cursorY = GetCursorPosition()
+ local scale = bar:GetEffectiveScale() or 1
+ cursorY = cursorY / scale
+ local desiredTop = cursorY + (thumbH / 2)
+ local maxTop = trackTop
+ local minTop = trackTop - (trackH - thumbH)
+ if desiredTop > maxTop then desiredTop = maxTop end
+ if desiredTop < minTop then desiredTop = minTop end
+ s:ClearAllPoints()
+ s:SetPoint("TOP", bar, "TOP", 0, -(maxTop - desiredTop))
+ cbpOnThumbUpdate(s)
+ end)
+ end)
+ thumb:SetScript("OnMouseUp", function(self)
+ self._dragging = false
+ self:SetScript("OnUpdate", nil)
+ cbSetThumbState(bar, "")
+ cbpSync(bar)
+ end)
+ thumb:SetScript("OnEnter", function() if not thumb._dragging then cbSetThumbState(bar, "-over") end end)
+ thumb:SetScript("OnLeave", function() if not thumb._dragging then cbSetThumbState(bar, "") end end)
+
+ -- mouse wheel scrolls by pixels.
+ scrollFrame:EnableMouseWheel(true)
+ scrollFrame:HookScript("OnMouseWheel", function(self, delta)
+ local range = (self.GetVerticalScrollRange and self:GetVerticalScrollRange()) or 0
+ if range <= 0 then return end
+ local cur = (self.GetVerticalScroll and self:GetVerticalScroll()) or 0
+ local step = opts.wheelStep or 30
+ local nv = cur - (delta * step)
+ if nv < 0 then nv = 0 elseif nv > range then nv = range end
+ self:SetVerticalScroll(nv)
+ end)
+
+ -- ---- syncing -------------------------------------------------------------
+ local function sync() cbpSync(bar) end
+ scrollFrame:HookScript("OnVerticalScroll", sync)
+ scrollFrame:HookScript("OnScrollRangeChanged", sync)
+
+ -- Throttled OnUpdate fallback so the thumb tracks (and the bar re-appears when content grows).
+ local accum = 0
+ scrollFrame:HookScript("OnUpdate", function(self, elapsed)
+ accum = accum + (elapsed or 0)
+ if accum < 0.1 then return end
+ accum = 0
+ if not thumb._dragging then cbpSync(bar) end
+ end)
+
+ scrollFrame._neCustomBar = bar
+ if C_Timer and C_Timer.After then C_Timer.After(0, sync) else sync() end
+ return bar
+end
+
+-- ============================================================================
+-- NE.scrollbar.BuildCustomMessageFrame — a LINE-SCROLL variant of BuildCustomPixel for a
+-- ScrollingMessageFrame (e.g. a chat log). Unlike a ScrollFrame, a ScrollingMessageFrame has no
+-- GetVerticalScrollRange/GetVerticalScroll; scroll position is line-based instead:
+-- * GetScrollOffset() -- 0 = scrolled to the BOTTOM (newest message); larger = further up
+-- toward older history.
+-- * SetScrollOffset(n) -- the widget clamps this itself, so an approximate max is fine here.
+-- * GetNumMessages() / GetNumLinesDisplayed() -- used only to size/position the thumb; not
+-- authoritative, just enough for a faithful-looking bar.
+-- Reuses the same track/thumb art + drag feel as BuildCustom/BuildCustomPixel. frac is inverted
+-- relative to BuildCustomPixel's cbpSync: offset 0 (newest, bottom) puts the thumb at the BOTTOM
+-- of the track, matching how a normal chat window's scrollbar looks when caught up on chat.
+-- No wheel handler is installed here — callers with an existing OnMouseWheel (e.g. guild chat)
+-- keep driving ScrollUp/ScrollDown themselves; this widget only mirrors the resulting position.
+-- Idempotent via scrollFrame._neCustomBar.
+-- ----------------------------------------------------------------------------
+
+-- BUG FIX (owner report: "no scrollbar showing" -- not just a hidden thumb, the whole bar). Root
+-- cause: this client's ScrollingMessageFrame doesn't expose GetNumMessages()/GetNumLinesDisplayed()
+-- (retail/later-client additions), so both guarded calls silently fell through to 0 with no error,
+-- maxOff computed to 0, and cbmSync's bar:Hide() below fired permanently. Neither total nor shown
+-- can be trusted from the widget itself on 3.3.5a:
+-- * total -- prefer msg._neTotalLines, a caller-maintained counter (Chat.lua increments it on
+-- every AddMessage and zeroes it on Clear) over the possibly-absent GetNumMessages().
+-- * shown -- prefer GetNumLinesDisplayed() if it DOES exist (harmless to keep the fast path for
+-- a future/other client where it's present), else estimate visible-line capacity from
+-- frame height / font line height -- GetFont() and GetHeight() are plain FontInstance/
+-- Region APIs, not ScrollingMessageFrame-specific, so those are safe to rely on here.
+local function cbmMaxOffset(msg)
+ local total = msg._neTotalLines
+ if not total then
+ total = (msg.GetNumMessages and msg:GetNumMessages()) or 0
+ end
+
+ local shown = msg.GetNumLinesDisplayed and msg:GetNumLinesDisplayed()
+ if not shown or shown <= 0 then
+ local fontHeight
+ if msg.GetFont then
+ local _, h = msg:GetFont()
+ fontHeight = h
+ end
+ fontHeight = fontHeight or 14
+ local h = (msg.GetHeight and msg:GetHeight()) or 0
+ shown = h > 0 and math.max(1, math.floor(h / (fontHeight + 2))) or 1
+ end
+
+ local maxOff = total - shown
+ if maxOff < 0 then maxOff = 0 end
+ return maxOff, total, shown
+end
+
+local function cbmSync(bar)
+ local msg = bar._scrollFrame
+ if not msg then return end
+ local maxOff, total, shown = cbmMaxOffset(msg)
+ if maxOff <= 0 then
+ bar:Hide()
+ if bar._upBtn then bar._upBtn:Hide() end
+ if bar._downBtn then bar._downBtn:Hide() end
+ return
+ end
+ bar:Show()
+ if bar._upBtn then bar._upBtn:Show() end
+ if bar._downBtn then bar._downBtn:Show() end
+
+ local trackH = bar:GetHeight() or 0
+ if trackH <= 0 then return end
+
+ local visible = shown > 0 and shown or 1
+ local totalLines = total > 0 and total or visible
+ local thumbH = trackH * math.min(1, visible / totalLines)
+ if thumbH < CB_MIN_THUMB then thumbH = CB_MIN_THUMB end
+ if thumbH > trackH then thumbH = trackH end
+ bar._thumb:SetHeight(thumbH)
+
+ local offset = (msg.GetScrollOffset and msg:GetScrollOffset()) or 0
+ local frac = 1 - (offset / maxOff) -- offset 0 (newest) -> frac 1 (thumb at bottom)
+ if frac < 0 then frac = 0 elseif frac > 1 then frac = 1 end
+ local travel = trackH - thumbH
+ bar._thumb:ClearAllPoints()
+ bar._thumb:SetPoint("TOP", bar, "TOP", 0, -(frac * travel))
+end
+
+-- Drag handler: convert the thumb's top Y into a line offset and write it back.
+local function cbmOnThumbUpdate(thumb)
+ local bar = thumb._bar
+ local msg = bar and bar._scrollFrame
+ if not msg then return end
+ local maxOff = cbmMaxOffset(msg)
+ if maxOff <= 0 then return end
+
+ local trackTop = bar:GetTop()
+ local thumbTop = thumb:GetTop()
+ local trackH = bar:GetHeight() or 0
+ local thumbH = thumb:GetHeight() or 0
+ local travel = trackH - thumbH
+ if not (trackTop and thumbTop) or travel <= 0 then return end
+
+ local frac = (trackTop - thumbTop) / travel -- 0 at top .. 1 at bottom
+ if frac < 0 then frac = 0 elseif frac > 1 then frac = 1 end
+ if msg.SetScrollOffset then msg:SetScrollOffset(maxOff * (1 - frac)) end
+end
+
+-- ScrollingMessageFrame has no native slider/arrow buttons to reparent+reskin (unlike BuildCustom's
+-- FauxScrollFrame case), so these are built entirely from scratch: a plain Frame (not a real Button
+-- widget, to sidestep any uncertainty over SetNormalTexture/SetPushedTexture accepting texture
+-- objects on this client) with one texture region whose atlas key swaps for hover/press, matching
+-- the same manual hover-state approach the thumb already uses (cbSetThumbState).
+local function cbmArrowSetState(tex, suffix, normalAtlas, overAtlas, downAtlas)
+ if suffix == "-over" then setAtlas(tex, overAtlas, false)
+ elseif suffix == "-down" then setAtlas(tex, downAtlas, false)
+ else setAtlas(tex, normalAtlas, false) end
+end
+
+local function cbmBuildArrow(bar, normalAtlas, overAtlas, downAtlas, anchor, onClick)
+ local btn = CreateFrame("Frame", nil, bar)
+ btn:SetSize(CB_ARROW_W, CB_ARROW_H)
+ btn:ClearAllPoints()
+ if anchor == "TOP" then
+ btn:SetPoint("BOTTOM", bar, "TOP", 0, CB_ARROW_GAP)
+ else
+ btn:SetPoint("TOP", bar, "BOTTOM", 0, -CB_ARROW_GAP)
+ end
+ btn:SetFrameStrata(bar:GetFrameStrata())
+ btn:SetFrameLevel(bar:GetFrameLevel() + 2)
+ btn:EnableMouse(true)
+
+ local tex = btn:CreateTexture(nil, "ARTWORK")
+ tex:SetAllPoints(btn)
+ setAtlas(tex, normalAtlas, false)
+
+ btn:SetScript("OnEnter", function() cbmArrowSetState(tex, "-over", normalAtlas, overAtlas, downAtlas) end)
+ btn:SetScript("OnLeave", function() cbmArrowSetState(tex, "", normalAtlas, overAtlas, downAtlas) end)
+ btn:SetScript("OnMouseDown", function() cbmArrowSetState(tex, "-down", normalAtlas, overAtlas, downAtlas) end)
+ btn:SetScript("OnMouseUp", function(self)
+ cbmArrowSetState(tex, "-over", normalAtlas, overAtlas, downAtlas)
+ onClick()
+ end)
+ return btn
+end
+
+function NE.scrollbar.BuildCustomMessageFrame(scrollFrame, opts)
+ if not scrollFrame then return end
+ if scrollFrame._neCustomBar then return scrollFrame._neCustomBar end
+
+ opts = opts or {}
+ local xInset = opts.x ~= nil and -opts.x or CB_X_INSET
+ -- Track is inset top/bottom to leave room for the arrow buttons anchored just outside it (same
+ -- CB_ARROW_GAP clearance BuildCustom's arrows use), same default-on convention as BuildCustom.
+ local wantArrows = opts.arrows ~= false
+ local yInset = wantArrows and (CB_ARROW_H + CB_ARROW_GAP) or 0
+
+ local bar = CreateFrame("Frame", nil, scrollFrame:GetParent() or scrollFrame)
+ bar:SetWidth(CB_WIDTH)
+ bar:ClearAllPoints()
+ bar:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", xInset, -yInset)
+ bar:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", xInset, yInset)
+ bar:SetFrameStrata("HIGH")
+ bar:SetFrameLevel((scrollFrame:GetFrameLevel() or 1) + 6)
+ bar._scrollFrame = scrollFrame
+
+ if wantArrows then
+ bar._upBtn = cbmBuildArrow(bar,
+ "minimal-scrollbar-arrow-top", "minimal-scrollbar-arrow-top-over", "minimal-scrollbar-arrow-top-down",
+ "TOP", function() if scrollFrame.ScrollUp then scrollFrame:ScrollUp() end end)
+ bar._downBtn = cbmBuildArrow(bar,
+ "minimal-scrollbar-arrow-bottom", "minimal-scrollbar-arrow-bottom-over", "minimal-scrollbar-arrow-bottom-down",
+ "BOTTOM", function() if scrollFrame.ScrollDown then scrollFrame:ScrollDown() end end)
+ end
+
+ -- track: top cap + bottom cap + tiled middle
+ local tTop = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tTop, "minimal-scrollbar-track-top", true)
+ tTop:SetWidth(CB_WIDTH); tTop:SetHeight(CB_CAP_H)
+ tTop:SetPoint("TOP", bar, "TOP", 0, 0)
+
+ local tBot = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tBot, "minimal-scrollbar-track-bottom", true)
+ tBot:SetWidth(CB_WIDTH); tBot:SetHeight(CB_CAP_H)
+ tBot:SetPoint("BOTTOM", bar, "BOTTOM", 0, 0)
+
+ local tMid = bar:CreateTexture(nil, "BACKGROUND")
+ setAtlas(tMid, "!minimal-scrollbar-track-middle", false)
+ tMid:SetPoint("TOPLEFT", tTop, "BOTTOMLEFT", 0, 0)
+ tMid:SetPoint("BOTTOMRIGHT", tBot, "TOPRIGHT", 0, 0)
+
+ -- ---- the thumb -----------------------------------------------------------
+ local thumb = CreateFrame("Frame", nil, bar)
+ thumb:SetWidth(CB_WIDTH)
+ thumb:SetHeight(CB_MIN_THUMB)
+ thumb:SetPoint("TOP", bar, "TOP", 0, 0)
+ thumb:SetFrameLevel((bar:GetFrameLevel() or 1) + 2)
+ thumb:EnableMouse(true)
+ thumb._bar = bar
+ bar._thumb = thumb
+
+ local thMid = thumb:CreateTexture(nil, "ARTWORK")
+ setAtlas(thMid, "minimal-scrollbar-small-thumb-middle", false)
+ thMid:SetPoint("TOPLEFT", thumb, "TOPLEFT", 0, -CB_CAP_H)
+ thMid:SetPoint("BOTTOMRIGHT", thumb, "BOTTOMRIGHT", 0, CB_CAP_H)
+ bar._thumbMid = thMid
+
+ local thTop = thumb:CreateTexture(nil, "OVERLAY")
+ setAtlas(thTop, "minimal-scrollbar-small-thumb-top", true)
+ thTop:SetWidth(CB_WIDTH); thTop:SetHeight(CB_CAP_H)
+ thTop:SetPoint("TOP", thumb, "TOP", 0, 0)
+ bar._thumbTop = thTop
+
+ local thBot = thumb:CreateTexture(nil, "OVERLAY")
+ setAtlas(thBot, "minimal-scrollbar-small-thumb-bottom", true)
+ thBot:SetWidth(CB_WIDTH); thBot:SetHeight(CB_CAP_H)
+ thBot:SetPoint("BOTTOM", thumb, "BOTTOM", 0, 0)
+ bar._thumbBot = thBot
+
+ -- drag: while held, follow the cursor (clamped) and write the scroll offset.
+ thumb:SetScript("OnMouseDown", function(self)
+ cbSetThumbState(bar, "-down")
+ self._dragging = true
+ self:SetScript("OnUpdate", function(s)
+ local trackTop = bar:GetTop()
+ local trackH = bar:GetHeight() or 0
+ local thumbH = s:GetHeight() or 0
+ if not trackTop then return end
+ local _, cursorY = GetCursorPosition()
+ local scale = bar:GetEffectiveScale() or 1
+ cursorY = cursorY / scale
+ local desiredTop = cursorY + (thumbH / 2)
+ local maxTop = trackTop
+ local minTop = trackTop - (trackH - thumbH)
+ if desiredTop > maxTop then desiredTop = maxTop end
+ if desiredTop < minTop then desiredTop = minTop end
+ s:ClearAllPoints()
+ s:SetPoint("TOP", bar, "TOP", 0, -(maxTop - desiredTop))
+ cbmOnThumbUpdate(s)
+ end)
+ end)
+ thumb:SetScript("OnMouseUp", function(self)
+ self._dragging = false
+ self:SetScript("OnUpdate", nil)
+ cbSetThumbState(bar, "")
+ cbmSync(bar)
+ end)
+ thumb:SetScript("OnEnter", function() if not thumb._dragging then cbSetThumbState(bar, "-over") end end)
+ thumb:SetScript("OnLeave", function() if not thumb._dragging then cbSetThumbState(bar, "") end end)
+
+ -- ---- syncing -------------------------------------------------------------
+ -- A ScrollingMessageFrame fires no OnVerticalScroll/OnScrollRangeChanged -- offset and message
+ -- count both change via plain method calls (AddMessage, ScrollUp/Down, the caller's own wheel
+ -- handler), not events. A throttled OnUpdate poll is the only reliable signal, same approach as
+ -- BuildCustom/BuildCustomPixel's fallback.
+ local accum = 0
+ scrollFrame:HookScript("OnUpdate", function(self, elapsed)
+ accum = accum + (elapsed or 0)
+ if accum < 0.1 then return end
+ accum = 0
+ if not thumb._dragging then cbmSync(bar) end
+ end)
+
+ scrollFrame._neCustomBar = bar
+ cbmSync(bar)
+ if C_Timer and C_Timer.After then C_Timer.After(0, function() cbmSync(bar) end) end
+ return bar
+end
+
+-- ============================================================================
+-- NE.scrollbar.SkinSlider — retail's MinimalSliderWithSteppers, for a horizontal Slider.
+--
+-- NewEra builds every edit-mode slider from MinimalSliderWithSteppersTemplate
+-- (EditMode/SettingsPopup.lua:173). The template is retail-only, but the ART is one 32x128 sheet
+-- (4567914) carrying the whole widget: both rounded track caps, the ONE-PIXEL run that tiles between
+-- them, the little DIAMOND thumb, and both chevron steppers. So the widget is rebuilt here out of its
+-- own pieces at their own sizes — no rotation, no substitution.
+--
+-- (It first shipped built from the vertical scrollbar's pieces turned 90 degrees, because this sheet
+-- is absent from the NewEra Art set. The owner supplied it; the workaround came back out.)
+--
+-- WHAT THIS DELIBERATELY DOES NOT DO IS STATES. The sheet has no hover or pressed variants, because
+-- retail's minimal slider does not change art on either — the feedback is the thumb moving. Inventing
+-- a tint here would be this addon's idea, not the reference's.
+--
+-- OptionsSliderTemplate's own groove goes whole: it is a BACKDROP (bgFile UI-SliderBar-Background
+-- inside edgeFile UI-SliderBar-Border), not a region that could be re-pointed at other art.
+--
+-- Idempotent via slider._neMinimalSlider. Fail-safe: SetAtlas returns false when the art is missing,
+-- leaving the piece untextured rather than erroring, so a stripped Textures/ still boots.
+-- ============================================================================
+
+local SL_TRACK_H = 17 -- native height of the track pieces
+local SL_CAP_W = 11 -- native width of one rounded cap
+local SL_THUMB_W = 20 -- the diamond, at its own size
+local SL_THUMB_H = 19
+
+function NE.scrollbar.SkinSlider(slider)
+ if not slider then return end
+ if slider._neMinimalSlider then return slider._neMinimalSlider end
+
+ if slider.SetBackdrop then pcall(slider.SetBackdrop, slider, nil) end
+ -- Sized to the DIAMOND, not the track: the thumb is two pixels taller than the bar it rides, which
+ -- is what makes it read as a knob rather than as part of the groove.
+ slider:SetHeight(SL_THUMB_H)
+
+ local d = {}
+
+ d.Left = slider:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(d.Left, "minimal_sliderbar_left", true)
+ d.Left:SetPoint("LEFT", slider, "LEFT", 0, 0)
+
+ d.Right = slider:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(d.Right, "minimal_sliderbar_right", true)
+ d.Right:SetPoint("RIGHT", slider, "RIGHT", 0, 0)
+
+ -- Only the middle stretches, and it is one pixel of source, so it stretches invisibly. The caps keep
+ -- their own width or the rounded ends smear — the same rule as the dropdown's textholder.
+ d.Middle = slider:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(d.Middle, "_minimal_sliderbar_middle", false)
+ d.Middle:SetHeight(SL_TRACK_H)
+ d.Middle:SetPoint("LEFT", d.Left, "RIGHT")
+ d.Middle:SetPoint("RIGHT", d.Right, "LEFT")
+
+ local thumb = slider.GetThumbTexture and slider:GetThumbTexture()
+ if thumb then
+ NE.tex.SetAtlas(thumb, "minimal_sliderbar_button", true)
+ thumb:SetSize(SL_THUMB_W, SL_THUMB_H)
+ d.Thumb = thumb
+ end
+
+ slider._neMinimalSlider = d
+ return d
+end
+
+-- AttachBottomShadow — DOWNPORT STUB. Needs WowScrollBox callbacks (BaseScrollBoxEvents,
+-- RegisterCallback, GetDerivedScrollRange) — retail-only. Returns nil on 3.3.5a.
+function NE.scrollbar.AttachBottomShadow(scrollBox, host, opts)
+ if not scrollBox then return end
+ if not (scrollBox.RegisterCallback and _G.BaseScrollBoxEvents and scrollBox.GetDerivedScrollRange) then
+ return -- DOWNPORT: modern scroll-box infra absent; v1 stub.
+ end
+ opts = opts or {}
+ local holder = CreateFrame("Frame", nil, host or scrollBox)
+ holder:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMLEFT", 0, 0)
+ holder:SetPoint("BOTTOMRIGHT", scrollBox, "BOTTOMRIGHT", 0, 0)
+ holder:SetHeight(opts.height or 66)
+ holder:SetFrameLevel((scrollBox:GetFrameLevel() or 1) + 5)
+ local tex = holder:CreateTexture(nil, "OVERLAY")
+ tex:SetAllPoints(holder)
+ NE.tex.SetAtlas(tex, "questlog-frame-gradient-bottom", false)
+ holder:SetAlpha(0)
+
+ local function update()
+ local range = (scrollBox.GetDerivedScrollRange and scrollBox:GetDerivedScrollRange()) or 0
+ if range <= 0 then holder:SetAlpha(0); return end
+ local pct = (scrollBox.GetScrollPercentage and scrollBox:GetScrollPercentage()) or 0
+ local delta = (1 - pct) * range
+ holder:SetAlpha(math.max(0, math.min(1, delta / holder:GetHeight())))
+ end
+ scrollBox:RegisterCallback(BaseScrollBoxEvents.OnScroll, update, holder)
+ scrollBox:RegisterCallback(BaseScrollBoxEvents.OnLayout, update, holder)
+ if C_Timer and C_Timer.After then C_Timer.After(0, update) end
+ holder.Update = update
+ return holder
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/SpellRanks.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/SpellRanks.lua
new file mode 100644
index 0000000..35ba9f3
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/SpellRanks.lua
@@ -0,0 +1,189 @@
+-- DragonUI_NewEra/core/SpellRanks.lua — per-ability rank tables built from the live spellbook.
+--
+-- DOWNPORT: NewEra ships `NE.spellbook.SPELLID.[name] = {every rank id}` as a GENERATED
+-- table dumped from the client db2 (Generated/ClassSpells.lua et al). We don't have that pipeline
+-- here, so this builds the equivalent by scanning the spellbook at login. The difference in
+-- coverage is harmless for our consumers: the generated table included ranks the player has NOT
+-- learned, and an unlearned rank's GetSpellCooldown reads 0 anyway.
+--
+-- WHY THIS IS LOAD-BEARING, not a nicety:
+--
+-- The CooldownViewer's rank resolution (ItemMixins.lua highestKnownRankID) is built on
+-- `select(7, GetSpellInfo(name))` — on Era/TBC's modern engine the 7th return is the spellID of
+-- the highest known rank. **On 3.3.5a it is castTime.** The WotLK signature is
+-- name, rank, icon, cost, isFunnel, powerType, castTime, minRange, maxRange
+-- (confirmed by !!!ClassicAPI/Util/C_Spell.lua:39, which destructures exactly that). Ported
+-- verbatim, `highestKnownRankID` would return e.g. 1500 for a 1.5s cast and treat it as a spell
+-- ID — silently, since it's a plausible-looking number. So on this client the spellbook scan is
+-- the ONLY correct source for "highest rank the player knows".
+--
+-- Rank matters here because the curated cooldown lists key each ability by its RANK 1 id, while
+-- the client tracks a cooldown against the EXACT rank cast — the "vanilla rank gotcha" documented
+-- at ItemMixins.lua:108-119.
+--
+-- PUBLIC:
+-- NE.spellbook.SPELLID[CLASS][name:lower()] -> { id, id, ... } ascending by rank
+-- NE.spellbook.KnownRankIDs(name) -> that list for the player's class, or nil
+-- NE.spellbook.HighestKnownRankID(spellID, name) -> highest learned rank id, else spellID
+
+local NE = DragonUI_NewEra
+NE.spellbook = NE.spellbook or {}
+local SB = NE.spellbook
+
+SB.SPELLID = SB.SPELLID or {}
+
+-- Lowercased set of every ability name currently in the player's spellbook, for the class in
+-- SB.SPELLID. Built from GetSpellBookItemName ALONE — deliberately no spell-ID resolution — so it
+-- still works when GetSpellLink parsing (compat/SpellBook.lua) fails for an entry. This is the
+-- authoritative "do I actually know this?" test; see SB.IsSpellNameKnown.
+SB.KNOWN_NAMES = SB.KNOWN_NAMES or {}
+
+local BOOKTYPE = BOOKTYPE_SPELL or "spell"
+
+-- "Rank 5" / "Rango 5" / "Rang 5" — every locale puts the digits last, so pull the trailing number
+-- and ignore the word. Non-ranked spells (talents, racials, most utility) return nil and sort as 0.
+local function rankNumber(rankText)
+ if not rankText then return 0 end
+ return tonumber(rankText:match("(%d+)%s*$")) or 0
+end
+
+-- Rebuild the whole table for the player's class from the live spellbook.
+function SB.BuildRankTable()
+ local _, class = UnitClass("player")
+ if not class then return end
+ if not (GetNumSpellTabs and GetSpellTabInfo and GetSpellBookItemName) then return end
+
+ local byName = {}
+ local ranks = {} -- name -> { [id] = rankNumber } for the sort below
+ local known = {}
+
+ for tab = 1, (GetNumSpellTabs() or 0) do
+ local _, _, offset, numSlots = GetSpellTabInfo(tab)
+ if offset and numSlots then
+ for slot = offset + 1, offset + numSlots do
+ local name, rankText = GetSpellBookItemName(slot, BOOKTYPE)
+ if name then
+ -- Record the NAME before any id resolution, so a spell whose link can't be parsed is
+ -- still correctly reported as known.
+ known[name:lower()] = true
+ -- compat/SpellBook.lua maps this onto GetSpellLink parsing on 3.3.5a; it returns
+ -- ("SPELL", spellID) and spellID may be nil if the link can't be parsed.
+ local _, spellID = GetSpellBookItemInfo(slot, BOOKTYPE)
+ if spellID then
+ local key = name:lower()
+ local list = byName[key]
+ if not list then list = {}; byName[key] = list; ranks[key] = {} end
+ -- Guard against the same id appearing twice (a spell present in two tabs).
+ if ranks[key][spellID] == nil then
+ list[#list + 1] = spellID
+ ranks[key][spellID] = rankNumber(rankText)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ -- Sort ascending by rank so the LAST element is the highest rank the player knows.
+ --
+ -- NOTE vs the NewEra source: its comment warns "the TBC seed merge APPENDS ids so last element
+ -- isn't the top rank". That caveat applies to their generated+merged table. Ours is derived
+ -- purely from the live spellbook and explicitly sorted here, so last == highest IS valid.
+ for key, list in pairs(byName) do
+ local r = ranks[key]
+ table.sort(list, function(a, b)
+ local ra, rb = r[a] or 0, r[b] or 0
+ if ra == rb then return a < b end
+ return ra < rb
+ end)
+ end
+
+ SB.SPELLID[class] = byName
+ SB.KNOWN_NAMES = known
+ SB.built = true
+ return byName
+end
+
+-- Build on demand if nothing has yet. PLAYER_LOGIN and SPELLS_CHANGED both rebuild on a deferred
+-- timer (see the watcher below), but a consumer can run BEFORE that timer fires — the cooldown
+-- viewers rebuild on PLAYER_ENTERING_WORLD, for instance. Without this, their first pass would read
+-- an empty table and conclude the player knows nothing.
+local function ensureBuilt()
+ if not SB.built then
+ local ok = pcall(SB.BuildRankTable)
+ if not ok then return false end
+ end
+ return SB.built and true or false
+end
+SB.EnsureBuilt = ensureBuilt
+
+-- The rank id list for an ability name, for the player's class.
+function SB.KnownRankIDs(name)
+ if not name then return nil end
+ ensureBuilt()
+ local _, class = UnitClass("player")
+ local byName = class and SB.SPELLID[class]
+ return byName and byName[name:lower()] or nil
+end
+
+-- Is this ability in the player's spellbook RIGHT NOW, at any rank?
+--
+-- This is the authoritative learn test, and it is deliberately name-keyed rather than id-keyed.
+-- IsSpellKnown(id) checks one EXACT rank, so a curated rank-1 id reports false the moment the
+-- player trains rank 2 — which is what made talent-granted and higher-rank abilities vanish from
+-- the cooldown viewers. Matching on name is immune to that, and equally immune to level
+-- requirements (relevant on level-squished servers, where a character can know an ability well
+-- below its stock level).
+function SB.IsSpellNameKnown(name)
+ if not name then return false end
+ ensureBuilt()
+ return SB.KNOWN_NAMES[name:lower()] and true or false
+end
+
+-- Highest rank of `name` the player has learned. Falls back to the passed spellID for single-rank
+-- abilities, racials, item use-spells, and anything not in the spellbook (e.g. an unlearned
+-- curated entry being previewed).
+function SB.HighestKnownRankID(spellID, name)
+ if not name then return spellID end
+ local list = SB.KnownRankIDs(name)
+ if list and #list > 0 then return list[#list] end
+ return spellID
+end
+
+-- Rebuild on the events that can change what's in the book. SPELLS_CHANGED fires on login and on
+-- talent/spell changes; LEARNED_SPELL_IN_TAB on training a new rank.
+local watcher = CreateFrame("Frame")
+watcher:RegisterEvent("PLAYER_LOGIN")
+watcher:RegisterEvent("SPELLS_CHANGED")
+watcher:RegisterEvent("LEARNED_SPELL_IN_TAB")
+-- Dual spec swaps the whole book. SPELLS_CHANGED does fire for it, but its ORDER against
+-- ACTIVE_TALENT_GROUP_CHANGED is not guaranteed, and consumers that refresh on the talent event were
+-- reading this table before it had caught up — a Holy talent stayed listed after switching to
+-- Discipline until something unrelated refreshed. Registering both makes the rebuild certain; the
+-- coalescing below means the extra event costs nothing when they arrive together.
+watcher:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
+watcher:SetScript("OnEvent", function()
+ -- Coalesce: SPELLS_CHANGED can burst during login and talent swaps, and a full book walk is
+ -- ~200 GetSpellLink string matches. One rebuild per frame is plenty.
+ if watcher._queued then return end
+ watcher._queued = true
+ C_Timer.After(0, function()
+ watcher._queued = false
+ local ok, err = pcall(SB.BuildRankTable)
+ if not ok then
+ if NE.Log then NE.Log("SPELLRANKS", "rebuild failed: " .. tostring(err)) end
+ return
+ end
+ -- Consumers gate on this table (the cooldown viewers' learn-check, for one), so anything that
+ -- changed the spellbook — training, a talent respec, a spec switch — has to re-run them now
+ -- that the table reflects reality. Without this a newly-learned ability only appeared after
+ -- some later unrelated rebuild.
+ for _, fn in ipairs(SB._onRebuilt) do pcall(fn) end
+ end)
+end)
+
+-- Subscribe to "the spellbook changed and the table is up to date".
+SB._onRebuilt = SB._onRebuilt or {}
+function SB.OnRebuilt(fn)
+ if type(fn) == "function" then SB._onRebuilt[#SB._onRebuilt + 1] = fn end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Tabs.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Tabs.lua
new file mode 100644
index 0000000..0b18238
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Tabs.lua
@@ -0,0 +1,266 @@
+-- DragonUI_NewEra/core/Tabs.lua — reusable retail-style tab reskin for classic
+-- CharacterFrameTabButtonTemplate-derived tabs.
+--
+-- DOWNPORT: NewEra Core/Tabs.lua → 3.3.5a. The 6-piece classic tab template
+-- (Left/Middle/Right + *Disabled) and PanelTemplates_SelectTab semantics are IDENTICAL on 3.3.5a
+-- (this is the same vanilla template Era inherited from), so the reskin logic ports directly.
+-- Changes: (1) atlas-entry lookups route through NE.tex._atlasEntry (our coord registry) not the
+-- NE_ATLAS global; (2) SetShown → Show/Hide in MakeSideTab:SetSelectedState (no SetShown on
+-- 3.3.5a); (3) NE.tooltip.Wire is optional (provided by a sibling module, feature-gated).
+--
+-- §2 CONTRACT: NE.tabs.* surface preserved.
+
+local NE = DragonUI_NewEra
+NE.tabs = NE.tabs or {}
+
+local ATLAS_BY_SUFFIX = {
+ Left = "uiframe-tab-left",
+ Right = "uiframe-tab-right",
+ Middle = "_uiframe-tab-center",
+ LeftDisabled = "uiframe-activetab-left",
+ RightDisabled = "uiframe-activetab-right",
+ MiddleDisabled = "_uiframe-activetab-center",
+}
+
+local SIZE_BY_SUFFIX = {
+ Left = { w = 35, h = 36 },
+ Right = { w = 37, h = 36 },
+ Middle = { h = 36 },
+ LeftDisabled = { w = 35, h = 42 },
+ RightDisabled = { w = 37, h = 42 },
+ MiddleDisabled = { h = 42 },
+}
+
+local function buildCustomHighlight(tab)
+ if tab._neCustomHL then return end
+ if not (NE.tex and NE.tex.SetAtlas) then return end
+
+ local existing = tab.GetHighlightTexture and tab:GetHighlightTexture()
+ if existing then
+ existing:SetTexture(nil) -- DOWNPORT: SetTexture("") is fine too; nil clears on 3.3.5a
+ existing:Hide()
+ end
+
+ local left = _G[tab:GetName() .. "Left"]
+ local right = _G[tab:GetName() .. "Right"]
+ local middle = _G[tab:GetName() .. "Middle"]
+ if not (left and right and middle) then return end
+
+ local hlLeft = tab:CreateTexture(nil, "HIGHLIGHT")
+ NE.tex.SetAtlas(hlLeft, "uiframe-tab-left", false)
+ hlLeft:SetSize(35, 36)
+ hlLeft:SetPoint("TOPLEFT", left, "TOPLEFT", 0, 0)
+ hlLeft:SetBlendMode("ADD")
+ hlLeft:SetAlpha(0.4)
+
+ local hlRight = tab:CreateTexture(nil, "HIGHLIGHT")
+ NE.tex.SetAtlas(hlRight, "uiframe-tab-right", false)
+ hlRight:SetSize(37, 36)
+ hlRight:SetPoint("TOPRIGHT", right, "TOPRIGHT", 0, 0)
+ hlRight:SetBlendMode("ADD")
+ hlRight:SetAlpha(0.4)
+
+ local hlMiddle = tab:CreateTexture(nil, "HIGHLIGHT")
+ NE.tex.SetAtlas(hlMiddle, "_uiframe-tab-center", false)
+ hlMiddle:SetHorizTile(true)
+ hlMiddle:SetHeight(36)
+ hlMiddle:SetPoint("TOPLEFT", hlLeft, "TOPRIGHT", 0, 0)
+ hlMiddle:SetPoint("TOPRIGHT", hlRight, "TOPLEFT", 0, 0)
+ hlMiddle:SetBlendMode("ADD")
+ hlMiddle:SetAlpha(0.4)
+
+ tab._neCustomHL = { left = hlLeft, middle = hlMiddle, right = hlRight }
+end
+
+-- Reskin a single tab in-place. Returns true on success.
+function NE.tabs.ReskinClassicTab(tabName, opts)
+ opts = opts or {}
+ local tab = _G[tabName]
+ if not tab or tab._neTabReskinned then return false end
+
+ for suffix, atlas in pairs(ATLAS_BY_SUFFIX) do
+ local tex = _G[tabName .. suffix]
+ if tex and NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(tex, atlas, false)
+ local sz = SIZE_BY_SUFFIX[suffix]
+ if sz then
+ if sz.w then tex:SetWidth(sz.w) end
+ if sz.h then tex:SetHeight(sz.h) end
+ end
+ end
+ end
+
+ local left = _G[tabName .. "Left"]
+ local right = _G[tabName .. "Right"]
+ local middle = _G[tabName .. "Middle"]
+ local leftDisabled = _G[tabName .. "LeftDisabled"]
+ local rightDisabled = _G[tabName .. "RightDisabled"]
+ local middleDisabled = _G[tabName .. "MiddleDisabled"]
+
+ if left then left:ClearAllPoints(); left:SetPoint("TOPLEFT", tab, "TOPLEFT", -3, 0) end
+ if right then right:ClearAllPoints(); right:SetPoint("TOPRIGHT", tab, "TOPRIGHT", 7, 0) end
+ if leftDisabled then leftDisabled:ClearAllPoints(); leftDisabled:SetPoint("TOPLEFT", tab, "TOPLEFT", -1, 0) end
+ if rightDisabled then rightDisabled:ClearAllPoints(); rightDisabled:SetPoint("TOPRIGHT", tab, "TOPRIGHT", 8, 0) end
+
+ if middle and left and right then
+ middle:ClearAllPoints()
+ middle:SetPoint("TOPLEFT", left, "TOPRIGHT", 0, 0)
+ middle:SetPoint("TOPRIGHT", right, "TOPLEFT", 0, 0)
+ middle:SetHorizTile(true)
+ end
+ if middleDisabled and leftDisabled and rightDisabled then
+ middleDisabled:ClearAllPoints()
+ middleDisabled:SetPoint("TOPLEFT", leftDisabled, "TOPRIGHT", 0, 0)
+ middleDisabled:SetPoint("TOPRIGHT", rightDisabled, "TOPLEFT", 0, 0)
+ middleDisabled:SetHorizTile(true)
+ end
+
+ buildCustomHighlight(tab)
+
+ if not opts.skipTextY then
+ tab.selectedTextY = opts.selectedTextY or -3
+ tab.deselectedTextY = opts.deselectedTextY or 2
+ end
+
+ tab._neTabReskinned = true
+ return true
+end
+
+-- Convert a reskinned tab into a TOP tab (points UP). Call AFTER ReskinClassicTab.
+local TOP_TAB_PCT = 0.85
+local function flipPiece(tex, atlas)
+ -- DOWNPORT: read from NE.tex._atlasEntry (our registry) not NE_ATLAS.
+ local entry = atlas and NE.tex._atlasEntry and NE.tex._atlasEntry(atlas)
+ if not (tex and entry) then return end
+ local cropBottom = entry.top + (entry.bottom - entry.top) * (1 - TOP_TAB_PCT)
+ tex:SetTexCoord(entry.left, entry.right, entry.bottom, cropBottom)
+ tex:SetHeight(tex:GetHeight() * TOP_TAB_PCT)
+end
+
+function NE.tabs.MakeTopTab(tabName)
+ local tab = _G[tabName]
+ if not tab or tab._neTopTab then return end
+ tab._neTopTab = true
+
+ for suffix, atlas in pairs(ATLAS_BY_SUFFIX) do flipPiece(_G[tabName .. suffix], atlas) end
+ local hl = tab._neCustomHL
+ if hl then flipPiece(hl.left, "uiframe-tab-left"); flipPiece(hl.middle, "_uiframe-tab-center"); flipPiece(hl.right, "uiframe-tab-right") end
+
+ local function anchor(name, point, x)
+ local t = _G[tabName .. name]
+ if t then t:ClearAllPoints(); t:SetPoint(point, tab, point, x, 0) end
+ end
+ anchor("Left", "BOTTOMLEFT", -3); anchor("Right", "BOTTOMRIGHT", 7)
+ anchor("LeftDisabled", "BOTTOMLEFT", -1); anchor("RightDisabled", "BOTTOMRIGHT", 8)
+ local function span(mid, l, r)
+ local m, lt, rt = _G[tabName .. mid], _G[tabName .. l], _G[tabName .. r]
+ if m and lt and rt then
+ m:ClearAllPoints()
+ m:SetPoint("BOTTOMLEFT", lt, "BOTTOMRIGHT", 0, 0)
+ m:SetPoint("BOTTOMRIGHT", rt, "BOTTOMLEFT", 0, 0)
+ m:SetHorizTile(true)
+ end
+ end
+ span("Middle", "Left", "Right"); span("MiddleDisabled", "LeftDisabled", "RightDisabled")
+ tab:SetHeight(math.floor((SIZE_BY_SUFFIX.Left.h or 36) * TOP_TAB_PCT + 0.5))
+ local txt = _G[tabName .. "Text"]
+ if txt then txt:ClearAllPoints(); txt:SetPoint("CENTER", tab, "CENTER", 0, -3) end
+ tab.selectedTextY = -1
+ tab.deselectedTextY = -3
+ if hl and hl.left and hl.right and hl.middle then
+ hl.left:ClearAllPoints(); hl.left:SetPoint("BOTTOMLEFT", _G[tabName .. "Left"], "BOTTOMLEFT", 0, 0)
+ hl.right:ClearAllPoints(); hl.right:SetPoint("BOTTOMRIGHT", _G[tabName .. "Right"], "BOTTOMRIGHT", 0, 0)
+ hl.middle:ClearAllPoints()
+ hl.middle:SetPoint("BOTTOMLEFT", hl.left, "BOTTOMRIGHT", 0, 0)
+ hl.middle:SetPoint("BOTTOMRIGHT", hl.right, "BOTTOMLEFT", 0, 0)
+ hl.middle:SetHorizTile(true)
+ end
+end
+
+local function sizeTabToText(tab, minWidth, textPadding)
+ if not tab then return end
+ local text = _G[tab:GetName() .. "Text"]
+ local textW = (text and text:GetWidth()) or 0
+ tab:SetWidth(math.max(minWidth or 70, math.floor(textW + (textPadding or 24))))
+end
+
+-- Anchor a chain of reskinned tabs along the parent frame's BOTTOMLEFT.
+function NE.tabs.SizeAndAnchorTabs(parent, tabNames, opts)
+ opts = opts or {}
+ local startX = opts.startX or 11
+ local startY = opts.startY or 2
+ local gap = opts.gap or 1
+ local minW = opts.minWidth or 70
+ local pad = opts.textPadding or 24
+ local pPoint = opts.parentPoint or "BOTTOMLEFT"
+
+ local prev = nil
+ for _, name in ipairs(tabNames) do
+ local tab = _G[name]
+ if tab then
+ sizeTabToText(tab, minW, pad)
+ tab:ClearAllPoints()
+ if prev then
+ tab:SetPoint("TOPLEFT", prev, "TOPRIGHT", gap, 0)
+ else
+ tab:SetPoint("TOPLEFT", parent, pPoint, startX, startY)
+ end
+ if not tab._neSizeHooked then
+ tab._neSizeHooked = true
+ tab:HookScript("OnShow", function(self)
+ sizeTabToText(self, minW, pad)
+ end)
+ end
+ prev = tab
+ end
+ end
+end
+
+-- SIDE TAB — retail's LargeSideTabButtonTemplate: a 43×55 button hanging off a panel's RIGHT edge.
+-- DOWNPORT: SetShown → Show/Hide in SetSelectedState. The questlog side-tab atlases are not in the
+-- Sprint-0 sheet set, so the side tab degrades (transparent textures) until §3 ships sheet 5684744.
+function NE.tabs.MakeSideTab(parent, opts)
+ opts = opts or {}
+ local active = opts.activeAtlas
+ local inactive = opts.inactiveAtlas or active
+ local iconSize = opts.iconSize or 29
+
+ local b = CreateFrame("Button", nil, parent)
+ b:SetSize(43, 55)
+
+ local bg = b:CreateTexture(nil, "BACKGROUND")
+ bg:SetPoint("CENTER")
+ NE.tex.SetAtlas(bg, "questlog-tab-side", true)
+
+ local icon = b:CreateTexture(nil, "ARTWORK")
+ icon:SetPoint("CENTER", b, "CENTER", -2, 0)
+ icon:SetSize(iconSize, iconSize)
+ NE.tex.SetAtlas(icon, inactive, false)
+ b.Icon, b._active, b._inactive, b._iconSize = icon, active, inactive, iconSize
+
+ local sel = b:CreateTexture(nil, "OVERLAY")
+ sel:SetPoint("CENTER")
+ NE.tex.SetAtlas(sel, "questlog-tab-side-glow-select", true)
+ sel:Hide()
+ b.Selected = sel
+
+ local hl = b:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetPoint("CENTER")
+ NE.tex.SetAtlas(hl, "questlog-tab-side-glow-hover", true)
+
+ if opts.onClick then
+ b:RegisterForClicks("LeftButtonUp")
+ b:SetScript("OnClick", opts.onClick)
+ end
+ if opts.tooltip and NE.tooltip and NE.tooltip.Wire then
+ NE.tooltip.Wire(b, opts.tooltip, { anchor = "ANCHOR_RIGHT" })
+ end
+
+ function b:SetSelectedState(on)
+ -- DOWNPORT: SetShown → Show/Hide.
+ if on then self.Selected:Show() else self.Selected:Hide() end
+ NE.tex.SetAtlas(self.Icon, on and self._active or self._inactive, false)
+ self.Icon:SetSize(self._iconSize, self._iconSize)
+ end
+ return b
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Texture.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Texture.lua
new file mode 100644
index 0000000..4e82f83
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/Texture.lua
@@ -0,0 +1,358 @@
+-- DragonUI_NewEra/core/Texture.lua — the canonical atlas/texture API for the addon.
+--
+-- DOWNPORT: NewEra Core/Texture.lua → 3.3.5a. NewEra resolved retail atlas nicknames through a
+-- GENERATED global table `NE_ATLAS` (built from wago.tools CSV) + per-FDID local BLP copies
+-- (`NE.tex.localFiles`). On 3.3.5a we don't ship that whole generated table — instead, per the
+-- ARCHITECT DECISION (CONTRACTS §2/§3): atlas COORDINATES come from coord tables that THIS Core
+-- agent owns (registered here + seeded in NineSliceLayouts.lua), the Asset agent supplies the
+-- fdid→BLP path map (Textures/Assets.lua via NE.tex.RegisterLocal), and we set texcoords
+-- manually (the DragonUI Atlas.lua pattern). We do NOT use C_Texture.RegisterAtlas — the
+-- C_Texture compat shim is only a fallback for direct atlas-name lookups NewEra makes.
+--
+-- TWO resolution registries:
+-- NE.tex.localFiles[fdid] = "Interface\\AddOns\\...blp" (Asset agent: RegisterLocal)
+-- NE.tex.atlases[name] = { file=fdid, left,right,top,bottom, width, height } (Core: RegisterAtlas)
+--
+-- PUBLIC §2 CONTRACT:
+-- NE.tex.RegisterLocal(fdid, path) -- fdid → BLP path
+-- NE.tex.Set(texture, fdidOrName[, ...]) -- THE unified setter (fdid OR atlas name)
+-- Plus NewEra's preserved surface: NE.tex.SetAtlas / GetAtlasRect / HasAtlas / AtlasMarkup / …
+
+local NE = DragonUI_NewEra
+NE.tex = NE.tex or {}
+
+-- FDID -> addon-relative texture path. Populated by Textures/Assets.lua (Asset agent).
+NE.tex.localFiles = NE.tex.localFiles or {}
+
+-- Atlas-name -> coord entry { file=fdid, left,right,top,bottom, width, height }.
+-- DOWNPORT: replaces NewEra's generated global NE_ATLAS. Owned by Core; seeded from the coord
+-- tables in NineSliceLayouts.lua + Tabs.lua + ScrollbarReskin.lua via RegisterAtlas.
+NE.tex.atlases = NE.tex.atlases or {}
+
+function NE.tex.RegisterLocal(fdid, path)
+ NE.tex.localFiles[fdid] = path
+end
+
+-- Local BLP path registered for an FDID (RegisterLocal), or nil.
+function NE.tex.Local(fdid)
+ return NE.tex.localFiles[fdid]
+end
+
+-- Register one atlas coord entry. `info` = { file=fdid, left,right,top,bottom[, width, height] }.
+-- DOWNPORT: this is the seam the architect chose over C_Texture.RegisterAtlas.
+function NE.tex.RegisterAtlas(name, info)
+ if not (name and info) then return end
+ NE.tex.atlases[name:lower()] = info
+end
+
+-- Bulk register: a table of { ["atlas-name"] = {file=,left=,...}, ... }. Used by the coord
+-- tables in NineSliceLayouts.lua to load a whole family at once.
+function NE.tex.RegisterAtlases(tbl)
+ if type(tbl) ~= "table" then return end
+ for name, info in pairs(tbl) do
+ NE.tex.atlases[name:lower()] = info
+ end
+end
+
+-- Resolve an atlas-name entry. Tries (1) our owned registry, (2) the generated global NE_ATLAS
+-- if some other layer ever ships it, (3) the C_Texture compat shim (direct atlas-name lookup
+-- fallback, per the architect decision). Returns the entry table or nil.
+-- DOWNPORT: NewEra read NE_ATLAS directly; we layer our registry on top + add the shim fallback.
+local function atlasEntry(name)
+ if not name then return nil end
+ local key = name:lower()
+ local e = NE.tex.atlases[key]
+ if e then return e end
+ if _G.NE_ATLAS then
+ e = _G.NE_ATLAS[key]
+ if e then return e end
+ end
+ -- Fallback: the C_Texture compat shim (only answers atlases registered with it).
+ if C_Texture and C_Texture.GetAtlasInfo then
+ local info = C_Texture.GetAtlasInfo(name)
+ if info then
+ -- Normalize the C_Texture shape (leftTexCoord/...) → our entry shape.
+ return {
+ file = info.file or info.fileDataID,
+ left = info.leftTexCoord or 0,
+ right = info.rightTexCoord or 1,
+ top = info.topTexCoord or 0,
+ bottom = info.bottomTexCoord or 1,
+ width = info.width,
+ height = info.height,
+ }
+ end
+ end
+ return nil
+end
+NE.tex._atlasEntry = atlasEntry -- exposed for diagnostics / sibling modules
+
+-- Resolve the BLP source for an atlas entry: prefer the shipped local copy, else the raw FDID.
+local function entrySource(entry)
+ return NE.tex.localFiles[entry.file] or entry.file
+end
+
+-- Resolved atlas rect (texcoords) for a nickname → left,right,top,bottom, or the full 0,1,0,1
+-- sheet if unknown.
+function NE.tex.GetAtlasRect(name)
+ local entry = atlasEntry(name)
+ if not entry then return 0, 1, 0, 1 end
+ return entry.left, entry.right, entry.top, entry.bottom
+end
+
+-- Convenience: true if the atlas is known.
+function NE.tex.HasAtlas(name)
+ return atlasEntry(name) ~= nil
+end
+
+-- Resolve a dotted key path ("a.b.c") to root.a.b.c, or nil if any hop is missing.
+function NE.tex.ResolveKeyPath(root, path)
+ local node = root
+ for key in path:gmatch("[^.]+") do
+ node = node and node[key]
+ if not node then return nil end
+ end
+ return node
+end
+
+-- Apply an atlas nickname to a texture. Returns true on success, false otherwise.
+-- DOWNPORT: NewEra forced filterMode "LINEAR"/"TRILINEAR" via a 4-arg SetTexture. 3.3.5a's
+-- Texture:SetTexture does NOT accept a filterMode arg (4th arg is ignored / can error on some
+-- builds), so we set the texture with the legal arg count and skip the filter hint. NewEra's
+-- SetTextureSliceMargins / ClearTextureSlice (retail 9-slice) also don't exist on 3.3.5a —
+-- guarded by feature-detect, so they simply no-op (plain SetTexCoord stretch, acceptable for v1).
+function NE.tex.SetAtlas(tex, name, useAtlasSize, filterMode)
+ if not (tex and name) then return false end
+ local entry = atlasEntry(name)
+ if not entry then
+ if NE.Log then NE.Log("ATLAS", "MISS atlas " .. tostring(name)) end
+ return false
+ end
+ local source = entrySource(entry)
+ -- DOWNPORT: 3.3.5a's Texture:SetTexture CANNOT read a raw FileDataID integer (retail/Era-only).
+ -- If the atlas's backing BLP wasn't shipped (no localFiles entry), `source` is the raw FDID —
+ -- setting it yields a BLANK texture while reporting success, which silently replaces native art
+ -- (e.g. blanked the stock close-button X). Report a miss so callers gracefully fall back.
+ if type(source) ~= "string" then
+ if NE.Log then NE.Log("ATLAS", "NO-LOCAL atlas " .. tostring(name) .. " (fdid " .. tostring(entry.file) .. " not shipped)") end
+ return false
+ end
+ tex:SetTexture(source) -- DOWNPORT: no filterMode arg on 3.3.5a
+ tex:SetTexCoord(entry.left, entry.right, entry.top, entry.bottom)
+ if useAtlasSize and entry.width and entry.height then
+ tex:SetSize(entry.width, entry.height)
+ end
+ -- DOWNPORT: retail's per-atlas 9-slice (SetTextureSliceMargins) is absent on 3.3.5a; feature-
+ -- gate it so the call no-ops there. NewEra's NE_ATLAS_SLICE table isn't shipped, so this never
+ -- fires on 3.3.5a regardless; left in for forward-compat with the C_Texture shim.
+ if tex.SetTextureSliceMargins and not (tex.GetObjectType and tex:GetObjectType() == "MaskTexture") then
+ local slice = _G.NE_ATLAS_SLICE and _G.NE_ATLAS_SLICE[name:lower()]
+ if slice then
+ if tex.SetTextureSliceMode then tex:SetTextureSliceMode(slice.mode or 0) end
+ tex:SetTextureSliceMargins(slice.l, slice.t, slice.r, slice.b)
+ tex._neSliced = true
+ elseif tex._neSliced then
+ if tex.ClearTextureSlice then tex:ClearTextureSlice() else tex:SetTextureSliceMargins(0, 0, 0, 0) end
+ tex._neSliced = nil
+ end
+ end
+ return true
+end
+
+-- THE unified §2 setter. `key` is an FDID (number) OR an atlas nickname (string).
+-- FDID: Set(tex, 374155[, useAtlasSize]) → resolves local path (or raw fdid), full sheet.
+-- name: Set(tex, "ui-frame-metal-cornertopleft-2x"[, useAtlasSize]) → atlas coords.
+-- DOWNPORT: this is the new contract entry-point NewEra didn't have (it only had SetAtlas);
+-- panels in later sprints call NE.tex.Set. Returns true on success.
+function NE.tex.Set(tex, key, useAtlasSize, ...)
+ if not (tex and key ~= nil) then return false end
+ if type(key) == "number" then
+ local source = NE.tex.localFiles[key] or key
+ tex:SetTexture(source)
+ tex:SetTexCoord(0, 1, 0, 1)
+ return true
+ end
+ return NE.tex.SetAtlas(tex, key, useAtlasSize, ...)
+end
+
+-- Inline-text markup for an atlas element — our CreateAtlasMarkup. Returns nil when unknown.
+function NE.tex.AtlasMarkup(name, width, height)
+ local entry = atlasEntry(name)
+ if not entry then return nil end
+ local path = NE.tex.localFiles[entry.file]
+ if not path then return nil end -- markup can't express a bare FDID reliably
+ local B = 1024
+ return ("|T%s:%d:%d:0:0:%d:%d:%d:%d:%d:%d|t"):format(
+ path, height or entry.height or 16, width or entry.width or 16, B, B,
+ math.floor(entry.left * B + 0.5), math.floor(entry.right * B + 0.5),
+ math.floor(entry.top * B + 0.5), math.floor(entry.bottom * B + 0.5))
+end
+
+-- Same as SetAtlas but for MaskTexture — CLAMPTOBLACKADDITIVE wrap so the mask's alpha is 0
+-- outside its drawn bounds.
+-- DOWNPORT: 3.3.5a Texture:SetTexture supports the (file, hWrap, vWrap) 3-arg form, so the
+-- wrap-mode args port; only the trailing filterMode is dropped.
+function NE.tex.SetAtlasMask(maskTex, name)
+ if not (maskTex and name) then return false end
+ local entry = atlasEntry(name)
+ if not entry then
+ if NE.Log then NE.Log("ATLAS", "MISS mask atlas " .. tostring(name)) end
+ return false
+ end
+ maskTex:SetTexture(entrySource(entry), "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
+ maskTex:SetTexCoord(entry.left, entry.right, entry.top, entry.bottom)
+ return true
+end
+
+-- Apply an ATLAS_MAP { [keypath] = { name=, target=, useAtlasSize= } } to resolved widgets.
+function NE.tex.ApplyAtlasMap(self, map, label)
+ for path, entry in pairs(map) do
+ local widget = NE.tex.ResolveKeyPath(self, path)
+ if widget then
+ if entry.target == "bar" and widget.SetStatusBarTexture then
+ NE.tex.SetAtlasOnStatusBar(widget, entry.name)
+ elseif widget.GetObjectType and widget:GetObjectType() == "MaskTexture" then
+ NE.tex.SetAtlasMask(widget, entry.name)
+ else
+ NE.tex.SetAtlas(widget, entry.name, entry.useAtlasSize)
+ end
+ elseif label and NE.Log then
+ NE.Log("ATLAS", "MISS path " .. label .. "." .. path)
+ end
+ end
+end
+
+-- DOWNPORT: NewEra's SmoothBarTex used SetSnapToPixelGrid/SetTexelSnappingBias — retail-only
+-- crispness tuning absent on 3.3.5a. Feature-gate each call so this is a safe no-op here.
+function NE.tex.SmoothBarTex(tex)
+ if not tex then return end
+ if tex.SetSnapToPixelGrid then tex:SetSnapToPixelGrid(true) end
+ if tex.SetTexelSnappingBias then tex:SetTexelSnappingBias(0.3) end
+end
+
+-- StatusBar atlas overlay (NewEra fights the engine's per-SetValue texcoord recompute with a
+-- custom overlay it controls). 3.3.5a StatusBars recompute texcoord the same way, so the overlay
+-- approach still applies. We keep it but drop the retail-only filterMode arg on SetTexture.
+local function updateAtlasOverlay(bar)
+ local a = bar._neAtlas
+ local overlay = bar._neOverlay
+ if not (a and overlay) then return end
+ local minV, maxV = bar:GetMinMaxValues()
+ minV, maxV = minV or 0, maxV or 1
+ local denom = (maxV ~= minV) and (maxV - minV) or 1
+ local v = bar:GetValue() or minV
+ local f = (v - minV) / denom
+ if not f or f ~= f then f = 0 end
+ if f < 0 then f = 0 elseif f > 1 then f = 1 end
+ local barW = bar:GetWidth() or 0
+ local barH = bar:GetHeight() or 0
+ if barW <= 0 or barH <= 0 or f <= 0 then
+ overlay:Hide()
+ else
+ overlay:Show()
+ overlay:SetSize(barW * f, barH)
+ local atlasW = a.right - a.left
+ overlay:SetTexCoord(a.left, a.left + atlasW * f, a.top, a.bottom)
+ end
+ local engineTex = bar:GetStatusBarTexture()
+ if engineTex then engineTex:SetAlpha(0) end
+ local ovR, ovG, ovB = overlay:GetVertexColor()
+ if ovR then overlay:SetVertexColor(ovR, ovG, ovB, 1) end
+end
+NE.tex._updateAtlasOverlay = updateAtlasOverlay
+
+NE.tex.atlasBars = NE.tex.atlasBars or {}
+
+local atlasUpdater
+local function ensureAtlasUpdater()
+ if atlasUpdater then return end
+ atlasUpdater = CreateFrame("Frame")
+ atlasUpdater:SetScript("OnUpdate", function()
+ for bar in pairs(NE.tex.atlasBars) do
+ updateAtlasOverlay(bar)
+ end
+ end)
+end
+
+function NE.tex.SetAtlasOnStatusBar(bar, name)
+ if not (bar and name) then return false end
+ local entry = atlasEntry(name)
+ if not entry then
+ if NE.Log then NE.Log("ATLAS", "MISS atlas " .. tostring(name)) end
+ return false
+ end
+ local source = entrySource(entry)
+
+ bar:SetStatusBarTexture(source)
+ local engineTex = bar:GetStatusBarTexture()
+ if engineTex then
+ engineTex:SetAlpha(0)
+ if engineTex.SetTexture then engineTex:SetTexture(source) end -- DOWNPORT: no filterMode arg
+ end
+
+ if not bar._neOverlay then
+ bar._neOverlay = bar:CreateTexture(nil, "ARTWORK", nil, 0)
+ end
+ bar._neOverlay:ClearAllPoints()
+ bar._neOverlay:SetPoint("TOPLEFT", bar, "TOPLEFT", 0, 0)
+ bar._neOverlay:SetTexture(source) -- DOWNPORT: no filterMode arg
+ NE.tex.SmoothBarTex(bar._neOverlay)
+ bar._neAtlas = { left = entry.left, right = entry.right, top = entry.top, bottom = entry.bottom }
+ bar._neOverlayAtlasName = name
+ NE.tex.atlasBars[bar] = true
+ ensureAtlasUpdater()
+
+ if not bar._neAtlasValueHooked then
+ bar._neAtlasValueHooked = true
+ bar:HookScript("OnValueChanged", updateAtlasOverlay)
+ bar:HookScript("OnMinMaxChanged", updateAtlasOverlay)
+ bar:HookScript("OnSizeChanged", updateAtlasOverlay)
+ hooksecurefunc(bar, "SetValue", function(self) updateAtlasOverlay(self) end)
+ hooksecurefunc(bar, "SetMinMaxValues", function(self) updateAtlasOverlay(self) end)
+ hooksecurefunc(bar, "SetStatusBarColor", function(self, r, g, b)
+ if self._neOverlay then self._neOverlay:SetVertexColor(r or 1, g or 1, b or 1, 1) end
+ end)
+ local cr, cg, cb = bar:GetStatusBarColor()
+ if cr then bar._neOverlay:SetVertexColor(cr, cg, cb, 1) end
+ end
+ updateAtlasOverlay(bar)
+ return true
+end
+
+-- The retail aura debuff-border crop (UI-Debuff-Overlays). Vanilla-era asset → ports as-is.
+function NE.tex.DebuffBorder(tex)
+ if not tex then return end
+ tex:SetTexture("Interface\\Buttons\\UI-Debuff-Overlays")
+ tex:SetTexCoord(0.296875, 0.5703125, 0, 0.515625)
+end
+
+-- Modern per-dispel-type debuff border (atlas). Degrades to the legacy DebuffBorder if the
+-- modern atlas art isn't registered yet.
+NE.tex.DISPEL_BORDER = {
+ Magic = { basic = "ui-debuff-border-magic-noicon-2x", dispel = "ui-debuff-border-magic-icon-2x" },
+ Curse = { basic = "ui-debuff-border-curse-noicon-2x", dispel = "ui-debuff-border-curse-icon-2x" },
+ Disease = { basic = "ui-debuff-border-disease-noicon-2x", dispel = "ui-debuff-border-disease-icon-2x" },
+ Poison = { basic = "ui-debuff-border-poison-noicon-2x", dispel = "ui-debuff-border-poison-icon-2x" },
+ Bleed = { basic = "ui-debuff-border-bleed-noicon-2x", dispel = "ui-debuff-border-bleed-icon-2x" },
+}
+local DISPEL_BORDER_NONE = "ui-debuff-border-default-noicon-2x"
+function NE.tex.DispelBorder(border, debuffType, showDispel)
+ if not border then return false end
+ local info = NE.tex.DISPEL_BORDER[debuffType or ""]
+ local atlas = info and ((showDispel and info.dispel) or info.basic) or DISPEL_BORDER_NONE
+ border:SetVertexColor(1, 1, 1)
+ if NE.tex.SetAtlas(border, atlas, false) then border:Show(); return true end
+ -- DOWNPORT: fail-safe to the legacy vanilla debuff border if the modern atlas isn't shipped.
+ NE.tex.DebuffBorder(border)
+ border:Show()
+ return false
+end
+
+-- The bags auto-sort button face. Degrades silently if the atlas isn't registered.
+function NE.tex.AutoSortButton(btn)
+ if not btn then return end
+ local n = btn:CreateTexture(nil, "ARTWORK")
+ NE.tex.SetAtlas(n, "bags-button-autosort-up", false); n:SetAllPoints(btn); btn:SetNormalTexture(n)
+ local p = btn:CreateTexture(nil, "ARTWORK")
+ NE.tex.SetAtlas(p, "bags-button-autosort-down", false); p:SetAllPoints(btn); btn:SetPushedTexture(p)
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/_HelloDemo.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/_HelloDemo.lua
new file mode 100644
index 0000000..ec4d055
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/core/_HelloDemo.lua
@@ -0,0 +1,124 @@
+-- DragonUI_NewEra/core/_HelloDemo.lua — Sprint-0 exit proof.
+--
+-- Registers slash /dnehello: builds a STANDALONE CreateFrame and runs NE.chrome.Apply to render a
+-- Dragonflight portrait-frame on it (title "New Era — Hello", a portrait, a close button). It must
+-- DEGRADE GRACEFULLY if the metal atlas art isn't registered yet — NE.chrome.Apply already falls
+-- back to a solid colour + flat border, and we additionally print a warning so the tester knows
+-- the art layer (Textures/Assets.lua, §3) hasn't landed. It must never hard-error.
+--
+-- Also registers itself into NE.qa.modules so the QA harness (/dnetest) can open/close it.
+
+local NE = DragonUI_NewEra
+
+local FRAME_NAME = "DragonUI_NewEra_HelloDemo"
+
+local function chat(msg)
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cff1784d1DragonUI NewEra|r " .. msg)
+ end
+end
+
+local demoFrame -- created lazily on first /dnehello (so the chrome toolkit is fully loaded)
+
+local function buildDemo()
+ if demoFrame then return demoFrame end
+
+ -- Standalone frame — NOT a Blizzard panel (that's Sprint 1). Named so the QA harness can find it.
+ local f = CreateFrame("Frame", FRAME_NAME, UIParent)
+ f:SetSize(338, 424)
+ f:SetPoint("CENTER")
+ f:SetFrameStrata("DIALOG")
+ f:EnableMouse(true)
+ f:SetMovable(true)
+ f:RegisterForDrag("LeftButton")
+ f:SetScript("OnDragStart", f.StartMoving)
+ f:SetScript("OnDragStop", f.StopMovingOrSizing)
+ f:Hide()
+
+ -- Detect whether the Sprint-0 metal art is registered so we can warn (but still render).
+ local artReady = NE.tex and NE.tex.HasAtlas and NE.tex.HasAtlas("ui-frame-portraitmetal-cornertopleft-2x")
+ and NE.tex.localFiles and NE.tex.localFiles[2406979] ~= nil
+
+ -- THE proof: turn the bare frame into a DF portrait-frame via the §2 chrome entry. Wrapped in
+ -- pcall so a not-yet-shipped art layer can never hard-error the demo.
+ local ok, err = pcall(function()
+ NE.chrome.Apply(f, {
+ layout = "PortraitFrameTemplate",
+ title = "New Era — Hello",
+ -- No portrait art supplied → PanelChrome defaults to the player portrait (native circular).
+ pixelPerfect = true,
+ })
+ end)
+
+ NE._helloApplyOk = ok
+ NE._helloApplyErr = (not ok) and err or nil
+ if not ok then
+ chat("|cffff5555Hello demo: chrome Apply errored:|r " .. tostring(err))
+ -- Minimal fallback so the frame is still visible/closable.
+ if f.SetBackdrop then
+ f:SetBackdrop({
+ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ edgeSize = 14, insets = { left = 4, right = 4, top = 4, bottom = 4 },
+ })
+ end
+ if not f.CloseButton then
+ f.CloseButton = CreateFrame("Button", nil, f, "UIPanelCloseButton")
+ f.CloseButton:SetPoint("TOPRIGHT", 1, 0)
+ end
+ end
+
+ -- A line of body text so the panel content area reads as populated.
+ local body = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ body:SetPoint("TOP", f, "TOP", 0, -64)
+ body:SetWidth(280)
+ body:SetJustifyH("CENTER")
+ if artReady then
+ body:SetText("Sprint-0 chrome OK.\nPortrait-frame nineslice + portrait + close button rendered from the downported Core toolkit.")
+ else
+ body:SetText("Sprint-0 chrome (degraded).\nMetal atlas art not registered yet — showing the solid-colour fallback. Ship Textures/Assets.lua to render the real frame.")
+ chat("|cffffcc00Hello demo: metal atlas art not registered (Textures/Assets.lua pending) — rendered the graceful fallback.|r")
+ end
+
+ demoFrame = f
+ return f
+end
+
+local function openDemo()
+ local f = buildDemo()
+ f:Show()
+ return f
+end
+
+local function closeDemo()
+ if demoFrame then demoFrame:Hide() end
+end
+
+-- Slash command.
+SLASH_DNEHELLO1 = "/dnehello"
+SlashCmdList["DNEHELLO"] = function()
+ local f = openDemo()
+ if f and f:IsShown() then
+ chat("Hello demo shown (/dnehello). Drag to move; close button to dismiss.")
+ end
+end
+
+-- Build the frame eagerly (hidden) at boot so the QA registry can carry the actual `frame`
+-- handle per the §2 spec. Wrapped in pcall + deferred via C_Timer so a not-yet-loaded sibling
+-- can't break load order; build at file scope is safe (CreateFrame + chrome are all available).
+local builtOk = pcall(buildDemo)
+
+-- Register into the QA harness registry. NE.qa.modules is seeded by bootstrap.lua.
+NE.qa = NE.qa or { modules = {} }
+NE.qa.modules = NE.qa.modules or {}
+NE.qa.modules[#NE.qa.modules + 1] = {
+ name = "HelloDemo",
+ frame = demoFrame, -- the actual frame (built above) per §2 spec
+ getFrame = function() return demoFrame or _G[FRAME_NAME] end,
+ open = function() openDemo() end,
+ close = function() closeDemo() end,
+}
+
+if not builtOk then
+ chat("|cffffcc00Hello demo: deferred build (will construct on first /dnehello).|r")
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/integration/Options.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/integration/Options.lua
new file mode 100644
index 0000000..57ab4f6
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/integration/Options.lua
@@ -0,0 +1,258 @@
+-- DragonUI_NewEra/integration/Options.lua
+-- Adds a "New Era" tab to the DragonUI options panel (the DragonUI_Options
+-- companion addon). The tab lists one enable toggle per registered NewEra panel.
+--
+-- DragonUI_Options is a separate, possibly LoadOnDemand-ish addon: it may load
+-- AFTER us, or already be loaded. We handle both: register now if its
+-- OptionsPanel is present, otherwise hook ADDON_LOADED for "DragonUI_Options".
+--
+-- DOWNPORT: new glue; the 1.15 NewEra addon had a bespoke settings frame. We
+-- proxy each panel's `enabled` flag into DragonUI's profile + PanelControls.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+local OPTIONS_ADDON = "DragonUI_Options"
+local TAB_KEY = "newera"
+local TAB_TITLE = "New Era"
+local TAB_ORDER = 16
+
+-- The list the builder iterates is owned by Register.lua. Keep a safe handle.
+NE.optionPanels = NE.optionPanels or {}
+
+-- Extension point: a module that owns settings beyond a simple enable toggle appends
+-- { order = , build = function(scroll, Controls) ... end }
+-- here, and the builder renders it after the built-in sections. Added for the Cooldown Manager,
+-- whose ten per-viewer settings live here rather than in an Edit Mode popup (see
+-- modules/cooldownviewer/PORT_PLAN.md §B1). Each section is pcall-guarded so one module's bad
+-- control can't blank the whole tab.
+NE.optionSections = NE.optionSections or {}
+
+function NE.RegisterOptionSection(spec)
+ if type(spec) ~= "table" or type(spec.build) ~= "function" then return end
+ spec.order = spec.order or 999
+ table.insert(NE.optionSections, spec)
+ table.sort(NE.optionSections, function(a, b) return a.order < b.order end)
+end
+
+-- ----------------------------------------------------------------------------
+-- builder(scroll): called by OptionsPanel each time the tab is shown.
+-- `scroll` is an AceGUI ScrollFrame; controls are added via NE.dragon.PanelControls
+-- (Controls:AddHeading / AddDescription / AddToggle, all of which :AddChild it).
+-- Must never error when the panel list is empty.
+-- ----------------------------------------------------------------------------
+local function builder(scroll)
+ local dragon = NE.dragon
+ local C = dragon and dragon.PanelControls
+ if not C then
+ -- No PanelControls to render with; nothing we can safely draw.
+ return
+ end
+
+ C:AddHeading(scroll, TAB_TITLE)
+ C:AddDescription(scroll,
+ "NewEra panels ported onto DragonUI. Toggle a panel below to enable or disable it. "
+ .. "Panels appear here as their modules load.")
+ if C.AddSpacer then C:AddSpacer(scroll) end
+
+ -- (Per-window enable toggles live in the "Windows" section below; we intentionally do NOT also
+ -- render NE.optionPanels here — that produced a redundant top-of-page "Character Panel" toggle.)
+
+ -- ----------------------------------------------------------------------------
+ -- Windows — enable/disable each replacement window. RELOAD-GATED: toggling only writes the flag;
+ -- a disabled module simply isn't booted on the next /reload (the Blizzard default frame is used).
+ -- ----------------------------------------------------------------------------
+ if NE.modules and NE.modules.IsEnabled then
+ C:AddHeading(scroll, "Windows")
+ C:AddDescription(scroll,
+ "Use DragonUI's window in place of the Blizzard default. Changes take effect after a /reload.")
+ -- The per-window "Retail bags" restyle toggle stays hidden (the combined bag replaces it), but
+ -- the combined bag itself IS toggleable so players can turn our bag UI off entirely and fall
+ -- back to the stock Blizzard bags. To also re-expose the per-window restyle, add a row:
+ -- { id = "bags", label = "Retail bags" }
+ local WINDOWS = {
+ { id = "character", label = "Character panel" },
+ { id = "Spellbook", label = "Spellbook" },
+ { id = "Talents", label = "Talents" },
+ { id = "Professions", label = "Professions" },
+ { id = "AuctionHouse", label = "Auction House" },
+ { id = "Social", label = "Social (Friends/Who/Guild/Chat/Raid)" },
+ { id = "LFG", label = "Looking For Group (Dungeon/Raid Finder)" },
+ { id = "combinedbag", label = "Combined bag (all-in-one)",
+ desc = "Our all-in-one bag window. Turn OFF to use the stock Blizzard bags instead. Reload (/reload) to apply." },
+ }
+ for _, w in ipairs(WINDOWS) do
+ local id = w.id
+ C:AddToggle(scroll, {
+ label = w.label,
+ desc = w.desc or "Reload (/reload) to apply.",
+ getFunc = function() return NE.modules.IsEnabled(id) and true or false end,
+ setFunc = function(v)
+ if NE.modules.SetEnabled then pcall(NE.modules.SetEnabled, id, v) end
+ end,
+ requiresReload = true,
+ })
+ end
+ end
+
+ -- Adventure Guide (Encounter Journal) doesn't go through the NE.modules registry above (it
+ -- wires into DragonUI's own ModuleRegistry as "ne_EncounterJournal", like every other DragonUI
+ -- module) -- add it here explicitly so it's visible on this tab too.
+ -- MoonWell's server-backed journal remains authoritative. Its NewEra
+ -- counterpart is omitted from the system TOC, so do not expose a dead
+ -- configuration switch here.
+ if NE.ej then
+ C:AddToggle(scroll, {
+ label = "Adventure Guide (Encounter Journal)",
+ desc = "Boss and loot browser. Requires a /reload to take effect (the micro button doesn't re-check this live).",
+ getFunc = function()
+ local modules = dragon.db.profile.modules
+ local m = modules and modules["ne_EncounterJournal"]
+ return not (type(m) == "table" and m.enabled == false)
+ end,
+ setFunc = function(v)
+ if not dragon.db.profile.modules then dragon.db.profile.modules = {} end
+ if not dragon.db.profile.modules["ne_EncounterJournal"] then
+ dragon.db.profile.modules["ne_EncounterJournal"] = {}
+ end
+ dragon.db.profile.modules["ne_EncounterJournal"].enabled = v
+ end,
+ requiresReload = true,
+ })
+ end
+
+ -- ----------------------------------------------------------------------------
+ -- Window scaling — a mode dropdown (UI scale / none / custom) + a custom-size slider per window.
+ -- Backed by NE.scale (NE.db.scale[window]); changes apply IMMEDIATELY to the live frame.
+ -- ----------------------------------------------------------------------------
+ if C.AddSpacer then C:AddSpacer(scroll) end
+ C:AddHeading(scroll, "Window Scaling")
+ local S = NE.scale
+ if not S then
+ -- core/Scale.lua isn't loaded — surface it instead of silently showing nothing.
+ C:AddDescription(scroll,
+ "Scaling controls are unavailable: the 'core\\Scale.lua' file isn't loaded. Make sure your "
+ .. "installed DragonUI_NewEra includes core/Scale.lua AND its line in the .toc, then /reload.")
+ elseif not (C.AddDropdown and C.AddSlider) then
+ C:AddDescription(scroll, "Scaling controls need a newer DragonUI options panel (AddSlider/AddDropdown).")
+ else
+ C:AddDescription(scroll,
+ "Each window's size: \"Use UI scale\" follows the game's UI Scale slider, \"No scaling\" "
+ .. "stays pixel-perfect, \"Custom\" uses its slider. The custom slider is greyed out and "
+ .. "locked unless that window's mode is set to Custom.")
+ local AceGUI = LibStub and LibStub("AceGUI-3.0")
+ local MODES = { ui = "Use UI scale", none = "No scaling", custom = "Custom" }
+ -- Keys are core/Scale.lua's window keys; every one of these frames registers itself with
+ -- NE.scale.SetFrame(key, frame) as it builds, so a change here lands on the live window.
+ -- A window that hasn't been opened yet this session simply has no frame registered — the
+ -- setting still persists and applies on its first build.
+ local WINDOWS = {
+ { key = "character", label = "Character" },
+ { key = "spellbook", label = "Spellbook" },
+ { key = "talents", label = "Talents" },
+ { key = "professions", label = "Professions" },
+ { key = "social", label = "Social" },
+ { key = "guild", label = "Guild" },
+ { key = "lfg", label = "Looking For Group" },
+ { key = "auctionhouse", label = "Auction House" },
+ { key = "cooldownmanager", label = "Cooldown Manager" },
+ { key = "encounterjournal", label = "Adventure Guide" },
+ { key = "combinedbag", label = "Combined Bag" },
+ }
+
+ -- One window's stacked controls: a centered column header, a mode dropdown, and a custom-scale
+ -- slider that disables/re-enables live as the mode dropdown changes (no /reload needed).
+ local function buildColumn(parent, key, label)
+ if AceGUI then
+ local hdr = AceGUI:Create("Heading")
+ hdr:SetText(label)
+ hdr:SetFullWidth(true)
+ parent:AddChild(hdr)
+ end
+ local slider
+ C:AddDropdown(parent, {
+ label = "Scale mode",
+ values = MODES,
+ getFunc = function() local m = S.Get(key); return m end,
+ setFunc = function(v)
+ S.SetMode(key, v)
+ if slider then slider:SetDisabled(v ~= "custom") end
+ end,
+ })
+ slider = C:AddSlider(parent, {
+ label = "Custom scale",
+ min = S.MIN or 0.5, max = S.MAX or 1.5, step = 0.05,
+ getFunc = function() local _, c = S.Get(key); return c end,
+ setFunc = function(v) S.SetCustom(key, v) end,
+ disabled = function() return S.Get(key) ~= "custom" end,
+ })
+ end
+
+ if AceGUI then
+ -- Side-by-side columns, each a vertical stack (wraps onto further rows via the Flow
+ -- layout once WINDOWS outgrows a single row of 0.32-width columns).
+ local row = AceGUI:Create("SimpleGroup")
+ row:SetFullWidth(true)
+ row:SetLayout("Flow")
+ scroll:AddChild(row)
+ for _, w in ipairs(WINDOWS) do
+ local col = AceGUI:Create("SimpleGroup")
+ col:SetRelativeWidth(0.32)
+ col:SetLayout("List")
+ row:AddChild(col)
+ buildColumn(col, w.key, w.label)
+ end
+ else
+ -- No AceGUI containers available: fall back to a flat vertical list.
+ for _, w in ipairs(WINDOWS) do
+ C:AddHeading(scroll, w.label)
+ buildColumn(scroll, w.key, w.label)
+ end
+ end
+ end
+
+ -- Module-contributed sections (NE.RegisterOptionSection). Guarded individually: a module whose
+ -- section errors must not take the rest of the tab down with it.
+ for _, section in ipairs(NE.optionSections) do
+ local ok, err = pcall(section.build, scroll, C)
+ if not ok and NE._warn then
+ NE._warn("options section '" .. tostring(section.id or "?") .. "' failed: " .. tostring(err))
+ end
+ end
+end
+NE.OptionsBuilder = builder
+
+-- ----------------------------------------------------------------------------
+-- Registration with whichever DragonUI_Options instance is available.
+-- ----------------------------------------------------------------------------
+local registered = false
+
+local function tryRegister()
+ if registered then return true end
+ local dragon = NE.dragon
+ local panel = dragon and dragon.OptionsPanel
+ if not (panel and type(panel.RegisterTab) == "function") then
+ return false
+ end
+ local ok, err = pcall(panel.RegisterTab, panel, TAB_KEY, TAB_TITLE, builder, TAB_ORDER)
+ if ok then
+ registered = true
+ return true
+ end
+ if NE._warn then NE._warn("OptionsPanel:RegisterTab failed: " .. tostring(err)) end
+ return false
+end
+
+-- Case A: DragonUI_Options already loaded (OptionsPanel present) -> register now.
+if not tryRegister() then
+ -- Case B: hook ADDON_LOADED and register once DragonUI_Options arrives.
+ local f = CreateFrame("Frame")
+ f:RegisterEvent("ADDON_LOADED")
+ f:SetScript("OnEvent", function(self, _, name)
+ -- Retry on the DragonUI_Options load specifically, but also opportunistically
+ -- on any load in case OptionsPanel appeared via a different addon name.
+ if (name == OPTIONS_ADDON or NE.dragon and NE.dragon.OptionsPanel) and tryRegister() then
+ self:UnregisterEvent("ADDON_LOADED")
+ end
+ end)
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/integration/Register.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/integration/Register.lua
new file mode 100644
index 0000000..fe5ac69
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/integration/Register.lua
@@ -0,0 +1,577 @@
+-- DragonUI_NewEra/integration/Register.lua
+-- The single DragonUI handshake every NewEra panel module routes through.
+--
+-- Later-sprint panels NEVER touch DragonUI internals directly. They call
+-- NE.RegisterPanel(spec)
+-- and this file wires that spec into DragonUI's ModuleRegistry, MoversSystem,
+-- and our own boot dispatcher + QA harness + options list. Every base-API call
+-- is defensively guarded so a missing/renamed base symbol logs a warning instead
+-- of erroring (load order between the parallel-built addon parts must never crash).
+--
+-- DOWNPORT: this whole file is new glue (no 1.15 NewEra counterpart); the 1.15
+-- addon had its own settings UI, here we proxy into DragonUI's unified UX.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+-- ----------------------------------------------------------------------------
+-- Small logging helper. Prefer DragonUI's :Print / :Error if present, else
+-- fall back to DEFAULT_CHAT_FRAME so a missing base API still surfaces.
+-- ----------------------------------------------------------------------------
+local function warn(msg)
+ local dragon = NE.dragon
+ if dragon and type(dragon.Error) == "function" then
+ -- DragonUI:Error(...) is a method (self-call).
+ local ok = pcall(dragon.Error, dragon, "[NewEra] " .. msg)
+ if ok then return end
+ end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffffcc55DragonUI_NewEra|r: " .. msg)
+ end
+end
+NE._warn = NE._warn or warn
+
+-- The ordered list the Options builder renders one toggle per panel from.
+-- Each entry: { id, title, desc, refresh, order }. Kept here (not in Options.lua)
+-- so it is populated even if DragonUI_Options never loads.
+NE.optionPanels = NE.optionPanels or {}
+
+-- ----------------------------------------------------------------------------
+-- DB bootstrap. Panel ENABLE flags + the per-panel `enabled` toggles live in
+-- DragonUI's profile (DragonUIDB) under profile.newera so the whole UX is
+-- unified. Our own DragonUI_NewEraDB only holds per-char panel state.
+-- ----------------------------------------------------------------------------
+local function ensureProfile()
+ local dragon = NE.dragon
+ if not (dragon and dragon.db and dragon.db.profile) then
+ return nil
+ end
+ local profile = dragon.db.profile
+ if type(profile.newera) ~= "table" then
+ profile.newera = { enabled = true }
+ end
+ if profile.newera.enabled == nil then
+ profile.newera.enabled = true
+ end
+ if type(profile.newera.modules) ~= "table" then
+ profile.newera.modules = {}
+ end
+ return profile.newera
+end
+NE.EnsureProfile = ensureProfile
+
+-- Convenience: the newera config sub-table (may be nil pre-login).
+function NE.Config()
+ return ensureProfile()
+end
+
+-- ----------------------------------------------------------------------------
+-- NE.OnReady — bootstrap.lua calls this once SavedVariables are loaded
+-- (after ADDON_LOADED for our addon, i.e. DragonUI.db is already an AceDB).
+-- ----------------------------------------------------------------------------
+function NE.OnReady()
+ local cfg = ensureProfile()
+ if not cfg then
+ warn("DragonUI.db.profile not available at OnReady; newera settings not initialised.")
+ return
+ end
+
+ -- Re-run boot for any panel that registered BEFORE OnReady (load-order
+ -- safety: a panel file may have called RegisterPanel before SavedVariables
+ -- were ready, so its modules.Register entry now has a real default to read).
+ for _, panel in ipairs(NE.optionPanels) do
+ if cfg.modules[panel.id] == nil or type(cfg.modules[panel.id]) ~= "table" then
+ cfg.modules[panel.id] = { enabled = true }
+ end
+ end
+end
+
+-- ----------------------------------------------------------------------------
+-- NE.RegisterPanel(spec)
+-- spec = { id, title, desc, frame, openFn, closeFn, defaultPoint, order }
+--
+-- The one helper every panel module calls. Wires the panel into:
+-- (1) profile.newera.modules[id] = { enabled = true } (default)
+-- (2) NE.modules.Register (our boot dispatcher, from Core agent)
+-- (3) NE.dragon.ModuleRegistry:Register (DragonUI's module list)
+-- (4) NE.dragon.MoversSystem:RegisterMover (legacy; removed from DragonUI — skipped when absent)
+-- (5) NE.qa.modules (the /dnetest harness)
+-- (6) NE.optionPanels (the "New Era" options tab)
+-- Every step is independently guarded.
+-- ----------------------------------------------------------------------------
+-- ----------------------------------------------------------------------------
+-- NE.RegisterHUDFrame(spec)
+-- spec = { name, frame, section, key, editorVisible, showTest, hideTest, onHide,
+-- perCharacter, editorSettings, editorTip }
+--
+-- The HUD-frame counterpart to RegisterPanel. RegisterPanel is for toggled WINDOWS, which drag by
+-- their own title bar and persist through NE.FrameUtil.PersistWindowPosition.
+--
+-- It is the WRONG seam for an always-on HUD element. DragonUI historically had TWO independent
+-- positioning systems and `/dui edit` only ever drove one of them:
+--
+-- addon.MoversSystem (core/movers.lua) -- what RegisterPanel wired. Its ToggleConfigMode was
+-- reachable only from a dead `elseif` branch in
+-- core/commands.lua (addon.EditorMode always exists,
+-- so the first branch always won). Now DELETED from
+-- DragonUI outright — core/movers.lua is no longer in
+-- core/core.xml's load list.
+-- addon.EditableFrames (core/api.lua) -- what /dui edit actually shows, via
+-- EditorMode:Show -> addon:ShowAllEditableFrames.
+-- The only surviving system, and the one this uses.
+--
+-- So a HUD frame must register as an EditableFrame or it is simply invisible to edit mode.
+--
+-- Position round-trip: DragonUI's editor saves via addon.SaveUIFramePosition, which writes
+-- `anchor` / `posX` / `posY` into profile[section][key]. Note that its own
+-- addon.ApplyUIFramePosition reads DIFFERENT field names (`x`/`y`, gated on an `override` flag
+-- nothing sets), so it will not restore what Save wrote — we therefore restore ourselves, reading
+-- the fields Save actually writes. This mirrors what MoversSystem:LoadPosition does for the same
+-- legacy configPath shape (core/movers.lua:358-366).
+-- ----------------------------------------------------------------------------
+
+-- ----------------------------------------------------------------------------
+-- NE.FramePositionKey(key, perCharacter) -> the profile key a HUD frame's position is stored under.
+--
+-- DragonUI's profile is shared across characters — that is what makes its class-keyed stores work at
+-- all — so a position saved under a bare key is a position every character on the account shares.
+-- For a HUD element that is often right (a unit frame belongs where you like unit frames); for the
+-- Cooldown Manager viewers it is not, because what a Priest wants tracked and where a Warlock wants
+-- it are different questions and the answer to the second follows the first.
+--
+-- Done by KEY rather than by a parallel store, because the key is the ONLY thing DragonUI's editor
+-- takes from us: it reads and writes profile[section][key] itself through configPath. Varying the key
+-- therefore buys per-character positions with no change to DragonUI at all (CONTRACTS §0).
+--
+-- Falls back to the shared key when the client cannot yet name the player. A shared position is a
+-- mild surprise; a position filed under "-nil" is one nothing will ever read back.
+function NE.FramePositionKey(key, perCharacter)
+ if not (key and perCharacter) then return key end
+ local name = UnitName and UnitName("player")
+ if not name or name == "" then return key end
+ local realm = GetRealmName and GetRealmName()
+ if realm and realm ~= "" then return key .. "-" .. name .. "-" .. realm end
+ return key .. "-" .. name
+end
+
+-- Seed a per-character position slot from the shared one, once.
+--
+-- A character upgrading into per-character positions must not watch its viewers jump back to the
+-- default — that reads as data loss, not as a feature. The shared entry is deliberately LEFT IN
+-- PLACE: every other character still needs it as their own seed, and deleting it would hand the
+-- upgrade to whoever logged in first and a default layout to everybody else.
+local function seedPerCharacterPosition(section, sharedKey, key)
+ if key == sharedKey then return end
+ local dragon = NE.dragon
+ local profile = dragon and dragon.db and dragon.db.profile
+ local sect = profile and profile[section]
+ if not (sect and sect[sharedKey]) or sect[key] then return end
+ local copy = {}
+ for k, v in pairs(sect[sharedKey]) do copy[k] = v end
+ sect[key] = copy
+end
+
+function NE.ApplySavedFramePosition(frame, section, key)
+ local dragon = NE.dragon
+ local profile = dragon and dragon.db and dragon.db.profile
+ local cfg = profile and profile[section] and profile[section][key]
+ if not (frame and cfg and cfg.anchor and cfg.posX and cfg.posY) then return false end
+ frame:ClearAllPoints()
+ frame:SetPoint(cfg.anchor, UIParent, cfg.anchor, cfg.posX, cfg.posY)
+ return true
+end
+
+-- ----------------------------------------------------------------------------
+-- NE.OpenFrameEditor(frame) -> true | false, reason
+--
+-- Enter DragonUI's editor mode, with one frame pre-selected. This is the seam a module uses to offer
+-- "go position this thing" from inside its own window, and it exists here rather than in the module
+-- for CONTRACTS §4: panel modules never reach into DragonUI internals directly.
+--
+-- THE ARGUMENT IS THE CONTENT FRAME, and the anchor is what gets selected. RegisterHUDFrame registers
+-- the CreateUIFrame anchor as the editable frame and hangs the content off it (see the note below), so
+-- the editor knows the anchor and nothing about the content. Handing SelectEditorFrame a content frame
+-- would put the coordinate readout and the Reset button on a frame the editor cannot move, and it
+-- would look like it worked. `.editorAnchor` is set by RegisterHUDFrame for exactly this.
+--
+-- Failure is a returned reason, not a print: the caller knows where its message belongs. In particular
+-- EditorMode:Show() returns SILENTLY in combat (DragonUI modules/editor_mode.lua:334 — an empty
+-- branch, no message), so a button wired straight to it would appear to do nothing at all mid-fight.
+-- ----------------------------------------------------------------------------
+
+function NE.OpenFrameEditor(frame)
+ local dragon = NE.dragon
+ local EM = dragon and dragon.EditorMode
+ if not (EM and type(EM.Show) == "function" and type(EM.IsActive) == "function") then
+ return false, "DragonUI's editor mode isn't available."
+ end
+ -- This check exists for the MESSAGE, not the behaviour: EditorMode:Show() already no-ops in
+ -- combat, and the IsActive re-check below would catch it either way — but "editor mode declined to
+ -- open" tells a player nothing, where "not during combat" tells them to try again in ten seconds.
+ if InCombatLockdown and InCombatLockdown() then
+ return false, "Editor mode can't be opened during combat."
+ end
+
+ if not EM:IsActive() then
+ local ok, err = pcall(EM.Show, EM)
+ if not ok then return false, "Editor mode failed to open: " .. tostring(err) end
+ end
+ -- Re-check rather than trusting the call, per the header.
+ if not EM:IsActive() then
+ return false, "Editor mode declined to open."
+ end
+
+ local target = frame and (frame.editorAnchor or frame) or nil
+ if target and type(dragon.SelectEditorFrame) == "function" then
+ pcall(dragon.SelectEditorFrame, target)
+ end
+ return true
+end
+
+-- ----------------------------------------------------------------------------
+-- NE.CloseFrameEditor() -> true | false, reason
+--
+-- The other half of OpenFrameEditor, for a module that wants to hand the player back to its own
+-- window. DragonUI's EditorMode:Hide saves every registered frame's position on the way out
+-- (HideAllEditableFrames(true)), so leaving this way loses nothing.
+-- ----------------------------------------------------------------------------
+function NE.CloseFrameEditor()
+ local dragon = NE.dragon
+ local EM = dragon and dragon.EditorMode
+ if not (EM and type(EM.Hide) == "function") then
+ return false, "DragonUI's editor mode isn't available."
+ end
+ local ok, err = pcall(EM.Hide, EM)
+ if not ok then return false, "Editor mode failed to close: " .. tostring(err) end
+ return true
+end
+
+-- Is the editor open right now?
+--
+-- Defaults to TRUE when DragonUI exposes no way to ask, which is the safe answer rather than the
+-- optimistic one: the only caller is a mouse handler on an editor anchor, and those anchors are
+-- EnableMouse(false) until addon.HideUIFrame turns them on — which only ever happens from
+-- ShowAllEditableFrames. No editor, no clicks to gate.
+function NE.IsFrameEditorActive()
+ local dragon = NE.dragon
+ local EM = dragon and dragon.EditorMode
+ if not (EM and type(EM.IsActive) == "function") then return true end
+ local ok, active = pcall(EM.IsActive, EM)
+ return (ok and active) and true or false
+end
+
+-- ----------------------------------------------------------------------------
+-- NE.OpenFrameEditorSettings(anchor) -> true | false
+--
+-- Open the settings a HUD frame registered through `spec.editorSettings`. Retail puts a system's own
+-- settings ON the frame in Edit Mode rather than in a separate options window, and this is that seam.
+--
+-- The field is a CALLBACK, not a menu or a panel: what a module opens when its frame is clicked is
+-- the module's business, and this file's business is only deciding when it may (CONTRACTS §4). The
+-- Cooldown Manager opens a dialog beside the frame; another module could open anything.
+--
+-- Selecting the frame first is deliberate, and matters most on right-click, which leaves DragonUI's
+-- own selection alone (CreateUIFrame only selects on LeftButton) — without this the coordinate
+-- readout and Reset button would still describe whatever was clicked last while the dialog edits
+-- something else.
+-- ----------------------------------------------------------------------------
+function NE.OpenFrameEditorSettings(anchor)
+ local open = anchor and anchor.neEditorSettings
+ if type(open) ~= "function" then return false end
+ if not NE.IsFrameEditorActive() then return false end
+
+ local dragon = NE.dragon
+ if dragon and type(dragon.SelectEditorFrame) == "function" then
+ pcall(dragon.SelectEditorFrame, anchor)
+ end
+ local ok, err = pcall(open, anchor)
+ if not ok then
+ warn("editor settings failed to open: " .. tostring(err))
+ return false
+ end
+ return true
+end
+
+-- Wire the click and its discoverability hint onto an editor anchor.
+--
+-- SetScript, not HookScript: CreateUIFrame sets OnMouseDown (left-click select), OnDragStart and
+-- OnDragStop, and none of OnMouseUp / OnEnter / OnLeave — so nothing here overwrites base behaviour.
+-- This is a runtime write to a frame we own, not an edit to DragonUI (CONTRACTS §0).
+--
+-- EITHER BUTTON opens it, because retail opens a system's dialog on SELECTION and left-click is what
+-- selects. Left-click is also the drag, so this fires on the release that ends one too — which is
+-- also the retail behaviour: the dialog for the thing you just moved stays with it.
+local function attachEditorSettings(anchor, spec)
+ anchor.neEditorSettings = spec.editorSettings
+
+ anchor:SetScript("OnMouseUp", function(self)
+ NE.OpenFrameEditorSettings(self)
+ end)
+
+ -- Settings nobody knows are there are settings nobody uses. The green handle is the only thing on
+ -- screen in edit mode, so the hint goes on it.
+ if spec.editorTip == false or not GameTooltip then return end
+ anchor:SetScript("OnEnter", function(self)
+ if not NE.IsFrameEditorActive() then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText(spec.label or spec.name, 1, 1, 1)
+ GameTooltip:AddLine("Drag to move.", 0.8, 0.8, 0.8)
+ GameTooltip:AddLine("Click for this frame's settings.", 0.4, 1, 0.4)
+ GameTooltip:Show()
+ end)
+ anchor:SetScript("OnLeave", function() GameTooltip:Hide() end)
+end
+
+-- RegisterEditableFrame on its own is ONLY metadata. The editor's drag affordances —
+-- RegisterForDrag, OnDragStart/OnDragStop (which auto-saves to configPath), the green nineslice
+-- overlay, the text label — are attached by DragonUI's frame FACTORY, addon.CreateUIFrame
+-- (core/api.lua:255). addon.HideUIFrame, which the editor calls on each registered frame, only does
+-- SetMovable(true)/EnableMouse(true) and shows an overlay that a plain CreateFrame doesn't have.
+--
+-- So the DragonUI pattern is: build a CreateUIFrame **anchor**, register the ANCHOR as the editable
+-- frame, and hang the real HUD content off it. modules/castbar.lua does exactly this with
+-- CastbarModule.anchor. We follow it.
+function NE.RegisterHUDFrame(spec)
+ if type(spec) ~= "table" or not (spec.name and spec.frame) then
+ warn("RegisterHUDFrame needs a name and frame; ignored.")
+ return
+ end
+ local dragon = NE.dragon
+ if not (dragon and type(dragon.RegisterEditableFrame) == "function"
+ and type(dragon.CreateUIFrame) == "function") then
+ warn("DragonUI editor API absent; '" .. tostring(spec.name) .. "' won't be movable.")
+ return
+ end
+
+ local section = spec.section or "widgets"
+ local sharedKey = spec.key or spec.name
+ -- Opt-in per frame, not a blanket change: a module that wants one placement across the account
+ -- keeps it by saying nothing.
+ local key = NE.FramePositionKey(sharedKey, spec.perCharacter)
+ seedPerCharacterPosition(section, sharedKey, key)
+ local content = spec.frame
+
+ -- DragonUI's CreateUIFrame labels the editor handle with `addon.L[frameName]`. AceLocale's
+ -- read metatable fires a non-breaking error for any key its locale doesn't define, so every
+ -- frame we register printed "Missing entry for 'CooldownViewerBuffBar'" on login.
+ --
+ -- The table GetLocale returns has an `__index` hook but NO `__newindex`, so seeding our own key
+ -- is a plain assignment into a runtime table — it does not modify DragonUI (CONTRACTS §0), and
+ -- it upgrades the handle's label from the raw frame name to something readable.
+ if spec.label and type(dragon.L) == "table" then
+ pcall(function()
+ if rawget(dragon.L, spec.name) == nil then dragon.L[spec.name] = spec.label end
+ end)
+ end
+
+ -- The draggable handle. Sized to the content's current footprint; kept in sync below.
+ local w = math.max(content:GetWidth() or 0, 32)
+ local h = math.max(content:GetHeight() or 0, 32)
+ local okAnchor, anchor = pcall(dragon.CreateUIFrame, w, h, spec.name)
+ if not okAnchor or not anchor then
+ warn("CreateUIFrame failed for '" .. tostring(spec.name) .. "': " .. tostring(anchor))
+ return
+ end
+
+ -- Position the ANCHOR (saved position wins over the module's default), then pin content to it.
+ if not NE.ApplySavedFramePosition(anchor, section, key) then
+ local d = spec.defaultPoint
+ anchor:ClearAllPoints()
+ if d then
+ anchor:SetPoint(d.point or "CENTER", UIParent, d.relativePoint or d.point or "CENTER",
+ d.x or 0, d.y or 0)
+ else
+ anchor:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
+ end
+ end
+
+ content:ClearAllPoints()
+ content:SetPoint("CENTER", anchor, "CENTER", 0, 0)
+
+ -- Keep the grab target matching what's actually on screen as icons come and go.
+ content:SetScript("OnSizeChanged", function(f)
+ local cw, ch = f:GetWidth(), f:GetHeight()
+ if cw and ch and cw > 0 and ch > 0 then anchor:SetSize(cw, ch) end
+ end)
+
+ -- Optional: this frame's own settings, on the frame, in edit mode.
+ if type(spec.editorSettings) == "function" then attachEditorSettings(anchor, spec) end
+
+ local ok, err = pcall(dragon.RegisterEditableFrame, dragon, {
+ name = spec.name,
+ frame = anchor,
+ configPath = { section, key },
+ -- Always offer it in edit mode. A HUD frame that is empty right now still needs to be
+ -- positionable — that is what showTest is for.
+ editorVisible = spec.editorVisible or function() return true end,
+ showTest = spec.showTest,
+ hideTest = spec.hideTest,
+ onHide = function()
+ if spec.hideTest then spec.hideTest() end
+ if spec.onHide then spec.onHide() end
+ end,
+ })
+ if not ok then
+ warn("RegisterEditableFrame failed for '" .. tostring(spec.name) .. "': " .. tostring(err))
+ return
+ end
+
+ spec.frame.editorAnchor = anchor
+ return anchor
+end
+
+function NE.RegisterPanel(spec)
+ if type(spec) ~= "table" or not spec.id then
+ warn("RegisterPanel called without a spec.id; ignored.")
+ return
+ end
+
+ local id = spec.id
+ local title = spec.title or id
+ local desc = spec.desc or ""
+
+ -- (1) DB default. Guarded: profile may not exist yet pre-login; the default
+ -- is re-asserted in OnReady for panels that registered early.
+ local cfg = ensureProfile()
+ if cfg then
+ if type(cfg.modules[id]) ~= "table" then
+ cfg.modules[id] = { enabled = true }
+ elseif cfg.modules[id].enabled == nil then
+ cfg.modules[id].enabled = true
+ end
+ end
+
+ -- enabled() reads the live DB value, defaulting to true when DB is absent.
+ local function isEnabled()
+ local c = ensureProfile()
+ if c and type(c.modules[id]) == "table" and c.modules[id].enabled ~= nil then
+ return c.modules[id].enabled and true or false
+ end
+ return true
+ end
+
+ -- (2) Boot dispatcher (Core agent's Core/Modules.lua). No-op gracefully if
+ -- NE.modules is nil so load order can't crash.
+ if NE.modules and type(NE.modules.Register) == "function" then
+ local ok, err = pcall(function()
+ NE.modules.Register{
+ name = id,
+ default = true,
+ onBoot = function()
+ -- Only open/show when the panel is enabled in config.
+ if isEnabled() and spec.openFn then
+ spec.openFn()
+ end
+ end,
+ }
+ end)
+ if not ok then
+ warn("NE.modules.Register failed for '" .. id .. "': " .. tostring(err))
+ end
+ end
+
+ -- (3) DragonUI ModuleRegistry. Real base API is either
+ -- NE.dragon.ModuleRegistry:Register(name, moduleTable, displayName, desc, order)
+ -- or the convenience wrapper NE.dragon:RegisterModule(...). Prefer whichever
+ -- the base actually exposes. We pass a lightweight module table carrying an
+ -- Enable refresh hook so DragonUI's enable/disable plumbing can drive us.
+ local moduleTable = {
+ ne_id = id,
+ Enable = function()
+ if spec.openFn then spec.openFn() end
+ end,
+ Disable = function()
+ if spec.closeFn then spec.closeFn() end
+ end,
+ Refresh = function()
+ if isEnabled() then
+ if spec.openFn then spec.openFn() end
+ else
+ if spec.closeFn then spec.closeFn() end
+ end
+ end,
+ }
+ local dragon = NE.dragon
+ if dragon then
+ local registered = false
+ local mr = dragon.ModuleRegistry
+ if mr and type(mr.Register) == "function" then
+ local ok, err = pcall(mr.Register, mr, "ne_" .. id, moduleTable, title, desc, spec.order)
+ if ok then
+ registered = true
+ else
+ warn("ModuleRegistry:Register failed for '" .. id .. "': " .. tostring(err))
+ end
+ end
+ if not registered and type(dragon.RegisterModule) == "function" then
+ local ok, err = pcall(dragon.RegisterModule, dragon, "ne_" .. id, moduleTable, title, desc, spec.order)
+ if not ok then
+ warn("RegisterModule failed for '" .. id .. "': " .. tostring(err))
+ end
+ elseif not registered and not mr then
+ warn("DragonUI exposes neither ModuleRegistry nor RegisterModule; '" .. id .. "' not in module list.")
+ end
+ end
+
+ -- (4) Mover — OPTIONAL, and absent on current DragonUI.
+ --
+ -- DragonUI deleted this system: core/core.xml now reads "module_base.lua / movers.lua removed —
+ -- EditableFrames + PositionPresets own layout". core/movers.lua is still on disk but no longer
+ -- in the load manifest, so addon.MoversSystem is simply nil. That used to print a red error per
+ -- registered window on every /reload (six of them: Professions, AuctionHouse, Guild, Social,
+ -- LFG, EncounterJournal), which is noise, not a fault.
+ --
+ -- NOTHING IS LOST. Every NewEra window drags itself — its own StartMoving/StopMovingOrSizing
+ -- handlers, and NE.FrameUtil.PersistWindowPosition (db.windowPos[key]) for the ones that persist
+ -- across sessions. And the mover was already inert here anyway: its drag overlay only appears in
+ -- MoversSystem config mode, reachable solely from a dead `elseif` in DragonUI's core/commands.lua
+ -- (addon.EditorMode always exists, so the first branch always won), so it never saved a position.
+ --
+ -- Deliberately NOT migrated to dragon:RegisterEditableFrame. That is the HUD seam — see
+ -- NE.RegisterHUDFrame below — and putting toggled WINDOWS in it would have `/dui edit` show and
+ -- reposition them through a second system that fights their own persisted position.
+ --
+ -- Kept live for older DragonUI installs that still ship movers.lua.
+ if dragon and dragon.MoversSystem and type(dragon.MoversSystem.RegisterMover) == "function" then
+ if spec.frame then
+ local ok, err = pcall(function()
+ dragon.MoversSystem:RegisterMover{
+ name = "ne_" .. id,
+ parent = spec.frame,
+ text = title,
+ configPath = { "widgets", "ne_" .. id },
+ defaultPoint = spec.defaultPoint,
+ }
+ end)
+ if not ok then
+ warn("RegisterMover failed for '" .. id .. "': " .. tostring(err))
+ end
+ end
+ -- No frame yet (lazily-created panel): the panel re-calls RegisterPanel
+ -- or registers its mover itself once the frame exists. Silent by design.
+ end
+ -- No `else` warn: MoversSystem being absent is the expected state on current DragonUI (see above).
+
+ -- (5) QA harness list.
+ if NE.qa then
+ NE.qa.modules = NE.qa.modules or {}
+ table.insert(NE.qa.modules, {
+ name = title,
+ frame = spec.frame,
+ open = spec.openFn,
+ close = spec.closeFn,
+ })
+ end
+
+ -- (6) Options tab list (rendered by Options.lua). Store a refresh fn so the
+ -- toggle callback can re-run this panel's enable without knowing internals.
+ table.insert(NE.optionPanels, {
+ id = id,
+ title = title,
+ desc = desc,
+ order = spec.order or 999,
+ refresh = moduleTable.Refresh,
+ })
+
+ return moduleTable
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Assets.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Assets.lua
new file mode 100644
index 0000000..c628f66
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Assets.lua
@@ -0,0 +1,51 @@
+-- DragonUI_NewEra/modules/auctionhouse/Assets.lua
+-- Visual-shell atlas registration for the modern Auction House window.
+
+local NE = DragonUI_NewEra
+if not (NE and NE.tex and NE.tex.RegisterLocal and NE.tex.RegisterAtlases) then return end
+
+NE.ah = NE.ah or {}
+
+local AP = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\AuctionHouse\\"
+local CP = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\Common\\"
+
+NE.tex.RegisterLocal(3054898, AP .. "3054898-auctionhouse-backgrounds.blp")
+NE.tex.RegisterLocal(3046538, AP .. "3046538-auctionhouse-chrome.blp")
+NE.tex.RegisterLocal(2922105, CP .. "2922105-common-iconmask.blp")
+
+NE.tex.RegisterAtlases({
+ ["auctionhouse-background-categories"] = { file=3054898, left=0.845215, right=0.912598, top=0.390625, bottom=0.813477, width=138, height=433 },
+ ["auctionhouse-background-index"] = { file=3054898, left=0.180664, right=0.471191, top=0.390625, bottom=0.793945, width=595, height=413 },
+ ["auctionhouse-background-sell-left"] = { file=3054898, left=0.000488, right=0.174805, top=0.389648, bottom=0.816406, width=357, height=437 },
+ ["auctionhouse-background-sell-right"] = { file=3054898, left=0.540527, right=0.735352, top=0.390625, bottom=0.798828, width=399, height=418 },
+ ["auctionhouse-background-summarylist"] = { file=3054898, left=0.472168, right=0.539551, top=0.390625, bottom=0.813477, width=138, height=433 },
+ ["auctionhouse-background-buy-noncommodities-header"] = { file=3054898, left=0.180664, right=0.481934, top=0.309570, bottom=0.388672, width=617, height=81 },
+ ["auctionhouse-background-buy-noncommodities-market"] = { file=3054898, left=0.472168, right=0.762695, top=0.000977, bottom=0.271484, width=595, height=277 },
+
+ ["auctionhouse-itemicon-border-white"] = { file=3046538, left=0.135742, right=0.268555, top=0.698242, bottom=0.831055, width=136, height=136 },
+
+ -- Sell-form chrome (UVs transcribed from NewEra Generated/AtlasData.lua; the shipped chrome BLP
+ -- is byte-identical to the reference's, verified by hash).
+ ["auctionhouse-itemheaderframe"] = { file=3046538, left=0.000977, right=0.668945, top=0.000977, bottom=0.141602, width=342, height=72 },
+ ["auctionhouse-itemicon-empty"] = { file=3046538, left=0.135742, right=0.233398, top=0.833008, bottom=0.930664, width=100, height=100 },
+ ["auctionhouse-selltab-left"] = { file=3046538, left=0.987305, right=0.996094, top=0.000977, bottom=0.023438, width=9, height=23 },
+ ["auctionhouse-selltab-middle"] = { file=3046538, left=0.000977, right=0.093750, top=0.967773, bottom=0.990234, width=95, height=23 },
+ ["auctionhouse-selltab-right"] = { file=3046538, left=0.270508, right=0.279297, top=0.428711, bottom=0.451172, width=9, height=23 },
+ ["auctionhouse-ui-inputfield-left"] = { file=3046538, left=0.235352, right=0.250977, top=0.833008, bottom=0.897461, width=8, height=33 },
+ ["auctionhouse-ui-inputfield-middle"] = { file=3046538, left=0.286133, right=0.633789, top=0.143555, bottom=0.208008, width=178, height=33 },
+ ["auctionhouse-ui-inputfield-right"] = { file=3046538, left=0.252930, right=0.268555, top=0.833008, bottom=0.897461, width=8, height=33 },
+
+ ["auctionhouse-icon-coin-copper"] = { file=3046538, left=0.940430, right=0.959961, top=0.122070, bottom=0.141602, width=20, height=20 },
+ ["auctionhouse-icon-coin-silver"] = { file=3046538, left=0.931641, right=0.951172, top=0.143555, bottom=0.163086, width=20, height=20 },
+ ["auctionhouse-icon-coin-gold"] = { file=3046538, left=0.961914, right=0.981445, top=0.122070, bottom=0.141602, width=20, height=20 },
+
+ ["auctionhouse-nav-button"] = { file=3046538, left=0.635742, right=0.901367, top=0.143555, bottom=0.206055, width=272, height=64 },
+ ["auctionhouse-nav-button-select"] = { file=3046538, left=0.286133, right=0.543945, top=0.274414, bottom=0.315430, width=264, height=42 },
+ ["auctionhouse-nav-button-highlight"] = { file=3046538, left=0.547852, right=0.805664, top=0.209961, bottom=0.250977, width=264, height=42 },
+ ["auctionhouse-nav-button-secondary"] = { file=3046538, left=0.286133, right=0.545898, top=0.209961, bottom=0.272461, width=266, height=64 },
+ ["auctionhouse-nav-button-secondary-select"] = { file=3046538, left=0.286133, right=0.524414, top=0.317383, bottom=0.358398, width=244, height=42 },
+ ["auctionhouse-nav-button-secondary-highlight"] = { file=3046538, left=0.545898, right=0.784180, top=0.274414, bottom=0.315430, width=244, height=42 },
+ ["auctionhouse-nav-button-tertiary-filterline"] = { file=3046538, left=0.270508, right=0.275391, top=0.468750, bottom=0.479492, width=5, height=11 },
+ ["auctionhouse-ui-row-select"] = { file=3046538, left=0.807617, right=0.920898, top=0.209961, bottom=0.227539, width=116, height=18 },
+ ["auctionhouse-ui-row-highlight"] = { file=3046538, left=0.547852, right=0.661133, top=0.252930, bottom=0.270508, width=116, height=18 },
+})
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/AuctionatorEmbed.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/AuctionatorEmbed.lua
new file mode 100644
index 0000000..5d2349e
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/AuctionatorEmbed.lua
@@ -0,0 +1,976 @@
+-- DragonUI_NewEra/modules/auctionhouse/AuctionatorEmbed.lua
+-- Full embed + reskin of Auctionator's UI inside the NE Auction House shell.
+--
+-- Auctionator builds ALL of its Buy/Sell/More UI as ONE frame -- Atr_Main_Panel, parented to the
+-- legacy AuctionFrame at TOPLEFT (210, 0), with its left-column controls hanging off at negative
+-- x offsets (down to -195) and a few pieces poking past the panel's right edge (+66). The old
+-- TabBridge behavior for its tabs was to UNCLOAK the legacy AuctionFrame and dismiss the shell,
+-- dropping the player back into the stock Blizzard window.
+--
+-- This module instead reparents Atr_Main_Panel INTO the shell's ExternalPane and keeps the legacy
+-- AuctionFrame permanently cloaked. That works because the cloak never :Hide()s AuctionFrame --
+-- it stays :IsShown() == true, which is the only thing Auctionator's own logic ever checks (its
+-- scans, shift-click search, tab state all test AuctionFrame:IsShown(), never alpha). The panel's
+-- children keep their panel-relative anchors, so the whole three-tab UI moves as one unit; only
+-- the handful of pieces that overflow the shell's 800px width get re-anchored individually.
+--
+-- The reskin pass then swaps Auctionator's parchment/tooltip-border chrome for the same treatment
+-- the shell's own panes use: uniform dark fill (WHITE8X8 0.06/0.06/0.07) + gold-trim AttachInset
+-- panels, minimal-scrollbar BuildCustom bars, zebra-striped rows with additive gold hover, the
+-- Browse-style dark search box, and reskinned Current/History/Other top tabs.
+--
+-- Everything is fail-safe: each step is pcall'd and nil-guarded, so a different Auctionator build
+-- missing a frame just skips that step; if Atr_Main_Panel never appears at all, ATR.IsEmbedded()
+-- stays false and TabBridge falls back to the old uncloak-and-dismiss behavior untouched.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+NE.ah = NE.ah or {}
+local AH = NE.ah
+AH.atr = AH.atr or {}
+local ATR = AH.atr
+
+----------------------------------------------------------------------
+-- Layout knobs. Auctionator was designed to sit in the legacy AuctionFrame, whose content area is
+-- ~820px wide (its own modal mask is 820 wide) -- WIDER than the shell's default 800. So while an
+-- Auctionator tab is active the shell is grown to ATR_W x ATR_H (SetWindowForExternal below) and
+-- restored to the shell's own BASE_W x BASE_H when a base Buy/Sell/Auctions tab is selected.
+--
+-- PANEL_X centers Auctionator's content (which spans panel x -195 .. +588, width ~783) inside the
+-- grown window: panel + 196 == ATR_W/2. PANEL_Y = -8 lifts the layout so the search row (panel
+-- y -45) lands near the shell's own top-bar controls and the 447-tall layout clears the money bar.
+----------------------------------------------------------------------
+local BASE_W, BASE_H = 800, 538 -- shell's own default (Window.lua createWindow)
+local ATR_W, ATR_H = 840, 538 -- footprint while an Auctionator tab is active (tunable)
+local PANEL_X, PANEL_Y = 223, -8
+-- Auctionator sizes its drop-target highlight 805px wide -- wider than the shell. Clamp.
+local HILITE_MAX_W = 780
+
+-- Auctionator natively anchors the Buy/More left column's dropdown at panel y=-49. The Sell tab's
+-- VISIBLE background box (the "wrap" frame built in reskinSellControls) sits 6px above
+-- Atr_SellControls' own XML top (-72), i.e. at panel y=-66 -- that's the box Buy/More needs to
+-- match, not sc's raw anchor. Shifting the column down by 17 lands the dropdown's top at -66,
+-- flush with that box's top edge; matching sc's own -72 (a 23px shift, the previous value here)
+-- overshot by 6px and made the whole column -- and its background box below -- sit visibly lower
+-- than the Sell tab's.
+local BUYMORE_SHIFT_Y = -17
+
+-- Auctionator anchors the Buy/More left column 5px further LEFT than the Sell tab's controls
+-- (its shopping-list buttons sit at panel x=-195, the Hlist at -193, vs Sell's sc at -190). To let
+-- the Buy/More backing box use the SAME left edge as the Sell box (LEFTCOL_L below == the Sell
+-- wrap's -192) without the buttons poking out its left side, nudge the whole column right by this
+-- much first. +3 lands the widest buttons flush inside the box's left edge (their 195px width is a
+-- hair wider than the ~194px box interior, so the far-right button edge sits ~1-2px into the dark
+-- gutter before the results list -- well clear of the list's own border at panel x=6).
+local BUYMORE_SHIFT_X = 3
+
+-- Extra downward nudge applied ON TOP of BUYMORE_SHIFT_Y to ONLY the shopping-list buttons
+-- (the `extra` field below), so the button cluster drops to the bottom of the left-column box and
+-- the last one (New Shopping List) lands just above LEFTCOL_B (-450) -- native New bottoms at
+-- panel y~-409 with the -17 shift, ~40px short of the box bottom, leaving dead space beneath it.
+-- The dropdown/list/Active-Items text (extra=0) stay top-aligned, and Atr_CheckActiveButton (the
+-- More tab's lone button) is deliberately left OUT of this nudge: it already sits at the box bottom
+-- on its own, so nudging it would push it past LEFTCOL_B.
+local BUYMORE_BTN_EXTRA_Y = -20
+
+-- Widgets making up the Buy/More left column. alignBuyMoreColumn() re-anchors each to
+-- Atr_Main_Panel; y is always (native y + BUYMORE_SHIFT_Y + extra).
+--
+-- Two x conventions:
+-- * No `w` (dropdown/list/Active-Items text): x is the widget's ORIGINAL XML anchor, and gets
+-- BUYMORE_SHIFT_X added so the column tucks inside the Sell-width box.
+-- * With `w` (the shopping-list buttons): the button is RESIZED to `w` and its x is a FINAL
+-- panel coord (BUYMORE_SHIFT_X is NOT added). Auctionator sizes these buttons 195px wide --
+-- 1px wider than the box interior (LEFTCOL_L..LEFTCOL_R == -192..+2 == 194) -- so they filled
+-- it edge-to-edge and looked stretched. Sizing them to 180 and left-anchoring at x=-185 (the
+-- Add/Remove pair split into two 86px halves) centres them with a ~7px margin each side.
+-- `center = true` (Atr_DropDownSL, Atr_ActiveItems_Text) overrides BOTH x and point: the widget
+-- is horizontally centered on the left column box (LEFTCOL_L..LEFTCOL_R) instead of using its
+-- native XML x, via alignBuyMoreColumn's own GetWidth()-based centering below.
+local BUYMORE_WIDGETS = {
+ { name = "Atr_DropDownSL", point = "TOPLEFT", x = -200, y = -49 },
+ { name = "Atr_Hlist_ScrollFrame", point = "TOPLEFT", x = -193, y = -75 },
+ { name = "Atr_Hlist", point = "TOPLEFT", x = -193, y = -75 },
+ { name = "Atr_ActiveItems_Text", point = "TOP", x = -360, y = -55, center = true },
+ { name = "Atr_AddToSListButton", point = "TOPLEFT", x = -185, y = -329, extra = BUYMORE_BTN_EXTRA_Y, w = 86 },
+ { name = "Atr_RemFromSListButton", point = "TOPLEFT", x = -91, y = -329, extra = BUYMORE_BTN_EXTRA_Y, w = 86 },
+ { name = "Atr_SrchSListButton", point = "TOPLEFT", x = -160, y = -349, extra = BUYMORE_BTN_EXTRA_Y, w = 130, center = true },
+ { name = "Atr_MngSListsButton", point = "TOPLEFT", x = -170, y = -369, extra = BUYMORE_BTN_EXTRA_Y, w = 145, center = true },
+ { name = "Atr_NewSListButton", point = "TOPLEFT", x = -160, y = -389, extra = BUYMORE_BTN_EXTRA_Y, w = 125, center = true },
+ { name = "Atr_CheckActiveButton", point = "TOPLEFT", x = -175, y = -409, w = 160, center = true },
+}
+
+-- Left-column dark backing (Buy & More tabs), in Atr_Main_Panel-relative coords.
+-- TOP (-66): matches the Sell tab's own background box top (the "wrap" in reskinSellControls,
+-- sc y=-72 + 6), so the two tabs' left panels start at the same height.
+-- LEFT (-192): EXACTLY the Sell tab's own background box left edge, so the panels are the same
+-- width. The Buy/More content natively sits 5px left of this, so it's shifted right first by
+-- BUYMORE_SHIFT_X to fit inside; without that shift a -192 box would clip the buttons' left
+-- edge (an earlier symptom), and a box widened to -197 to avoid clipping made the Buy/More
+-- panel visibly wider than the Sell tab's.
+-- RIGHT (2): flush with the seam before the right results list (its border starts at panel x=6).
+-- BOTTOM (-450): EXACTLY the Sell tab's own background box bottom (the "wrap" in
+-- reskinSellControls resolves to panel y=-450), so all three tabs share one identical panel
+-- footprint. A previous value (-478) ran the box well past the last shopping-list button and
+-- past where the Sell box ends, leaving a big empty gap below the buttons on the Buy/More tabs.
+-- (The Buy/More content is shorter than the Sell tab's, so a little empty space at the bottom
+-- is unavoidable with a shared footprint -- but the box no longer extends beyond Sell's.)
+local LEFTCOL_L, LEFTCOL_T = -192, -66
+local LEFTCOL_R, LEFTCOL_B = 2, -450
+
+local WHITE = "Interface\\Buttons\\WHITE8X8"
+
+local function isAddonLoaded(name)
+ if NE.IsAddOnLoaded then return NE.IsAddOnLoaded(name) end
+ if _G.IsAddOnLoaded then
+ local ok, loaded = pcall(_G.IsAddOnLoaded, name)
+ return ok and loaded and true or false
+ end
+ return false
+end
+
+function ATR.IsEmbedded()
+ return ATR._embedded and true or false
+end
+
+-- Grow the shell to Auctionator's native footprint while one of its tabs is active, and restore
+-- the shell's own size on the base tabs. Called from Window.lua's setMode. The shell's chrome
+-- (rock bg, streaks, nineslice, close button, money bar, base tabs) all anchor to the frame's
+-- edges, so it reflows cleanly on resize; the base panes are hidden while external, so their
+-- resize is invisible.
+function ATR.SetWindowForExternal(active)
+ local f = AH.frame
+ if not (f and ATR._embedded) then return end
+ if active then
+ if not ATR._sizedForAtr then
+ ATR._sizedForAtr = true
+ f:SetSize(ATR_W, ATR_H)
+ end
+ elseif ATR._sizedForAtr then
+ ATR._sizedForAtr = nil
+ f:SetSize(BASE_W, BASE_H)
+ end
+end
+
+----------------------------------------------------------------------
+-- Shared styling helpers (mirroring the conventions in Browse.lua/Sell.lua/Window.lua).
+----------------------------------------------------------------------
+
+-- Reverse of Window.lua's cloak, which may already have run over Atr_Main_Panel while it was
+-- still a child of AuctionFrame (AH.Show cloaks the whole legacy tree before this module gets to
+-- reparent on the same event): alpha back to 1, plus mouse re-enabled ONLY on the interactive
+-- widget types. Show/Hide states are intentionally untouched.
+--
+-- Why type-aware (not the blanket EnableMouse(true) that Window.lua's restoreFrameTree uses):
+-- Auctionator's containers -- Atr_Main_Panel itself, Atr_Hlist, Atr_HeadingsBar, Atr_SellControls
+-- -- are plain s with NO enableMouse in the XML, i.e. mouse-TRANSPARENT by design so clicks
+-- fall through to the row/heading buttons layered over them. Blanket-enabling mouse turned the
+-- full-area Atr_Main_Panel into a click-eater sitting over its own AuctionatorEntry/HEntry rows, so
+-- NO list row was clickable on ANY tab. The cloak only ever disabled mouse on widgets that HAD it
+-- (Auctionator's Buttons/EditBoxes/ScrollFrames -- verified: every drop target in Auctionator.xml
+-- is a Button), so re-enabling just those restores exactly the original interactivity and leaves
+-- the containers transparent.
+local function restoreTree(frame, depth)
+ if not frame or (depth or 0) > 10 then return end
+ if frame.SetAlpha then pcall(frame.SetAlpha, frame, 1) end
+ local ot = frame.GetObjectType and frame:GetObjectType()
+ if ot == "Button" or ot == "CheckButton" or ot == "EditBox" or ot == "Slider"
+ or ot == "ScrollFrame" then
+ if frame.EnableMouse then pcall(frame.EnableMouse, frame, true) end
+ end
+ if ot == "ScrollFrame" or ot == "Slider" then
+ if frame.EnableMouseWheel then pcall(frame.EnableMouseWheel, frame, true) end
+ end
+ if frame.GetChildren then
+ local kids = { frame:GetChildren() }
+ for i = 1, #kids do restoreTree(kids[i], (depth or 0) + 1) end
+ end
+end
+
+local function darkFill(host, r, g, b, a)
+ local t = host:CreateTexture(nil, "BACKGROUND")
+ t:SetTexture(WHITE)
+ t:SetVertexColor(r or 0.06, g or 0.06, b or 0.07, a or 0.95)
+ t:SetAllPoints(host)
+ return t
+end
+
+-- Textured panel background that fills `host` at the art's TRUE proportions: it shows a centered
+-- sub-region of the atlas whose aspect matches the host, instead of stretching the whole art to
+-- fit (which squashes the wide sell-panel art when the host is a different shape -- e.g.
+-- Auctionator's tall-narrow left column). Re-crops on size change; falls back to the flat dark
+-- fill if the atlas isn't registered.
+local function croppedInsetBg(host, atlasName)
+ local t = host:CreateTexture(nil, "BACKGROUND")
+ t:SetAllPoints(host)
+ local entry = NE.tex and NE.tex._atlasEntry and NE.tex._atlasEntry(atlasName)
+ if not (entry and NE.tex.SetAtlas and NE.tex.SetAtlas(t, atlasName, false)) then
+ t:SetTexture(WHITE)
+ t:SetVertexColor(0.06, 0.06, 0.07, 0.95)
+ return t
+ end
+ local uL, uR = entry.left or 0, entry.right or 1
+ local vT, vB = entry.top or 0, entry.bottom or 1
+ local artAspect = (entry.width or 1) / (entry.height or 1)
+ local function recrop()
+ local fw, fh = host:GetWidth(), host:GetHeight()
+ if not (fw and fh and fw > 0 and fh > 0) then return end
+ local fa = fw / fh
+ if fa < artAspect then
+ -- host narrower than the art: keep full height, crop width to a centered vertical slice.
+ local keep = fa / artAspect
+ local m = (uR - uL) * (1 - keep) / 2
+ t:SetTexCoord(uL + m, uR - m, vT, vB)
+ else
+ -- host wider than the art: keep full width, crop height to a centered horizontal band.
+ local keep = artAspect / fa
+ local m = (vB - vT) * (1 - keep) / 2
+ t:SetTexCoord(uL, uR, vT + m, vB - m)
+ end
+ end
+ recrop()
+ if C_Timer and C_Timer.After then C_Timer.After(0, recrop) end
+ host:HookScript("OnSizeChanged", recrop)
+ return t
+end
+
+local function attachInset(host, tlx, tly, brx, bry)
+ if NE.nineslice and NE.nineslice.AttachInset then
+ local ok, ns = pcall(NE.nineslice.AttachInset, host, tlx or 0, tly or 0, brx or 0, bry or 0)
+ if ok then return ns end
+ end
+end
+
+-- Hide a frame's own texture regions whose file path contains any of the given substrings
+-- (plain find). Used to kill parchment/dialog-header art baked into Auctionator's XML that has
+-- no global name to address directly.
+local function hideTexturesByFile(frame, ...)
+ if not (frame and frame.GetRegions) then return end
+ local pats = { ... }
+ local regions = { frame:GetRegions() }
+ for i = 1, #regions do
+ local r = regions[i]
+ if r and r.IsObjectType and r:IsObjectType("Texture") and r.GetTexture then
+ local file = r:GetTexture()
+ if type(file) == "string" then
+ for j = 1, #pats do
+ if string.find(file, pats[j], 1, true) then
+ r:SetTexture(nil)
+ r:Hide()
+ break
+ end
+ end
+ end
+ end
+ end
+end
+
+-- Browse.lua's search-box treatment for a stock InputBoxTemplate-style EditBox: hide the gold
+-- input art, dark tooltip backdrop instead. Handles both named pieces ($parentLeft/Middle/Right)
+-- and unnamed Common-Input-Border regions.
+--
+-- `flat` (used for MoneyInputFrameTemplate's Gold/Silver/Copper boxes) draws a plain dark fill and
+-- NO border edge. The bordered backdrop's tiled edge (12px per side) is wider than the ~25px
+-- Silver/Copper boxes can hold, so it overflowed the box bounds and bled over the denomination
+-- coin icons anchored just past each box's right edge -- making the coins look like they sat under
+-- the boxes. A borderless fill stays strictly within the box (the coins, drawn on a higher layer,
+-- read cleanly beside it) and keeps all three money boxes looking consistent with each other.
+local function reskinInput(eb, flat)
+ if not eb or eb._neAtrSkinned then return end
+ eb._neAtrSkinned = true
+ local n = eb.GetName and eb:GetName()
+ local found = false
+ if n then
+ for _, s in ipairs({ "Left", "Middle", "Right" }) do
+ local t = _G[n .. s]
+ if t and t.Hide then t:Hide(); found = true end
+ end
+ end
+ if not found then
+ hideTexturesByFile(eb, "Common-Input-Border")
+ end
+ if flat then
+ if eb.SetBackdrop then eb:SetBackdrop(nil) end
+ -- Sublevel -8 (BACKGROUND's lowest) so this always draws BEHIND Auctionator's own coin-icon
+ -- texture on the same box: Auctionator creates that icon at BACKGROUND/0 during its own
+ -- frame setup (before this reskin pass ever runs), and WoW draws same-layer/sublevel
+ -- textures in creation order -- our fill, being newer, would otherwise paint OVER the coin
+ -- (exactly what silver/copper showed: the coin half-hidden under the fill's right edge).
+ local fill = eb:CreateTexture(nil, "BACKGROUND", nil, -8)
+ fill:SetTexture(WHITE)
+ fill:SetVertexColor(0, 0, 0, 0.5)
+ fill:SetAllPoints(eb)
+ return
+ end
+ if eb.SetBackdrop then
+ eb:SetBackdrop({
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = true, tileSize = 16, edgeSize = 12,
+ insets = { left = 3, right = 3, top = 3, bottom = 3 },
+ })
+ eb:SetBackdropColor(0, 0, 0, 0.6)
+ eb:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
+ end
+end
+
+-- NE list-row treatment (zebra stripe + additive gold hover/pressed) for Auctionator's row
+-- buttons. Replacing the highlight texture keeps LockHighlight-based selection working -- the
+-- locked state just shows our gold wash instead of the stock blue.
+local function reskinRow(btn, i)
+ if not btn or btn._neAtrSkinned then return end
+ btn._neAtrSkinned = true
+ local bg = btn:CreateTexture(nil, "BACKGROUND")
+ bg:SetTexture(WHITE)
+ bg:SetAllPoints(btn)
+ bg:SetVertexColor(0.07, 0.07, 0.08, (i % 2 == 0) and 0.30 or 0.20)
+ btn:SetHighlightTexture(WHITE)
+ local hl = btn:GetHighlightTexture()
+ if hl then hl:SetVertexColor(1, 0.82, 0, 0.12); hl:SetBlendMode("ADD") end
+ btn:SetPushedTexture(WHITE)
+ local pt = btn:GetPushedTexture()
+ if pt then pt:SetVertexColor(1, 0.82, 0, 0.20); pt:SetBlendMode("ADD") end
+end
+
+-- Minimal scrollbar for a FauxScrollFrame, with the same strata fix every list in this window
+-- needs (see Browse.lua): the shell is DIALOG, BuildCustom's bar defaults to HIGH -- behind the
+-- rows -- and the arrows were leveled against the bar's build-time strata.
+local function buildBar(scroll, opts)
+ if not (scroll and NE.scrollbar and NE.scrollbar.BuildCustom) then return end
+ local ok, bar = pcall(NE.scrollbar.BuildCustom, scroll, opts)
+ if ok and bar then
+ -- Keep Auctionator's FauxScroll container mouse-transparent so entry buttons behind/around it
+ -- remain the primary click target even when overflow enables scrolling.
+ if scroll.EnableMouse then pcall(scroll.EnableMouse, scroll, false) end
+ if scroll.EnableMouseWheel then pcall(scroll.EnableMouseWheel, scroll, true) end
+ if not scroll._neAtrMouseHooked then
+ scroll._neAtrMouseHooked = true
+ scroll:HookScript("OnShow", function(s)
+ if s.EnableMouse then pcall(s.EnableMouse, s, false) end
+ if s.EnableMouseWheel then pcall(s.EnableMouseWheel, s, true) end
+ end)
+ end
+
+ -- FauxScrollFrameTemplate still owns an internal slider (ScrollBar) even when we draw a
+ -- custom bar. If that stock slider pops back shown while overflow exists, it can sit as an
+ -- invisible mouse layer over row buttons. Keep it non-interactive and hidden at all times.
+ local sName = scroll.GetName and scroll:GetName()
+ local stock = sName and _G[sName .. "ScrollBar"]
+ if stock then
+ if stock.EnableMouse then pcall(stock.EnableMouse, stock, false) end
+ stock:SetAlpha(0)
+ if not stock._neAtrHideHooked then
+ stock._neAtrHideHooked = true
+ stock:HookScript("OnShow", function(s) s:Hide() end)
+ end
+ stock:Hide()
+ end
+
+ -- Keep the row buttons explicitly interactive; this is idempotent and avoids regressions if
+ -- an external cloak/restore pass touched click-enable state earlier.
+ if sName and string.find(sName, "AuctionatorScrollFrame", 1, true) then
+ for i = 1, 15 do
+ local row = _G["AuctionatorEntry" .. i]
+ if row and row.EnableMouse then row:EnableMouse(true) end
+ end
+ end
+
+ bar:SetFrameStrata("DIALOG")
+ bar:SetFrameLevel((scroll:GetFrameLevel() or 1) + 10)
+ if bar._upBtn then bar._upBtn:SetFrameStrata("DIALOG"); bar._upBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ if bar._downBtn then bar._downBtn:SetFrameStrata("DIALOG"); bar._downBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+
+ -- BuildCustom parents the bar to the scroll frame's PARENT (Atr_Main_Panel), not the scroll
+ -- frame, so it does NOT inherit Hide() when Auctionator hides a list on tabs that don't use
+ -- it -- the left Hlist is hidden on the Sell tab, but its bar was left floating over the sell
+ -- controls (cutting off the copper money box). Its self-sync only runs on the scroll frame's
+ -- OnUpdate, which doesn't fire while hidden, so it never corrected itself. Tie the bar's
+ -- visibility to the scroll frame's own shown state.
+ if not scroll._neAtrBarVis then
+ scroll._neAtrBarVis = true
+ local function setBarShown(on)
+ if on then bar:Show() else bar:Hide() end
+ if bar._upBtn then if on then bar._upBtn:Show() else bar._upBtn:Hide() end end
+ if bar._downBtn then if on then bar._downBtn:Show() else bar._downBtn:Hide() end end
+ end
+ scroll:HookScript("OnShow", function() setBarShown(true) end)
+ scroll:HookScript("OnHide", function() setBarShown(false) end)
+ if not scroll:IsShown() then setBarShown(false) end
+ end
+ end
+end
+
+----------------------------------------------------------------------
+-- Embed steps
+----------------------------------------------------------------------
+
+local function embedPanel()
+ local p = _G.Atr_Main_Panel
+ local pane = AH.frame.ExternalPane
+ p:SetParent(pane)
+ p:ClearAllPoints()
+ p:SetPoint("TOPLEFT", pane, "TOPLEFT", PANEL_X, PANEL_Y)
+ p:SetFrameLevel((pane:GetFrameLevel() or 1) + 3)
+ restoreTree(p, 0)
+end
+
+-- Re-anchor the pieces whose XML positions poke past the shell's 800px width (they were tuned
+-- for the 832-wide legacy AuctionFrame). All of these are positioned ONLY in XML -- Auctionator's
+-- Lua never re-anchors them -- so a one-time re-anchor here is stable.
+local function anchorExtras()
+ local pane = AH.frame.ExternalPane
+
+ -- "Options" button (Atr_FullScanButton chains off its LEFT in XML and follows automatically).
+ -- Its XML slot (165px right of the Search button) lands at shell x ~809 -- off the frame.
+ local opt = _G.Auctionator1Button
+ if opt then
+ opt:ClearAllPoints()
+ opt:SetPoint("TOPRIGHT", pane, "TOPRIGHT", -10, PANEL_Y - 46)
+ end
+
+ -- The CLOSE button duplicates the shell's own close X, and its OnClick
+ -- (HideUIPanel(parent:GetParent())) would now target the shell's ContentRoot. Retire it.
+ local close = _G.AuctionatorCloseButton
+ if close then
+ close:Hide()
+ if not close._neAtrHideHooked then
+ close._neAtrHideHooked = true
+ close:HookScript("OnShow", function(s) s:Hide() end)
+ end
+ end
+
+ -- "Cancel Auctions" becomes the right anchor of the bottom cluster (Atr_Buy1_Button chains off
+ -- its LEFT in XML). XML put the cluster 66px past the panel's right edge.
+ local cancel = _G.Atr_CancelSelectionButton
+ if cancel then
+ cancel:ClearAllPoints()
+ cancel:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", -10, 33)
+ end
+
+ -- Current/History/Other tabs hang off Atr_HeadingsBar's TOPRIGHT, which (605 wide at panel x+6
+ -- inside a 548-wide panel) pokes ~10px past the shell's right edge. Pull them back inside.
+ local tabs = _G.Atr_ListTabs
+ if tabs and _G.Atr_HeadingsBar then
+ tabs:ClearAllPoints()
+ tabs:SetPoint("BOTTOMRIGHT", _G.Atr_HeadingsBar, "TOPRIGHT", -25, -22)
+ end
+
+ -- Drop-target highlight: XML anchors it 66px past the panel's right edge and code SetSize()s
+ -- it 805 wide (sell-tab whole-pane drop) or 610 wide (item slotted). Re-anchor inside the
+ -- shell and clamp any future SetSize to the shell's width.
+ local hilite = _G.Atr_Hilite1
+ if hilite then
+ hilite:ClearAllPoints()
+ hilite:SetPoint("TOPRIGHT", _G.Atr_Main_Panel, "TOPRIGHT", 34, -70)
+ end
+ for _, name in ipairs({ "Atr_Hilite1", "Atr_Hilite1_btn" }) do
+ local h = _G[name]
+ if h and not h._neAtrClamp then
+ h._neAtrClamp = true
+ hooksecurefunc(h, "SetSize", function(s, w, hh)
+ if s._neAtrClamping then return end
+ if w and w > HILITE_MAX_W then
+ s._neAtrClamping = true
+ s:SetSize(HILITE_MAX_W, hh)
+ s._neAtrClamping = nil
+ end
+ end)
+ local w, hh = h:GetSize()
+ if w and w > HILITE_MAX_W then h:SetSize(HILITE_MAX_W, hh) end
+ end
+ end
+end
+
+-- Push the Buy/More left column (dropdown/list/shopping-list buttons) down by BUYMORE_SHIFT_Y so
+-- its top lines up with the Sell tab's sell-controls frame (see the constant's comment above).
+-- Each widget keeps its ORIGINAL x and its position relative to the others -- only the shared
+-- panel-relative y changes, so this can't disturb anything about their own internal layout
+-- (list row spacing, button widths, dropdown menu content), just where the group sits vertically.
+local LEFTCOL_CENTER_X = (LEFTCOL_L + LEFTCOL_R) / 2
+
+local function alignBuyMoreColumn()
+ local p = _G.Atr_Main_Panel
+ if not p then return end
+ for i = 1, #BUYMORE_WIDGETS do
+ local w = BUYMORE_WIDGETS[i]
+ local f = _G[w.name]
+ if f then
+ -- Buttons (w.w) carry their own explicit width; everything else keeps its native width.
+ -- Applied BEFORE the center/x branches below so `center` still respects a manual `w` if
+ -- both are ever set on the same entry, instead of silently ignoring it.
+ if w.w and f.SetWidth then f:SetWidth(w.w) end
+
+ local point = w.point
+ local x
+ if w.center then
+ -- Horizontally center the widget's own width on the left column box, regardless of its
+ -- native XML anchor -- always via TOPLEFT so the box-relative math (LEFTCOL_L/R) applies.
+ point = "TOPLEFT"
+ local fw = (f.GetWidth and f:GetWidth()) or 0
+ x = LEFTCOL_CENTER_X - (fw / 2)
+ elseif w.w then
+ x = w.x
+ else
+ x = w.x + BUYMORE_SHIFT_X
+ end
+ f:ClearAllPoints()
+ f:SetPoint(point, p, point, x, w.y + BUYMORE_SHIFT_Y + (w.extra or 0))
+ end
+ end
+end
+
+-- Auctionator's dialogs + modal mask are parented to UIParent at fixed offsets tuned for the
+-- legacy frame's screen position; the shell is centered. Re-anchor them over the shell.
+local function anchorDialogs()
+ local f = AH.frame
+
+ local mask = _G.Atr_Mask
+ if mask then
+ local function layoutMask()
+ mask:ClearAllPoints()
+ mask:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -20)
+ mask:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 4)
+ -- FULLSCREEN sits above the shell's DIALOG but below the dialogs' FULLSCREEN_DIALOG.
+ mask:SetFrameStrata("FULLSCREEN")
+ if mask.SetBackdropColor then mask:SetBackdropColor(0, 0, 0, 0.6) end
+ end
+ layoutMask()
+ -- Same issue as the dialogs below: Auctionator re-anchors/resizes Atr_Mask (tuned for the
+ -- legacy standalone AuctionFrame) itself each time it shows one, which only covered part of
+ -- this bigger shell -- leaving the uncovered strip look like a stray shadow/seam next to
+ -- whatever dialog was open. Re-apply on every OnShow so it always covers the full shell.
+ if not mask._neAtrLayoutHooked then
+ mask._neAtrLayoutHooked = true
+ mask:HookScript("OnShow", layoutMask)
+ end
+ end
+
+ local dialogNames = {
+ "Atr_Buy_Confirm_Frame", "Atr_CheckActives_Frame", "Atr_CancelAuction_Confirm_Frame",
+ "Atr_FullScanFrame", "Atr_Adv_Search_Dialog", "Atr_Error_Frame", "Atr_Confirm_Frame",
+ }
+ local function centerDialog(d)
+ d:ClearAllPoints()
+ d:SetPoint("CENTER", f, "CENTER", 0, 0)
+ d:SetFrameStrata("FULLSCREEN_DIALOG")
+ end
+ for i = 1, #dialogNames do
+ local d = _G[dialogNames[i]]
+ if d then
+ centerDialog(d)
+ -- Auctionator re-anchors some of these to UIParent CENTER (its own default, tuned for a
+ -- standalone frame) every time it opens them, undoing the one-time anchor above -- Full
+ -- Scan in particular reset to the screen's center instead of the shell's on each open.
+ -- Re-apply on every OnShow so it always lands on the shell no matter who last moved it.
+ if not d._neAtrCenterHooked then
+ d._neAtrCenterHooked = true
+ d:HookScript("OnShow", function(self) centerDialog(self) end)
+ end
+ end
+ end
+end
+
+----------------------------------------------------------------------
+-- Reskin steps
+----------------------------------------------------------------------
+
+local function reskinLists()
+ local p = _G.Atr_Main_Panel
+
+ -- Results list: one gold-trim inset wrapping the headings bar + the 15 row buttons, dark fill
+ -- behind -- replacing the CharacterCreate parchment label bar.
+ local headings = _G.Atr_HeadingsBar
+ local lastRow = _G.AuctionatorEntry15
+ if headings and lastRow and not ATR._listInset then
+ local inset = CreateFrame("Frame", nil, p)
+ inset:SetPoint("TOPLEFT", headings, "TOPLEFT", 0, -14)
+ inset:SetPoint("BOTTOMRIGHT", lastRow, "BOTTOMRIGHT", 6, -4)
+ inset:EnableMouse(false)
+ -- Created AFTER the row buttons (same default level, later creation order = drawn on top),
+ -- so without this the dark fill would paint OVER the rows. Drop to the panel's own level;
+ -- AttachInset's border pieces live on a child a level up, which keeps the gold trim visible.
+ inset:SetFrameLevel(p:GetFrameLevel() or 1)
+ -- Right-hand list uses the shell Sell tab's own right-panel background art (shared across
+ -- Auctionator's Buy/Sell/More tabs, since it's one frame), cropped to the list's proportions
+ -- so the art isn't stretched.
+ croppedInsetBg(inset, "auctionhouse-background-sell-right")
+ attachInset(inset)
+ ATR._listInset = inset
+
+ -- Headings bar: kill the parchment sheet, draw the shell's header-strip tone behind the
+ -- column-heading buttons instead. The strip spans the vertical band the column text/buttons
+ -- occupy (the parchment was 64 tall with the labels vertically centered).
+ local mid = _G.Atr_HeadingsBarMiddle
+ if mid then mid:SetTexture(nil); mid:Hide() end
+ local strip = headings:CreateTexture(nil, "BACKGROUND")
+ strip:SetTexture(WHITE)
+ strip:SetVertexColor(0.06, 0.06, 0.07, 0.95)
+ strip:SetPoint("TOPLEFT", headings, "TOPLEFT", 0, -14)
+ strip:SetPoint("RIGHT", inset, "RIGHT", -1, 0)
+ strip:SetHeight(32)
+ end
+ for i = 1, 15 do reskinRow(_G["AuctionatorEntry" .. i], i) end
+ if _G.AuctionatorScrollFrame then
+ -- x=-12 pushes the bar (and its wider arrows) OUT toward the scroll frame's right edge, closer
+ -- to the results panel's right gold border, over the empty margin past the last column's text.
+ -- x=8 sat noticeably too far left/inward of the border (the 584px rows overhang the 588px
+ -- scroll frame slightly, and the bordered inset sits ~6px further out again, but that left a
+ -- visible gap between the bar and the border).
+ buildBar(_G.AuctionatorScrollFrame, { x = -12, alwaysShow = false })
+ end
+
+ -- Left column (recent searches / active items / shopping lists). Strip the list's own
+ -- parchment backdrop + tooltip-border divider; the unified column panel below provides the
+ -- background instead.
+ local hl = _G.Atr_Hlist
+ if hl and not hl._neAtrSkinned then
+ hl._neAtrSkinned = true
+ if hl.SetBackdrop then hl:SetBackdrop(nil) end
+ hideTexturesByFile(hl, "Tooltip-Border")
+ end
+
+ -- Dark textured backing for the whole left column -- dropdown + list + shopping-list buttons --
+ -- with the same recessed inset border as the right-hand results list.
+ -- Toggled with the list's shown state so it doesn't paint over the Sell tab's sell-controls box
+ -- (the left column is empty on the Sell tab).
+ if hl and p and not ATR._leftCol then
+ local col = CreateFrame("Frame", nil, p)
+ col:SetFrameLevel(p:GetFrameLevel() or 1)
+ col:EnableMouse(false)
+ col:SetPoint("TOPLEFT", p, "TOPLEFT", LEFTCOL_L, LEFTCOL_T)
+ col:SetPoint("BOTTOMRIGHT", p, "TOPLEFT", LEFTCOL_R, LEFTCOL_B)
+ croppedInsetBg(col, "auctionhouse-background-sell-left")
+ attachInset(col)
+ ATR._leftCol = col
+ local function setColShown(on) if on then col:Show() else col:Hide() end end
+ hl:HookScript("OnShow", function() setColShown(true) end)
+ hl:HookScript("OnHide", function() setColShown(false) end)
+ if not hl:IsShown() then col:Hide() end
+ end
+ if _G.Atr_Hlist_ScrollFrame then
+ -- x=-9 centres the bar (+ its 17px-wide arrows) in the ~18px gutter between the Hlist rows'
+ -- right edge (~panel x-16) and the left-column box's right edge (+2). x=-15 pushed the arrows
+ -- ~5px past the box; x=-9 keeps the whole scrollbar inside it.
+ buildBar(_G.Atr_Hlist_ScrollFrame, { x = -9 })
+ -- Nudge the built bar DOWN 6px. BuildCustom anchors it flush to the scroll frame's top/bottom
+ -- (inset 25px for arrow clearance); dropping it a touch keeps the up-arrow clear of the list's
+ -- top row and sits the whole bar more comfortably inside the box.
+ local sf = _G.Atr_Hlist_ScrollFrame
+ local bar = sf._neCustomBar
+ if bar and not bar._neHlistDrop then
+ bar._neHlistDrop = true
+ bar:ClearAllPoints()
+ bar:SetPoint("TOPLEFT", sf, "TOPRIGHT", 9, -25 - 6)
+ bar:SetPoint("BOTTOMLEFT", sf, "BOTTOMRIGHT", 9, 25 - 6)
+ end
+ end
+
+ -- Left-list rows are created lazily inside Atr_DisplayHlist -- restyle current ones now and
+ -- any new ones after every display pass.
+ local function reskinHEntries()
+ local i = 1
+ while _G["AuctionatorHEntry" .. i] do
+ reskinRow(_G["AuctionatorHEntry" .. i], i)
+ i = i + 1
+ end
+ end
+ reskinHEntries()
+ if type(_G.Atr_DisplayHlist) == "function" and not ATR._hlistHook then
+ ATR._hlistHook = true
+ hooksecurefunc("Atr_DisplayHlist", reskinHEntries)
+ end
+end
+
+local function reskinSellControls()
+ local p = _G.Atr_Main_Panel
+ local sc = _G.Atr_SellControls
+ if sc and p and not sc._neAtrSkinned then
+ sc._neAtrSkinned = true
+ if sc.SetBackdrop then sc:SetBackdrop(nil) end
+ -- Fill + gold inset on a LOW frame parented to the panel (not sc), so it draws BEHIND sc's
+ -- own controls (a child of sc created later would paint over the money boxes). Extended
+ -- past sc's bottom by 40px so the Deposit line -- which Auctionator anchors just below sc's
+ -- own edge -- sits inside the border instead of hanging beneath it.
+ local wrap = CreateFrame("Frame", nil, p)
+ wrap:SetPoint("TOPLEFT", sc, "TOPLEFT", -2, 6)
+ wrap:SetPoint("BOTTOMRIGHT", sc, "BOTTOMRIGHT", 2, -40)
+ wrap:SetFrameLevel(p:GetFrameLevel() or 1)
+ wrap:EnableMouse(false)
+ -- Sell-left background art, cropped to the sell-controls column's tall-narrow proportions so
+ -- the wide art shows an undistorted slice instead of being squashed, plus the same recessed
+ -- inset border as the Buy/More left column so all three tabs' left panels match.
+ croppedInsetBg(wrap, "auctionhouse-background-sell-left")
+ attachInset(wrap)
+ ATR._sellWrap = wrap
+ -- Item slot: the shell Sell pane's empty-slot plate behind the 37px drop button, framed by
+ -- the same "itemheaderframe" plate the normal (non-Auctionator) Sell tab wraps around its
+ -- icon+name row (Sell.lua's `disp`/`hf`, native art 342x72 around a 54px icon inset 12,9).
+ -- Scaled down to THIS icon's actual size (queried at runtime, not hardcoded) so the plate's
+ -- proportions stay correct regardless of exactly how big Auctionator's own icon button is,
+ -- and clamped so the plate can never poke past `wrap`'s own right edge. Drawn on `wrap`
+ -- (parented to the panel, not sc) so it sits BEHIND sc's own icon/text, same as the rest of
+ -- this wrap's background art. Only shown while an item is actually slotted for sale -- like
+ -- the native Sell tab's item row, an empty slot shows no header plate.
+ local slot = _G.Atr_SellControls_Tex
+ if slot and NE.tex and NE.tex.SetAtlas then
+ local bg = slot:CreateTexture(nil, "BACKGROUND")
+ bg:SetAllPoints(slot)
+ NE.tex.SetAtlas(bg, "auctionhouse-itemicon-empty", false)
+
+ local hf = wrap:CreateTexture(nil, "ARTWORK")
+ if NE.tex.SetAtlas(hf, "auctionhouse-itemheaderframe", false) then
+ -- Extra native-art px added to the plate's height (top edge stays put, so this all lands
+ -- at the bottom) -- at the plain 72-tall scale the icon's bottom edge touched/overlapped
+ -- the plate's own bottom border line.
+ local HEIGHT_PAD = 10
+ local function layoutHf()
+ local iconW = slot:GetWidth()
+ if not (iconW and iconW > 0) then iconW = 37 end
+ local scale = iconW / 54
+ local maxW = wrap:GetWidth()
+ if maxW and maxW > 0 then
+ local capScale = (maxW - 4) / 342
+ if capScale > 0 and scale > capScale then scale = capScale end
+ end
+ hf:SetSize(342 * scale, (72 + HEIGHT_PAD) * scale)
+ hf:ClearAllPoints()
+ hf:SetPoint("TOPLEFT", slot, "TOPLEFT", -18 * scale, 9 * scale)
+ end
+ layoutHf()
+ if C_Timer and C_Timer.After then C_Timer.After(0, layoutHf) end
+ wrap:HookScript("OnSizeChanged", layoutHf)
+
+ -- Gate on an item being slotted AND the Sell tab itself actually being the active tab.
+ -- GetAuctionSellItemInfo() stays populated even while Buy/More are active (the slotted
+ -- item persists across tabs), and sc:IsShown() alone isn't reliable -- Auctionator keeps
+ -- Atr_SellControls in the shown state even off the Sell tab. Atr_Hlist (the Buy/More
+ -- results/search list) IS reliably toggled per-tab elsewhere in this file, so use its
+ -- shown state as the tab signal instead: Sell is active exactly when Hlist is hidden.
+ local hl = _G.Atr_Hlist
+ local function updateHfShown()
+ local sellActive = sc:IsShown() and not (hl and hl:IsShown())
+ if sellActive and GetAuctionSellItemInfo and GetAuctionSellItemInfo() then
+ hf:Show()
+ else
+ hf:Hide()
+ end
+ end
+ hf:Hide()
+ sc:HookScript("OnShow", updateHfShown)
+ sc:HookScript("OnHide", updateHfShown)
+ if hl then
+ hl:HookScript("OnShow", updateHfShown)
+ hl:HookScript("OnHide", updateHfShown)
+ end
+ if not ATR._sellHfWatcher then
+ local watcher = CreateFrame("Frame")
+ watcher:RegisterEvent("NEW_AUCTION_UPDATE")
+ watcher:RegisterEvent("AUCTION_HOUSE_SHOW")
+ watcher:SetScript("OnEvent", updateHfShown)
+ ATR._sellHfWatcher = watcher
+ end
+ updateHfShown()
+ end
+ end
+ end
+ -- MoneyInputFrame gold/silver/copper boxes get the borderless flat fill (see reskinInput's
+ -- `flat`) so the backdrop border can't overflow the narrow boxes onto their coin icons.
+ for _, base in ipairs({ "Atr_StackPrice", "Atr_ItemPrice", "Atr_StartingPrice" }) do
+ for _, den in ipairs({ "Gold", "Silver", "Copper" }) do
+ reskinInput(_G[base .. den], true)
+ end
+ end
+ reskinInput(_G.Atr_Batch_NumAuctions)
+ reskinInput(_G.Atr_Batch_Stacksize)
+end
+
+local function reskinSearchRow()
+ local box = _G.Atr_Search_Box
+ if box and not box._neAtrSearch then
+ box._neAtrSearch = true
+ reskinInput(box)
+ box:SetTextInsets(20, 8, 0, 0)
+ local icon = box:CreateTexture(nil, "OVERLAY")
+ icon:SetSize(14, 14)
+ icon:SetPoint("LEFT", box, "LEFT", 5, 0)
+ icon:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
+ end
+ -- EditBoxes inside the dialogs get the same input treatment.
+ reskinInput(_G.Atr_Buy_Confirm_Numstacks)
+ for _, n in ipairs({ "Atr_AS_Searchtext", "Atr_AS_Minlevel", "Atr_AS_Maxlevel",
+ "Atr_AS_MinItemlevel", "Atr_AS_MaxItemlevel" }) do
+ reskinInput(_G[n])
+ end
+end
+
+-- Current/History/Other are stock TabButtonTemplate tabs (same 6-piece art as the classic
+-- bottom tabs) sitting on TOP of the list -- reskin then flip upward.
+local function reskinListTabs()
+ if not (NE.tabs and NE.tabs.ReskinClassicTab) then return end
+ for i = 1, 3 do
+ local name = "Atr_ListTabsTab" .. i
+ if _G[name] then
+ NE.tabs.ReskinClassicTab(name)
+ if NE.tabs.MakeTopTab then NE.tabs.MakeTopTab(name) end
+ end
+ end
+end
+
+local function reskinDialogs()
+ local names = {
+ "Atr_Buy_Confirm_Frame", "Atr_CheckActives_Frame", "Atr_CancelAuction_Confirm_Frame",
+ "Atr_FullScanFrame", "Atr_Adv_Search_Dialog", "Atr_Error_Frame", "Atr_Confirm_Frame",
+ }
+ for i = 1, #names do
+ local d = _G[names[i]]
+ if d and not d._neAtrSkinned then
+ d._neAtrSkinned = true
+ if d.SetBackdrop then d:SetBackdrop(nil) end
+ -- the "Full Scan" / "Advanced Search" parchment banner texture
+ hideTexturesByFile(d, "UI-DialogBox-Header")
+ darkFill(d, 0.06, 0.06, 0.07, 0.97)
+ attachInset(d)
+ -- Red 3-slice on the dialogs' own buttons (Full Scan's Start Scanning/DONE/Reload,
+ -- Advanced Search's Search/Cancel, the buy/cancel confirmations). Window.lua Watches the
+ -- SHELL frame, which covers everything under ExternalPane -- but these dialogs are parented
+ -- to UIParent, not to the shell, so that sweep never reaches them and they kept the stock
+ -- UIPanelButtonTemplate art. Watch (rather than a one-shot sweep) re-runs on every OnShow,
+ -- which also picks up anything Auctionator builds into a dialog lazily.
+ if NE.buttonskin and NE.buttonskin.Watch then pcall(NE.buttonskin.Watch, d) end
+ end
+ end
+ -- Nested results box inside the Full Scan dialog: border only (no second fill on a fill).
+ local fsr = _G.Atr_FullScanResults
+ if fsr and not fsr._neAtrSkinned then
+ fsr._neAtrSkinned = true
+ if fsr.SetBackdrop then fsr:SetBackdrop(nil) end
+ attachInset(fsr)
+ end
+end
+
+----------------------------------------------------------------------
+-- Behaviour hooks
+----------------------------------------------------------------------
+
+-- Auctionator writes "Auctionator+ - Buy/Sell/More..." into its own in-panel title fontstring on
+-- every tab switch. Hide the fontstring and mirror the text into the shell's title bar instead
+-- (Window.lua's setTitleForMode also reads ATR._lastTitle for external modes).
+local function hookTitle()
+ local t = _G.AuctionatorTitle
+ if not t or ATR._titleHooked then return end
+ ATR._titleHooked = true
+ t:SetAlpha(0)
+ ATR._lastTitle = t:GetText()
+ hooksecurefunc(t, "SetText", function(_, txt)
+ ATR._lastTitle = txt
+ local f = AH.frame
+ if f and f:IsShown() and type(f._mode) == "string"
+ and string.find(f._mode, "external:", 1, true) == 1 then
+ if NE.panelchrome and NE.panelchrome.SetTitle then
+ NE.panelchrome.SetTitle(f, txt or "")
+ end
+ end
+ end)
+end
+
+-- Keep the shell's tab state in sync when Auctionator switches its own pane WITHOUT going
+-- through our tabs -- e.g. alt-clicking a bag item or dropping an item on the sell pane both
+-- call Atr_SelectPane(SELL_TAB) internally. ATR._clicking guards the reverse direction (our
+-- TabBridge onSelect clicking the native tab would otherwise bounce back through here).
+local function hookTabSync()
+ if ATR._tabHooked or type(_G.Atr_AuctionFrameTab_OnClick) ~= "function" then return end
+ ATR._tabHooked = true
+ hooksecurefunc("Atr_AuctionFrameTab_OnClick", function(self, index)
+ if not ATR._embedded or ATR._clicking then return end
+ if type(index) ~= "number" then
+ index = self and self.GetID and self:GetID()
+ end
+ if not index or index < 4 then return end
+ local f = AH.frame
+ if not (f and f:IsShown() and f.ExternalTabDefs) then return end
+ local key = "AuctionatorNative" .. index
+ local mode = "external:" .. key
+ if f._mode == mode or not f.ExternalTabDefs[key] then return end
+ -- Auctionator already switched its pane; mirror it in the shell without a second click
+ -- (TabBridge's onSelect skips the click while ATR._syncing is set).
+ ATR._syncing = true
+ if AH.SetMode then pcall(AH.SetMode, mode) end
+ ATR._syncing = nil
+ end)
+end
+
+local function hookRowClickSafety()
+ if ATR._rowClickHooked then return end
+ ATR._rowClickHooked = true
+
+ local function apply()
+ local sf = _G.AuctionatorScrollFrame
+ if sf then
+ if sf.EnableMouse then pcall(sf.EnableMouse, sf, false) end
+ if sf.EnableMouseWheel then pcall(sf.EnableMouseWheel, sf, true) end
+ local rowLevel = (sf:GetFrameLevel() or 1) + 2
+ for i = 1, 15 do
+ local row = _G["AuctionatorEntry" .. i]
+ if row then
+ if row.SetFrameLevel and (row:GetFrameLevel() or 1) < rowLevel then
+ row:SetFrameLevel(rowLevel)
+ end
+ if row.EnableMouse then row:EnableMouse(true) end
+ if row.RegisterForClicks then row:RegisterForClicks("LeftButtonUp") end
+ end
+ end
+ end
+ end
+
+ apply()
+ if type(_G.Atr_RedisplayAuctions) == "function" then
+ hooksecurefunc("Atr_RedisplayAuctions", apply)
+ end
+ if _G.AuctionatorScrollFrame and not _G.AuctionatorScrollFrame._neAtrClickSafetyHook then
+ _G.AuctionatorScrollFrame._neAtrClickSafetyHook = true
+ _G.AuctionatorScrollFrame:HookScript("OnShow", apply)
+ end
+end
+
+----------------------------------------------------------------------
+-- Driver
+----------------------------------------------------------------------
+
+local function tryEmbed()
+ if ATR._embedded then return end
+ if not isAddonLoaded("Auctionator") then return end
+ if not (_G.Atr_Main_Panel and AH.frame and AH.frame.ExternalPane) then return end
+
+ local steps = {
+ embedPanel, anchorExtras, alignBuyMoreColumn, anchorDialogs,
+ reskinLists, reskinSellControls, reskinSearchRow, reskinListTabs, reskinDialogs,
+ hookTitle, hookTabSync, hookRowClickSafety,
+ }
+ for i = 1, #steps do pcall(steps[i]) end
+
+ ATR._embedded = true
+ -- Window.lua reads this to keep the legacy AuctionFrame cloaked on external tabs.
+ AH._atrEmbedded = true
+
+ if AH.RefreshExternalTabs then pcall(AH.RefreshExternalTabs) end
+end
+ATR.TryEmbed = tryEmbed
+
+local ev = CreateFrame("Frame")
+ev:RegisterEvent("PLAYER_LOGIN")
+ev:RegisterEvent("ADDON_LOADED")
+ev:RegisterEvent("AUCTION_HOUSE_SHOW")
+ev:SetScript("OnEvent", function(_, event, arg1)
+ if event == "ADDON_LOADED" and arg1 ~= "Auctionator" and arg1 ~= "Blizzard_AuctionUI" then
+ return
+ end
+ tryEmbed()
+ -- Atr_Init (which creates Atr_Main_Panel) runs off Blizzard_AuctionUI's ADDON_LOADED, which
+ -- lands in the same frame as the first AUCTION_HOUSE_SHOW -- retry shortly after, same as
+ -- Window.lua's native-tab rescans.
+ if event == "AUCTION_HOUSE_SHOW" and C_Timer and C_Timer.After then
+ C_Timer.After(0, tryEmbed)
+ C_Timer.After(0.2, tryEmbed)
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Auctions.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Auctions.lua
new file mode 100644
index 0000000..915ae50
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Auctions.lua
@@ -0,0 +1,682 @@
+-- DragonUI_NewEra/modules/auctionhouse/Auctions.lua
+-- Auctions tab visual shell: Auctions/Bids sub-tabs (themed as the same "pill" tab-plate the Sell
+-- pane's "Create Auction" label uses -- see buildPillTab below), a summary panel grouping your
+-- listings by item (retail's per-item summary; Era's owner/bidder API has no server-side
+-- equivalent, so it's grouped client-side), and the owner/bidder listing with a Cancel Auction
+-- button.
+--
+-- Legacy API: GetNumAuctionItems("owner"/"bidder"), GetAuctionItemInfo(list,i),
+-- GetAuctionItemTimeLeft(list,i), CanCancelAuction(i), CancelAuction(i);
+-- events AUCTION_OWNED_LIST_UPDATE, AUCTION_BIDDER_LIST_UPDATE.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+NE.ah = NE.ah or {}
+local AH = NE.ah
+
+-- Keep alwaysShow custom Faux bars visually in sync with real overflow state.
+local function syncAlwaysShowFauxBar(scroll, totalRows, visibleRows)
+ if not scroll then return end
+ local bar = scroll._neCustomBar
+ if not (bar and bar._alwaysShow) then return end
+
+ local canScroll = (totalRows or 0) > (visibleRows or 0)
+ bar:Show()
+ if bar._upBtn then bar._upBtn:Show() end
+ if bar._downBtn then bar._downBtn:Show() end
+ if bar._thumb then
+ if canScroll then bar._thumb:Show() else bar._thumb:Hide() end
+ end
+
+ if not canScroll then
+ local sliderName = scroll.GetName and scroll:GetName()
+ local slider = (sliderName and _G[sliderName .. "ScrollBar"]) or scroll.ScrollBar or scroll.scrollBar
+ if slider then
+ if slider.SetMinMaxValues then slider:SetMinMaxValues(0, 0) end
+ if slider.SetValue then slider:SetValue(0) end
+ end
+ end
+end
+
+local function moneyText(copper)
+ if not copper or copper <= 0 then return "-" end
+ if GetCoinTextureString then
+ return GetCoinTextureString(copper)
+ end
+ return tostring(copper)
+end
+
+-- Prefer the SHORT list-column strings ("Short"/"Medium"/"Long"/"Very Long") over the verbose
+-- "_DETAIL" globals ("Greater than 12 hours") -- the DETAIL variants are meant for tooltips and
+-- are too wide for this column, wrapping onto a second line that bled into the Buyout column.
+local function timeLeftText(code)
+ if code == 1 then return AUCTION_TIME_LEFT1 or "Short" end
+ if code == 2 then return AUCTION_TIME_LEFT2 or "Medium" end
+ if code == 3 then return AUCTION_TIME_LEFT3 or "Long" end
+ if code == 4 then return AUCTION_TIME_LEFT4 or "Very Long" end
+ return "-"
+end
+
+function AH.BuildAuctionsPane(parent)
+ local pane = CreateFrame("Frame", "NE_AuctionHouseAuctionsPane", parent)
+ pane:SetAllPoints(parent)
+
+ ----------------------------------------------------------------------
+ -- Sub-tabs (Auctions / Bids): themed as the SAME "pill" tab-plate the Sell pane's "Create
+ -- Auction" label uses (auctionhouse-selltab-left/middle/right), straddling the panel's top
+ -- edge, instead of the classic CharacterFrameTabButtonTemplate reskin used by earlier rounds --
+ -- that template's MakeTopTab flip + PanelTemplates_SetTab not honoring the reskin were a
+ -- repeated source of bugs (upside-down art, both tabs reading "active" at once). This reuses
+ -- proven, already-working art from Sell.lua instead. That atlas only has ONE visual state (no
+ -- separate "active" pieces the way the classic tab template does), so selection is conveyed by
+ -- dimming the inactive pill (alpha + grey text) rather than swapping art.
+ ----------------------------------------------------------------------
+ local PILL_H = 23
+ local SUBTABS = {
+ { id = 1, text = AUCTION_HOUSE_AUCTIONS_SUB_TAB or AUCTIONS or "Auctions" },
+ { id = 2, text = AUCTION_HOUSE_BIDS_SUB_TAB or BIDS or "Bids" },
+ }
+
+ -- Explicit SetWidth/SetHeight on btn FIRST, then anchor the art pieces off btn's own corners --
+ -- NOT the other way around (anchoring btn's own rect off its children's positions would be a
+ -- circular reference: btn's rect has to be fully resolved before any anchor point on it, TOP/
+ -- BOTTOM/LEFT/RIGHT/CENTER, can be used as a reference by something else).
+ local function buildPillTab(text)
+ local btn = CreateFrame("Button", nil, pane)
+ btn:SetHeight(PILL_H)
+ btn:EnableMouse(true)
+ btn:SetFrameLevel((pane:GetFrameLevel() or 0) + 20) -- draws above the panels' nineslice borders
+
+ local tabText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ tabText:SetText(text)
+ -- 9 (left cap) + 12+textWidth+12 (middle, padded like Sell's plate) + 9 (right cap).
+ btn:SetWidth((tabText:GetStringWidth() or 40) + 42)
+
+ local tabL = btn:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(tabL, "auctionhouse-selltab-left", true) end
+ tabL:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
+ tabL:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 0, 0)
+
+ local tabR = btn:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(tabR, "auctionhouse-selltab-right", true) end
+ tabR:SetPoint("TOPRIGHT", btn, "TOPRIGHT", 0, 0)
+ tabR:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 0, 0)
+
+ local tabM = btn:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(tabM, "auctionhouse-selltab-middle", false) end
+ tabM:SetPoint("TOPLEFT", tabL, "TOPRIGHT", 0, 0)
+ tabM:SetPoint("BOTTOMRIGHT", tabR, "BOTTOMLEFT", 0, 0)
+
+ tabText:ClearAllPoints()
+ tabText:SetPoint("CENTER", btn, "CENTER", 0, 0)
+ btn.Text = tabText
+ return btn
+ end
+
+ local subButtons = {}
+ do
+ local prev
+ for i = 1, #SUBTABS do
+ local d = SUBTABS[i]
+ local btn = buildPillTab(d.text)
+ btn:SetID(d.id)
+ if prev then
+ btn:SetPoint("BOTTOMLEFT", prev, "BOTTOMRIGHT", 6, 0)
+ else
+ -- x=80 matches the horizontal start every earlier tab attempt used here (proven clear of
+ -- the window's portrait). y=-81 = the panels' own -78 top offset (below) minus a further
+ -- 3px, so the pill's bottom edge overlaps 3px DOWN into the panel's top border -- same
+ -- straddle convention Sell.lua's "Create Auction" plate uses against its own form.
+ btn:SetPoint("BOTTOMLEFT", pane, "TOPLEFT", 80, -81)
+ end
+ subButtons[d.id] = btn
+ prev = btn
+ end
+ end
+
+ -- Selection: dim the inactive pill (there's only one art style, no separate "active" atlas).
+ local function setTabArt(id, selected)
+ local btn = subButtons[id]
+ if not btn then return end
+ btn:SetAlpha(selected and 1 or 0.55)
+ if btn.Text and btn.Text.SetTextColor then
+ if selected then btn.Text:SetTextColor(1, 0.82, 0) else btn.Text:SetTextColor(0.6, 0.6, 0.6) end
+ end
+ end
+
+ ----------------------------------------------------------------------
+ -- Panels. TOP is a FIXED offset (-78) below the pane's own top -- clears the portrait/title band
+ -- and stays constant regardless of which sub-tab is selected (the pill tabs above don't resize
+ -- on selection, unlike the old classic-tab attempt, so there's no jump to guard against either
+ -- way, but a fixed offset is still simplest). BOTTOM is +27 from the pane's bottom -- the SAME 27
+ -- Browse.lua's own results/detail panels use against this same window (Window.lua's
+ -- buildMoneyFrame inset also tops out at 27), so the footer row (Cancel button) lines up with
+ -- the money frame across the window's width.
+ ----------------------------------------------------------------------
+ local left = CreateFrame("Frame", nil, pane)
+ left:SetWidth(168)
+ left:SetPoint("TOP", pane, "TOP", 0, -78)
+ left:SetPoint("LEFT", pane, "LEFT", 4, 0)
+ left:SetPoint("BOTTOM", pane, "BOTTOM", 0, 27)
+ pane.Left = left
+
+ -- Stretched to fill "left" (matches every other panel's background in this window -- rightBg
+ -- right below does the same) instead of a fixed 138x433 SetSize, which left a ~27px raw gap on
+ -- the right where the panel's own chrome showed through and rows/highlights ran past the art.
+ local leftBg = left:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(leftBg, "auctionhouse-background-summarylist", false)
+ end
+ leftBg:SetPoint("TOPLEFT", left, "TOPLEFT", 3, -3)
+ leftBg:SetPoint("BOTTOMRIGHT", left, "BOTTOMRIGHT", -3, 3)
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, left, 0, 0, 0, 0)
+ end
+
+ -- TOPLEFT touches "left" directly (0 gap) -- matching Browse.lua's own categories/results split
+ -- (categories 168 wide at pane x=4, results at pane x=172: zero gap between them). The two
+ -- panels' own inset borders butting together is what reads as "the divider"; an explicit gap
+ -- here just left a bare strip of raw chrome between them instead.
+ local right = CreateFrame("Frame", nil, pane)
+ right:SetPoint("TOPLEFT", left, "TOPRIGHT", 0, 0)
+ right:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", -5, 27)
+ pane.Right = right
+
+ local rightBg = right:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(rightBg, "auctionhouse-background-index", false)
+ end
+ rightBg:SetPoint("TOPLEFT", right, "TOPLEFT", 3, -22)
+ rightBg:SetPoint("BOTTOMRIGHT", right, "BOTTOMRIGHT", -22, 0)
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, right, 0, -19, -22, 0)
+ end
+
+ local selectedSub = 1
+ local selectedOwnerIndex = nil
+ local rowsData = {}
+ local summaryEntries = { { all = true } }
+ local selectedSummaryIndex = 1
+ local cancelBtn
+ local requestActiveList
+ local refreshResults
+ local refreshSummaryRows
+
+ local function activeListType()
+ if selectedSub == 1 then return "owner" end
+ return "bidder"
+ end
+
+ local function updateCancelState()
+ if selectedSub ~= 1 or not selectedOwnerIndex then
+ cancelBtn:Disable()
+ return
+ end
+ if type(CanCancelAuction) == "function" then
+ if CanCancelAuction(selectedOwnerIndex) then
+ cancelBtn:Enable()
+ else
+ cancelBtn:Disable()
+ end
+ else
+ cancelBtn:Enable()
+ end
+ end
+
+ local function captureRows()
+ if type(GetAuctionItemInfo) ~= "function" then
+ rowsData = {}
+ return rowsData
+ end
+
+ local kind = activeListType()
+ local batchCount = 0
+ if type(GetNumAuctionItems) == "function" then
+ local n = GetNumAuctionItems(kind)
+ batchCount = (type(n) == "number") and n or 0
+ end
+
+ local out = {}
+ for i = 1, batchCount do
+ -- Field order per THIS server's own bundled APIDocumentation addon (13 return values, no
+ -- "levelColumnName" slot the generic retail/Wowpedia signature has) -- texture is position
+ -- 2, buyoutPrice is position 9 and bidAmount is position 10 on THIS client, not 10/11.
+ local ok, name, texture, _, _, _, _, _, _, buyoutPrice, bidAmount = pcall(GetAuctionItemInfo, kind, i)
+ if not ok then
+ break
+ end
+ if not name then
+ break
+ end
+ local tl = type(GetAuctionItemTimeLeft) == "function" and GetAuctionItemTimeLeft(kind, i) or nil
+ out[#out + 1] = {
+ index = i,
+ name = name,
+ texture = texture,
+ bidAmount = bidAmount,
+ buyoutPrice = buyoutPrice,
+ timeLeft = tl,
+ }
+ end
+ rowsData = out
+ return out
+ end
+
+ -- "All Auctions"/"All Bids" + one row per distinct item name, in first-seen order (retail
+ -- groups by item type server-side; Era's flat owner/bidder list has no such grouping, so this
+ -- is built client-side from whatever captureRows just read).
+ local function buildSummaryEntries()
+ local entries = { { all = true } }
+ local seen = {}
+ for i = 1, #rowsData do
+ local r = rowsData[i]
+ if not seen[r.name] then
+ seen[r.name] = true
+ entries[#entries + 1] = { name = r.name, texture = r.texture }
+ end
+ end
+ return entries
+ end
+
+ -- rowsData filtered down to the selected summary entry (or everything, for "All").
+ local function filteredRows()
+ local sel = summaryEntries[selectedSummaryIndex]
+ if not sel or sel.all then return rowsData end
+ local out = {}
+ for i = 1, #rowsData do
+ if rowsData[i].name == sel.name then out[#out + 1] = rowsData[i] end
+ end
+ return out
+ end
+
+ local function refreshActiveList()
+ captureRows()
+ summaryEntries = buildSummaryEntries()
+ if selectedSummaryIndex > #summaryEntries then selectedSummaryIndex = 1 end
+ if refreshSummaryRows then refreshSummaryRows() end
+ if refreshResults then refreshResults() end
+ updateCancelState()
+ end
+
+ local function setSub(id)
+ selectedSub = id
+ selectedOwnerIndex = nil
+ selectedSummaryIndex = 1
+ for i = 1, #SUBTABS do
+ setTabArt(SUBTABS[i].id, SUBTABS[i].id == id)
+ end
+ if pane.EmptyText then
+ pane.EmptyText:SetText((id == 1) and "You have no auctions." or "You have no bids.")
+ end
+ if cancelBtn then cancelBtn:SetShown(id == 1) end
+ requestActiveList()
+ refreshActiveList()
+ end
+
+ for id, btn in pairs(subButtons) do
+ btn:SetScript("OnClick", function(self) setSub(self:GetID()) end)
+ end
+
+ requestActiveList = function()
+ if selectedSub == 1 then
+ if type(GetOwnerAuctionItems) == "function" then
+ pcall(GetOwnerAuctionItems, 0)
+ end
+ else
+ if type(GetBidderAuctionItems) == "function" then
+ pcall(GetBidderAuctionItems, 0)
+ end
+ end
+ end
+
+ ----------------------------------------------------------------------
+ -- Summary panel: "All Auctions"/"All Bids" + one row per distinct item, scrollable. Selecting
+ -- a row filters the right list down to that item's individual auctions.
+ ----------------------------------------------------------------------
+ local SUM_ROW_H = 21
+ local SUM_VISIBLE_ROWS = 20
+ local summaryRows = {}
+ local summaryScroll = CreateFrame("ScrollFrame", pane:GetName() .. "SummaryScroll", left, "FauxScrollFrameTemplate")
+ summaryScroll:SetPoint("TOPLEFT", left, "TOPLEFT", 3, -3)
+ summaryScroll:SetPoint("BOTTOMRIGHT", left, "BOTTOMRIGHT", -20, 3)
+
+ if NE.scrollbar and NE.scrollbar.BuildCustom then
+ -- arrows = true: the reference's Auctions-tab scrollbar (both its summary list and its
+ -- results list) shows up/down arrow buttons, unlike the plain Buy-tab bar.
+ -- x = -4 (not the usual -8): `right` sits flush against `left`'s edge (0 gap, no divider
+ -- padding between the panels), so the arrow buttons -- 17px wide, centered on an 8px track --
+ -- overhang the track by 4.5px each side and were poking past `left`'s edge into the seam at
+ -- the standard -8 inset. -4 pulls the whole bar+arrows left so they clear it.
+ local ok, bar = pcall(NE.scrollbar.BuildCustom, summaryScroll, { x = -4, alwaysShow = true, arrows = true })
+ if ok and bar then
+ bar:SetFrameStrata("DIALOG")
+ bar:SetFrameLevel((left:GetFrameLevel() or 1) + 10)
+ if bar._upBtn then bar._upBtn:SetFrameStrata("DIALOG"); bar._upBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ if bar._downBtn then bar._downBtn:SetFrameStrata("DIALOG"); bar._downBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ end
+ end
+
+ local function ensureSummaryRow(i)
+ local row = summaryRows[i]
+ if row then return row end
+
+ row = CreateFrame("Button", nil, left)
+ row:SetFrameStrata("DIALOG")
+ row:SetFrameLevel((left:GetFrameLevel() or 1) + 8)
+ row:SetHeight(SUM_ROW_H)
+ if i == 1 then
+ row:SetPoint("TOPLEFT", left, "TOPLEFT", 3, -3)
+ row:SetPoint("TOPRIGHT", left, "TOPRIGHT", -20, -3)
+ else
+ row:SetPoint("TOPLEFT", summaryRows[i - 1], "BOTTOMLEFT", 0, 0)
+ row:SetPoint("TOPRIGHT", summaryRows[i - 1], "BOTTOMRIGHT", 0, 0)
+ end
+
+ row.Sel = row:CreateTexture(nil, "BACKGROUND")
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(row.Sel, "auctionhouse-ui-row-select", false)) then
+ row.Sel:SetTexture("Interface\\Buttons\\WHITE8X8")
+ row.Sel:SetVertexColor(1, 0.82, 0, 0.18)
+ end
+ row.Sel:SetAllPoints(row)
+ row.Sel:Hide()
+
+ row:SetHighlightTexture("Interface\\Buttons\\WHITE8X8")
+ local hl = row:GetHighlightTexture()
+ if hl then
+ hl:SetVertexColor(1, 0.82, 0, 0.10)
+ hl:SetBlendMode("ADD")
+ end
+
+ row.Icon = row:CreateTexture(nil, "ARTWORK")
+ row.Icon:SetSize(14, 14)
+ row.Icon:SetPoint("LEFT", row, "LEFT", 4, 0)
+
+ row.Text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ row.Text:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ row.Text:SetJustifyH("LEFT")
+
+ row:SetScript("OnClick", function(self)
+ if not self._index then return end
+ selectedSummaryIndex = self._index
+ selectedOwnerIndex = nil
+ if refreshSummaryRows then refreshSummaryRows() end
+ if refreshResults then refreshResults() end
+ updateCancelState()
+ end)
+
+ summaryRows[i] = row
+ return row
+ end
+
+ refreshSummaryRows = function()
+ local total = #summaryEntries
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(summaryScroll, total, SUM_VISIBLE_ROWS, SUM_ROW_H) end
+ syncAlwaysShowFauxBar(summaryScroll, total, SUM_VISIBLE_ROWS)
+ local offset = (FauxScrollFrame_GetOffset and FauxScrollFrame_GetOffset(summaryScroll)) or 0
+
+ for i = 1, SUM_VISIBLE_ROWS do
+ local row = ensureSummaryRow(i)
+ local e = summaryEntries[i + offset]
+ if e then
+ row._index = i + offset
+ if e.all then
+ row.Icon:Hide()
+ row.Text:ClearAllPoints()
+ row.Text:SetPoint("LEFT", row, "LEFT", 6, 0)
+ row.Text:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ row.Text:SetText((selectedSub == 1) and (AUCTION_HOUSE_ALL_AUCTIONS or "All Auctions")
+ or (AUCTION_HOUSE_ALL_BIDS or "All Bids"))
+ else
+ row.Icon:Show()
+ row.Icon:SetTexture(e.texture or "Interface\\Icons\\INV_Misc_QuestionMark")
+ row.Text:ClearAllPoints()
+ row.Text:SetPoint("LEFT", row.Icon, "RIGHT", 4, 0)
+ row.Text:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ row.Text:SetText(e.name or "-")
+ end
+ row.Sel:SetShown(row._index == selectedSummaryIndex)
+ row:Show()
+ else
+ row._index = nil
+ row:Hide()
+ end
+ end
+ end
+
+ summaryScroll:SetScript("OnVerticalScroll", function(self, offset)
+ if FauxScrollFrame_OnVerticalScroll then
+ FauxScrollFrame_OnVerticalScroll(self, offset, SUM_ROW_H, refreshSummaryRows)
+ else
+ refreshSummaryRows()
+ end
+ end)
+
+ ----------------------------------------------------------------------
+ -- Right list: column headers + faux-scroll rows.
+ ----------------------------------------------------------------------
+ local h1 = right:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h1:SetPoint("TOPLEFT", right, "TOPLEFT", 14, -8)
+ h1:SetText(NAME or "Name")
+
+ local h2 = right:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h2:SetPoint("TOPLEFT", right, "TOPLEFT", 238, -8)
+ h2:SetText(AUCTION_HOUSE_HEADER_CURRENT_BID or "Current Bid")
+
+ local h3 = right:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h3:SetPoint("TOPLEFT", right, "TOPLEFT", 340, -8)
+ h3:SetText(AUCTION_BUYOUT_PRICE or BUYOUT or "Buyout")
+
+ -- Right-anchored + right-justified from the SAME frame ("right") and the SAME -30 (scrollbar
+ -- gutter) / -8 (row inset) offsets row.Time itself resolves to below -- guarantees this header
+ -- lines up with the actual data column instead of drifting from an unrelated absolute x. Widened
+ -- to 115 (was 88) so "Very Long" / "Greater than..." style strings fit on one line.
+ local h4 = right:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h4:SetPoint("TOPRIGHT", right, "TOPRIGHT", -38, -8)
+ h4:SetWidth(115)
+ h4:SetJustifyH("RIGHT")
+ h4:SetText(TIME_LEFT or "Time Left")
+
+ local empty = right:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ empty:SetPoint("CENTER", right, "CENTER", 0, 44)
+ empty:SetText("You have no auctions.")
+ empty:SetTextColor(1, 0.82, 0)
+ pane.EmptyText = empty
+
+ local ROW_H = 20
+ local VISIBLE_ROWS = 17
+ local rows = {}
+ local scroll = CreateFrame("ScrollFrame", pane:GetName() .. "ResultsScroll", right, "FauxScrollFrameTemplate")
+ scroll:SetPoint("TOPLEFT", right, "TOPLEFT", 4, -24)
+ scroll:SetPoint("BOTTOMRIGHT", right, "BOTTOMRIGHT", -26, 28)
+
+ if NE.scrollbar and NE.scrollbar.BuildCustom then
+ local ok, bar = pcall(NE.scrollbar.BuildCustom, scroll, { x = -8, alwaysShow = true, arrows = true })
+ if ok and bar then
+ bar:SetFrameStrata("DIALOG")
+ bar:SetFrameLevel((right:GetFrameLevel() or 1) + 10)
+ if bar._upBtn then bar._upBtn:SetFrameStrata("DIALOG"); bar._upBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ if bar._downBtn then bar._downBtn:SetFrameStrata("DIALOG"); bar._downBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ end
+ end
+
+ local function fsUpdate(frame, total, shown, step)
+ if FauxScrollFrame_Update then
+ FauxScrollFrame_Update(frame, total, shown, step)
+ else
+ frame.offset = frame.offset or 0
+ end
+ end
+
+ local function fsGetOffset(frame)
+ if FauxScrollFrame_GetOffset then
+ return FauxScrollFrame_GetOffset(frame) or 0
+ end
+ return frame.offset or 0
+ end
+
+ local function ensureRow(i)
+ local row = rows[i]
+ if row then return row end
+
+ row = CreateFrame("Button", nil, right)
+ row:SetFrameStrata("DIALOG")
+ row:SetFrameLevel((right:GetFrameLevel() or 1) + 8)
+ row:SetHeight(ROW_H)
+ if i == 1 then
+ row:SetPoint("TOPLEFT", right, "TOPLEFT", 6, -24)
+ row:SetPoint("TOPRIGHT", right, "TOPRIGHT", -30, -24)
+ else
+ row:SetPoint("TOPLEFT", rows[i - 1], "BOTTOMLEFT", 0, -1)
+ row:SetPoint("TOPRIGHT", rows[i - 1], "BOTTOMRIGHT", 0, -1)
+ end
+
+ row.Bg = row:CreateTexture(nil, "BACKGROUND")
+ row.Bg:SetTexture("Interface\\Buttons\\WHITE8X8")
+ row.Bg:SetAllPoints(row)
+ row.Bg:SetVertexColor(0.07, 0.07, 0.08, (i % 2 == 0) and 0.30 or 0.20)
+
+ row:SetHighlightTexture("Interface\\Buttons\\WHITE8X8")
+ local hl = row:GetHighlightTexture()
+ if hl then
+ hl:SetVertexColor(1, 0.82, 0, 0.12)
+ hl:SetBlendMode("ADD")
+ end
+
+ row.Select = row:CreateTexture(nil, "ARTWORK")
+ row.Select:SetTexture("Interface\\Buttons\\WHITE8X8")
+ row.Select:SetAllPoints(row)
+ row.Select:SetVertexColor(1.0, 0.82, 0.0, 0.16)
+ row.Select:Hide()
+
+ row.Name = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ row.Name:SetPoint("LEFT", row, "LEFT", 8, 0)
+ row.Name:SetPoint("RIGHT", row, "LEFT", 220, 0)
+ row.Name:SetJustifyH("LEFT")
+
+ row.Bid = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ row.Bid:SetPoint("LEFT", row, "LEFT", 232, 0)
+ row.Bid:SetWidth(90)
+ row.Bid:SetJustifyH("LEFT")
+
+ row.Buyout = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ row.Buyout:SetPoint("LEFT", row, "LEFT", 334, 0)
+ row.Buyout:SetWidth(90)
+ row.Buyout:SetJustifyH("LEFT")
+
+ row.Time = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ row.Time:SetPoint("RIGHT", row, "RIGHT", -8, 0)
+ row.Time:SetWidth(115)
+ row.Time:SetJustifyH("RIGHT")
+
+ row:SetScript("OnClick", function(self)
+ -- SHIFT links the item in chat, CTRL dresses it up; otherwise select the row.
+ if self._index then
+ local link = GetAuctionItemLink and GetAuctionItemLink(activeListType(), self._index)
+ if link and AH.HandleItemClick(link) then return end
+ end
+ if selectedSub ~= 1 then return end
+ selectedOwnerIndex = self._index
+ refreshResults()
+ updateCancelState()
+ end)
+
+ rows[i] = row
+ return row
+ end
+
+ refreshResults = function()
+ local list = filteredRows()
+ local total = #list
+ fsUpdate(scroll, total, VISIBLE_ROWS, ROW_H + 1)
+ syncAlwaysShowFauxBar(scroll, total, VISIBLE_ROWS)
+ local offset = fsGetOffset(scroll)
+ local maxOffset = total - VISIBLE_ROWS
+ if maxOffset < 0 then maxOffset = 0 end
+ if offset > maxOffset then offset = maxOffset end
+
+ if total > 0 then
+ empty:Hide()
+ else
+ empty:Show()
+ end
+
+ for i = 1, VISIBLE_ROWS do
+ local row = ensureRow(i)
+ local data = list[i + offset]
+ if data then
+ row._index = data.index
+ row.Name:SetText(data.name or "-")
+ row.Bid:SetText(moneyText(data.bidAmount))
+ row.Buyout:SetText(moneyText(data.buyoutPrice))
+ row.Time:SetText(timeLeftText(data.timeLeft))
+ if selectedSub == 1 and selectedOwnerIndex == data.index then
+ row.Select:Show()
+ else
+ row.Select:Hide()
+ end
+ row:Show()
+ else
+ row._index = nil
+ row:Hide()
+ end
+ end
+ end
+
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ if FauxScrollFrame_OnVerticalScroll then
+ FauxScrollFrame_OnVerticalScroll(self, offset, ROW_H + 1, refreshResults)
+ else
+ local step = ROW_H + 1
+ local nextOffset = math.floor((offset / step) + 0.5)
+ if nextOffset < 0 then nextOffset = 0 end
+ self.offset = nextOffset
+ refreshResults()
+ end
+ end)
+
+ -- Footer row: sits in the SAME 27px band "right"/"left" stop short of (not a further offset
+ -- below it -- that doubled the reserved space and crammed the button down against the window's
+ -- very bottom edge). y=3..27 mirrors Window.lua's own money frame inset (also 3..27), so the
+ -- button's vertical center lines up with the money display across the window.
+ -- x=-27 lines the button's RIGHT edge up with the inset frame's own right edge: "right" stops
+ -- 5px short of pane's right edge (matching the Sell tab's panel), and its visible inset
+ -- (rightBg/AttachInset) sits a further 22px in from THAT (see rightBg/AttachInset below) --
+ -- 5+22 = 27.
+ cancelBtn = CreateFrame("Button", nil, pane, "UIPanelButtonTemplate")
+ cancelBtn:SetSize(150, 24)
+ cancelBtn:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", -27, 3)
+ cancelBtn:SetText(AUCTION_HOUSE_CANCEL_AUCTION_BUTTON or CANCEL_AUCTION or "Cancel Auction")
+ cancelBtn:Disable()
+ cancelBtn:SetScript("OnClick", function()
+ if selectedSub ~= 1 or not selectedOwnerIndex then return end
+ if type(CancelAuction) == "function" then
+ pcall(CancelAuction, selectedOwnerIndex)
+ end
+ end)
+
+ local watcher = CreateFrame("Frame", nil, pane)
+ watcher:RegisterEvent("AUCTION_OWNED_LIST_UPDATE")
+ watcher:RegisterEvent("AUCTION_BIDDER_LIST_UPDATE")
+ watcher:RegisterEvent("AUCTION_HOUSE_SHOW")
+ watcher:SetScript("OnEvent", function(_, event)
+ if event == "AUCTION_HOUSE_SHOW" then
+ requestActiveList()
+ end
+ if not pane:IsShown() then return end
+ if event == "AUCTION_OWNED_LIST_UPDATE" and selectedSub ~= 1 then return end
+ if event == "AUCTION_BIDDER_LIST_UPDATE" and selectedSub ~= 2 then return end
+ refreshActiveList()
+ end)
+
+ setSub(selectedSub)
+ requestActiveList()
+ refreshActiveList()
+
+ pane:HookScript("OnShow", function()
+ requestActiveList()
+ refreshActiveList()
+ end)
+
+ return pane
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Browse.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Browse.lua
new file mode 100644
index 0000000..2e6f88f
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Browse.lua
@@ -0,0 +1,2555 @@
+-- DragonUI_NewEra/modules/auctionhouse/Browse.lua
+-- Buy tab visual shell and Blizzard API search bridge.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+NE.ah = NE.ah or {}
+local AH = NE.ah
+
+-- "Any rarity" sentinel for QueryAuctionItems' quality argument. That argument is an EXACT match
+-- against the item's quality, NOT a minimum, and -1 is the only value that disables the filter --
+-- 0 is a real quality (Poor). See the ISSUE #31 note on pane.Filter in buildBrowsePane. Every
+-- QueryAuctionItems call site in this addon must pass this, never 0, when it wants "all rarities".
+local ANY_QUALITY = -1
+AH.ANY_QUALITY = ANY_QUALITY
+
+-- ============================================================================
+-- Shared "list" auction-query arbiter. ISSUE #31.
+--
+-- The 3.3.5a client has exactly ONE "list" query slot. Three consumers in this addon write to it
+-- (the browse scan, the item drill-down, the Sell tab's market lookup) and anything else loaded
+-- can too -- Auctionator, the cloaked stock AuctionFrame, other addons. AUCTION_ITEM_LIST_UPDATE
+-- carries no indication of who asked, so a watcher that reads the slot unconditionally will
+-- happily parse somebody else's result set as its own.
+--
+-- Whoever issues a query claims the slot by name; every watcher must confirm it still owns the
+-- slot before reading GetAuctionItemInfo("list", ...). An update we did not cause leaves
+-- listOwner pointing at someone else (or nil, for a foreign query), so everyone ignores it.
+-- ============================================================================
+function AH.ClaimListQuery(owner)
+ AH.listOwner = owner
+end
+
+function AH.OwnsListQuery(owner)
+ return AH.listOwner == owner
+end
+
+-- Keep alwaysShow custom Faux bars visually in sync with the actual row overflow state.
+-- Track/arrows stay visible; thumb only shows when the list can truly scroll.
+local function syncAlwaysShowFauxBar(scroll, totalRows, visibleRows)
+ if not scroll then return end
+ local bar = scroll._neCustomBar
+ if not (bar and bar._alwaysShow) then return end
+
+ local canScroll = (totalRows or 0) > (visibleRows or 0)
+ bar:Show()
+ if bar._upBtn then bar._upBtn:Show() end
+ if bar._downBtn then bar._downBtn:Show() end
+ if bar._thumb then
+ if canScroll then bar._thumb:Show() else bar._thumb:Hide() end
+ end
+
+ -- Clamp the hidden Faux slider when unscrollable so stale values cannot keep the thumb active.
+ if not canScroll then
+ local sliderName = scroll.GetName and scroll:GetName()
+ local slider = (sliderName and _G[sliderName .. "ScrollBar"]) or scroll.ScrollBar or scroll.scrollBar
+ if slider then
+ if slider.SetMinMaxValues then slider:SetMinMaxValues(0, 0) end
+ if slider.SetValue then slider:SetValue(0) end
+ end
+ end
+end
+
+-- Aggregate browse row click drills into a per-item detail page (all individual auctions of that
+-- item); the detail page's Bid/Buyout buttons act on the selected listing via these two confirms.
+StaticPopupDialogs["NE_AH_BROWSE_BID"] = {
+ text = "Place a bid of %s?",
+ button1 = YES or "Yes",
+ button2 = NO or "No",
+ OnAccept = function(_, data)
+ if not data or not data.index then return end
+ local ok, err = pcall(PlaceAuctionBid, "list", data.index, data.price)
+ if not ok and NE.Log then
+ NE.Log("AH", "PlaceAuctionBid(bid) error: " .. tostring(err))
+ end
+ end,
+ timeout = 0,
+ hideOnEscape = 1,
+ whileDead = 1,
+ showAlert = 1,
+}
+StaticPopupDialogs["NE_AH_BROWSE_BUYOUT"] = {
+ text = "Buy out this auction for %s?",
+ button1 = YES or "Yes",
+ button2 = NO or "No",
+ OnAccept = function(_, data)
+ if not data or not data.index then return end
+ local ok, err = pcall(PlaceAuctionBid, "list", data.index, data.price)
+ if not ok and NE.Log then
+ NE.Log("AH", "PlaceAuctionBid(buyout) error: " .. tostring(err))
+ end
+ end,
+ timeout = 0,
+ hideOnEscape = 1,
+ whileDead = 1,
+ showAlert = 1,
+}
+
+local CATEGORY_TREE = {
+ { name = "Weapons", children = { "One-Handed Axes", "Two-Handed Axes", "Bows", "Guns", "One-Handed Maces", "Two-Handed Maces", "Polearms", "One-Handed Swords", "Two-Handed Swords", "Staves", "Fist Weapons", "Daggers", "Thrown" } },
+ { name = "Armor", children = { "Cloth", "Leather", "Mail", "Plate", "Shields", "Librams", "Idols", "Totems", "Sigils" } },
+ { name = "Container", children = { "Bags", "Soul Bags", "Herb Bags", "Enchanting Bags", "Engineering Bags", "Gem Bags", "Mining Bags" } },
+ { name = "Consumable", children = { "Food & Drink", "Potion", "Elixir", "Flask", "Bandage", "Scroll", "Item Enhancement" } },
+ { name = "Trade Goods", children = { "Parts", "Explosives", "Devices", "Jewelcrafting", "Cloth", "Leather", "Metal & Stone", "Meat", "Herb", "Elemental", "Enchanting", "Materials", "Armor Enchantment", "Weapon Enchantment" } },
+ { name = "Projectile", children = { "Wand", "Arrow", "Bullet" } },
+ { name = "Quiver", children = { "Quiver", "Ammo Pouch" } },
+ { name = "Recipe", children = { "Book", "Leatherworking", "Tailoring", "Engineering", "Blacksmithing", "Cooking", "Alchemy", "First Aid", "Enchanting", "Fishing", "Jewelcrafting", "Inscription" } },
+ { name = "Gems", children = { "Red", "Blue", "Yellow", "Purple", "Green", "Orange", "Meta", "Simple", "Prismatic" } },
+ { name = "Miscellaneous", children = { "Junk", "Reagent", "Pet", "Holiday", "Other" } },
+ { name = "Quest Items", children = { "Quest" } },
+}
+
+local function buildCategoryList(parent)
+ local ROW_H = 21
+ local ROW_GAP = 1
+ -- Row count sized to fill the categories panel's own background art down to its bottom edge
+ -- (previously 18 rows against a hardcoded 424px list height -- neither number matched the other,
+ -- leaving a large dead gap below the last row; both now derive from the same VISIBLE_ROWS).
+ local VISIBLE_ROWS = 19
+
+ local list = CreateFrame("Frame", nil, parent)
+ list:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, -6)
+ list:SetSize(152, VISIBLE_ROWS * ROW_H + (VISIBLE_ROWS - 1) * ROW_GAP)
+
+ local rows = {}
+ local flat = {}
+ local selectedCat = nil
+ local selectedSub = nil
+ local selectedInv = nil
+ local currentOffset = 0
+
+ -- classIndex/subClassIndex passed to QueryAuctionItems are POSITIONAL (luaIndex into
+ -- GetAuctionItemClasses()/GetAuctionItemSubClasses(classIndex)), not name-matched. Source the
+ -- category tree from those same calls so the index we click is guaranteed to be the index the
+ -- server expects -- a hand-maintained name list (CATEGORY_TREE) can silently drift out of order
+ -- from the live class/subclass table and file every search under the wrong category. CATEGORY_TREE
+ -- is kept only as a display-name fallback for the (never-expected) case these globals are missing.
+ local classNamesCache
+ local subClassCache = {}
+
+ local function getClassNames()
+ if classNamesCache then return classNamesCache end
+ local names = {}
+ if type(GetAuctionItemClasses) == "function" then
+ local ok, res = pcall(function() return { GetAuctionItemClasses() } end)
+ if ok and res and #res > 0 then names = res end
+ end
+ if #names == 0 then
+ for i = 1, #CATEGORY_TREE do names[i] = CATEGORY_TREE[i].name end
+ end
+ classNamesCache = names
+ return names
+ end
+
+ local function getSubClassNames(classIndex)
+ local cached = subClassCache[classIndex]
+ if cached then return cached end
+ local subs = {}
+ if type(GetAuctionItemSubClasses) == "function" then
+ local ok, res = pcall(function() return { GetAuctionItemSubClasses(classIndex) } end)
+ if ok and res then subs = res end
+ end
+ if #subs == 0 and CATEGORY_TREE[classIndex] then
+ subs = CATEGORY_TREE[classIndex].children or {}
+ end
+ subClassCache[classIndex] = subs
+ return subs
+ end
+
+ -- Third tier: inventory-type slots (Head, Shoulder, ...) within a selected class+subclass, e.g.
+ -- Armor > Leather > Head. GetAuctionInvTypes(classIndex, subClassIndex) returns a FLAT list of
+ -- (invTypeToken, shouldDisplay) pairs -- NOT one name per return like GetAuctionItemClasses -- so
+ -- unlike the two tiers above this can't just be indexed 1:1; we walk it in twos, keep only the
+ -- shouldDisplay entries, and record each one's ORIGINAL pair position as its invTypeIndex (the
+ -- luaIndex QueryAuctionItems' invTypeIndex arg expects), not its position among the filtered/shown
+ -- entries. invTypeToken (e.g. "INVTYPE_HEAD") is also the name of a global localized string with
+ -- the human-readable slot name, same convention PaperDoll/Item tooltips use.
+ local invTypeCache = {}
+
+ local function getInvTypes(classIndex, subClassIndex)
+ local key = classIndex .. ":" .. subClassIndex
+ local cached = invTypeCache[key]
+ if cached then return cached end
+ local list = {}
+ if type(GetAuctionInvTypes) == "function" then
+ local ok, res = pcall(function() return { GetAuctionInvTypes(classIndex, subClassIndex) } end)
+ if ok and res then
+ local pos = 0
+ for i = 1, #res, 2 do
+ pos = pos + 1
+ local token, display = res[i], res[i + 1]
+ if display and token and token ~= "" then
+ list[#list + 1] = { text = _G[token] or token, index = pos }
+ end
+ end
+ end
+ end
+ invTypeCache[key] = list
+ return list
+ end
+
+ local function maxOffset()
+ local m = #flat - VISIBLE_ROWS
+ if m < 0 then m = 0 end
+ return m
+ end
+
+ local function setOffset(v)
+ if v < 0 then v = 0 end
+ local m = maxOffset()
+ if v > m then v = m end
+ currentOffset = v
+ local sliderName = list.Scroll and list.Scroll.GetName and list.Scroll:GetName()
+ local slider = sliderName and _G[sliderName .. "ScrollBar"]
+ if slider and slider.SetValue then
+ slider:SetValue(v * (ROW_H + ROW_GAP))
+ elseif list.Scroll and list.Scroll.SetVerticalScroll then
+ list.Scroll:SetVerticalScroll(v * (ROW_H + ROW_GAP))
+ end
+ end
+
+ local function getOffset()
+ if list.Scroll and FauxScrollFrame_GetOffset then
+ local offset = FauxScrollFrame_GetOffset(list.Scroll) or 0
+ currentOffset = offset
+ return offset
+ end
+ return currentOffset
+ end
+
+ local function flatten()
+ local out = {}
+ local classNames = getClassNames()
+
+ for ci = 1, #classNames do
+ local catSelected = (selectedCat == ci)
+ out[#out + 1] = { text = classNames[ci], kind = "category", ci = ci, selected = catSelected }
+ if catSelected then
+ local subs = getSubClassNames(ci)
+ for si = 1, #subs do
+ local subSelected = (selectedSub == si)
+ out[#out + 1] = {
+ text = subs[si] or tostring(si),
+ kind = "subCategory",
+ ci = ci,
+ si = si,
+ selected = subSelected,
+ }
+ if subSelected then
+ local invTypes = getInvTypes(ci, si)
+ for ii = 1, #invTypes do
+ local inv = invTypes[ii]
+ out[#out + 1] = {
+ text = inv.text,
+ kind = "invType",
+ ci = ci,
+ si = si,
+ vi = inv.index,
+ selected = (selectedInv == inv.index),
+ }
+ end
+ end
+ end
+ end
+ end
+ return out
+ end
+
+ -- classIndex/subClassIndex/invTypeIndex for QueryAuctionItems. 0 == "any" (the client's own
+ -- sentinel for an unset luaIndex filter slot), matching what a deselected row means.
+ function list:GetSelectedIndices()
+ return selectedCat or 0, selectedSub or 0, selectedInv or 0
+ end
+
+ local function styleRow(row, info)
+ row.Bg:Hide()
+ row.Line:Hide()
+
+ if info.kind == "category" then
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(row.NormalTexture, "auctionhouse-nav-button", false)
+ NE.tex.SetAtlas(row.SelectedTexture, "auctionhouse-nav-button-select", false)
+ NE.tex.SetAtlas(row.HighlightTexture, "auctionhouse-nav-button-highlight", false)
+ end
+ row.NormalTexture:SetSize(136, 32)
+ row.NormalTexture:ClearAllPoints()
+ row.NormalTexture:SetPoint("TOPLEFT", row, "TOPLEFT", -2, 0)
+
+ row.SelectedTexture:SetSize(132, 21)
+ row.SelectedTexture:ClearAllPoints()
+ row.SelectedTexture:SetPoint("LEFT", row, "LEFT", 0, 0)
+
+ row.HighlightTexture:SetSize(132, 21)
+ row.HighlightTexture:ClearAllPoints()
+ row.HighlightTexture:SetPoint("LEFT", row, "LEFT", 0, 0)
+
+ row.Text:SetFontObject(GameFontNormal)
+ row.Text:SetPoint("LEFT", row, "LEFT", 8, 0)
+ row.Text:SetTextColor(1.0, 0.82, 0.0)
+ else
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(row.NormalTexture, "auctionhouse-nav-button-secondary", false)
+ NE.tex.SetAtlas(row.SelectedTexture, "auctionhouse-nav-button-secondary-select", false)
+ NE.tex.SetAtlas(row.HighlightTexture, "auctionhouse-nav-button-secondary-highlight", false)
+ end
+ row.NormalTexture:SetSize(133, 32)
+ row.NormalTexture:ClearAllPoints()
+ row.NormalTexture:SetPoint("TOPLEFT", row, "TOPLEFT", 1, 0)
+
+ row.SelectedTexture:SetSize(122, 21)
+ row.SelectedTexture:ClearAllPoints()
+ row.SelectedTexture:SetPoint("TOPLEFT", row, "TOPLEFT", 10, 0)
+
+ row.HighlightTexture:SetSize(122, 21)
+ row.HighlightTexture:ClearAllPoints()
+ row.HighlightTexture:SetPoint("TOPLEFT", row, "TOPLEFT", 10, 0)
+
+ -- Third tier (invType, e.g. "Head" under Armor > Leather) indents further than a plain
+ -- subcategory so the two nesting depths read distinctly, matching the reference's deeper
+ -- sub-sub-category rows.
+ -- Sub/inv rows sit on a narrower secondary button (133px, vs. 136 for a category) AND start
+ -- more indented -- GameFontNormal (used for the top-level rows) was overflowing that reduced
+ -- width for longer names like "One-Handed Axes"/"Two-Handed Swords". Smaller font object fits
+ -- comfortably within the button texture at every indent depth.
+ row.Text:SetFontObject(GameFontHighlightSmall)
+ row.Text:ClearAllPoints()
+ local indent = (info.kind == "invType") and 28 or 18
+ row.Text:SetPoint("LEFT", row, "LEFT", indent, 0)
+ row.Text:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ row.Text:SetTextColor(0.88, 0.9, 0.95)
+ row.Line:Show()
+ end
+
+ row.SelectedTexture:SetShown(info.selected and true or false)
+ row.HighlightTexture:Hide()
+ end
+
+ local viewport = CreateFrame("Frame", nil, list)
+ viewport:SetPoint("TOPLEFT", list, "TOPLEFT", 0, 0)
+ viewport:SetPoint("TOPRIGHT", list, "TOPRIGHT", 0, 0)
+ viewport:SetHeight(VISIBLE_ROWS * ROW_H + (VISIBLE_ROWS - 1) * ROW_GAP)
+ viewport:EnableMouseWheel(true)
+
+ local function refreshRows()
+ flat = flatten()
+ local offset = getOffset()
+ local m = maxOffset()
+ if offset < 0 then offset = 0 end
+ if offset > m then
+ offset = m
+ setOffset(offset)
+ else
+ currentOffset = offset
+ end
+
+ for i = 1, VISIBLE_ROWS do
+ local info = flat[i + offset]
+ local row = rows[i]
+ if not row then
+ row = CreateFrame("Button", nil, viewport)
+ row:SetSize(146, ROW_H)
+
+ local bg = row:CreateTexture(nil, "BACKGROUND")
+ bg:SetTexture("Interface\\Buttons\\WHITE8X8")
+ bg:SetAllPoints(row)
+ bg:Hide()
+ row.Bg = bg
+
+ row.NormalTexture = row:CreateTexture(nil, "BACKGROUND")
+ row.SelectedTexture = row:CreateTexture(nil, "ARTWORK")
+ row.HighlightTexture = row:CreateTexture(nil, "BORDER")
+ row.HighlightTexture:Hide()
+
+ row.Line = row:CreateTexture(nil, "BACKGROUND")
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(row.Line, "auctionhouse-nav-button-tertiary-filterline", true)
+ end
+ row.Line:SetPoint("LEFT", row, "LEFT", 18, 3)
+ row.Line:Hide()
+
+ local text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ text:SetPoint("LEFT", row, "LEFT", 8, 0)
+ text:SetPoint("RIGHT", row, "RIGHT", -6, 0)
+ text:SetJustifyH("LEFT")
+ row.Text = text
+
+ row:SetScript("OnEnter", function(self)
+ if self.HighlightTexture then self.HighlightTexture:Show() end
+ end)
+ row:SetScript("OnLeave", function(self)
+ if self.HighlightTexture then self.HighlightTexture:Hide() end
+ end)
+ row:RegisterForClicks("LeftButtonUp")
+ row:SetScript("OnClick", function(self)
+ if self._kind == "category" then
+ if selectedCat == self._ci then
+ selectedCat = nil
+ else
+ selectedCat = self._ci
+ end
+ selectedSub = nil
+ selectedInv = nil
+ elseif self._kind == "subCategory" then
+ if selectedSub == self._si then
+ selectedSub = nil
+ else
+ selectedSub = self._si
+ end
+ selectedInv = nil
+ elseif self._kind == "invType" then
+ if selectedInv == self._vi then
+ selectedInv = nil
+ else
+ selectedInv = self._vi
+ end
+ else
+ return
+ end
+
+ refreshRows()
+ end)
+
+ rows[i] = row
+ end
+
+ if i == 1 then
+ row:SetPoint("TOPLEFT", viewport, "TOPLEFT", 0, 0)
+ else
+ row:SetPoint("TOPLEFT", rows[i - 1], "BOTTOMLEFT", 0, -ROW_GAP)
+ end
+
+ if info then
+ row._kind = info.kind
+ row._ci = info.ci
+ row._si = info.si
+ row._vi = info.vi
+
+ row.Text:SetText(info.text)
+ styleRow(row, info)
+
+ row:Show()
+ else
+ row._kind = nil
+ row._ci = nil
+ row._si = nil
+ row._vi = nil
+ row:Hide()
+ end
+ end
+
+ if list.Scroll and FauxScrollFrame_Update then
+ FauxScrollFrame_Update(list.Scroll, #flat, VISIBLE_ROWS, ROW_H + ROW_GAP)
+ end
+ syncAlwaysShowFauxBar(list.Scroll, #flat, VISIBLE_ROWS)
+ end
+
+ -- FauxScrollFrameTemplate's handlers (FauxScrollFrame_Update etc., FrameXML/UIPanelTemplates.lua)
+ -- resolve the scrollbar's sub-widgets via frame:GetName() string concatenation, NOT parentKey --
+ -- an anonymous (nil-named) FauxScrollFrame throws "attempt to concatenate ... (a nil value)" the
+ -- moment FauxScrollFrame_Update runs on it. Must have a real global name.
+ local scroll = CreateFrame("ScrollFrame", "NE_AuctionHouseBrowseCategoryScroll", list, "FauxScrollFrameTemplate")
+ -- Explicit TOPLEFT+BOTTOMRIGHT (not just TOPLEFT+BOTTOMLEFT) so this frame actually has a defined
+ -- width -- BuildCustom's bar anchors off scroll:GetRight(), which is meaningless on a frame that
+ -- was never given a right edge, and could leave the bar mispositioned/invisible.
+ -- The right edge must sit INSET from viewport's own right edge (not flush with it) -- BuildCustom
+ -- places the bar opts.x (8px here) further right of scroll's right edge, same as the results-list
+ -- and item-detail scroll frames, which each reserve a >=18px gutter for exactly this. A flush (0)
+ -- inset here pushed the bar 8px past the whole category panel's border, off its visible bounds --
+ -- reserving 18px keeps the bar's 8px-wide track inside `list`/`viewport`'s actual bounds.
+ scroll:SetPoint("TOPLEFT", viewport, "TOPRIGHT", -30, -2)
+ scroll:SetPoint("BOTTOMRIGHT", viewport, "BOTTOMRIGHT", -18, 2)
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ if FauxScrollFrame_OnVerticalScroll then
+ FauxScrollFrame_OnVerticalScroll(self, offset, ROW_H + ROW_GAP, refreshRows)
+ return
+ end
+ local step = ROW_H + ROW_GAP
+ local nextOffset = math.floor((offset / step) + 0.5)
+ currentOffset = nextOffset
+ refreshRows()
+ end)
+ list.Scroll = scroll
+
+ -- Physical scrollbar track+thumb (FauxScrollFrameTemplate alone draws NOTHING visible -- it's
+ -- just offset math plus a hidden slider; every other FauxScroll list in this addon gets its
+ -- visible bar from this same hand-built widget, driven off that hidden slider's min/max/value).
+ -- BuildCustom's bar defaults to "HIGH" strata. AH.frame itself is explicitly "DIALOG" (Window.lua),
+ -- and a frame's strata is inherited from its parent AT CREATION unless overridden -- so `list`/
+ -- `viewport`/`scroll`, none of which set their own strata, are ALL "DIALOG" too, same as the rows
+ -- built on them. That puts a "HIGH"-strata bar BEHIND the row buttons, hiding it completely (same
+ -- trap already fixed for the results-list and item-detail scrollbars below). Force it to match.
+ if NE.scrollbar and NE.scrollbar.BuildCustom then
+ local ok, bar = pcall(NE.scrollbar.BuildCustom, scroll, { x = -8, alwaysShow = true })
+ if ok and bar then
+ bar:SetFrameStrata("DIALOG")
+ bar:SetFrameLevel((viewport:GetFrameLevel() or 1) + 10)
+ -- The arrow buttons' strata/level were set INSIDE BuildCustom against the bar's strata AT
+ -- THAT TIME ("HIGH") -- promoting the bar to DIALOG afterward left them a strata behind,
+ -- same trap as the bar-vs-rows one described above, so they never rendered (there but hidden).
+ if bar._upBtn then bar._upBtn:SetFrameStrata("DIALOG"); bar._upBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ if bar._downBtn then bar._downBtn:SetFrameStrata("DIALOG"); bar._downBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ end
+ end
+
+ local function onMouseWheel(_, delta)
+ if delta > 0 then
+ setOffset(currentOffset - 1)
+ else
+ setOffset(currentOffset + 1)
+ end
+ refreshRows()
+ end
+ viewport:SetScript("OnMouseWheel", onMouseWheel)
+ list:SetScript("OnMouseWheel", onMouseWheel)
+ list:EnableMouseWheel(true)
+
+ refreshRows()
+
+ -- Deselect all three tiers. Used by the shift-click-to-search bridge: a "find this bag item"
+ -- search must not be constrained by (and then silently miss under) whatever category happened
+ -- to be selected from a previous browse.
+ function list:ClearSelection()
+ selectedCat, selectedSub, selectedInv = nil, nil, nil
+ refreshRows()
+ end
+
+ list.Refresh = refreshRows
+ list.Rows = rows
+ return list
+end
+
+function AH.BuildBrowsePane(parent)
+ local pane = CreateFrame("Frame", nil, parent)
+ pane:SetAllPoints(parent)
+
+ -- Build right-to-left with RELATIVE anchors (searchButton -> filterButton -> searchBox) so the
+ -- three controls can never overlap regardless of individual widths. The previous version anchored
+ -- filterButton and searchButton independently off pane:TOPRIGHT with fixed offsets that didn't
+ -- account for both widths, so filterButton's right 12px sat underneath searchButton's left edge.
+ local searchButton = CreateFrame("Button", nil, pane, "UIPanelButtonTemplate")
+ searchButton:SetSize(120, 22)
+ searchButton:SetPoint("TOPRIGHT", pane, "TOPRIGHT", -8, -38)
+ searchButton:SetText(SEARCH or "Search")
+ pane.SearchButton = searchButton
+
+ local filterButton = CreateFrame("Button", nil, pane, "UIPanelButtonTemplate")
+ filterButton:SetSize(90, 22)
+ filterButton:SetPoint("RIGHT", searchButton, "LEFT", -10, 0)
+ filterButton:SetText(AUCTION_FILTER_SUBCATEGORY_INVENTORY_TYPE or "Filter")
+ pane.FilterButton = filterButton
+
+ -- Styled to match the professions crafting search box (modules/professions/RecipeList.lua) --
+ -- plain EditBox + dark tooltip backdrop + left search icon + right clear "x", instead of the
+ -- stock InputBoxTemplate's gold-inset look, for visual consistency across the addon's search
+ -- fields.
+ local searchBox = CreateFrame("EditBox", nil, pane)
+ searchBox:SetAutoFocus(false)
+ searchBox:SetHeight(20)
+ searchBox:SetWidth(220)
+ searchBox:SetPoint("TOPLEFT", pane, "TOPLEFT", 220, -38)
+ searchBox:SetPoint("RIGHT", filterButton, "LEFT", -10, 0)
+ searchBox:SetFontObject(_G.ChatFontNormal or _G.GameFontHighlightSmall)
+ searchBox:SetTextInsets(20, 18, 0, 0)
+ if searchBox.SetBackdrop then
+ searchBox:SetBackdrop({
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = true, tileSize = 16, edgeSize = 12,
+ insets = { left = 4, right = 4, top = 4, bottom = 4 },
+ })
+ searchBox:SetBackdropColor(0, 0, 0, 0.6)
+ searchBox:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
+ end
+ searchBox:SetText("")
+ searchBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
+ pane.SearchBox = searchBox
+
+ local searchIcon = searchBox:CreateTexture(nil, "OVERLAY")
+ searchIcon:SetSize(14, 14)
+ searchIcon:SetPoint("LEFT", searchBox, "LEFT", 4, 0)
+ searchIcon:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
+
+ local searchClear = CreateFrame("Button", nil, searchBox)
+ searchClear:SetSize(16, 16)
+ searchClear:SetPoint("RIGHT", searchBox, "RIGHT", -2, 0)
+ local searchClearTex = searchClear:CreateTexture(nil, "OVERLAY")
+ searchClearTex:SetAllPoints(searchClear)
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(searchClearTex, "common-search-clearbutton", false)) then
+ searchClearTex:SetTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up")
+ end
+ searchClear:Hide()
+ searchClear:SetScript("OnClick", function()
+ searchBox:SetText("")
+ searchBox:ClearFocus()
+ end)
+ searchBox:HookScript("OnTextChanged", function(self)
+ local txt = self:GetText() or ""
+ if txt ~= "" then searchClear:Show() else searchClear:Hide() end
+ end)
+ pane.SearchClear = searchClear
+
+ -- Filter popup: Level Range + Usable Items Only. Plain manual frame (not a dropdown/menu widget
+ -- unavailable on 3.3.5a). Styled with the SAME gold-trim inset + dark fill used everywhere else
+ -- in this window (categories/results panels), instead of the flat parchment DialogBox backdrop,
+ -- for visual consistency. Sits in "FULLSCREEN_DIALOG" strata (above the window's own "DIALOG"
+ -- strata) so it always draws above the results list header/rows regardless of sibling creation
+ -- order -- the previous version shared "DIALOG" with the results list, which (being built later
+ -- in this function) drew on top of the panel and made it unreadable.
+ -- ISSUE #31: the rarity slot of QueryAuctionItems is an EXACT quality match, not a minimum, and
+ -- its "any rarity" sentinel is -1 -- NOT 0. Server side (TrinityCore AuctionHouseMgr.cpp,
+ -- BuildListAuctionItems): `if (quality != 0xffffffff && proto->Quality != quality) continue;`.
+ -- Stock FrameXML agrees: Blizzard_AuctionUI.lua's BrowseDropDown_Initialize gives the ALL entry
+ -- `info.value = -1`, and BrowseDropDown_OnLoad/AuctionFrameBrowse_Reset select -1.
+ -- This field was previously named minQuality and defaulted to 0, so "All" searched for items of
+ -- EXACTLY Poor quality and found nothing. Renamed to `quality` so the wrong mental model that
+ -- caused the bug can't be read back out of the field name.
+ pane.Filter = { minLevel = 0, maxLevel = 0, usable = false, quality = ANY_QUALITY }
+
+ local filterPanel = CreateFrame("Frame", nil, pane)
+ filterPanel:SetFrameStrata("FULLSCREEN_DIALOG")
+ filterPanel:EnableMouse(true)
+ filterPanel:Hide()
+ filterPanel:SetPoint("TOPRIGHT", filterButton, "BOTTOMRIGHT", 0, -6)
+ filterPanel:SetSize(184, 262)
+ pane.FilterPanel = filterPanel
+
+ -- Grey fill matching THIS window's own header-strip tone (the results list's Price/Name/
+ -- Available header row, a few lines below), not a value borrowed from an unrelated module --
+ -- that mismatched against everything else in the Auction House window, which is uniformly dark.
+ local filterBg = filterPanel:CreateTexture(nil, "BACKGROUND")
+ filterBg:SetTexture("Interface\\Buttons\\WHITE8X8")
+ filterBg:SetVertexColor(0.06, 0.06, 0.07, 0.97)
+ filterBg:SetAllPoints(filterPanel)
+
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, filterPanel, 0, 0, 0, 0)
+ end
+
+ local lvlTitle = filterPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ lvlTitle:SetPoint("TOPLEFT", filterPanel, "TOPLEFT", 14, -14)
+ lvlTitle:SetText(AUCTION_HOUSE_FILTER_DROP_DOWN_LEVEL_RANGE or "Level Range")
+ lvlTitle:SetTextColor(1, 0.82, 0)
+
+ local function lvlBox()
+ local b = CreateFrame("EditBox", nil, filterPanel, "InputBoxTemplate")
+ b:SetSize(32, 20)
+ b:SetAutoFocus(false)
+ b:SetNumeric(true)
+ b:SetMaxLetters(2)
+ b:SetScript("OnEscapePressed", function(s) s:ClearFocus() end)
+ b:SetScript("OnEnterPressed", function(s) s:ClearFocus() end)
+ return b
+ end
+
+ local minBox = lvlBox()
+ minBox:SetPoint("TOPLEFT", filterPanel, "TOPLEFT", 22, -36)
+ local dash = filterPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ dash:SetPoint("LEFT", minBox, "RIGHT", 10, 0)
+ dash:SetText("-")
+ local maxBox = lvlBox()
+ maxBox:SetPoint("LEFT", dash, "RIGHT", 10, 0)
+
+ local function commitLevels()
+ pane.Filter.minLevel = minBox:GetNumber() or 0
+ pane.Filter.maxLevel = maxBox:GetNumber() or 0
+ end
+ minBox:SetScript("OnEditFocusLost", commitLevels)
+ maxBox:SetScript("OnEditFocusLost", commitLevels)
+ minBox:SetScript("OnTextChanged", commitLevels)
+ maxBox:SetScript("OnTextChanged", commitLevels)
+
+ local divider = filterPanel:CreateTexture(nil, "ARTWORK")
+ divider:SetTexture("Interface\\Buttons\\WHITE8X8")
+ divider:SetVertexColor(1, 0.82, 0, 0.25)
+ divider:SetHeight(1)
+ divider:SetPoint("TOPLEFT", filterPanel, "TOPLEFT", 12, -62)
+ divider:SetPoint("TOPRIGHT", filterPanel, "TOPRIGHT", -12, -62)
+
+ local usable = CreateFrame("CheckButton", nil, filterPanel, "UICheckButtonTemplate")
+ usable:SetSize(24, 24)
+ usable:SetPoint("TOPLEFT", filterPanel, "TOPLEFT", 10, -72)
+ local usableLabel = filterPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ usableLabel:SetPoint("LEFT", usable, "RIGHT", 2, 0)
+ usableLabel:SetText(AUCTION_HOUSE_FILTER_USABLE_ONLY or USABLE_ITEMS or "Usable Items Only")
+ usable:SetScript("OnClick", function(self)
+ pane.Filter.usable = self:GetChecked() and true or false
+ end)
+
+ local divider2 = filterPanel:CreateTexture(nil, "ARTWORK")
+ divider2:SetTexture("Interface\\Buttons\\WHITE8X8")
+ divider2:SetVertexColor(1, 0.82, 0, 0.25)
+ divider2:SetHeight(1)
+ divider2:SetPoint("TOPLEFT", filterPanel, "TOPLEFT", 12, -104)
+ divider2:SetPoint("TOPRIGHT", filterPanel, "TOPRIGHT", -12, -104)
+
+ local rarityTitle = filterPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ rarityTitle:SetPoint("TOPLEFT", filterPanel, "TOPLEFT", 14, -114)
+ rarityTitle:SetText(RARITY or "Rarity")
+ rarityTitle:SetTextColor(1, 0.82, 0)
+
+ -- Rarity radios (All + Poor..Epic). No dedicated radio-button XML template is trusted here --
+ -- "RefreshButtonTemplate" already proved reference-addon template assumptions don't hold on this
+ -- exact client build (see the detailRefresh comment above), so this reuses the SAME
+ -- UICheckButtonTemplate the Usable Items checkbox above already uses safely, with hand-rolled
+ -- mutual exclusivity instead of relying on a native radio widget.
+ local rarityBtns = {}
+ local function setRarity(val)
+ -- ANY_QUALITY (-1), not 0 -- see the pane.Filter note above. The "All" row used to pass nil
+ -- here, which `or 0` then turned into a Poor-only filter.
+ pane.Filter.quality = val or ANY_QUALITY
+ for _, rb in ipairs(rarityBtns) do rb:SetChecked(rb._val == val) end
+ end
+ local function rarityRow(y, val, label)
+ local rb = CreateFrame("CheckButton", nil, filterPanel, "UICheckButtonTemplate")
+ rb:SetSize(18, 18)
+ rb:SetPoint("TOPLEFT", filterPanel, "TOPLEFT", 12, y)
+ rb._val = val
+ local fs = filterPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ fs:SetPoint("LEFT", rb, "RIGHT", 4, 0)
+ fs:SetText(label)
+ rb:SetScript("OnClick", function() setRarity(val) end)
+ rarityBtns[#rarityBtns + 1] = rb
+ end
+ rarityRow(-134, ANY_QUALITY, ALL or "All")
+ rarityRow(-153, 0, NE.itembtn and NE.itembtn.WrapTextByQuality(_G.ITEM_QUALITY0_DESC or "Poor", 0) or (_G.ITEM_QUALITY0_DESC or "Poor"))
+ rarityRow(-172, 1, NE.itembtn and NE.itembtn.WrapTextByQuality(_G.ITEM_QUALITY1_DESC or "Common", 1) or (_G.ITEM_QUALITY1_DESC or "Common"))
+ rarityRow(-191, 2, NE.itembtn and NE.itembtn.WrapTextByQuality(_G.ITEM_QUALITY2_DESC or "Uncommon", 2) or (_G.ITEM_QUALITY2_DESC or "Uncommon"))
+ rarityRow(-210, 3, NE.itembtn and NE.itembtn.WrapTextByQuality(_G.ITEM_QUALITY3_DESC or "Rare", 3) or (_G.ITEM_QUALITY3_DESC or "Rare"))
+ rarityRow(-229, 4, NE.itembtn and NE.itembtn.WrapTextByQuality(_G.ITEM_QUALITY4_DESC or "Epic", 4) or (_G.ITEM_QUALITY4_DESC or "Epic"))
+ setRarity(ANY_QUALITY)
+
+ filterButton:SetScript("OnClick", function() filterPanel:SetShown(not filterPanel:IsShown()) end)
+
+ -- Click-outside-to-close: a fullscreen catcher one frame level under the panel, same strata so
+ -- it still sits above the rest of the window and intercepts the outside click.
+ local filterCatcher = CreateFrame("Button", nil, filterPanel)
+ filterCatcher:SetPoint("TOPLEFT", UIParent, "TOPLEFT")
+ filterCatcher:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT")
+ filterCatcher:SetFrameStrata("FULLSCREEN_DIALOG")
+ filterCatcher:SetFrameLevel(math.max((filterPanel:GetFrameLevel() or 1) - 1, 0))
+ filterCatcher:EnableMouse(true)
+ filterCatcher:Hide()
+ filterCatcher:SetScript("OnClick", function() filterPanel:Hide() end)
+ filterPanel:HookScript("OnShow", function() filterCatcher:Show() end)
+ filterPanel:HookScript("OnHide", function() filterCatcher:Hide() end)
+ pane:HookScript("OnHide", function() filterPanel:Hide() end)
+
+ local list = CreateFrame("Frame", nil, pane)
+ list:SetPoint("TOPLEFT", pane, "TOPLEFT", 172, -73)
+ -- -5, matching the Sell tab's right-hand panel (Sell.lua's `right`) -- previously -24, which cut
+ -- this panel 19px shorter than Sell's and left its right edge not lining up across tabs.
+ list:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", -5, 27)
+ pane.Results = list
+
+ local listFallback = list:CreateTexture(nil, "BACKGROUND")
+ listFallback:SetTexture("Interface\\Buttons\\WHITE8X8")
+ listFallback:SetVertexColor(0.02, 0.02, 0.025, 0.92)
+ listFallback:SetPoint("TOPLEFT", list, "TOPLEFT", 3, -22)
+ listFallback:SetPoint("BOTTOMRIGHT", list, "BOTTOMRIGHT", -22, 0)
+
+ local listBg = list:CreateTexture(nil, "BACKGROUND")
+ local appliedIndexAtlas = false
+ if NE.tex and NE.tex.SetAtlas then
+ appliedIndexAtlas = NE.tex.SetAtlas(listBg, "auctionhouse-background-index", false) and true or false
+ end
+ if appliedIndexAtlas then
+ listBg:SetPoint("TOPLEFT", list, "TOPLEFT", 3, -22)
+ listBg:SetPoint("BOTTOMRIGHT", list, "BOTTOMRIGHT", -22, 0)
+ else
+ listBg:SetTexture(nil)
+ end
+
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, list, 0, -19, -22, 0)
+ end
+
+ local headerStrip = list:CreateTexture(nil, "BORDER")
+ headerStrip:SetTexture("Interface\\Buttons\\WHITE8X8")
+ headerStrip:SetVertexColor(0.06, 0.06, 0.07, 0.95)
+ headerStrip:SetPoint("TOPLEFT", list, "TOPLEFT", 3, -1)
+ headerStrip:SetPoint("TOPRIGHT", list, "TOPRIGHT", -22, -1)
+ headerStrip:SetHeight(21)
+
+ -- Sortable column headers (issue #17: no way to sort results by price/quantity). Each header is
+ -- a click target: first click sorts the aggregated results by that column ascending, second click
+ -- flips direction. Sorting happens client-side over the fully-scanned aggregate (sortDisplayRows,
+ -- defined with the paging helpers below and forward-declared here because the header buttons are
+ -- built before the data pipeline exists).
+ local sortKey = nil
+ local sortAsc = true
+ local applySort -- assigned below once the paging helpers it drives exist
+ local headerButtons = {}
+
+ -- Right-anchored header `x` values are chosen so the header text's right edge lands exactly on its
+ -- column's right edge in the rows below: rows are inset -30 from `list`, their own content another
+ -- -8 (or -96 for Lvl), so -38 / -126 here. ("Available" previously sat at -80, floating 42px left
+ -- of the numbers it labels -- with a Lvl column now next to it that gap reads as a misprint.)
+ local headers = {
+ { text = AUCTION_HOUSE_BROWSE_HEADER_PRICE or "Price", x = 10, w = 120, just = "LEFT", key = "price" },
+ { text = AUCTION_HOUSE_BROWSE_HEADER_NAME or NAME or "Name", x = 180, w = 260, just = "LEFT", key = "name" },
+ -- "Lvl" (the stock AH's own name for the required-level column) rather than a spelled-out
+ -- "Req. Level": the same column has to fit the much tighter item-detail header strip below, and
+ -- one label across both views beats a wide one here and an abbreviation there.
+ { text = "Lvl", x = -126, w = 64, just = "RIGHT", right = true, key = "level" },
+ { text = AUCTION_HOUSE_BROWSE_HEADER_QUANTITY or "Available", x = -38, w = 72, just = "RIGHT", right = true, key = "qty" },
+ }
+
+ for i = 1, #headers do
+ local h = headers[i]
+ local hb = CreateFrame("Button", nil, list)
+ hb:SetSize(h.w, 21)
+ if h.right then
+ hb:SetPoint("TOPRIGHT", list, "TOPRIGHT", h.x, -1)
+ else
+ hb:SetPoint("TOPLEFT", list, "TOPLEFT", h.x, -1)
+ end
+
+ local fs = hb:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ fs:SetPoint("LEFT", hb, "LEFT", 0, 0)
+ fs:SetWidth(h.w)
+ fs:SetJustifyH(h.just)
+ fs:SetText(h.text)
+ hb.Label = fs
+
+ -- Same sort-direction arrow sheet the stock Who/Guild column headers use; up = ascending.
+ local arrow = hb:CreateTexture(nil, "OVERLAY")
+ arrow:SetTexture("Interface\\Buttons\\UI-SortArrow")
+ arrow:SetSize(9, 8)
+ arrow:Hide()
+ hb.Arrow = arrow
+
+ hb:SetHighlightTexture("Interface\\Buttons\\WHITE8X8")
+ local hhl = hb:GetHighlightTexture()
+ if hhl then
+ hhl:SetVertexColor(1, 0.82, 0, 0.08)
+ hhl:SetBlendMode("ADD")
+ end
+
+ hb._sortKey = h.key
+ hb._just = h.just
+ hb:SetScript("OnClick", function(self)
+ if applySort then applySort(self._sortKey) end
+ end)
+ headerButtons[#headerButtons + 1] = hb
+ end
+
+ local function updateHeaderArrows()
+ for i = 1, #headerButtons do
+ local hb = headerButtons[i]
+ if hb._sortKey == sortKey then
+ -- Positioned off the label's live string width so the arrow hugs the text no matter the
+ -- localized header length. Right-justified labels get the arrow on their left instead.
+ local tw = (hb.Label.GetStringWidth and hb.Label:GetStringWidth()) or 40
+ hb.Arrow:ClearAllPoints()
+ if hb._just == "RIGHT" then
+ hb.Arrow:SetPoint("RIGHT", hb, "RIGHT", -(tw + 4), 0)
+ else
+ hb.Arrow:SetPoint("LEFT", hb, "LEFT", math.min(tw + 3, hb:GetWidth() - 9), 0)
+ end
+ if sortAsc then
+ hb.Arrow:SetTexCoord(0, 0.5625, 1.0, 0)
+ else
+ hb.Arrow:SetTexCoord(0, 0.5625, 0, 1.0)
+ end
+ hb.Arrow:Show()
+ else
+ hb.Arrow:Hide()
+ end
+ end
+ end
+
+ local empty = list:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ empty:SetPoint("CENTER", list, "CENTER", 0, 44)
+ empty:SetText("Choose search criteria and press \"Search\"")
+ empty:SetTextColor(1, 0.82, 0)
+ pane.EmptyText = empty
+
+ local RESULTS_TOP = -24
+ local ROW_H = 20
+
+ local rows = {}
+ local rowsHost = CreateFrame("Frame", nil, list)
+ rowsHost:SetPoint("TOPLEFT", list, "TOPLEFT", 0, 0)
+ rowsHost:SetPoint("BOTTOMRIGHT", list, "BOTTOMRIGHT", 0, 0)
+ rowsHost:SetFrameStrata("DIALOG")
+ rowsHost:SetFrameLevel((list:GetFrameLevel() or 1) + 20)
+
+ -- FauxScrollFrameTemplate's handlers resolve the scrollbar's sub-widgets via frame:GetName()
+ -- string concatenation, NOT parentKey -- an anonymous (nil-named) FauxScrollFrame throws
+ -- "attempt to concatenate ... (a nil value)" the moment FauxScrollFrame_Update runs on it. THIS
+ -- was the actual reason the Buy tab got stuck on "Searching..." forever: refreshResults() called
+ -- FauxScrollFrame_Update on this nameless frame, the resulting error aborted refreshResults()
+ -- before it ever reached the empty:Hide()/row-population code below it, even though the data
+ -- (listRows) was already correctly populated by that point.
+ local scroll = CreateFrame("ScrollFrame", "NE_AuctionHouseBrowseResultsScroll", list, "FauxScrollFrameTemplate")
+ scroll:SetPoint("TOPLEFT", list, "TOPLEFT", 2, RESULTS_TOP)
+ scroll:SetPoint("BOTTOMRIGHT", list, "BOTTOMRIGHT", -26, 4)
+ local resultsCustomBar
+
+ -- Physical scrollbar track+thumb -- FauxScrollFrameTemplate alone draws nothing visible, just
+ -- offset math plus a hidden slider. Sits in the 26px gutter reserved above, so it never overlaps
+ -- the DIALOG-strata rows (they're inset -30 from list's right edge, see ensureRow below).
+ -- BuildCustom's bar defaults to "HIGH" strata, but rowsHost (the actual row buttons, below) is
+ -- explicitly "DIALOG" so it draws above the plain-strata list background -- that puts the rows
+ -- ABOVE a HIGH-strata scrollbar too, hiding it entirely. Same trap already fixed for the item-
+ -- detail scroll bar (see detailScroll below); force this bar to match.
+ if NE.scrollbar and NE.scrollbar.BuildCustom then
+ local ok, bar = pcall(NE.scrollbar.BuildCustom, scroll, { x = -8, alwaysShow = true })
+ if ok and bar then
+ resultsCustomBar = bar
+ bar:SetFrameStrata("DIALOG")
+ bar:SetFrameLevel((rowsHost:GetFrameLevel() or 1) + 10)
+ -- See the matching comment on the category-list scrollbar above -- the arrow buttons need
+ -- the same post-hoc strata promotion the bar itself gets, or they stay stuck at "HIGH".
+ if bar._upBtn then bar._upBtn:SetFrameStrata("DIALOG"); bar._upBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ if bar._downBtn then bar._downBtn:SetFrameStrata("DIALOG"); bar._downBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ end
+ end
+
+ local function setResultsScrollbarShown(shown)
+ if not resultsCustomBar then return end
+ if shown then
+ resultsCustomBar:Show()
+ if resultsCustomBar._upBtn then resultsCustomBar._upBtn:Show() end
+ if resultsCustomBar._downBtn then resultsCustomBar._downBtn:Show() end
+ else
+ resultsCustomBar:Hide()
+ if resultsCustomBar._upBtn then resultsCustomBar._upBtn:Hide() end
+ if resultsCustomBar._downBtn then resultsCustomBar._downBtn:Hide() end
+ end
+ end
+
+ local function setBrowseResultsShown(shown)
+ if shown then
+ list:Show()
+ else
+ list:Hide()
+ end
+ setResultsScrollbarShown(shown)
+ end
+
+ local function fsUpdate(frame, total, shown, step)
+ if FauxScrollFrame_Update then
+ FauxScrollFrame_Update(frame, total, shown, step)
+ else
+ frame.offset = frame.offset or 0
+ end
+ end
+
+ local function fsGetOffset(frame)
+ if FauxScrollFrame_GetOffset then
+ return FauxScrollFrame_GetOffset(frame) or 0
+ end
+ return frame.offset or 0
+ end
+
+ local function moneyText(copper)
+ if not copper or copper <= 0 then return "-" end
+ if GetCoinTextureString then
+ return GetCoinTextureString(copper)
+ end
+ return tostring(copper)
+ end
+
+ -- Required-level cell for both the aggregate results list and the item-detail listing rows.
+ -- Returns text + colour: "-" for the level-1 / no-requirement case (same "nothing to show here"
+ -- glyph moneyText uses), and red when the requirement is above the player's own level, matching
+ -- the stock Auction House's Lvl column.
+ local function levelText(level)
+ if not level or level <= 1 then return "-", 1, 1, 1 end
+ local playerLevel = UnitLevel and UnitLevel("player") or 0
+ if playerLevel > 0 and level > playerLevel then
+ local c = RED_FONT_COLOR
+ if c then return tostring(level), c.r, c.g, c.b end
+ return tostring(level), 1, 0.1, 0.1
+ end
+ return tostring(level), 1, 1, 1
+ end
+
+ local listRows = {} -- every auction the scan collected, across all server pages
+ local listTotal = 0 -- auctions the server says match, across all server pages
+ local displayRows = {} -- listRows collapsed to one entry per item name
+ local pageRows = {} -- the slice of displayRows currently drawn
+
+ -- Page navigation -- Prev/Next + a "Page N of M" label in the gutter under the results list, using
+ -- the same proven-safe UIPanelButtonTemplate the Search/Filter buttons above already use.
+ --
+ -- These paginate the aggregated ITEM rows, NOT the server's auction pages. A search used to map
+ -- one server page onto one displayed page, which mixed two different units: QueryAuctionItems
+ -- returns up to NUM_AUCTION_ITEMS_PER_PAGE (50) AUCTIONS, but buildDisplayRows collapses those by
+ -- item name, so 50 auctions of 8 distinct glyphs drew 8 rows next to "of 6 pages" and read like 42
+ -- rows went missing -- and each row's "cheapest" price was only the cheapest on that one page, not
+ -- the market's. runSearch now scans every server page up front (see queryPage/collectScanPage) and
+ -- paginates the aggregate client-side, so both numbers are in items and the prices are real minima.
+ local ITEMS_PER_PAGE = 50
+ local currentPage = 0
+ local lastQuery = nil
+
+ -- Scan state. MAX_SCAN_PAGES bounds a pathological search (an empty-text browse of a busy AH can
+ -- match tens of thousands of auctions); scanTruncated records that we stopped early so the label
+ -- can say so rather than quietly presenting a partial aggregate as the whole market.
+ local MAX_SCAN_PAGES = 20
+ local scanning = false
+ local scanPage = 0
+ local scanPages = 1
+ local scanTruncated = false
+ -- True from the moment a page is requested until its auctions are banked into listRows. Both the
+ -- AUCTION_ITEM_LIST_UPDATE event and a poll timer can reach collectScanPage for the SAME page, so
+ -- without this whichever arrived second would append that page's auctions a second time -- the
+ -- aggregate would double every quantity and the auction count would exceed the server's total.
+ local scanPending = false
+ -- Bumped whenever the search is replaced or cleared. A scan spans many queries and timer
+ -- callbacks, so every one of them carries the token it started under and bails if it no longer
+ -- matches -- otherwise a retry still pending from an abandoned scan would resume into the NEXT
+ -- search and splice a stale page's auctions into its aggregate.
+ local scanToken = 0
+
+ local pagePrev = CreateFrame("Button", nil, pane, "UIPanelButtonTemplate")
+ pagePrev:SetSize(24, 20)
+ pagePrev:SetText("<")
+ pagePrev:SetPoint("BOTTOMLEFT", list, "BOTTOMLEFT", 3, -24)
+ pagePrev:Disable()
+
+ local pageNext = CreateFrame("Button", nil, pane, "UIPanelButtonTemplate")
+ pageNext:SetSize(24, 20)
+ pageNext:SetText(">")
+ pageNext:SetPoint("LEFT", pagePrev, "RIGHT", 4, 0)
+ pageNext:Disable()
+
+ local pageLabel = pane:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ pageLabel:SetPoint("LEFT", pageNext, "RIGHT", 8, 0)
+ pageLabel:SetText("Page 1 of 1")
+
+ local function totalDisplayPages()
+ return math.max(1, math.ceil(#displayRows / ITEMS_PER_PAGE))
+ end
+
+ local function updatePageControls()
+ local totalItems = #displayRows
+ local totalPages = totalDisplayPages()
+
+ -- Prev/Next stay disabled mid-scan: displayRows is still growing a page at a time, so the page
+ -- count it implies right now is not the final one.
+ if scanning then
+ pageLabel:SetText(string.format("Scanning page %d of %d...",
+ math.min(scanPage + 1, scanPages), scanPages))
+ pagePrev:Disable()
+ pageNext:Disable()
+ return
+ end
+
+ if totalItems <= 0 then
+ pageLabel:SetText("Page 1 of 1")
+ else
+ local first = currentPage * ITEMS_PER_PAGE + 1
+ local last = math.min(first + ITEMS_PER_PAGE - 1, totalItems)
+ pageLabel:SetText(string.format("Page %d of %d (items %d-%d of %d, from %d auction%s%s)",
+ currentPage + 1, totalPages, first, last, totalItems,
+ listTotal, listTotal == 1 and "" or "s",
+ scanTruncated and " -- partial scan" or ""))
+ end
+
+ if currentPage <= 0 then pagePrev:Disable() else pagePrev:Enable() end
+ if currentPage + 1 >= totalPages then pageNext:Disable() else pageNext:Enable() end
+ end
+
+ -- "browse" (aggregate results list, below) or "itembuy" (the per-item drill-down built near the
+ -- end of this function). Both share the client's single "list" query slot and its
+ -- AUCTION_ITEM_LIST_UPDATE event, so this flag routes that shared watcher to the right refresher
+ -- -- opening the drill-down re-queries "list" scoped to one item, which would otherwise silently
+ -- clobber the aggregate browse rows with that one item's data.
+ local activeQuery = "browse"
+ -- Forward decls: assigned once the drill-down widgets exist, near the end of this function, but
+ -- referenced earlier (the aggregate row's OnClick, the shared watcher below).
+ local openItemDetail
+ local refreshDetailRows
+
+ -- Switching away from Buy (Sell/Auctions tab, or closing the window) while the item drill-down
+ -- is open would otherwise leave activeQuery == "itembuy" and keep routing the shared
+ -- AUCTION_ITEM_LIST_UPDATE watcher away from the browse list on return.
+ pane:HookScript("OnHide", function()
+ activeQuery = "browse"
+ if pane.ItemDetail then pane.ItemDetail:Hide() end
+ setBrowseResultsShown(true)
+ end)
+
+ local function getListCounts()
+ if type(GetNumAuctionItems) ~= "function" then
+ return 0, 0
+ end
+ local batch, total = GetNumAuctionItems("list")
+ batch = (type(batch) == "number" and batch) or 0
+ total = (type(total) == "number" and total) or batch
+ if total < batch then
+ total = batch
+ end
+ return batch, total
+ end
+
+ -- Read whatever the client currently has in its "list" slot -- i.e. ONE server page of auctions.
+ -- Pure: the caller (collectScanPage) owns listRows/listTotal, because a search accumulates many
+ -- of these pages before anything is drawn.
+ local function capturePageAuctions()
+ local rows = {}
+ local batchCount, totalCount = getListCounts()
+
+ local cap = batchCount
+ if cap <= 0 then
+ cap = NUM_AUCTION_ITEMS_PER_PAGE or 50
+ end
+
+ for index = 1, cap do
+ -- Field order per THIS server's own bundled APIDocumentation addon (13 return values, no
+ -- "levelColumnName" slot the generic retail/Wowpedia signature has): name, texture, count,
+ -- quality, canUse, level, minBid, minIncrement, buyoutPrice, bidAmount, highestBidder, owner,
+ -- sold. minBid is position 7 and buyoutPrice is position 9 on THIS client.
+ -- `level` (slot 6) is the item's REQUIRED level on this client, not its item level -- it's what
+ -- the stock AH's own "Lvl" column shows.
+ local name, texture, count, quality, _, level, minBid, _, buyoutPrice = GetAuctionItemInfo("list", index)
+ if not name then
+ break
+ end
+ local link = GetAuctionItemLink and GetAuctionItemLink("list", index)
+ -- No auction index is kept: it only addresses the page currently in the client's "list" slot,
+ -- and these rows outlive that page (they're accumulated across the whole scan). Acting on a
+ -- specific listing goes through the drill-down, which re-queries "list" for one item.
+ rows[#rows + 1] = {
+ name = name,
+ texture = texture,
+ quality = quality,
+ count = count,
+ level = level,
+ minBid = minBid,
+ buyoutPrice = buyoutPrice,
+ link = link,
+ }
+ end
+
+ return rows, batchCount, totalCount
+ end
+
+ -- Group the raw per-auction rows above by item name into one row per item (icon, quality-colored
+ -- name, cheapest current price, total quantity across every auction of that item) -- matching the
+ -- reference's aggregated Browse view. Clicking an aggregate row re-queries and drills into the
+ -- individual auctions (openItemDetail, near the end of this function). listRows spans every server
+ -- page by the time the scan finishes, so the price here is the whole result set's minimum.
+ local function buildDisplayRows()
+ local groups = {}
+ local order = {}
+ for i = 1, #listRows do
+ local r = listRows[i]
+ local g = groups[r.name]
+ if not g then
+ g = { name = r.name, texture = r.texture, quality = r.quality, count = 0 }
+ groups[r.name] = g
+ order[#order + 1] = g
+ end
+ g.link = g.link or r.link
+ -- Required level is a property of the item, so it's uniform across a group by construction --
+ -- first non-nil wins (the server can leave it nil on a row it hasn't filled in yet).
+ g.level = g.level or r.level
+ g.count = g.count + (r.count or 1)
+ if r.buyoutPrice and r.buyoutPrice > 0 then
+ if not g.minBuyout or r.buyoutPrice < g.minBuyout then g.minBuyout = r.buyoutPrice end
+ end
+ if r.minBid and r.minBid > 0 then
+ if not g.minBid or r.minBid < g.minBid then g.minBid = r.minBid end
+ end
+ end
+ displayRows = order
+ end
+
+ -- Slice displayRows down to the page the pager is on. Call after every buildDisplayRows and after
+ -- any currentPage change; clamps currentPage so a shrinking result set can't strand the pager past
+ -- the last page.
+ local function buildPageRows()
+ local totalPages = totalDisplayPages()
+ if currentPage < 0 then currentPage = 0 end
+ if currentPage > totalPages - 1 then currentPage = totalPages - 1 end
+
+ local slice = {}
+ local first = currentPage * ITEMS_PER_PAGE + 1
+ local last = math.min(first + ITEMS_PER_PAGE - 1, #displayRows)
+ for i = first, last do
+ slice[#slice + 1] = displayRows[i]
+ end
+ pageRows = slice
+ end
+
+ -- Fill the entire scroll viewport instead of a hardcoded row count -- previously a fixed 17 rows
+ -- left most of the panel's actual (much taller) height as dead space below the last row, same bug
+ -- already fixed on the item-detail rows below (see detailVisibleRows).
+ local function resultsVisibleRows()
+ local h = scroll:GetHeight() or 0
+ return math.max(1, math.floor(h / (ROW_H + 1)))
+ end
+
+ local function ensureRow(i)
+ local row = rows[i]
+ if row then return row end
+
+ row = CreateFrame("Button", nil, rowsHost)
+ row:SetFrameStrata("DIALOG")
+ row:SetHeight(ROW_H)
+ row:SetFrameLevel((list:GetFrameLevel() or 1) + 10)
+ if i == 1 then
+ row:SetPoint("TOPLEFT", list, "TOPLEFT", 6, RESULTS_TOP)
+ row:SetPoint("TOPRIGHT", list, "TOPRIGHT", -30, RESULTS_TOP)
+ else
+ row:SetPoint("TOPLEFT", rows[i - 1], "BOTTOMLEFT", 0, -1)
+ row:SetPoint("TOPRIGHT", rows[i - 1], "BOTTOMRIGHT", 0, -1)
+ end
+
+ local rowBg = row:CreateTexture(nil, "BACKGROUND")
+ rowBg:SetTexture("Interface\\Buttons\\WHITE8X8")
+ rowBg:SetAllPoints(row)
+ rowBg:SetVertexColor(0.07, 0.07, 0.08, (i % 2 == 0) and 0.30 or 0.20)
+ row.Bg = rowBg
+
+ row:SetHighlightTexture("Interface\\Buttons\\WHITE8X8")
+ local hl = row:GetHighlightTexture()
+ if hl then
+ hl:SetVertexColor(1, 0.82, 0, 0.12)
+ hl:SetBlendMode("ADD")
+ end
+
+ local price = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ price:SetPoint("LEFT", row, "LEFT", 4, 0)
+ price:SetWidth(164)
+ price:SetJustifyH("LEFT")
+ row.Price = price
+
+ local icon = row:CreateTexture(nil, "ARTWORK")
+ icon:SetSize(16, 16)
+ icon:SetPoint("LEFT", row, "LEFT", 174, 0)
+ icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ row.Icon = icon
+
+ local name = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ name:SetPoint("LEFT", icon, "RIGHT", 4, 0)
+ name:SetPoint("RIGHT", row, "RIGHT", -148, 0)
+ name:SetJustifyH("LEFT")
+ row.Name = name
+
+ local level = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ level:SetPoint("RIGHT", row, "RIGHT", -96, 0)
+ level:SetWidth(44)
+ level:SetJustifyH("RIGHT")
+ row.Level = level
+
+ local qty = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ qty:SetPoint("RIGHT", row, "RIGHT", -8, 0)
+ qty:SetWidth(80)
+ qty:SetJustifyH("RIGHT")
+ row.Qty = qty
+
+ -- Click drills into this item's individual auctions (the ItemBuy-style page built near the end
+ -- of this function), matching the reference's two-tier Browse -> item-detail flow. These rows
+ -- are aggregated across auctions, so show a normal item tooltip from the item link rather than
+ -- an auction-index tooltip bound to one specific listing.
+ row:RegisterForClicks("LeftButtonUp")
+ row:SetScript("OnClick", function(self)
+ -- SHIFT links the item in chat, CTRL dresses it up; otherwise drill into the item.
+ if self._data and self._data.link and AH.HandleItemClick(self._data.link) then return end
+ if openItemDetail then openItemDetail(self._data) end
+ end)
+ -- WireLiveTooltip, not a bare OnEnter: the tooltip has to keep refreshing while the mouse sits
+ -- still, or pressing SHIFT after hovering never brings up the item comparison (it only worked
+ -- if SHIFT was already held on mouse-over). Same stock UpdateTooltip contract bag buttons use.
+ local function rowTooltip(self)
+ if not (self._data and self._data.name) then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ local shown = self._data.link and pcall(GameTooltip.SetHyperlink, GameTooltip, self._data.link)
+ if not shown then
+ GameTooltip:ClearLines()
+ GameTooltip:AddLine(self._data.name, 1, 1, 1)
+ end
+ GameTooltip:Show()
+ end
+ if NE.FrameUtil and NE.FrameUtil.WireLiveTooltip then
+ NE.FrameUtil.WireLiveTooltip(row, rowTooltip)
+ else
+ row:SetScript("OnEnter", rowTooltip)
+ row:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ end
+
+ rows[i] = row
+ return row
+ end
+
+ local function refreshResults()
+ local drawCount = #pageRows
+ local visibleRows = resultsVisibleRows()
+ fsUpdate(scroll, drawCount, visibleRows, ROW_H + 1)
+ syncAlwaysShowFauxBar(scroll, drawCount, visibleRows)
+ local offset = fsGetOffset(scroll)
+ local maxOffset = drawCount - visibleRows
+ if maxOffset < 0 then maxOffset = 0 end
+ if offset > maxOffset then offset = maxOffset end
+
+ if drawCount > 0 then
+ empty:Hide()
+ else
+ empty:Show()
+ if scanning then
+ empty:SetText("Searching...")
+ elseif listTotal > 0 then
+ empty:SetText("Loading results...")
+ else
+ empty:SetText("No results. Adjust filters and search again.")
+ end
+ end
+
+ for i = 1, visibleRows do
+ local row = ensureRow(i)
+ local data = pageRows[i + offset]
+ if data then
+ row._data = data
+ row.Price:SetText(moneyText((data.minBuyout and data.minBuyout > 0) and data.minBuyout or data.minBid or 0))
+ row.Name:SetText(data.name or "?")
+ if data.quality and data.quality > 1 and GetItemQualityColor then
+ local r, g, b = GetItemQualityColor(data.quality)
+ row.Name:SetTextColor(r, g, b)
+ else
+ row.Name:SetTextColor(1, 1, 1)
+ end
+ if row.Icon then
+ row.Icon:SetTexture(data.texture or "Interface\\Icons\\INV_Misc_QuestionMark")
+ end
+ local lvText, lr, lg, lb = levelText(data.level)
+ row.Level:SetText(lvText)
+ row.Level:SetTextColor(lr, lg, lb)
+ row.Qty:SetText(data.count and tostring(data.count) or "1")
+ row:Show()
+ else
+ row._data = nil
+ row:Hide()
+ end
+ end
+ -- Hide any previously-built rows beyond the current viewport (only relevant if the panel's
+ -- effective height ever shrinks between refreshes; harmless no-op otherwise).
+ for i = visibleRows + 1, #rows do
+ local row = rows[i]
+ row._data = nil
+ row:Hide()
+ end
+ end
+
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ if FauxScrollFrame_OnVerticalScroll then
+ FauxScrollFrame_OnVerticalScroll(self, offset, ROW_H + 1, refreshResults)
+ else
+ local step = ROW_H + 1
+ local nextOffset = math.floor((offset / step) + 0.5)
+ if nextOffset < 0 then nextOffset = 0 end
+ self.offset = nextOffset
+ refreshResults()
+ end
+ end)
+
+ -- Ask the server for one server page of the last search, reusing the filter/category params
+ -- runSearch captured into `lastQuery`. This is a step of the scan, not a user-facing pager action:
+ -- collectScanPage (further down, driven by AUCTION_ITEM_LIST_UPDATE) calls it again for the next
+ -- page until every page is in, then the aggregate is drawn and Prev/Next page through THAT.
+ local queryPage
+
+ -- Abort the scan. Anything already collected stays on screen -- a partial aggregate still beats an
+ -- empty list -- but scanTruncated makes the pager label admit it's partial, since these rows'
+ -- prices are only minima across the pages we actually got.
+ local function abortScan(message)
+ scanning = false
+ scanPending = false
+ scanTruncated = true
+ if #pageRows > 0 then
+ if NE.Log then NE.Log("AH", "browse scan aborted: " .. tostring(message)) end
+ else
+ scanTruncated = false
+ empty:SetText(message)
+ empty:Show()
+ end
+ updatePageControls()
+ end
+
+ -- Tear down an INTERRUPTED scan. ISSUE #31 ("the addon automatically triggers the search and
+ -- refresh buttons without user input").
+ --
+ -- collectScanPage bails early when the pane is hidden (tab switch) or the item drill-down has
+ -- taken over activeQuery -- both before it reaches `scanPending = false`. That left scanning and
+ -- scanPending set with scanToken unchanged, and nothing else ever cleared them: runSearch and
+ -- resetSearch are the only other writers and both need a user action. So the dead scan sat armed,
+ -- and the next AUCTION_ITEM_LIST_UPDATE from ANY source -- the Sell tab's market lookup,
+ -- Auctionator, the stock UI -- got accepted as its next page: it banked whatever rows were in the
+ -- shared "list" slot into our aggregate, then chained queryPage for another server page. That is
+ -- the search firing with nobody pressing anything (and the drill-down's mirror of it at
+ -- refreshDetailRows is the Refresh button appearing to press itself).
+ --
+ -- The query arbiter above already stops us acting on updates we didn't cause; this closes the
+ -- hole from the other side so there is no stranded state left for even our OWN events to revive.
+ -- Deliberately redundant: this state machine has leaked once already.
+ local function cancelScan()
+ if not (scanning or scanPending) then return end
+ scanning = false
+ scanPending = false
+ scanToken = scanToken + 1 -- orphan any in-flight retry/poll callbacks
+ -- Whatever pages did land stay on screen, but the pager has to admit the aggregate is partial:
+ -- these rows' prices are only minima across the pages we actually got.
+ if #pageRows > 0 then scanTruncated = true end
+ updatePageControls()
+ end
+
+ -- Window.lua hides the inactive pane on a tab switch, which is the other way collectScanPage
+ -- bails out mid-scan. Same teardown.
+ pane:HookScript("OnHide", cancelScan)
+
+ -- Lets the Sell tab hold its market lookup while a scan is paging (see Sell.lua queryMarket):
+ -- a query fired into the middle of a scan steals the shared throttle slot and makes queryPage
+ -- burn its retries and abortScan.
+ function AH.IsBrowseScanning()
+ return scanning and true or false
+ end
+
+ -- The core rate-limits back-to-back list queries, and a scan issues one per server page. Wait out
+ -- a busy slot rather than abandoning the scan mid-way, which would leave a partial aggregate whose
+ -- prices silently aren't market minima.
+ local scanRetries = 0
+ local MAX_SCAN_RETRIES = 50 -- ~10s at 0.2s steps
+
+ queryPage = function(page, token)
+ if token ~= scanToken then return end
+ if not lastQuery then return end
+
+ if not (CanSendAuctionQuery and CanSendAuctionQuery("list")) then
+ scanRetries = scanRetries + 1
+ if scanRetries <= MAX_SCAN_RETRIES and C_Timer and C_Timer.After then
+ C_Timer.After(0.2, function() queryPage(page, token) end)
+ else
+ abortScan("Auction query is throttled. Try again in a moment.")
+ end
+ return
+ end
+ scanRetries = 0
+
+ scanning = true
+ scanPage = page
+
+ local q = lastQuery
+ AH.ClaimListQuery("browse")
+ local ok, err = pcall(QueryAuctionItems, q.text, q.minLevel, q.maxLevel, q.invTypeIndex, q.classIndex, q.subClassIndex, page, q.isUsable, q.quality, false)
+ if not ok then
+ -- Surface the real Lua error instead of a generic message -- if this still isn't the right
+ -- call shape, the exact "bad argument #N" text tells us which slot to fix next.
+ if NE.Log then NE.Log("AH", "QueryAuctionItems error: " .. tostring(err)) end
+ abortScan("Search failed: " .. tostring(err))
+ return
+ end
+ scanPending = true
+ updatePageControls()
+ end
+
+ local function runSearch()
+ -- Fresh scan from page 0: drop everything the previous search accumulated.
+ listRows = {}
+ listTotal = 0
+ displayRows = {}
+ pageRows = {}
+ currentPage = 0
+ scanPage = 0
+ scanPages = 1
+ scanTruncated = false
+ scanToken = scanToken + 1
+ scanPending = false
+ -- Mark the scan live before the first draw below, so an empty list reads "Searching..." and not
+ -- "No results" while queryPage is still waiting out a throttled query slot.
+ scanning = true
+ -- A fresh browse search always returns to the aggregate list -- close any open item drill-down
+ -- so it can't keep routing the shared "list" query event and show a stale item's rows.
+ activeQuery = "browse"
+ if pane.ItemDetail then pane.ItemDetail:Hide() end
+ setBrowseResultsShown(true)
+
+ local q = searchBox:GetText() or ""
+ q = string.gsub(q, "^%s+", "")
+ q = string.gsub(q, "%s+$", "")
+
+ -- 3.3.5a's QueryAuctionItems signature, confirmed against the stock client's own FrameXML
+ -- (Blizzard_AuctionUI.lua AuctionFrameBrowse_SearchHelper): name, minLevel, maxLevel,
+ -- invTypeIndex, classIndex, subClassIndex, page, isUsable, quality[, getAll]. Every one of
+ -- those except getAll takes a concrete type (number/luaIndex/bool), so pass real typed values
+ -- rather than nil, or a strict argument check on this core can reject the call outright.
+ --
+ -- The class/subclass/invtype slots are luaIndex values (1-based positions in
+ -- GetAuctionItemClasses() etc.), so 0 correctly means "no filter" -- the client maps it to the
+ -- server's 0xffffffff "any" sentinel. `quality` is NOT an index: it's a raw quality value that
+ -- goes to the wire as-is, so its "any" sentinel has to be written out as -1. That asymmetry is
+ -- exactly what issue #31 tripped over.
+ local filter = pane.Filter or {}
+ local minLevel = filter.minLevel or 0
+ local maxLevel = filter.maxLevel or 0
+ local isUsable = filter.usable and true or false
+ -- ANY_QUALITY (-1) is the "no rarity filter" sentinel; 0 would mean "exactly Poor". Issue #31.
+ local quality = filter.quality or ANY_QUALITY
+
+ -- classIndex/subClassIndex/invTypeIndex from the left category tree (0/0/0 == no category
+ -- filter). These are POSITIONAL luaIndex values into GetAuctionItemClasses()/
+ -- GetAuctionItemSubClasses()/GetAuctionInvTypes(), which is exactly what pane.CategoryList
+ -- tracks a click against -- see buildCategoryList's getClassNames/getSubClassNames/getInvTypes.
+ -- Previously hardcoded to 0,0,0 here, so clicking a category never filtered.
+ local classIndex, subClassIndex, invTypeIndex = 0, 0, 0
+ if pane.CategoryList and pane.CategoryList.GetSelectedIndices then
+ classIndex, subClassIndex, invTypeIndex = pane.CategoryList:GetSelectedIndices()
+ end
+
+ lastQuery = {
+ text = q, minLevel = minLevel, maxLevel = maxLevel, invTypeIndex = invTypeIndex,
+ classIndex = classIndex, subClassIndex = subClassIndex, isUsable = isUsable, quality = quality,
+ }
+ refreshResults()
+ queryPage(0, scanToken)
+ end
+
+ -- Scroll the results list back to the top. The bar the user sees is our own custom one, but the
+ -- offset refreshResults reads comes from the Faux slider underneath it (same lookup as
+ -- syncAlwaysShowFauxBar), so that's what has to be driven.
+ local function resetResultsScroll()
+ if not scroll then return end
+ if FauxScrollFrame_SetOffset then
+ FauxScrollFrame_SetOffset(scroll, 0)
+ else
+ scroll.offset = 0
+ end
+ local sliderName = scroll.GetName and scroll:GetName()
+ local slider = (sliderName and _G[sliderName .. "ScrollBar"]) or scroll.ScrollBar or scroll.scrollBar
+ if slider and slider.SetValue then slider:SetValue(0) end
+ end
+
+ -- Pure client-side paging over the already-scanned aggregate: no re-query, so no throttle and no
+ -- wait. Guarded against running mid-scan because displayRows is still growing then, so the slice
+ -- taken here would be re-sliced from different content a moment later.
+ local function showPage(page)
+ if scanning then return end
+ currentPage = page
+ buildPageRows()
+ resetResultsScroll()
+ refreshResults()
+ updatePageControls()
+ end
+
+ pagePrev:SetScript("OnClick", function()
+ if currentPage > 0 then showPage(currentPage - 1) end
+ end)
+ pageNext:SetScript("OnClick", function()
+ if currentPage + 1 < totalDisplayPages() then showPage(currentPage + 1) end
+ end)
+
+ -- The price a row DISPLAYS (min buyout, else min bid) is also the price it sorts by, so what the
+ -- user sees in the Price column is exactly what the column orders on. nil = no price shown ("-").
+ local function effectiveItemPrice(g)
+ if g.minBuyout and g.minBuyout > 0 then return g.minBuyout end
+ if g.minBid and g.minBid > 0 then return g.minBid end
+ return nil
+ end
+
+ -- In-place sort of the full aggregate (all pages of it) by the active header column. Called
+ -- after every buildDisplayRows so a mid-scan redraw and a finished scan both come out ordered.
+ local function sortDisplayRows()
+ if not sortKey then return end
+ local key, asc = sortKey, sortAsc
+ table.sort(displayRows, function(a, b)
+ local av, bv
+ if key == "price" then
+ av, bv = effectiveItemPrice(a), effectiveItemPrice(b)
+ -- Priceless rows ("-") always sink to the bottom, whichever direction is active.
+ if (av ~= nil) ~= (bv ~= nil) then return av ~= nil end
+ if av == nil then av, bv = 0, 0 end
+ elseif key == "qty" then
+ av, bv = a.count or 0, b.count or 0
+ elseif key == "level" then
+ av, bv = a.level or 0, b.level or 0
+ else
+ av, bv = a.name or "", b.name or ""
+ end
+ if av ~= bv then
+ if asc then return av < bv else return av > bv end
+ end
+ -- Equal keys: fall back to name ascending (returns false on full ties, keeping the
+ -- comparator a valid strict order for table.sort).
+ return (a.name or "") < (b.name or "")
+ end)
+ end
+
+ applySort = function(key)
+ if sortKey == key then
+ sortAsc = not sortAsc
+ else
+ sortKey = key
+ sortAsc = true
+ end
+ updateHeaderArrows()
+ sortDisplayRows()
+ -- Reordering invalidates the current page slice; snap back to page 1 of the new order.
+ currentPage = 0
+ buildPageRows()
+ resetResultsScroll()
+ refreshResults()
+ updatePageControls()
+ end
+
+ -- Exposed so Window.lua can clear this pane's search state when the WHOLE Auction House window
+ -- closes (AH.frame:Hide(), via AUCTION_HOUSE_CLOSED/ESC/close-button) -- not the same as switching
+ -- away from the Buy tab, which only hides `pane` itself and is already handled by the OnHide hook
+ -- below. Hiding the top-level frame does NOT fire OnHide on this pane (WoW only fires a frame's
+ -- own OnHide from its own explicit :Hide() call, not from an ancestor's), so without this the next
+ -- AUCTION_HOUSE_SHOW would still be showing whatever was searched last session.
+ local function resetSearch()
+ listRows = {}
+ listTotal = 0
+ displayRows = {}
+ pageRows = {}
+ activeQuery = "browse"
+ currentPage = 0
+ lastQuery = nil
+ -- Clearing lastQuery already makes queryPage a no-op, so an in-flight scan can't resume against
+ -- the search we just threw away; drop the scanning flag too so the label/pager leave that state.
+ scanning = false
+ scanPage = 0
+ scanPages = 1
+ scanTruncated = false
+ scanToken = scanToken + 1
+ scanPending = false
+ if pane.ItemDetail then pane.ItemDetail:Hide() end
+ setBrowseResultsShown(true)
+ searchBox:SetText("")
+ searchBox:ClearFocus()
+ empty:SetText("Choose search criteria and press \"Search\"")
+ refreshResults()
+ updatePageControls()
+ end
+ pane.ResetSearch = resetSearch
+
+ searchButton:SetScript("OnClick", runSearch)
+ searchBox:SetScript("OnEnterPressed", function(self)
+ self:ClearFocus()
+ runSearch()
+ end)
+
+ -- The client reports GetNumAuctionItems("list") counts before GetAuctionItemInfo has the
+ -- per-index data ready, so a single read can still race the server response. Poll on a short timer
+ -- until this page's rows actually populate (or we give up) instead of trying just once.
+ local pollAttempts = 0
+ local MAX_POLL_ATTEMPTS = 40 -- ~4s at 0.1s steps
+
+ -- One page of the scan landed: fold it into listRows, redraw so results visibly accumulate, then
+ -- either chain to the next server page or finish. Drives the whole scan together with queryPage.
+ local collectScanPage
+ collectScanPage = function(token)
+ if token ~= scanToken then return end
+ if not scanning then return end
+ if not scanPending then return end
+ if not (pane and pane:IsShown()) then return end
+ if activeQuery ~= "browse" then return end
+
+ local rows, batchCount, totalCount = capturePageAuctions()
+
+ -- Page not fully materialized yet -- wait for the rest rather than banking a short page, which
+ -- would drop auctions out of the aggregate AND (since a short page looks like the last page)
+ -- could end the scan early.
+ if batchCount > 0 and #rows < batchCount then
+ pollAttempts = pollAttempts + 1
+ if pollAttempts < MAX_POLL_ATTEMPTS and C_Timer and C_Timer.After then
+ C_Timer.After(0.1, function() collectScanPage(token) end)
+ return
+ end
+ end
+ pollAttempts = 0
+
+ -- Past every bail-out: this page is being banked exactly once, right now.
+ scanPending = false
+
+ listTotal = totalCount
+ for i = 1, #rows do
+ listRows[#listRows + 1] = rows[i]
+ end
+
+ local perPage = NUM_AUCTION_ITEMS_PER_PAGE or 50
+ local pages = math.max(1, math.ceil(totalCount / perPage))
+ scanTruncated = pages > MAX_SCAN_PAGES
+ if scanTruncated then pages = MAX_SCAN_PAGES end
+ scanPages = pages
+
+ -- A page that came back short of the server's own per-page cap is the last one, whatever the
+ -- total claimed -- keeps a stale/rounded total from spinning the scan on empty pages forever.
+ local more = scanPage + 1 < pages and #rows >= perPage
+ -- Clear `scanning` BEFORE drawing on the final page: refreshResults and updatePageControls both
+ -- render scan-in-progress text, and this pass is the finished one.
+ if not more then scanning = false end
+
+ buildDisplayRows()
+ sortDisplayRows()
+ buildPageRows()
+ -- pcall-wrapped: refreshResults calls the stock FauxScrollFrame_Update, which errors if the
+ -- scroll frame is ever anonymous again (see the naming comment above) -- don't let a future
+ -- regression there leave the UI stuck showing stale text with no visible feedback.
+ local rrOk, rrErr = pcall(refreshResults)
+ if not rrOk and NE.Log then
+ NE.Log("AH", "refreshResults error: " .. tostring(rrErr))
+ end
+ updatePageControls()
+
+ if more then
+ queryPage(scanPage + 1, token)
+ end
+ end
+
+ local watcher = CreateFrame("Frame", nil, pane)
+ watcher:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
+ watcher:SetScript("OnEvent", function()
+ if not pane:IsShown() then return end
+ if type(AuctionFrameBrowse_Update) == "function" then
+ pcall(AuctionFrameBrowse_Update)
+ end
+ -- AuctionFrameBrowse_Update (stock FrameXML, called above so the legacy client's own internal
+ -- state stays consistent) can reassert Show()/alpha on the legacy AH frame as part of its own
+ -- normal update logic, undoing the one-time cloak from window-open time -- re-cloak on every
+ -- single event (browse AND the item drill-down's own re-query both fire this) so its old
+ -- parchment/stone background can never bleed back through.
+ if AH.SuppressLegacyAuctionFrame then pcall(AH.SuppressLegacyAuctionFrame) end
+ if AH.SuppressModernAuctionFrame then pcall(AH.SuppressModernAuctionFrame) end
+
+ -- The item drill-down issues its own "list" query scoped to one item -- route this shared
+ -- event to its refresher instead of the aggregate browse path while it's the active query.
+ -- ISSUE #31: only when the drill-down's own query is the one that produced this update. It used
+ -- to refresh on EVERY event, so a Sell-tab market lookup or an Auctionator scan reloaded the
+ -- seller list out from under the user -- the Refresh button appearing to press itself.
+ if activeQuery == "itembuy" then
+ if refreshDetailRows and AH.OwnsListQuery("itembuy") then
+ local rdOk, rdErr = pcall(refreshDetailRows)
+ if not rdOk and NE.Log then
+ NE.Log("AH", "refreshDetailRows error: " .. tostring(rdErr))
+ end
+ end
+ return
+ end
+
+ -- This event is the scan's clock: each server page we asked for arrives here, gets folded in,
+ -- and collectScanPage requests the next one. Ignore the event outside a scan -- an unsolicited
+ -- AUCTION_ITEM_LIST_UPDATE (the stock UI, another addon, an owner/bidder query) is about a
+ -- "list" slot we didn't fill and must not be spliced into our aggregate.
+ -- ISSUE #31: `scanning` alone was not enough -- an interrupted scan could sit armed with
+ -- scanning == true and let a foreign update drive it (see cancelScan). Require that the browse
+ -- scan is also the current owner of the shared query slot.
+ if not scanning then return end
+ if not AH.OwnsListQuery("browse") then return end
+ pollAttempts = 0
+ local csOk, csErr = pcall(collectScanPage, scanToken)
+ if not csOk and NE.Log then
+ NE.Log("AH", "collectScanPage error: " .. tostring(csErr))
+ end
+ end)
+
+ -- ================================================================================
+ -- Item drill-down (retail's Buy -> ItemBuy two-tier flow): clicking an aggregate row above
+ -- re-queries "list" scoped to that exact item name and lists every individual auction (Current
+ -- Bid / Buyout / Qty / Seller / Time Left). Click a listing to select it, then Bid/Buyout acts on
+ -- it. Confirmed against a live screenshot of the actual NewEra reference addon: the search bar
+ -- STAYS visible/unhidden -- this overlay only occupies the categories/results footprint below
+ -- it (same TOPLEFT the results list itself uses, 172,-73), with a plain Back/count/refresh row
+ -- at its own top (no border around just that row), then the item header card and the auction
+ -- list as two SEPARATE bordered boxes beneath it -- not one border wrapping the whole thing.
+ -- ================================================================================
+ local detail = CreateFrame("Frame", nil, pane)
+ detail:SetFrameStrata("DIALOG")
+ detail:SetFrameLevel((list:GetFrameLevel() or 1) + 30)
+ detail:SetPoint("TOPLEFT", pane, "TOPLEFT", 172, -73)
+ -- Matches `list`'s own right edge (-5, same as Sell's `right` panel) so this overlay lines up
+ -- with the panel it sits on top of.
+ detail:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", -5, 27)
+ detail:EnableMouse(true) -- swallow clicks so the hidden results list beneath can't be hit
+ detail:Hide()
+ pane.ItemDetail = detail
+
+ local detailBg = detail:CreateTexture(nil, "BACKGROUND")
+ detailBg:SetTexture("Interface\\Buttons\\WHITE8X8")
+ detailBg:SetVertexColor(0.045, 0.045, 0.05, 0.98)
+ detailBg:SetAllPoints(detail)
+
+ local detailBack = CreateFrame("Button", nil, detail, "UIPanelButtonTemplate")
+ detailBack:SetSize(90, 22)
+ detailBack:SetPoint("TOPLEFT", detail, "TOPLEFT", 8, -8)
+ detailBack:SetText(BACK or "Back")
+
+ -- Refresh button: plain text, same UIPanelButtonTemplate every other button on this page uses.
+ -- Two prior icon attempts both failed on THIS client build -- a hand-rolled
+ -- Interface\Buttons\UI-RefreshButton texcoord split guessed the sheet layout wrong (rendered as
+ -- a cut-off half-circle), and "RefreshButtonTemplate" (which the reference addon's own
+ -- ItemList.lua assumes) doesn't exist as an inheritable XML template here at all -- CreateFrame
+ -- threw "Couldn't find inherited node", which errored the ENTIRE pane builder (safeBuild's pcall
+ -- caught it) and dropped the whole Buy tab to its fallback screen. A plain text button needs no
+ -- texture asset at all, so it can't hit either failure mode.
+ local detailRefresh = CreateFrame("Button", nil, detail, "UIPanelButtonTemplate")
+ detailRefresh:SetSize(70, 20)
+ detailRefresh:SetPoint("TOPRIGHT", detail, "TOPRIGHT", -6, -10)
+ detailRefresh:SetText(REFRESH or "Refresh")
+
+ local detailCount = detail:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ detailCount:SetPoint("RIGHT", detailRefresh, "LEFT", -8, 0)
+ detailCount:SetTextColor(1, 1, 1)
+
+ -- "Sort Per Item" toggle (issue #17): the listings arrive cheapest-TOTAL-first, which buries a
+ -- well-priced 20-stack under every cheap single. When checked, the list ranks by per-item buyout
+ -- instead. Display is unaffected -- the Buyout (total) and Per Item columns are both always
+ -- visible in the list, and the Bid/Buyout ACTION buttons always charge the listing's real total,
+ -- which is what the confirm popup shows.
+ -- Default ON: per-item price is the number that actually compares listings fairly, so it's the
+ -- default ranking; untick to fall back to cheapest-total-first.
+ local detailPerItem = true
+ local detailPerItemCheck = CreateFrame("CheckButton", nil, detail, "UICheckButtonTemplate")
+ detailPerItemCheck:SetSize(22, 22)
+ detailPerItemCheck:SetPoint("LEFT", detailBack, "RIGHT", 12, 0)
+ detailPerItemCheck:SetChecked(true)
+ local detailPerItemLabel = detail:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ detailPerItemLabel:SetPoint("LEFT", detailPerItemCheck, "RIGHT", 2, 0)
+ detailPerItemLabel:SetText("Sort Per Item")
+ detailPerItemCheck:SetScript("OnClick", function(self)
+ detailPerItem = self:GetChecked() and true or false
+ -- Re-read + re-sort the page the client already holds -- no fresh server query needed.
+ if refreshDetailRows then pcall(refreshDetailRows) end
+ end)
+
+ -- Item header card (its own separate bordered box, not shared with the Back row above it). Real
+ -- retail art per the reference addon's own atlas dump (ReferenceAddons/NewEra/Generated/
+ -- AtlasData.lua) -- both atlases live on the SAME two sheet files this module already ships
+ -- locally (3054898/3046538), just not previously registered here. Flat-fill/plain-square
+ -- fallback if SetAtlas ever can't find them (missing/renamed asset), so a bad lookup degrades
+ -- gracefully instead of leaving the header blank.
+ local detailHeader = CreateFrame("Frame", nil, detail)
+ detailHeader:SetPoint("TOPLEFT", detail, "TOPLEFT", 6, -36)
+ detailHeader:SetPoint("TOPRIGHT", detail, "TOPRIGHT", -6, -36)
+ detailHeader:SetHeight(76)
+
+ local detailHeaderFallback = detailHeader:CreateTexture(nil, "BACKGROUND", nil, -1)
+ detailHeaderFallback:SetTexture("Interface\\Buttons\\WHITE8X8")
+ detailHeaderFallback:SetVertexColor(0.06, 0.06, 0.07, 0.95)
+ detailHeaderFallback:SetAllPoints(detailHeader)
+
+ local detailHeaderBg = detailHeader:CreateTexture(nil, "BACKGROUND")
+ if NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(detailHeaderBg, "auctionhouse-background-buy-noncommodities-header", false) then
+ detailHeaderBg:SetPoint("TOPLEFT", detailHeader, "TOPLEFT", 3, -2)
+ detailHeaderBg:SetPoint("BOTTOMRIGHT", detailHeader, "BOTTOMRIGHT", -3, 2)
+ end
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, detailHeader, 0, 0, 0, 0)
+ end
+
+ -- Item icon button + hover tooltip. Plain Frame, not CreateTexture, because only frames/buttons
+ -- can receive OnEnter/OnLeave on 3.3.5a.
+ local detailIconBtn = CreateFrame("Button", nil, detailHeader)
+ detailIconBtn:SetSize(54, 54)
+ detailIconBtn:SetPoint("LEFT", detailHeader, "LEFT", 12, 0)
+
+ local detailIcon = detailIconBtn:CreateTexture(nil, "ARTWORK")
+ detailIcon:SetSize(46, 46)
+ detailIcon:SetPoint("CENTER")
+ detailIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+
+ -- Rarity glow -- NOT a ring atlas. A cropped/masked circular ring (the previous
+ -- "auctionhouse-itemicon-border-white" atlas) reads as a big blue smear here because circular
+ -- icon masking (CreateMaskTexture/AddMaskTexture) doesn't work on this 3.3.5a client (see
+ -- core/Portrait.lua) -- there's no way to clip the ring down to just the visible band. Swapped
+ -- to the SAME glow technique the professions crafting reagent slots and the bags window use
+ -- instead: UI-ActionButton-Border, ADD blend, tinted per quality, oversized by ~35% so its own
+ -- built-in transparent margin reaches the icon's edge.
+ local detailRing = detailIconBtn:CreateTexture(nil, "OVERLAY")
+ detailRing:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
+ detailRing:SetBlendMode("ADD")
+ local detailGlowOver = math.max(8, math.floor(54 * 0.35 + 0.5))
+ detailRing:SetPoint("TOPLEFT", detailIconBtn, "TOPLEFT", -detailGlowOver, detailGlowOver)
+ detailRing:SetPoint("BOTTOMRIGHT", detailIconBtn, "BOTTOMRIGHT", detailGlowOver, -detailGlowOver)
+ detailRing:Hide()
+
+ -- Live-refreshed so shift-compare works mid-hover (see the aggregate row above).
+ local function detailIconTooltip(self)
+ local item = detail.CurrentItem
+ if not item then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ local shown = item.link and pcall(GameTooltip.SetHyperlink, GameTooltip, item.link)
+ if not shown then
+ GameTooltip:ClearLines()
+ GameTooltip:AddLine(item.name or "", 1, 1, 1)
+ end
+ GameTooltip:Show()
+ end
+ if NE.FrameUtil and NE.FrameUtil.WireLiveTooltip then
+ NE.FrameUtil.WireLiveTooltip(detailIconBtn, detailIconTooltip)
+ else
+ detailIconBtn:SetScript("OnEnter", detailIconTooltip)
+ detailIconBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ end
+ -- The drill-down header icon is an item too: same shift-to-link / ctrl-to-dress-up as the rows.
+ detailIconBtn:RegisterForClicks("LeftButtonUp")
+ detailIconBtn:SetScript("OnClick", function()
+ local item = detail.CurrentItem
+ if item and item.link then AH.HandleItemClick(item.link) end
+ end)
+
+ local detailName = detailHeader:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
+ detailName:SetPoint("LEFT", detailIconBtn, "RIGHT", 14, 0)
+ detailName:SetPoint("RIGHT", detailHeader, "RIGHT", -14, 0)
+ detailName:SetJustifyH("LEFT")
+
+ -- Bid / Buyout footer bar (acts on the currently-selected listing below; OnClick wired further
+ -- down once detailSelected exists).
+ local detailBar = CreateFrame("Frame", nil, detail)
+ detailBar:SetPoint("BOTTOMLEFT", detail, "BOTTOMLEFT", 6, 6)
+ detailBar:SetPoint("BOTTOMRIGHT", detail, "BOTTOMRIGHT", -6, 6)
+ detailBar:SetHeight(22)
+
+ local detailBuyoutBtn = CreateFrame("Button", nil, detailBar, "UIPanelButtonTemplate")
+ detailBuyoutBtn:SetSize(110, 22)
+ detailBuyoutBtn:SetPoint("RIGHT", detailBar, "RIGHT", 0, 0)
+ detailBuyoutBtn:SetText(AUCTION_HOUSE_BUYOUT_BUTTON or BUYOUT or "Buyout")
+
+ local detailBidBtn = CreateFrame("Button", nil, detailBar, "UIPanelButtonTemplate")
+ detailBidBtn:SetSize(110, 22)
+ detailBidBtn:SetPoint("RIGHT", detailBuyoutBtn, "LEFT", -6, 0)
+ detailBidBtn:SetText(AUCTION_HOUSE_BID_BUTTON or BID or "Bid")
+
+ -- Per-auction list.
+ local detailList = CreateFrame("Frame", nil, detail)
+ detailList:SetPoint("TOPLEFT", detailHeader, "BOTTOMLEFT", 0, -10)
+ detailList:SetPoint("BOTTOMRIGHT", detailBar, "TOPRIGHT", 0, 6)
+
+ -- Same always-present-fallback + optional-atlas-overlay pattern as the main results list's own
+ -- listFallback/listBg pair above (search for "auctionhouse-background-index").
+ local detailListFallback = detailList:CreateTexture(nil, "BACKGROUND")
+ detailListFallback:SetTexture("Interface\\Buttons\\WHITE8X8")
+ detailListFallback:SetVertexColor(0.02, 0.02, 0.025, 0.92)
+ detailListFallback:SetPoint("TOPLEFT", detailList, "TOPLEFT", 3, -22)
+ detailListFallback:SetPoint("BOTTOMRIGHT", detailList, "BOTTOMRIGHT", -22, 0)
+
+ local detailListBg = detailList:CreateTexture(nil, "BACKGROUND")
+ if NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(detailListBg, "auctionhouse-background-buy-noncommodities-market", false) then
+ detailListBg:SetPoint("TOPLEFT", detailList, "TOPLEFT", 3, -22)
+ detailListBg:SetPoint("BOTTOMRIGHT", detailList, "BOTTOMRIGHT", -22, 0)
+ else
+ detailListBg:SetTexture(nil)
+ end
+
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, detailList, 0, -19, -22, 0)
+ end
+
+ local detailHeaderStrip = detailList:CreateTexture(nil, "BORDER")
+ detailHeaderStrip:SetTexture("Interface\\Buttons\\WHITE8X8")
+ detailHeaderStrip:SetVertexColor(0.06, 0.06, 0.07, 0.95)
+ detailHeaderStrip:SetPoint("TOPLEFT", detailList, "TOPLEFT", 3, -1)
+ detailHeaderStrip:SetPoint("TOPRIGHT", detailList, "TOPRIGHT", -22, -1)
+ detailHeaderStrip:SetHeight(21)
+
+ -- Auctionator-style column order: the per-item price leads (it's the number listings are
+ -- compared by), then the whole-stack Buyout and Current Bid, then the item's required level, a
+ -- Quantity column wide enough for a grouped row's "6 stacks of 20" breakdown, and the Seller.
+ -- Per Item and Buyout are BOTH always visible -- swapping one display into the other's column (an
+ -- earlier iteration) made it impossible to see what a whole stack costs while comparing unit
+ -- prices. Seller shows "Multiple" on a grouped row that spans several sellers; the row hover
+ -- tooltip (see ensureDetailRow) is still where the full list of them lives.
+ --
+ -- The seven columns only fit across the ~575px row if the three money columns give up the slack
+ -- they used to carry (110 -> 92), so every x/w below is load-bearing against its neighbours.
+ -- Header x values match each column's own offset in ensureDetailRow plus the rows' 6px inset;
+ -- Time Left's right anchor is -38 (rows are inset -30 from detailList, their text another -8),
+ -- not the -80 it previously used, which floated the label 42px clear of its own column.
+ local detailCols = {
+ { text = "Per Item", x = 6, w = 92, just = "RIGHT" },
+ { text = AUCTION_HOUSE_HEADER_BUYOUT or BUYOUT or "Buyout", x = 104, w = 92, just = "RIGHT" },
+ { text = AUCTION_HOUSE_HEADER_CURRENT_BID or BID or "Current Bid", x = 202, w = 92, just = "RIGHT" },
+ { text = "Lvl", x = 300, w = 36, just = "RIGHT" },
+ { text = AUCTION_HOUSE_HEADER_QUANTITY or "Quantity", x = 344, w = 82, just = "LEFT" },
+ { text = AUCTION_HOUSE_HEADER_SELLER or "Seller", x = 430, w = 76, just = "LEFT" },
+ { text = AUCTION_HOUSE_HEADER_TIME_LEFT or "Time Left", x = -38, w = 62, just = "RIGHT", right = true },
+ }
+ for i = 1, #detailCols do
+ local h = detailCols[i]
+ local fs = detailList:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ fs:SetPoint("TOPLEFT", detailList, "TOPLEFT", h.x, -7)
+ if h.right then
+ fs:ClearAllPoints()
+ fs:SetPoint("TOPRIGHT", detailList, "TOPRIGHT", h.x, -7)
+ end
+ fs:SetWidth(h.w)
+ fs:SetJustifyH(h.just)
+ fs:SetText(h.text)
+ end
+
+ local detailEmpty = detailList:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ detailEmpty:SetPoint("CENTER", detailList, "CENTER", 0, 44)
+ detailEmpty:SetTextColor(1, 0.82, 0)
+
+ local DETAIL_ROW_H = 20
+ local DETAIL_TOP = -24
+
+ local detailRowWidgets = {}
+ local detailRowsHost = CreateFrame("Frame", nil, detailList)
+ detailRowsHost:SetPoint("TOPLEFT", detailList, "TOPLEFT", 0, 0)
+ detailRowsHost:SetPoint("BOTTOMRIGHT", detailList, "BOTTOMRIGHT", 0, 0)
+ detailRowsHost:SetFrameStrata("DIALOG")
+ detailRowsHost:SetFrameLevel((detailList:GetFrameLevel() or 1) + 20)
+
+ local detailScroll = CreateFrame("ScrollFrame", "NE_AuctionHouseItemBuyScroll", detailList, "FauxScrollFrameTemplate")
+ detailScroll:SetPoint("TOPLEFT", detailList, "TOPLEFT", 2, DETAIL_TOP)
+ detailScroll:SetPoint("BOTTOMRIGHT", detailList, "BOTTOMRIGHT", -26, 4)
+ if NE.scrollbar and NE.scrollbar.BuildCustom then
+ local ok, bar = pcall(NE.scrollbar.BuildCustom, detailScroll, { x = -8 })
+ -- BuildCustom's bar defaults to "HIGH" strata, which is correct everywhere else it's used (plain
+ -- MEDIUM-strata list hosts), but `detail` (this overlay's root) is deliberately "DIALOG" strata
+ -- so it draws above the underlying browse results list -- every descendant frame that doesn't
+ -- set its own strata (detailList, detailListBg/detailRowsHost) inherits that DIALOG level, which
+ -- then sits ABOVE the scrollbar's HIGH strata and hides it behind the list background. Force the
+ -- bar to match this overlay's own strata and sit above its row buttons.
+ if ok and bar then
+ bar:SetFrameStrata("DIALOG")
+ bar:SetFrameLevel((detailRowsHost:GetFrameLevel() or 1) + 10)
+ -- Same post-hoc strata promotion the arrow buttons need everywhere else this pattern is used.
+ if bar._upBtn then bar._upBtn:SetFrameStrata("DIALOG"); bar._upBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ if bar._downBtn then bar._downBtn:SetFrameStrata("DIALOG"); bar._downBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ end
+ end
+
+ local detailRowsData = {}
+ local detailSelected = nil
+ local detailAutoResyncDone = false
+
+ -- Fill the entire scroll viewport instead of a hardcoded row count -- previously a fixed 8 rows
+ -- left most of the panel's actual (much taller) height as dead space below the last row.
+ local function detailVisibleRows()
+ local h = detailScroll:GetHeight() or 0
+ return math.max(1, math.floor(h / (DETAIL_ROW_H + 1)))
+ end
+
+ -- GetAuctionItemTimeLeft is an enum (1-4) on this client; handle a raw seconds value too, just
+ -- in case.
+ local function detailTimeLeftText(v)
+ if not v then return "" end
+ local TL = {
+ [1] = AUCTION_TIME_LEFT1 or "Short", [2] = AUCTION_TIME_LEFT2 or "Medium",
+ [3] = AUCTION_TIME_LEFT3 or "Long", [4] = AUCTION_TIME_LEFT4 or "Very Long",
+ }
+ if v >= 1 and v <= 4 and TL[v] then return TL[v] end
+ return SecondsToTime and SecondsToTime(v) or tostring(v)
+ end
+
+ local function updateDetailActions()
+ local r = detailSelected
+ local canBuyout = r and r.buyout and r.buyout > 0
+ local canBid = r and r.nextBid and r.nextBid > 0
+ detailBuyoutBtn:SetEnabled(canBuyout and true or false)
+ detailBidBtn:SetEnabled(canBid and true or false)
+ end
+
+ local function ensureDetailRow(i)
+ local row = detailRowWidgets[i]
+ if row then return row end
+
+ row = CreateFrame("Button", nil, detailRowsHost)
+ row:SetFrameStrata("DIALOG")
+ row:SetHeight(DETAIL_ROW_H)
+ row:SetFrameLevel((detailList:GetFrameLevel() or 1) + 10)
+ if i == 1 then
+ row:SetPoint("TOPLEFT", detailList, "TOPLEFT", 6, DETAIL_TOP)
+ row:SetPoint("TOPRIGHT", detailList, "TOPRIGHT", -30, DETAIL_TOP)
+ else
+ row:SetPoint("TOPLEFT", detailRowWidgets[i - 1], "BOTTOMLEFT", 0, -1)
+ row:SetPoint("TOPRIGHT", detailRowWidgets[i - 1], "BOTTOMRIGHT", 0, -1)
+ end
+
+ local rowBg = row:CreateTexture(nil, "BACKGROUND")
+ rowBg:SetTexture("Interface\\Buttons\\WHITE8X8")
+ rowBg:SetAllPoints(row)
+ rowBg:SetVertexColor(0.07, 0.07, 0.08, (i % 2 == 0) and 0.30 or 0.20)
+ row.Bg = rowBg
+
+ local sel = row:CreateTexture(nil, "ARTWORK")
+ sel:SetTexture("Interface\\Buttons\\WHITE8X8")
+ sel:SetVertexColor(1, 0.82, 0, 0.25)
+ sel:SetAllPoints(row)
+ sel:Hide()
+ row.Sel = sel
+
+ row:SetHighlightTexture("Interface\\Buttons\\WHITE8X8")
+ local hl = row:GetHighlightTexture()
+ if hl then
+ hl:SetVertexColor(1, 0.82, 0, 0.12)
+ hl:SetBlendMode("ADD")
+ end
+
+ -- Offsets here are the source of truth the detailCols header x values are derived from; keep
+ -- the two in step when touching either.
+ local each = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ each:SetPoint("LEFT", row, "LEFT", 0, 0)
+ each:SetWidth(92)
+ each:SetJustifyH("RIGHT")
+ row.Each = each
+
+ local buyout = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ buyout:SetPoint("LEFT", row, "LEFT", 98, 0)
+ buyout:SetWidth(92)
+ buyout:SetJustifyH("RIGHT")
+ row.Buyout = buyout
+
+ local bid = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ bid:SetPoint("LEFT", row, "LEFT", 196, 0)
+ bid:SetWidth(92)
+ bid:SetJustifyH("RIGHT")
+ row.Bid = bid
+
+ local level = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ level:SetPoint("LEFT", row, "LEFT", 294, 0)
+ level:SetWidth(36)
+ level:SetJustifyH("RIGHT")
+ row.Level = level
+
+ local qty = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ qty:SetPoint("LEFT", row, "LEFT", 338, 0)
+ qty:SetWidth(82)
+ qty:SetJustifyH("LEFT")
+ row.Qty = qty
+
+ local seller = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ seller:SetPoint("LEFT", row, "LEFT", 424, 0)
+ seller:SetWidth(76)
+ seller:SetJustifyH("LEFT")
+ row.Seller = seller
+
+ local time = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ time:SetPoint("RIGHT", row, "RIGHT", -8, 0)
+ time:SetWidth(62)
+ time:SetJustifyH("RIGHT")
+ row.Time = time
+
+ -- The Seller column only has room for one name, so a grouped row spanning several sellers shows
+ -- "Multiple" and defers the full list to this tooltip.
+ row:SetScript("OnEnter", function(self)
+ local d = self._data
+ if not d then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:ClearLines()
+ if (d.num or 1) > 1 then
+ GameTooltip:AddLine(string.format("%d auctions", d.num), 1, 0.82, 0)
+ end
+ -- Unique sellers, capped so one mass-poster's wall of alts can't grow a giant tooltip.
+ -- owner can be nil on this client until the server fills it in; skip those.
+ local sellers = d.owners or { d.owner }
+ local seen, uniq = {}, {}
+ for i = 1, #sellers do
+ local s = sellers[i]
+ if s and s ~= "" and not seen[s] then
+ seen[s] = true
+ uniq[#uniq + 1] = s
+ end
+ end
+ if #uniq > 0 then
+ local MAX_SHOWN = 8
+ local label = (#uniq == 1) and (AUCTION_HOUSE_HEADER_SELLER or "Seller")
+ or ((AUCTION_HOUSE_HEADER_SELLER or "Seller") .. "s")
+ local names = table.concat(uniq, ", ", 1, math.min(#uniq, MAX_SHOWN))
+ if #uniq > MAX_SHOWN then
+ names = names .. string.format(" (+%d more)", #uniq - MAX_SHOWN)
+ end
+ GameTooltip:AddLine(label .. ": " .. names, 1, 1, 1, true)
+ end
+ if GameTooltip:NumLines() > 0 then
+ GameTooltip:Show()
+ else
+ GameTooltip:Hide()
+ end
+ end)
+ row:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ row:RegisterForClicks("LeftButtonUp")
+ row:SetScript("OnClick", function(self)
+ -- SHIFT links the item in chat, CTRL dresses it up; otherwise select this listing.
+ if self._data and self._data.index then
+ local link = GetAuctionItemLink and GetAuctionItemLink("list", self._data.index)
+ if link and AH.HandleItemClick(link) then return end
+ end
+ detailSelected = self._data
+ for _, rr in ipairs(detailRowWidgets) do
+ if rr.Sel then rr.Sel:SetShown(rr._data ~= nil and rr._data == detailSelected) end
+ end
+ updateDetailActions()
+ end)
+
+ detailRowWidgets[i] = row
+ return row
+ end
+
+ local function readDetailPage()
+ local out = {}
+ local batch = 0
+ if type(GetNumAuctionItems) == "function" then
+ batch = select(1, GetNumAuctionItems("list")) or 0
+ end
+ -- This client's QueryAuctionItems does a SUBSTRING search, not an exact-name match (there's no
+ -- dedicated exact-match argument in its signature) -- searching "Linen Cloth" also returns
+ -- "Bolt of Linen Cloth". Auctionator and the default 3.3.5a AH UI both filter the requeried
+ -- page down to an EXACT name match client-side before listing/allowing a purchase; do the same
+ -- here, or a click on "Linen Cloth" could end up placing a bid on a totally different listing
+ -- that merely shares the substring.
+ local wantName = detail.CurrentItem and detail.CurrentItem.name
+ for i = 1, batch do
+ -- Same 13-value field order as capturePageAuctions above.
+ local name, _, count, _, _, level, minBid, minInc, buyout, bidAmt, _, owner = GetAuctionItemInfo("list", i)
+ if name and (not wantName or name == wantName) then
+ local cur = (bidAmt and bidAmt > 0) and bidAmt or minBid
+ local tl = GetAuctionItemTimeLeft and GetAuctionItemTimeLeft("list", i)
+ out[#out + 1] = {
+ index = i, count = count or 1, level = level,
+ curBid = cur, nextBid = (cur or 0) + (minInc or 0),
+ buyout = buyout, owner = owner,
+ hasBid = (bidAmt and bidAmt > 0) and true or false,
+ timeText = detailTimeLeftText(tl),
+ }
+ end
+ end
+
+ -- Hybrid Auctionator-style condensing (issue #17 follow-up): listings that are identical from
+ -- a buyer's point of view -- same stack size, same buyout, same starting bid, and NO bid in
+ -- progress -- collapse into one row ("6 stacks of 1", "3 stacks of 2"), exactly how
+ -- Auctionator's own scan does it (AtrScan:AddScanItem's numAuctions counter). Listings with an
+ -- active bid (or no buyout at all) stay as individual rows: each carries its own bid state, so
+ -- grouping them would hide which specific auction you'd be bidding against. A grouped row
+ -- remembers every member's auction index (cheapest-index first); Bid/Buyout act on the first
+ -- one, and the AUCTION_ITEM_LIST_UPDATE that follows a purchase re-reads and re-groups, so the
+ -- stack count visibly ticks down with each buy. `key` identifies a row across those refreshes
+ -- (group identity for grouped rows, auction index for singles) so the selection can survive.
+ local groups = {}
+ local condensed = {}
+ for i = 1, #out do
+ local r = out[i]
+ if r.buyout and r.buyout > 0 and not r.hasBid then
+ local key = (r.count or 1) .. ":" .. r.buyout .. ":" .. (r.curBid or 0)
+ local g = groups[key]
+ if not g then
+ r.key = "g:" .. key
+ r.num = 1
+ r.indices = { r.index }
+ r.owners = { r.owner }
+ groups[key] = r
+ condensed[#condensed + 1] = r
+ else
+ g.num = g.num + 1
+ g.indices[#g.indices + 1] = r.index
+ g.owners[#g.owners + 1] = r.owner
+ -- Per-member fields only survive on the group while they're uniform across it.
+ if g.owner ~= r.owner then g.owner = nil end
+ if g.timeText ~= r.timeText then g.timeText = "" end
+ end
+ else
+ r.key = "i:" .. r.index
+ r.num = 1
+ condensed[#condensed + 1] = r
+ end
+ end
+ out = condensed
+
+ -- Explicit cheapest-first ordering (issue #17) instead of trusting whatever order the server
+ -- returned the page in: listings WITH a buyout first (ranked by total buyout, or by per-item
+ -- buyout when the Sort Per Item toggle is on), then bid-only listings ranked by current bid,
+ -- with the auction index as a stable final tiebreak.
+ local function rankPrice(r)
+ local bo = (r.buyout and r.buyout > 0) and r.buyout or nil
+ local bid = r.curBid or 0
+ if detailPerItem then
+ local c = (r.count and r.count > 0) and r.count or 1
+ if bo then bo = bo / c end
+ bid = bid / c
+ end
+ return bo, bid
+ end
+ table.sort(out, function(a, b)
+ local abo, abid = rankPrice(a)
+ local bbo, bbid = rankPrice(b)
+ if (abo ~= nil) ~= (bbo ~= nil) then return abo ~= nil end
+ if abo and bbo and abo ~= bbo then return abo < bbo end
+ if not abo and abid ~= bbid then return abid < bbid end
+ return a.index < b.index
+ end)
+ return out
+ end
+
+ -- Seller cell. A grouped row keeps `owner` only while every member shares it (see the grouping
+ -- above), so fall back to its `owners` list: still one distinct name after de-duping (a seller
+ -- posting six identical stacks is the common case) shows that name, genuinely mixed shows
+ -- "Multiple", and all-unknown shows nothing -- owner comes back nil on this client until the
+ -- server fills it in, and "?" on a whole page of fresh listings reads like an error.
+ local function sellerText(d)
+ if d.owner and d.owner ~= "" then return d.owner end
+ local owners = d.owners
+ if owners then
+ local seen, count, last = {}, 0, nil
+ for i = 1, #owners do
+ local s = owners[i]
+ if s and s ~= "" and not seen[s] then
+ seen[s] = true
+ count = count + 1
+ last = s
+ end
+ end
+ if count == 1 then return last end
+ if count > 1 then return "Multiple" end
+ end
+ return ""
+ end
+
+ refreshDetailRows = function()
+ detailRowsData = readDetailPage()
+ local drawCount = #detailRowsData
+ local visibleRows = detailVisibleRows()
+ if FauxScrollFrame_Update then
+ FauxScrollFrame_Update(detailScroll, drawCount, visibleRows, DETAIL_ROW_H + 1)
+ end
+ syncAlwaysShowFauxBar(detailScroll, drawCount, visibleRows)
+ local offset = (FauxScrollFrame_GetOffset and FauxScrollFrame_GetOffset(detailScroll)) or 0
+
+ if drawCount > 0 then
+ detailEmpty:Hide()
+ else
+ detailEmpty:Show()
+ detailEmpty:SetText("No listings.")
+ end
+ -- Total AUCTIONS, not visible rows -- condensing collapses identical listings into one row,
+ -- but this readout should keep meaning "how many listings exist".
+ local totalAuctions = 0
+ for i = 1, drawCount do
+ totalAuctions = totalAuctions + (detailRowsData[i].num or 1)
+ end
+ detailCount:SetText(tostring(totalAuctions))
+
+ -- Selection survives a refresh only if the same row identity is still present -- matched by
+ -- `key`, which for a grouped row is its (stack size, buyout, bid) identity rather than any one
+ -- auction index, so buying one stack out of "6 stacks of 20" keeps the shrunk group selected
+ -- for the next buy. When the identity is gone entirely (last one bought), drop the selection
+ -- instead of leaving the footer buttons pointed at a listing that no longer exists.
+ if detailSelected then
+ local stillThere = nil
+ for i = 1, #detailRowsData do
+ if detailRowsData[i].key == detailSelected.key then
+ stillThere = detailRowsData[i]
+ break
+ end
+ end
+ detailSelected = stillThere
+ end
+
+ for i = 1, visibleRows do
+ local row = ensureDetailRow(i)
+ local data = detailRowsData[i + offset]
+ if data then
+ row._data = data
+ -- Per Item = buyout / stack size, ceil'd so a fractional copper never understates the
+ -- cost. Display-only; the popup/purchase path uses the listing's real totals.
+ local per = (data.count and data.count > 0) and data.count or 1
+ row.Each:SetText(moneyText(math.ceil((data.buyout or 0) / per)))
+ row.Buyout:SetText(moneyText(data.buyout or 0))
+ row.Bid:SetText(moneyText(data.curBid or 0))
+ local lvText, lr, lg, lb = levelText(data.level)
+ row.Level:SetText(lvText)
+ row.Level:SetTextColor(lr, lg, lb)
+ if (data.num or 1) > 1 then
+ row.Qty:SetText(string.format("%d stacks of %d", data.num, data.count or 1))
+ else
+ row.Qty:SetText(tostring(data.count or 1))
+ end
+ row.Seller:SetText(sellerText(data))
+ row.Time:SetText(data.timeText or "")
+ if row.Sel then row.Sel:SetShown(data == detailSelected) end
+ row:Show()
+ else
+ row._data = nil
+ if row.Sel then row.Sel:Hide() end
+ row:Hide()
+ end
+ end
+ -- Hide any previously-built rows beyond the current viewport (only relevant if the panel's
+ -- effective height ever shrinks between refreshes; harmless no-op otherwise).
+ for i = visibleRows + 1, #detailRowWidgets do
+ local row = detailRowWidgets[i]
+ row._data = nil
+ if row.Sel then row.Sel:Hide() end
+ row:Hide()
+ end
+ updateDetailActions()
+
+ -- detailVisibleRows() reads detailScroll:GetHeight(), but the very first refresh after
+ -- openItemDetail's detail:Show() can land before this branch of the anchor chain has settled
+ -- (same class of stale-zero-size read BuildCustom's own first sync() guards against below) --
+ -- that undercounts the viewport, makes FauxScrollFrame_Update think there's more to scroll than
+ -- there really is, and leaves the scrollbar/arrows stuck showing even though every row fits.
+ -- One deferred re-run per item-open with a settled layout self-corrects it. Flag is set BEFORE
+ -- scheduling (not inside the callback) so the callback's own call to refreshDetailRows sees it
+ -- already true and doesn't reschedule itself forever; openItemDetail resets it per new item.
+ if not detailAutoResyncDone and C_Timer and C_Timer.After then
+ detailAutoResyncDone = true
+ C_Timer.After(0, function()
+ if activeQuery == "itembuy" then
+ pcall(refreshDetailRows)
+ end
+ end)
+ end
+ end
+
+ detailScroll:SetScript("OnVerticalScroll", function(self, offset)
+ if FauxScrollFrame_OnVerticalScroll then
+ FauxScrollFrame_OnVerticalScroll(self, offset, DETAIL_ROW_H + 1, refreshDetailRows)
+ end
+ end)
+
+ detailBuyoutBtn:SetScript("OnClick", function()
+ local r = detailSelected
+ if not (r and r.buyout and r.buyout > 0) then return end
+ StaticPopup_Show("NE_AH_BROWSE_BUYOUT", moneyText(r.buyout), nil, { index = r.index, price = r.buyout })
+ end)
+ detailBidBtn:SetScript("OnClick", function()
+ local r = detailSelected
+ if not (r and r.nextBid and r.nextBid > 0) then return end
+ StaticPopup_Show("NE_AH_BROWSE_BID", moneyText(r.nextBid), nil, { index = r.index, price = r.nextBid })
+ end)
+
+ detailBack:SetScript("OnClick", function()
+ activeQuery = "browse"
+ detail:Hide()
+ setBrowseResultsShown(true)
+ end)
+ detailRefresh:SetScript("OnClick", function()
+ if detail.CurrentItem and openItemDetail then openItemDetail(detail.CurrentItem) end
+ end)
+
+ openItemDetail = function(data)
+ if not data or not data.name then return end
+ detail.CurrentItem = data
+ -- Drilling in takes the shared "list" slot away from the browse scan. Retire the scan properly
+ -- instead of letting collectScanPage bail on activeQuery and strand it armed (issue #31).
+ cancelScan()
+ activeQuery = "itembuy"
+ detailSelected = nil
+ detailRowsData = {}
+ detailAutoResyncDone = false
+
+ detailIcon:SetTexture(data.texture or "Interface\\Icons\\INV_Misc_QuestionMark")
+ detailName:SetText(data.name)
+ if data.quality and data.quality > 1 and GetItemQualityColor then
+ local r, g, b = GetItemQualityColor(data.quality)
+ detailName:SetTextColor(r, g, b)
+ detailRing:SetVertexColor(r, g, b)
+ detailRing:Show()
+ else
+ detailName:SetTextColor(1, 1, 1)
+ detailRing:Hide()
+ end
+ detailCount:SetText("")
+ detailEmpty:SetText("Searching...")
+ detailEmpty:Show()
+ updateDetailActions()
+ setBrowseResultsShown(false)
+ detail:Show()
+
+ if not (CanSendAuctionQuery and CanSendAuctionQuery("list")) then
+ detailEmpty:SetText("Auction query is throttled. Try again in a moment.")
+ return
+ end
+ -- Re-query scoped to this exact item name. THIS server's QueryAuctionItems signature (per its
+ -- own bundled APIDocumentation) has no dedicated exact-match flag the way some other clients'
+ -- do -- passing the full item name as the search text returns just this item barring rare
+ -- substring collisions (e.g. "Belt" would also match "Belt of Deep Shadow").
+ -- ISSUE #31 ("the list of sellers does not appear, even if that item is available"): the
+ -- rarity slot was hardcoded to 0, which is an EXACT match on Poor quality, not "any" -- so this
+ -- drill-down only ever returned listings for grey items and the seller list came back empty for
+ -- everything else. ANY_QUALITY (-1) disables the filter. Note this must stay unfiltered even
+ -- though we know the item's quality: filtering by it would be redundant, and data.quality can be
+ -- nil when the item isn't in the local cache yet.
+ AH.ClaimListQuery("itembuy")
+ local ok, err = pcall(QueryAuctionItems, data.name, 0, 0, 0, 0, 0, 0, false, ANY_QUALITY, false)
+ if not ok and NE.Log then
+ NE.Log("AH", "ItemBuy QueryAuctionItems error: " .. tostring(err))
+ end
+ end
+
+ local categories = CreateFrame("Frame", nil, pane)
+ categories:SetPoint("TOPLEFT", pane, "TOPLEFT", 4, -73)
+ -- Height 437, not 438 -- anchored from the TOP (fixed height, unlike every other panel here which
+ -- anchors its BOTTOM at pane+27), so a hardcoded 438 landed its bottom 1px below the shared
+ -- pane+27 divider line the results/detail lists and the Auctions tab's panels all sit on.
+ categories:SetSize(168, 437)
+ pane.Categories = categories
+
+ local catBg = categories:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(catBg, "auctionhouse-background-categories", false)
+ end
+ catBg:SetPoint("TOPLEFT", categories, "TOPLEFT", 3, -3)
+ catBg:SetSize(138, 433)
+
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, categories, 0, 0, 0, 0)
+ end
+
+ local ok, catList = pcall(buildCategoryList, categories)
+ if ok then
+ pane.CategoryList = catList
+ else
+ pane.CategoryList = nil
+ end
+
+ -- Shift-clicking a bag item fills this search box (issue #17). On a stock client,
+ -- ChatEdit_InsertLink routes an item shift-click (no chat box open) into the legacy Browse tab's
+ -- BrowseName edit box -- but that whole frame is permanently alpha-cloaked here (Window.lua's
+ -- suppressLegacyAuctionFrame), so the name landed in an invisible box and the visible search bar
+ -- stayed empty. Post-hook the same entry point: when no chat edit box took the link and this
+ -- pane is the one on screen, mirror the item's name into our search box and run the search
+ -- immediately (Auctionator-style; one shift-click = results on screen). hooksecurefunc (not a
+ -- replacement) so the stock routing, other addons' hooks, and taint behavior are all untouched.
+ if not AH._browseChatLinkHooked and type(hooksecurefunc) == "function" then
+ AH._browseChatLinkHooked = true
+ hooksecurefunc("ChatEdit_InsertLink", function(text)
+ if type(text) ~= "string" or not string.find(text, "item:", 1, true) then return end
+ -- Shift-clicking a row INSIDE this window is a request to link that item in chat, not to
+ -- search for it -- AH.HandleItemClick (Window.lua) raises this flag around its own
+ -- ChatEdit_InsertLink call so a chat box that isn't open can't turn the click into a search
+ -- that discards the results being looked at. Bag/chat shift-clicks still search as before.
+ if AH._suppressLinkSearch then return end
+ -- A shown chat edit box already consumed the link; 3.3.5a uses the single global
+ -- ChatFrameEditBox, but check the retail-style active-window API too in case this client
+ -- backports it.
+ if ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then return end
+ if ChatFrameEditBox and ChatFrameEditBox.IsShown and ChatFrameEditBox:IsShown() then return end
+ if not (pane:IsShown() and AH.frame and AH.frame:IsShown()) then return end
+ local name = GetItemInfo and GetItemInfo(text)
+ if not name or name == "" then
+ -- Item not in the local cache yet: fall back to the display name inside the link itself.
+ name = string.match(text, "%[(.-)%]")
+ end
+ if not name or name == "" then return end
+ searchBox:SetText(name)
+ searchBox:ClearFocus()
+ -- Name search only: drop any category selection, or a herb shift-clicked while "Weapons"
+ -- was highlighted would query Weapons-only and come back empty. The filter popup's level/
+ -- rarity/usable settings are left alone -- they're deliberate, visible-on-reopen choices.
+ if pane.CategoryList and pane.CategoryList.ClearSelection then
+ pane.CategoryList:ClearSelection()
+ end
+ runSearch()
+ end)
+ end
+
+ return pane
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Sell.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Sell.lua
new file mode 100644
index 0000000..35f4a48
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Sell.lua
@@ -0,0 +1,851 @@
+-- DragonUI_NewEra/modules/auctionhouse/Sell.lua
+-- Sell tab: drag an item into the slot, set quantity/price/duration, see the item's current
+-- market listings on the right, and post the auction. Legacy 3.3.5a API (verified against this
+-- server's own bundled APIDocumentation + Auctionator, same pattern Browse.lua/Auctions.lua use):
+-- ClickAuctionSellItemButton() -- cursor item -> sell slot
+-- GetAuctionSellItemInfo() -> name, texture, count, quality, canUse, price (6 values)
+-- GetAuctionDeposit(duration, minBid, buyoutPrice, stackSize, numStacks) -- deposit cost
+-- PostAuction(minBid, buyoutPrice, duration, stackSize, numStacks) -- create the auction
+-- QueryAuctionItems(name, minLevel, maxLevel, invTypeIndex, classIndex, subClassIndex, page,
+-- isUsable, quality, getAll) -- market search
+-- `quality` is an EXACT match, not a minimum; -1 = any rarity (0 = Poor).
+-- GetAuctionItemInfo("list", i) -> name,tex,count,quality,canUse,level,minBid,minIncrement,
+-- buyoutPrice,bidAmount,highBidder,owner,saleStatus (13 values)
+-- Events: NEW_AUCTION_UPDATE (sell slot changed), AUCTION_ITEM_LIST_UPDATE (list query done).
+--
+-- Layout mirrors the reference NewEra Sell.lua (ReferenceAddons/NewEra/AuctionHouse/Sell.lua):
+-- Create-Auction tab plate, itemheaderframe item display, aligned label rows with the modern
+-- auctionhouse-ui-inputfield input skin, money input with in-field coin icons, coin-icon
+-- Deposit/Total readouts, and a sortable market list with a Refresh control. Widgets are
+-- hand-built on 3.3.5a primitives -- the retail templates the reference leaned on
+-- (InputBoxScriptTemplate, WowStyle1DropdownTemplate, MoneyDisplayFrameTemplate) don't exist here.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+NE.ah = NE.ah or {}
+local AH = NE.ah
+
+local DURATIONS = {
+ { code = 1, label = AUCTION_DURATION_ONE or "12 Hours" },
+ { code = 2, label = AUCTION_DURATION_TWO or "24 Hours" },
+ { code = 3, label = AUCTION_DURATION_THREE or "48 Hours" },
+}
+
+-- Deposit lookup: this server's real signature takes (duration, minBid, buyoutPrice, stackSize,
+-- numStacks); fall back to the older 1-arg form (duration only) if that call shape errors.
+local function computeDeposit(durationCode, minBid, buyoutPrice, stack, numStacks)
+ if not GetAuctionDeposit then return 0 end
+ local ok, d = pcall(GetAuctionDeposit, durationCode, minBid, buyoutPrice, stack, numStacks)
+ if ok and type(d) == "number" then return d end
+ ok, d = pcall(GetAuctionDeposit, durationCode)
+ if ok and type(d) == "number" then return d end
+ return 0
+end
+
+----------------------------------------------------------------------
+-- Hand-built modern widgets (shared makeInput/makeMoneyInput shapes
+-- from the reference, on 3.3.5a primitives).
+----------------------------------------------------------------------
+-- auctionhouse-ui-inputfield is a 2x asset: caps 16x66, middle 356x66. Display at INPUT_H and
+-- keep the cap aspect (16:66) so the rounded ends aren't distorted.
+local INPUT_H = 33
+local CAP_W = math.max(6, math.floor(16 * (INPUT_H / 66) + 0.5))
+
+-- opts.justify ("RIGHT") + opts.rightInset reserve room inside the box for a trailing coin icon.
+local function makeInput(parent, w, opts)
+ opts = opts or {}
+ local eb = CreateFrame("EditBox", nil, parent)
+ eb:SetSize(w, INPUT_H)
+ eb:SetAutoFocus(false)
+ eb:SetFontObject(opts.font or _G.ChatFontNormal or _G.GameFontHighlight)
+ eb:SetTextInsets(CAP_W + 3, (opts.rightInset or CAP_W) + 3, 0, 0)
+ if opts.justify and eb.SetJustifyH then eb:SetJustifyH(opts.justify) end
+ eb:SetScript("OnEscapePressed", function(s) s:ClearFocus() end)
+ eb:SetScript("OnEnterPressed", function(s) s:ClearFocus() end)
+ if NE.tex and NE.tex.SetAtlas then
+ local l = eb:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(l, "auctionhouse-ui-inputfield-left", false)
+ l:SetSize(CAP_W, INPUT_H); l:SetPoint("LEFT", eb, "LEFT", 0, 0)
+ local r = eb:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(r, "auctionhouse-ui-inputfield-right", false)
+ r:SetSize(CAP_W, INPUT_H); r:SetPoint("RIGHT", eb, "RIGHT", 0, 0)
+ local m = eb:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(m, "auctionhouse-ui-inputfield-middle", false)
+ m:SetHeight(INPUT_H); m:SetPoint("LEFT", l, "RIGHT", 0, 0); m:SetPoint("RIGHT", r, "LEFT", 0, 0)
+ end
+ return eb
+end
+
+-- Money input mirroring retail LargeMoneyInputFrame: gold (expands) | silver | copper, -6 gaps,
+-- each box with its modern coin icon INSIDE-right + a right-justified number.
+local MONEY_W = 230
+local SC_W = 64
+local COIN = 12
+local function makeMoneyInput(parent)
+ local f = CreateFrame("Frame", nil, parent)
+ f:SetSize(MONEY_W, INPUT_H)
+ local function field(coinAtlas, letters, place)
+ local b = makeInput(f, SC_W, { justify = "RIGHT", rightInset = COIN + 9 })
+ b:SetNumeric(true)
+ b:SetMaxLetters(letters)
+ place(b)
+ local ic = b:CreateTexture(nil, "OVERLAY")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(ic, coinAtlas, false) end
+ ic:SetSize(COIN, COIN)
+ ic:SetPoint("RIGHT", b, "RIGHT", -10, 0)
+ b:SetScript("OnTextChanged", function() if f._onChanged then f._onChanged() end end)
+ return b
+ end
+ f.copper = field("auctionhouse-icon-coin-copper", 2, function(b) b:SetPoint("RIGHT", f, "RIGHT", 0, 0) end)
+ f.silver = field("auctionhouse-icon-coin-silver", 2, function(b) b:SetPoint("RIGHT", f.copper, "LEFT", -6, 0) end)
+ f.gold = field("auctionhouse-icon-coin-gold", 8, function(b)
+ b:ClearAllPoints(); b:SetPoint("LEFT", f, "LEFT", 0, 0); b:SetPoint("RIGHT", f.silver, "LEFT", -6, 0)
+ end)
+ f.GetCopper = function()
+ return (f.gold:GetNumber() or 0) * 10000 + (f.silver:GetNumber() or 0) * 100 + (f.copper:GetNumber() or 0)
+ end
+ f.SetCopper = function(_, c)
+ c = math.max(0, math.floor((c or 0) + 0.5))
+ f.gold:SetNumber(math.floor(c / 10000))
+ f.silver:SetNumber(math.floor(c / 100) % 100)
+ f.copper:SetNumber(c % 100)
+ end
+ f.SetOnChanged = function(_, fn) f._onChanged = fn end
+ return f
+end
+
+-- Read-only money readout with the modern coin icons (stand-in for retail's
+-- MoneyDisplayFrameTemplate): gold segment only when nonzero, silver/copper always,
+-- so a zero amount reads "0s 0c" like the reference.
+local function makeMoneyDisplay(parent)
+ local f = CreateFrame("Frame", nil, parent)
+ f:SetSize(170, 16)
+ local atlases = { "auctionhouse-icon-coin-gold", "auctionhouse-icon-coin-silver", "auctionhouse-icon-coin-copper" }
+ f.segs = {}
+ for i = 1, 3 do
+ local fs = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ local ic = f:CreateTexture(nil, "OVERLAY")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(ic, atlases[i], false) end
+ ic:SetSize(COIN, COIN)
+ f.segs[i] = { fs = fs, ic = ic }
+ end
+ function f:SetAmount(copper)
+ copper = math.max(0, math.floor((copper or 0) + 0.5))
+ local vals = { math.floor(copper / 10000), math.floor(copper / 100) % 100, copper % 100 }
+ local anchor
+ for i = 1, 3 do
+ local seg = self.segs[i]
+ seg.fs:ClearAllPoints()
+ seg.ic:ClearAllPoints()
+ if i > 1 or vals[1] > 0 then
+ if anchor then
+ seg.fs:SetPoint("LEFT", anchor, "RIGHT", 6, 0)
+ else
+ seg.fs:SetPoint("LEFT", self, "LEFT", 0, 0)
+ end
+ seg.fs:SetText(tostring(vals[i]))
+ seg.fs:Show()
+ seg.ic:SetPoint("LEFT", seg.fs, "RIGHT", 2, 0)
+ seg.ic:Show()
+ anchor = seg.ic
+ else
+ seg.fs:Hide()
+ seg.ic:Hide()
+ end
+ end
+ end
+ f:SetAmount(0)
+ return f
+end
+
+-- Sort-direction arrow off the gear-manager flyout sheet -- the one rotatable arrow texture
+-- already proven on this client (modules/character/EquipmentFlyout.lua). Upper half of the
+-- sheet is the arrow pointing up; vertical texcoord flip points it down.
+local ARROW_TEX = "Interface\\PaperDollInfoFrame\\UI-GearManager-FlyoutButton"
+local function setArrowDirection(tex, descending)
+ if descending then
+ tex:SetTexCoord(0.15625, 0.84375, 0.5, 0)
+ else
+ tex:SetTexCoord(0.15625, 0.84375, 0, 0.5)
+ end
+end
+
+function AH.BuildSellPane(parent)
+ local pane = CreateFrame("Frame", nil, parent)
+ pane:SetAllPoints(parent)
+
+ local SF = {
+ durationIndex = 2, buyoutMode = true, currentCount = 0,
+ marketRows = {}, marketName = nil,
+ sortKey = "buyout", sortDesc = false,
+ }
+ pane.Sell = SF
+
+ ----------------------------------------------------------------------
+ -- Panels (reference geometry: ItemSellFrame 363x440 flush on its bg,
+ -- ItemSellList filling the rest). Bottom is +27 from the pane's bottom edge --
+ -- the SAME convention the Buy tab's list/detail panels and the Auctions tab's
+ -- left/right panels use (Window.lua's money inset top sits at exactly pane+27),
+ -- so every tab's panel-bottom divider lines up flush with the currency bar
+ -- instead of sitting a couple px above/below it depending on which tab is open.
+ ----------------------------------------------------------------------
+ local form = CreateFrame("Frame", nil, pane)
+ form:SetPoint("TOPLEFT", pane, "TOPLEFT", 4, -69)
+ form:SetPoint("BOTTOMLEFT", pane, "BOTTOMLEFT", 4, 27)
+ form:SetWidth(363)
+ pane.Left = form
+
+ local formBg = form:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(formBg, "auctionhouse-background-sell-left", false)
+ end
+ formBg:SetPoint("TOPLEFT", form, "TOPLEFT", 3, -3)
+ formBg:SetSize(357, 437)
+
+ local right = CreateFrame("Frame", nil, pane)
+ right:SetPoint("TOPLEFT", form, "TOPRIGHT", 1, 0)
+ right:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", -5, 27)
+ pane.Right = right
+
+ local rightBg = right:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(rightBg, "auctionhouse-background-sell-right", false)
+ end
+ rightBg:SetPoint("TOPLEFT", right, "TOPLEFT", 3, -22)
+ rightBg:SetSize(399, 418)
+
+ -- Recessed gold-trim borders, same treatment as the Buy tab's panels (reference:
+ -- full-panel inset on the form, header-strip + scrollbar-gutter inset on the list).
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, form, 0, 0, 0, 0)
+ pcall(NE.nineslice.AttachInset, right, 0, -19, -22, 0)
+ end
+
+ ----------------------------------------------------------------------
+ -- "Create Auction" tab plate, straddling the form's top edge. On its own
+ -- high-level child frame so it draws ABOVE the inset-border nineslice.
+ ----------------------------------------------------------------------
+ local tabFrame = CreateFrame("Frame", nil, form)
+ tabFrame:SetAllPoints(form)
+ tabFrame:EnableMouse(false)
+ tabFrame:SetFrameLevel((form:GetFrameLevel() or 0) + 20)
+ local tabL = tabFrame:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(tabL, "auctionhouse-selltab-left", true) end
+ tabL:SetPoint("BOTTOMLEFT", form, "TOPLEFT", 42, -3)
+ local tabText = tabFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ tabText:SetText(AUCTION_CREATE_AUCTION or CREATE_AUCTION or "Create Auction")
+ tabText:SetPoint("LEFT", tabL, "RIGHT", 12, 0)
+ local tabM = tabFrame:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(tabM, "auctionhouse-selltab-middle", false) then
+ tabM:SetHeight(23)
+ end
+ tabM:SetPoint("TOPLEFT", tabL, "TOPRIGHT", 0, 0)
+ tabM:SetPoint("RIGHT", tabText, "RIGHT", 12, 0)
+ local tabR = tabFrame:CreateTexture(nil, "ARTWORK")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(tabR, "auctionhouse-selltab-right", true) end
+ tabR:SetPoint("TOPLEFT", tabM, "TOPRIGHT", 0, 0)
+ tabR:SetPoint("BOTTOMLEFT", tabM, "BOTTOMRIGHT", 0, 0)
+
+ ----------------------------------------------------------------------
+ -- Item display: itemheaderframe row plate with the 54px item slot on its
+ -- left and the item name to its right (empty slot shows no label, like the
+ -- reference). Both the slot and the wide row accept the click/drop.
+ ----------------------------------------------------------------------
+ local disp = CreateFrame("Button", nil, form)
+ disp:SetSize(342, 72)
+ disp:SetPoint("TOP", form, "TOP", 0, -14)
+ pane.ItemDisplay = disp
+
+ local hf = disp:CreateTexture(nil, "BORDER")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(hf, "auctionhouse-itemheaderframe", false) end
+ hf:SetSize(342, 72)
+ hf:SetPoint("TOPLEFT", disp, "TOPLEFT", 0, 0)
+
+ local slot = CreateFrame("Button", nil, disp)
+ slot:SetSize(54, 54)
+ slot:SetPoint("LEFT", disp, "LEFT", 12, 0)
+ pane.ItemSlot = slot
+
+ local slotBG = slot:CreateTexture(nil, "BACKGROUND")
+ slotBG:SetAllPoints(slot)
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(slotBG, "auctionhouse-itemicon-empty", false) end
+
+ local sicon = slot:CreateTexture(nil, "ARTWORK")
+ sicon:SetPoint("TOPLEFT", slot, "TOPLEFT", 2, -2)
+ sicon:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -2, 2)
+ sicon:Hide()
+ pane.ItemIcon = sicon
+
+ local itemName = disp:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
+ itemName:SetPoint("LEFT", slot, "RIGHT", 12, 0)
+ itemName:SetPoint("RIGHT", disp, "RIGHT", -12, 0)
+ itemName:SetJustifyH("LEFT")
+ itemName:SetText("")
+ pane.ItemName = itemName
+
+ -- Click/drag places from the cursor; right-click an occupied slot returns the item to the
+ -- bag. Tooltip anchors to the SLOT (not the wide row) so it never lands off to the side.
+ local function doPlace(mouseButton)
+ if not ClickAuctionSellItemButton then return end
+ if mouseButton == "RightButton" then
+ if GetAuctionSellItemInfo and GetAuctionSellItemInfo() then
+ ClickAuctionSellItemButton()
+ if ClearCursor then ClearCursor() end
+ end
+ else
+ -- Places the cursor item, or picks the slotted item back up when the cursor is empty.
+ ClickAuctionSellItemButton()
+ end
+ end
+ local slotEnter
+ slotEnter = function()
+ GameTooltip:SetOwner(slot, "ANCHOR_RIGHT")
+ if GetAuctionSellItemInfo and GetAuctionSellItemInfo() and GameTooltip.SetAuctionSellItem then
+ GameTooltip:SetAuctionSellItem()
+ else
+ GameTooltip:SetText(AUCTION_ITEM_TEXT or "Auction Item", 1, 1, 1)
+ end
+ GameTooltip:Show()
+ -- Keep refreshing while hovered so pressing SHIFT mid-hover raises the comparison tooltip
+ -- (NE.FrameUtil.WireLiveTooltip documents the stock UpdateTooltip contract). Registered on
+ -- `slot` rather than on whichever button was entered: the tooltip is deliberately anchored to
+ -- the icon even when the mouse is over the wider `disp` row, so `slot` is always the OWNER --
+ -- and GameTooltip_OnUpdate only ever calls UpdateTooltip on the owner.
+ slot.UpdateTooltip = slotEnter
+ end
+ local function slotLeave()
+ slot.UpdateTooltip = nil
+ GameTooltip:Hide()
+ end
+ for _, b in ipairs({ disp, slot }) do
+ b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
+ b:SetScript("OnClick", function(_, mouseButton) doPlace(mouseButton) end)
+ b:SetScript("OnReceiveDrag", function()
+ if ClickAuctionSellItemButton then ClickAuctionSellItemButton() end
+ end)
+ b:SetScript("OnEnter", slotEnter)
+ b:SetScript("OnLeave", slotLeave)
+ end
+
+ ----------------------------------------------------------------------
+ -- Aligned form rows: right-justified 93px label + control 18px to its right
+ -- (reference AuctionHouseSellFrameTemplate row shape).
+ ----------------------------------------------------------------------
+ local LABEL_W = 93
+ local function alignedRow(below, labelText)
+ local r = CreateFrame("Frame", nil, form)
+ r:SetSize(330, 30)
+ if below then
+ r:SetPoint("TOPLEFT", below, "BOTTOMLEFT", 0, -15)
+ else
+ r:SetPoint("TOPLEFT", disp, "BOTTOMLEFT", 0, -10)
+ end
+ local l = r:CreateFontString(nil, "ARTWORK", "GameFontNormal")
+ l:SetSize(LABEL_W, 0)
+ l:SetJustifyH("RIGHT")
+ l:SetPoint("LEFT", r, "LEFT", 0, 0)
+ l:SetText(labelText)
+ r.Label = l
+ return r
+ end
+
+ -- Quantity (+ Max).
+ local qRow = alignedRow(nil, AUCTION_QUANTITY_LABEL or "Quantity")
+ local qty = makeInput(qRow, 134)
+ qty:SetNumeric(true)
+ qty:SetMaxLetters(5)
+ qty:SetPoint("LEFT", qRow.Label, "RIGHT", 18, 0)
+ qty:SetText("1")
+ pane.Quantity = qty
+
+ local maxBtn = CreateFrame("Button", nil, qRow, "UIPanelButtonTemplate")
+ maxBtn:SetSize(50, 20)
+ maxBtn:SetPoint("LEFT", qty, "RIGHT", 10, 0)
+ maxBtn:SetText(MAX or "Max")
+
+ -- Buyout price (per item; relabeled "Starting Bid" when Buyout Mode is off).
+ local pRow = alignedRow(qRow, AUCTION_BUYOUT_PRICE or "Buyout Price")
+ local priceLabel = pRow.Label
+ local money = makeMoneyInput(pRow)
+ money:SetPoint("LEFT", pRow.Label, "RIGHT", 18, 0)
+ pane.BuyoutPrice = money
+
+ local function priceCopper()
+ return money:GetCopper()
+ end
+
+ -- Duration: dropdown-look button in the same input skin; click cycles the three options.
+ local dRow = alignedRow(pRow, AUCTION_DURATION or "Duration")
+ local duration = CreateFrame("Button", nil, dRow)
+ duration:SetSize(142, INPUT_H)
+ duration:SetPoint("LEFT", dRow.Label, "RIGHT", 18, 0)
+ if NE.tex and NE.tex.SetAtlas then
+ local l = duration:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(l, "auctionhouse-ui-inputfield-left", false)
+ l:SetSize(CAP_W, INPUT_H); l:SetPoint("LEFT", duration, "LEFT", 0, 0)
+ local r = duration:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(r, "auctionhouse-ui-inputfield-right", false)
+ r:SetSize(CAP_W, INPUT_H); r:SetPoint("RIGHT", duration, "RIGHT", 0, 0)
+ local m = duration:CreateTexture(nil, "BACKGROUND")
+ NE.tex.SetAtlas(m, "auctionhouse-ui-inputfield-middle", false)
+ m:SetHeight(INPUT_H); m:SetPoint("LEFT", l, "RIGHT", 0, 0); m:SetPoint("RIGHT", r, "LEFT", 0, 0)
+ end
+ local durText = duration:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ durText:SetPoint("CENTER", duration, "CENTER", -6, 0)
+ durText:SetText(DURATIONS[SF.durationIndex].label)
+ local durArrow = duration:CreateTexture(nil, "OVERLAY")
+ durArrow:SetTexture(ARROW_TEX)
+ setArrowDirection(durArrow, true)
+ durArrow:SetSize(12, 11)
+ durArrow:SetPoint("RIGHT", duration, "RIGHT", -10, 0)
+ pane.DurationButton = duration
+
+ -- Deposit / Total Price coin readouts.
+ local depRow = alignedRow(dRow, AUCTION_DEPOSIT_LABEL or DEPOSIT or "Deposit")
+ local dep = makeMoneyDisplay(depRow)
+ dep:SetPoint("LEFT", depRow.Label, "RIGHT", 18, 0)
+ pane.Deposit = dep
+
+ local totRow = alignedRow(depRow, AUCTION_TOTAL_VALUE or "Total Price")
+ local total = makeMoneyDisplay(totRow)
+ total:SetPoint("LEFT", totRow.Label, "RIGHT", 18, 0)
+ pane.Total = total
+
+ ----------------------------------------------------------------------
+ -- Create Auction + Buyout Mode
+ ----------------------------------------------------------------------
+ local createBtn = CreateFrame("Button", nil, form, "UIPanelButtonTemplate")
+ createBtn:SetSize(160, 22)
+ createBtn:SetPoint("TOP", totRow, "BOTTOM", 0, -16)
+ createBtn:SetText(AUCTION_CREATE_AUCTION or "Create Auction")
+ createBtn:Disable()
+ pane.CreateButton = createBtn
+
+ local buyoutMode = CreateFrame("CheckButton", nil, form, "UICheckButtonTemplate")
+ buyoutMode:SetSize(28, 28)
+ buyoutMode:SetPoint("BOTTOMLEFT", form, "BOTTOMLEFT", 10, 8)
+ buyoutMode:SetChecked(true)
+ local buyoutLabel = form:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ buyoutLabel:SetPoint("LEFT", buyoutMode, "RIGHT", 4, 0)
+ buyoutLabel:SetText(AUCTION_HOUSE_BUYOUT_MODE or "Buyout Mode")
+
+ ----------------------------------------------------------------------
+ -- Market listings (right side): sortable header strip + refresh, rows on a
+ -- faux scroll with the addon's custom scrollbar (same shape as the Buy tab).
+ ----------------------------------------------------------------------
+ local headerStrip = right:CreateTexture(nil, "BORDER")
+ headerStrip:SetTexture("Interface\\Buttons\\WHITE8X8")
+ headerStrip:SetVertexColor(0.06, 0.06, 0.07, 0.95)
+ headerStrip:SetPoint("TOPLEFT", right, "TOPLEFT", 3, -1)
+ headerStrip:SetPoint("TOPRIGHT", right, "TOPRIGHT", -22, -1)
+ headerStrip:SetHeight(21)
+
+ local applySort -- forward decl (used by header clicks)
+ local refreshMarketRows
+
+ local headerDefs = {
+ { key = "buyout", text = AUCTION_BUYOUT_PRICE or "Buyout", x = 8, w = 130 },
+ { key = "qty", text = AUCTION_QUANTITY_LABEL or "Qty", x = 152, w = 48 },
+ { key = "seller", text = AUCTION_SELLER or "Seller", x = 206, w = 110 },
+ }
+ local headerBtns = {}
+ local function updateSortArrows()
+ for i = 1, #headerBtns do
+ local hb = headerBtns[i]
+ if hb._key == SF.sortKey then
+ setArrowDirection(hb.Arrow, SF.sortDesc)
+ hb.Arrow:Show()
+ else
+ hb.Arrow:Hide()
+ end
+ end
+ end
+ for i = 1, #headerDefs do
+ local hd = headerDefs[i]
+ local hb = CreateFrame("Button", nil, right)
+ hb:SetSize(hd.w, 19)
+ hb:SetPoint("TOPLEFT", right, "TOPLEFT", hd.x, -2)
+ hb._key = hd.key
+ local fs = hb:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ fs:SetPoint("LEFT", hb, "LEFT", 4, 0)
+ fs:SetText(hd.text)
+ hb.Text = fs
+ local ar = hb:CreateTexture(nil, "OVERLAY")
+ ar:SetTexture(ARROW_TEX)
+ ar:SetSize(10, 9)
+ ar:SetPoint("LEFT", fs, "RIGHT", 3, 0)
+ ar:Hide()
+ hb.Arrow = ar
+ hb:SetScript("OnClick", function(self)
+ if SF.sortKey == self._key then
+ SF.sortDesc = not SF.sortDesc
+ else
+ SF.sortKey, SF.sortDesc = self._key, false
+ end
+ applySort()
+ updateSortArrows()
+ refreshMarketRows()
+ end)
+ headerBtns[i] = hb
+ end
+
+ -- Text refresh button (an icon-texture refresh already proved unreliable on this client --
+ -- see Browse.lua's detailRefresh note), sitting at the header strip's right end.
+ local refreshBtn = CreateFrame("Button", nil, right, "UIPanelButtonTemplate")
+ refreshBtn:SetSize(64, 18)
+ refreshBtn:SetPoint("TOPRIGHT", right, "TOPRIGHT", -26, -2)
+ refreshBtn:SetText(REFRESH or "Refresh")
+ pane.RefreshButton = refreshBtn
+
+ local emptyText = right:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ emptyText:SetPoint("CENTER", right, "CENTER", 0, 40)
+ emptyText:SetText("")
+ emptyText:SetTextColor(1, 0.82, 0)
+ pane.EmptyText = emptyText
+
+ local ROW_H = 18
+ local VISIBLE_ROWS = 21
+ local rows = {}
+ -- FauxScrollFrameTemplate resolves its scrollbar sub-widgets via GetName() concatenation --
+ -- it must have a real global name (see Browse.lua for the failure mode).
+ local scroll = CreateFrame("ScrollFrame", "NE_AuctionHouseSellMarketScroll", right, "FauxScrollFrameTemplate")
+ scroll:SetPoint("TOPLEFT", right, "TOPLEFT", 4, -26)
+ scroll:SetPoint("BOTTOMRIGHT", right, "BOTTOMRIGHT", -26, 8)
+
+ -- Physical scrollbar track+thumb in the reserved gutter. Must be forced to the window's
+ -- DIALOG strata or the rows draw over it (same trap as every other list in this window).
+ if NE.scrollbar and NE.scrollbar.BuildCustom then
+ local ok, bar = pcall(NE.scrollbar.BuildCustom, scroll, { x = -8 })
+ if ok and bar then
+ bar:SetFrameStrata("DIALOG")
+ bar:SetFrameLevel((right:GetFrameLevel() or 1) + 10)
+ -- Arrow buttons' strata/level were set against the bar's strata AT BUILD TIME ("HIGH") --
+ -- promoting the bar to DIALOG afterward (same trap as every other list in this window)
+ -- leaves them a strata behind unless they're bumped too.
+ if bar._upBtn then bar._upBtn:SetFrameStrata("DIALOG"); bar._upBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ if bar._downBtn then bar._downBtn:SetFrameStrata("DIALOG"); bar._downBtn:SetFrameLevel(bar:GetFrameLevel() + 1) end
+ end
+ end
+
+ local function fsUpdate(frame, totalCount, shown, step)
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(frame, totalCount, shown, step)
+ else frame.offset = frame.offset or 0 end
+ end
+ local function fsGetOffset(frame)
+ if FauxScrollFrame_GetOffset then return FauxScrollFrame_GetOffset(frame) or 0 end
+ return frame.offset or 0
+ end
+
+ applySort = function()
+ local key, desc = SF.sortKey, SF.sortDesc
+ table.sort(SF.marketRows, function(a, b)
+ local av, bv
+ if key == "qty" then
+ av, bv = a.count or 0, b.count or 0
+ elseif key == "seller" then
+ av, bv = string.lower(a.owner or ""), string.lower(b.owner or "")
+ else
+ av, bv = a.perUnit or math.huge, b.perUnit or math.huge
+ end
+ if av == bv then return (a.index or 0) < (b.index or 0) end
+ if desc then return av > bv end
+ return av < bv
+ end)
+ end
+
+ local function ensureRow(i)
+ local row = rows[i]
+ if row then return row end
+
+ row = CreateFrame("Button", nil, right)
+ row:SetHeight(ROW_H)
+ if i == 1 then
+ row:SetPoint("TOPLEFT", right, "TOPLEFT", 6, -26)
+ row:SetPoint("TOPRIGHT", right, "TOPRIGHT", -30, -26)
+ else
+ row:SetPoint("TOPLEFT", rows[i - 1], "BOTTOMLEFT", 0, -1)
+ row:SetPoint("TOPRIGHT", rows[i - 1], "BOTTOMRIGHT", 0, -1)
+ end
+
+ row.Bg = row:CreateTexture(nil, "BACKGROUND")
+ row.Bg:SetTexture("Interface\\Buttons\\WHITE8X8")
+ row.Bg:SetAllPoints(row)
+ row.Bg:SetVertexColor(0.07, 0.07, 0.08, (i % 2 == 0) and 0.30 or 0.20)
+
+ row:SetHighlightTexture("Interface\\Buttons\\WHITE8X8")
+ local hl = row:GetHighlightTexture()
+ if hl then
+ hl:SetVertexColor(1, 0.82, 0, 0.12)
+ hl:SetBlendMode("ADD")
+ end
+
+ row.Buyout = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ row.Buyout:SetPoint("LEFT", row, "LEFT", 6, 0)
+ row.Buyout:SetWidth(130)
+ row.Buyout:SetJustifyH("LEFT")
+
+ row.Qty = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ row.Qty:SetPoint("LEFT", row, "LEFT", 150, 0)
+ row.Qty:SetWidth(46)
+ row.Qty:SetJustifyH("LEFT")
+
+ row.Seller = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ row.Seller:SetPoint("LEFT", row, "LEFT", 204, 0)
+ row.Seller:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ row.Seller:SetJustifyH("LEFT")
+
+ -- Click a listing to copy its per-unit buyout into the price input.
+ row:SetScript("OnClick", function(self)
+ if not self._buyout or not self._count or self._count <= 0 then return end
+ money:SetCopper(self._buyout / self._count)
+ end)
+
+ rows[i] = row
+ return row
+ end
+
+ local function coin(copper)
+ if not copper or copper <= 0 then return "0" end
+ if GetCoinTextureString then return GetCoinTextureString(copper) end
+ return tostring(copper)
+ end
+
+ refreshMarketRows = function()
+ local data = SF.marketRows
+ local totalCount = #data
+ fsUpdate(scroll, totalCount, VISIBLE_ROWS, ROW_H + 1)
+ local offset = fsGetOffset(scroll)
+
+ if totalCount > 0 then
+ emptyText:Hide()
+ else
+ emptyText:Show()
+ end
+
+ for i = 1, VISIBLE_ROWS do
+ local row = ensureRow(i)
+ local d = data[i + offset]
+ if d then
+ row._buyout, row._count = d.buyout, d.count
+ row.Buyout:SetText(coin(d.buyout))
+ row.Qty:SetText(tostring(d.count))
+ row.Seller:SetText(d.owner or "")
+ row:Show()
+ else
+ row._buyout, row._count = nil, nil
+ row:Hide()
+ end
+ end
+ end
+ pane.RefreshMarketRows = refreshMarketRows
+
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ if FauxScrollFrame_OnVerticalScroll then
+ FauxScrollFrame_OnVerticalScroll(self, offset, ROW_H + 1, refreshMarketRows)
+ else
+ local step = ROW_H + 1
+ self.offset = math.max(0, math.floor((offset / step) + 0.5))
+ refreshMarketRows()
+ end
+ end)
+
+ ----------------------------------------------------------------------
+ -- Behaviour
+ ----------------------------------------------------------------------
+ local function readMarket()
+ local out = {}
+ local n = (GetNumAuctionItems and GetNumAuctionItems("list")) or 0
+ for i = 1, n do
+ local ok, name, _, count, _, _, _, _, _, buyoutPrice, _, _, owner =
+ pcall(GetAuctionItemInfo, "list", i)
+ if not ok then break end
+ if name and buyoutPrice and buyoutPrice > 0 then
+ local cnt = count or 1
+ out[#out + 1] = {
+ index = i, count = cnt, buyout = buyoutPrice, owner = owner,
+ perUnit = (cnt > 0) and (buyoutPrice / cnt) or buyoutPrice,
+ }
+ end
+ end
+ return out
+ end
+
+ local function queryMarket(force)
+ local name = GetAuctionSellItemInfo and GetAuctionSellItemInfo()
+ if not name then
+ SF.marketName = nil
+ SF.marketRows = {}
+ emptyText:SetText("")
+ refreshMarketRows()
+ return
+ end
+ if name == SF.marketName and not force then return end
+
+ -- ISSUE #31: the Buy tab's scan owns the shared "list" query slot while it is paging. Firing
+ -- this lookup into the middle of it steals the throttle slot, so the scan burns its retries and
+ -- aborts with a partial aggregate. Wait for it -- bounded, so a wedged scan can't stall this
+ -- forever. Retry with force=true or the name-unchanged guard above would swallow it.
+ if AH.IsBrowseScanning and AH.IsBrowseScanning() then
+ SF._marketWaits = (SF._marketWaits or 0) + 1
+ if SF._marketWaits <= 20 and C_Timer and C_Timer.After then
+ emptyText:SetText(SEARCHING or "Searching...")
+ C_Timer.After(0.25, function() queryMarket(true) end)
+ return
+ end
+ end
+ SF._marketWaits = 0
+
+ SF.marketName = name
+ if not (CanSendAuctionQuery and CanSendAuctionQuery("list")) then
+ emptyText:SetText(AUCTION_HOUSE_THROTTLED or "Auction query is throttled. Try again in a moment.")
+ return
+ end
+ emptyText:SetText(SEARCHING or "Searching...")
+ SF.marketRows = {}
+ refreshMarketRows()
+ -- ISSUE #31: -1 ("any rarity"), not 0 -- QueryAuctionItems' quality argument is an exact match
+ -- and 0 means "exactly Poor", so this market lookup previously came back empty for every item
+ -- that wasn't grey. Mirrors AH.ANY_QUALITY in Browse.lua.
+ if AH.ClaimListQuery then AH.ClaimListQuery("sellmarket") end
+ pcall(QueryAuctionItems, name, 0, 0, 0, 0, 0, 0, false, -1, false)
+ end
+
+ refreshBtn:SetScript("OnClick", function()
+ queryMarket(true)
+ end)
+
+ local function updatePostState()
+ local slotted = GetAuctionSellItemInfo and GetAuctionSellItemInfo()
+ local stack = tonumber(qty:GetText()) or 0
+ local price = priceCopper()
+ local ok = slotted ~= nil
+ and stack >= 1
+ and stack <= (SF.currentCount or 0)
+ and price > 0
+ createBtn:SetEnabled(ok and true or false)
+ end
+ pane.UpdatePostState = updatePostState
+
+ local function recalcPrice()
+ local stack = tonumber(qty:GetText()) or 0
+ local perItem = priceCopper()
+ local totalPrice = perItem * stack
+ local minBid = totalPrice
+ local buyoutPrice = SF.buyoutMode and totalPrice or 0
+
+ local depositCopper = computeDeposit(DURATIONS[SF.durationIndex].code, minBid, buyoutPrice, stack, 1)
+ dep:SetAmount(depositCopper)
+ total:SetAmount(SF.buyoutMode and buyoutPrice or minBid)
+ SF._minBid, SF._buyout, SF._stack, SF._depositCopper = minBid, buyoutPrice, stack, depositCopper
+ updatePostState()
+ end
+ pane.RecalculatePrice = recalcPrice
+ money:SetOnChanged(recalcPrice)
+
+ local function updateItem()
+ local name, tex, count = GetAuctionSellItemInfo()
+ if tex then
+ sicon:SetTexture(tex)
+ sicon:Show()
+ else
+ sicon:Hide()
+ end
+ if name then
+ itemName:SetText(name)
+ SF.currentCount = count or 1
+ qty:SetText(tostring(SF.currentCount))
+ else
+ itemName:SetText("")
+ SF.currentCount = 0
+ end
+ recalcPrice()
+ queryMarket()
+ end
+
+ qty:SetScript("OnTextChanged", function(self)
+ local n = tonumber(self:GetText()) or 0
+ if n > (SF.currentCount or 0) and SF.currentCount > 0 then
+ self:SetNumber(SF.currentCount)
+ return
+ end
+ recalcPrice()
+ end)
+
+ maxBtn:SetScript("OnClick", function()
+ qty:SetNumber(SF.currentCount or 0)
+ recalcPrice()
+ end)
+
+ duration:SetScript("OnClick", function()
+ SF.durationIndex = (SF.durationIndex % #DURATIONS) + 1
+ durText:SetText(DURATIONS[SF.durationIndex].label)
+ recalcPrice()
+ end)
+
+ buyoutMode:SetScript("OnClick", function(self)
+ SF.buyoutMode = self:GetChecked() and true or false
+ priceLabel:SetText(SF.buyoutMode and (AUCTION_BUYOUT_PRICE or "Buyout Price") or (AUCTION_STARTING_PRICE or "Starting Bid"))
+ recalcPrice()
+ end)
+
+ createBtn:SetScript("OnClick", function()
+ if not (GetAuctionSellItemInfo and GetAuctionSellItemInfo()) then return end
+ local stack = SF._stack or 0
+ if stack < 1 then return end
+ local minBid = SF._minBid or 0
+ local buyoutPrice = SF._buyout or 0
+ if minBid <= 0 then return end
+ local durationCode = DURATIONS[SF.durationIndex].code
+ local ok = pcall(PostAuction, minBid, buyoutPrice, durationCode, stack, 1)
+ if ok then
+ if PlaySound and SOUNDKIT and SOUNDKIT.LOOT_WINDOW_COIN_SOUND then
+ PlaySound(SOUNDKIT.LOOT_WINDOW_COIN_SOUND)
+ end
+ if DEFAULT_CHAT_FRAME and ERR_AUCTION_STARTED then
+ DEFAULT_CHAT_FRAME:AddMessage(ERR_AUCTION_STARTED)
+ end
+ end
+ end)
+
+ ----------------------------------------------------------------------
+ -- Events
+ ----------------------------------------------------------------------
+ local watcher = CreateFrame("Frame", nil, pane)
+ watcher:RegisterEvent("NEW_AUCTION_UPDATE")
+ watcher:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
+ watcher:RegisterEvent("AUCTION_HOUSE_SHOW")
+ watcher:RegisterEvent("PLAYER_MONEY")
+ watcher:SetScript("OnEvent", function(_, event)
+ if not pane:IsShown() then
+ if event == "AUCTION_HOUSE_SHOW" then updateItem() end
+ return
+ end
+ if event == "NEW_AUCTION_UPDATE" then
+ updateItem()
+ elseif event == "AUCTION_ITEM_LIST_UPDATE" then
+ -- ISSUE #31: only read the shared "list" slot when OUR market lookup is what filled it.
+ -- This used to fire on every update, so a Buy-tab scan page or an Auctionator search landed
+ -- in this tab's market table as if it were price data for the slotted item.
+ if AH.OwnsListQuery and not AH.OwnsListQuery("sellmarket") then return end
+ SF.marketRows = readMarket()
+ if #SF.marketRows == 0 then
+ emptyText:SetText(BROWSE_NO_RESULTS_TEXT or "No auctions found for this item.")
+ end
+ applySort()
+ updateSortArrows()
+ refreshMarketRows()
+ elseif event == "PLAYER_MONEY" then
+ updatePostState()
+ end
+ end)
+
+ pane:HookScript("OnShow", function()
+ updateItem()
+ end)
+
+ updateSortArrows()
+ updateItem()
+
+ return pane
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/TabBridge.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/TabBridge.lua
new file mode 100644
index 0000000..cef6af8
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/TabBridge.lua
@@ -0,0 +1,141 @@
+-- DragonUI_NewEra/modules/auctionhouse/TabBridge.lua
+-- External tab embedding contract. Auctionator integration is additive and optional.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+NE.ah = NE.ah or {}
+local AH = NE.ah
+AH.bridge = AH.bridge or {}
+
+local function isAddonLoaded(name)
+ if NE.IsAddOnLoaded then return NE.IsAddOnLoaded(name) end
+ if _G.IsAddOnLoaded then
+ local ok, loaded = pcall(_G.IsAddOnLoaded, name)
+ return ok and loaded and true or false
+ end
+ return false
+end
+
+AH.bridge.providers = AH.bridge.providers or {}
+
+function AH.bridge.RegisterProvider(id, provider)
+ if not id or type(provider) ~= "table" then return end
+ AH.bridge.providers[id] = provider
+ if AH.RefreshExternalTabs then
+ AH.RefreshExternalTabs()
+ end
+end
+
+function AH.bridge.GetExternalTabs(frame)
+ local tabs = {}
+ for id, provider in pairs(AH.bridge.providers) do
+ local enabled = true
+ if type(provider.isEnabled) == "function" then
+ local ok, v = pcall(provider.isEnabled, frame)
+ enabled = ok and v and true or false
+ end
+ if enabled and type(provider.getTabs) == "function" then
+ local ok, out = pcall(provider.getTabs, frame)
+ if ok and type(out) == "table" then
+ for i = 1, #out do
+ local t = out[i]
+ if type(t) == "table" and t.key and t.text and type(t.onSelect) == "function" then
+ t.providerId = id
+ tabs[#tabs + 1] = t
+ end
+ end
+ end
+ end
+ end
+ -- Sort by KEY, not label: native-mirrored keys are "AuctionatorNative4/5/6", so this preserves
+ -- Auctionator's own tab order (Buy, Sell, More...) instead of alphabetizing the labels
+ -- (Buy, More..., Sell).
+ table.sort(tabs, function(a, b)
+ return tostring(a.key) < tostring(b.key)
+ end)
+ return tabs
+end
+
+-- Built-in Auctionator provider: appears only when Auctionator is loaded.
+-- This does not reimplement Auctionator logic; it delegates into Auctionator/legacy tabs when possible.
+AH.bridge.RegisterProvider("Auctionator", {
+ isEnabled = function()
+ return isAddonLoaded("Auctionator")
+ end,
+
+ getTabs = function(frame)
+ local list = {}
+
+ -- If Auctionator added extra native AuctionFrame tabs (>3), mirror each as an embedded tab.
+ local idx = 4
+ while _G["AuctionFrameTab" .. idx] do
+ local src = _G["AuctionFrameTab" .. idx]
+ local textObj = _G[src:GetName() .. "Text"]
+ local label = (textObj and textObj.GetText and textObj:GetText()) or (src.GetText and src:GetText()) or ("Tab " .. idx)
+ list[#list + 1] = {
+ key = "AuctionatorNative" .. idx,
+ text = label,
+ onSelect = function(ctx)
+ local NE = _G.DragonUI_NewEra
+ local ah = NE and NE.ah
+ local atr = ah and ah.atr
+ if atr and atr.IsEmbedded and atr.IsEmbedded() then
+ -- Embedded (AuctionatorEmbed.lua): Atr_Main_Panel lives inside this shell's
+ -- ExternalPane, so click the native tab to drive Auctionator's own pane logic and
+ -- KEEP the legacy AuctionFrame cloaked and the shell up. When this select came from
+ -- the Atr_AuctionFrameTab_OnClick sync hook (atr._syncing), Auctionator has already
+ -- switched its pane -- clicking again would bounce back through the hook.
+ if not atr._syncing then
+ atr._clicking = true
+ if src and src.Click then pcall(src.Click, src) end
+ atr._clicking = nil
+ end
+ if ah.SuppressLegacyAuctionFrame then ah.SuppressLegacyAuctionFrame() end
+ return
+ end
+ -- Fallback (embedding unavailable): reverse the alpha/mouse cloak so AuctionFrame and
+ -- Atr_Main_Panel are visible, then dismiss the NE shell so Auctionator's own UI is
+ -- unobstructed.
+ if ah then
+ if ah.UncloakLegacyAuctionFrame then ah.UncloakLegacyAuctionFrame() end
+ if ah.Hide then ah.Hide() end
+ end
+ if src and src.Click then pcall(src.Click, src) end
+ end,
+ }
+ idx = idx + 1
+ end
+
+ if #list == 0 then
+ list[#list + 1] = {
+ key = "Auctionator",
+ text = "Auctionator",
+ onSelect = function(ctx)
+ local NE = _G.DragonUI_NewEra
+ local ah = NE and NE.ah
+ local atr = ah and ah.atr
+ -- Embedded (AuctionatorEmbed.lua): keep the shell up and the legacy frame cloaked;
+ -- otherwise fall back to uncloak-and-dismiss so Auctionator's own UI is unobstructed.
+ local embedded = atr and atr.IsEmbedded and atr.IsEmbedded()
+ if not embedded and ah then
+ if ah.UncloakLegacyAuctionFrame then ah.UncloakLegacyAuctionFrame() end
+ if ah.Hide then ah.Hide() end
+ end
+ -- Auctionator's BUY_TAB/SELL_TAB/MORE_TAB constants are LOCAL to its own file (never
+ -- published as globals), so _G.BUY_TAB is always nil -- this call silently no-op'd
+ -- forever. Its addon source hardcodes BUY_TAB = 3; Atr_SelectPane just needs that
+ -- numeric value to find the tab it tagged with matching .auctionatorTab at creation.
+ if type(_G.Atr_SelectPane) == "function" and not (embedded and atr._syncing) then
+ if atr then atr._clicking = true end
+ pcall(_G.Atr_SelectPane, 3)
+ if atr then atr._clicking = nil end
+ end
+ if embedded and ah.SuppressLegacyAuctionFrame then ah.SuppressLegacyAuctionFrame() end
+ end,
+ }
+ end
+
+ return list
+ end,
+})
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Window.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Window.lua
new file mode 100644
index 0000000..64b9b28
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/auctionhouse/Window.lua
@@ -0,0 +1,754 @@
+-- DragonUI_NewEra/modules/auctionhouse/Window.lua
+-- Visual shell host for the modern Auction House panel with optional external tab embedding.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+NE.ah = NE.ah or {}
+local AH = NE.ah
+
+local MODULE = "AuctionHouse"
+local FRAME_NAME = "NE_AuctionHouseFrame"
+
+-- The window is called "Auction House" whichever built-in tab is showing, matching retail (and the
+-- physical thing you walked up to) rather than naming the tab twice -- the tab strip already says
+-- Buy / Sell / Auctions. 3.3.5a has no AUCTION_HOUSE global; BUTTON_LAG_AUCTIONHOUSE is the only
+-- GlobalStrings entry whose value is exactly "Auction House", and it IS localized per client, so
+-- it beats hardcoding English. Literal fallback in case a locale ever drops it.
+local AH_WINDOW_TITLE = BUTTON_LAG_AUCTIONHOUSE or "Auction House"
+
+-- Ctrl-click dress-up preview. In WoW 3.3.5a the dressup function is DressUpItemLink(link);
+-- DressUpLink is the retail/modern name and does not exist in this client. The legacy
+-- AuctionFrame is kept alive (alpha 0) to hold the AH session. In environments where
+-- SideDressUpFrame.parentFrame points to AuctionFrame, DressUpVisual would route to the bare
+-- side model instead of the full Dressing Room; temporarily clear parentFrame so it falls
+-- through to DressUpFrame, then restore.
+function AH.DressUpItem(link)
+ if not link or not DressUpItemLink then return end
+ local side = _G.SideDressUpFrame
+ local saved
+ if side then saved = side.parentFrame; side.parentFrame = nil end
+ DressUpItemLink(link)
+ if side then side.parentFrame = saved end
+end
+
+-- Shared modified-click router for every item row in this window: SHIFT links the item into an
+-- open chat edit box, CTRL opens the dressing room. Returns true when it consumed the click, so
+-- callers can fall through to their normal (plain-click) behaviour otherwise.
+--
+-- Deliberately NOT a plain HandleModifiedItemClick(link) call, even though that is the stock
+-- router (ItemButtonTemplate.lua) and has exactly this CHATLINK-then-DRESSUP precedence: its
+-- DRESSUP branch goes straight to DressUpItemLink and would bypass the SideDressUpFrame workaround
+-- above. Same order, our dress-up path.
+function AH.HandleItemClick(link)
+ if not link then return false end
+
+ if IsModifiedClick and IsModifiedClick("CHATLINK") and type(ChatEdit_InsertLink) == "function" then
+ -- Suppress this addon's OWN ChatEdit_InsertLink hook (Browse.lua) for the duration of the
+ -- call. That hook turns a shift-click into an auction search whenever no chat edit box takes
+ -- the link -- which is what we want when shift-clicking an item in the BAGS while the window
+ -- is open (issue #17), but not for a row inside the results list, where it would throw away
+ -- the results being looked at and re-search the item just clicked. hooksecurefunc post-hooks
+ -- run synchronously inside the call, so a plain flag around it is enough; no timer needed.
+ AH._suppressLinkSearch = true
+ local ok, inserted = pcall(ChatEdit_InsertLink, link)
+ AH._suppressLinkSearch = false
+ if ok and inserted then return true end
+ -- No chat box took it: fall through, so shift-click behaves like a plain click rather than
+ -- silently doing nothing.
+ end
+
+ if IsModifiedClick and IsModifiedClick("DRESSUP") then
+ AH.DressUpItem(link)
+ return true
+ end
+
+ return false
+end
+
+local BASE_MODES = {
+ Buy = true,
+ Sell = true,
+ Auctions = true,
+}
+
+local function isModuleEnabled()
+ local dragon = NE.dragon
+ if not (dragon and dragon.db and dragon.db.profile and dragon.db.profile.newera) then return true end
+ local m = dragon.db.profile.newera.modules and dragon.db.profile.newera.modules[MODULE]
+ if type(m) == "table" and m.enabled ~= nil then
+ return m.enabled and true or false
+ end
+ return true
+end
+
+-- Force the AH background/chrome sheets GPU-resident for the whole session via a permanent,
+-- SHOWN 1x1 BACKGROUND frame (same pattern as Spellbook.lua's prewarmBackgroundBLP). createWindow()
+-- builds ALL panes (Buy/Sell/Auctions) at PLAYER_LOGIN while the real window frame is still
+-- :Hide()'n -- every background/inset/nav-button texture cropped from these two sheets gets
+-- SetTexture()'d onto a texture that never actually draws until the player first opens the AH.
+-- If the BLP hasn't finished streaming from disk by then, the crop resolves to a black frame or
+-- the old Blizzard art peeking through. A shown frame forces the upload up front and keeps the
+-- file resident (not evicted) for the rest of the session.
+local AH_PREWARM_FILES = { 3054898, 3046538, 2922105 }
+local function prewarmAuctionHouseTextures()
+ if AH._bgPrewarmed then return end
+ AH._bgPrewarmed = true
+ local pw = CreateFrame("Frame", nil, UIParent)
+ pw:SetFrameStrata("BACKGROUND"); pw:SetFrameLevel(1)
+ pw:SetSize(1, 1)
+ pw:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
+ for _, fdid in ipairs(AH_PREWARM_FILES) do
+ local path = NE.tex and NE.tex.localFiles and NE.tex.localFiles[fdid]
+ if path then
+ local t = pw:CreateTexture(nil, "BACKGROUND")
+ t:SetAllPoints(pw)
+ t:SetTexture(path)
+ end
+ end
+ pw:Show()
+ AH._bgPrewarm = pw
+end
+
+local function disableMouseTree(frame, depth)
+ if not frame or (depth or 0) > 10 then return end
+ if frame.EnableMouse then pcall(frame.EnableMouse, frame, false) end
+ if frame.EnableMouseWheel then pcall(frame.EnableMouseWheel, frame, false) end
+ if frame.SetAlpha then pcall(frame.SetAlpha, frame, 0) end
+ if frame.GetChildren then
+ local kids = { frame:GetChildren() }
+ for i = 1, #kids do
+ disableMouseTree(kids[i], (depth or 0) + 1)
+ end
+ end
+end
+
+local function restoreFrameTree(frame, depth)
+ if not frame or (depth or 0) > 10 then return end
+ if frame.SetAlpha then pcall(frame.SetAlpha, frame, 1) end
+ if frame.EnableMouse then pcall(frame.EnableMouse, frame, true) end
+ if frame.GetChildren then
+ local kids = { frame:GetChildren() }
+ for i = 1, #kids do
+ restoreFrameTree(kids[i], (depth or 0) + 1)
+ end
+ end
+end
+
+local function cloakFrame(frame)
+ if not frame then return end
+ disableMouseTree(frame, 0)
+ if frame.SetAlpha then pcall(frame.SetAlpha, frame, 0) end
+end
+
+local function suppressLegacyAuctionFrame()
+ local f = _G.AuctionFrame
+ if not f then return end
+ cloakFrame(f)
+
+ if not f._neAHCloakHooked then
+ f._neAHCloakHooked = true
+ f:HookScript("OnShow", function(self)
+ cloakFrame(self)
+ end)
+ end
+
+ -- Some tab buttons are global-named and can still be visible/clickable above alpha-cloaked content.
+ for i = 1, 16 do
+ local tab = _G["AuctionFrameTab" .. i]
+ if tab then
+ cloakFrame(tab)
+ end
+ end
+
+ local childNames = {
+ "AuctionFrameBrowse",
+ "AuctionFrameBid",
+ "AuctionFrameAuctions",
+ "AuctionFrameMoneyFrame",
+ "BrowseName",
+ "BrowseMinLevel",
+ "BrowseMaxLevel",
+ }
+ for i = 1, #childNames do
+ local child = _G[childNames[i]]
+ if child then
+ cloakFrame(child)
+ end
+ end
+end
+
+local function suppressModernAuctionFrame()
+ local f = _G.AuctionHouseFrame
+ if not f then return end
+ if IsUsingLegacyAuctionClient and IsUsingLegacyAuctionClient() then return end
+
+ cloakFrame(f)
+ if not f._neAHCloakHooked then
+ f._neAHCloakHooked = true
+ f:HookScript("OnShow", function(self)
+ cloakFrame(self)
+ end)
+ end
+end
+
+-- Exposed so Browse.lua's AUCTION_ITEM_LIST_UPDATE watcher can re-cloak on every event, not just
+-- at window-open time -- the stock AuctionFrameBrowse_Update() (called every such event so the
+-- legacy client's own internal state stays consistent) can reassert Show()/alpha on the legacy
+-- frame as part of its own normal update logic, silently undoing the one-time cloak below and
+-- letting its old parchment/stone background bleed back through.
+AH.SuppressLegacyAuctionFrame = suppressLegacyAuctionFrame
+AH.SuppressModernAuctionFrame = suppressModernAuctionFrame
+
+-- Reverse the cloak applied by suppressLegacyAuctionFrame so Auctionator's UI becomes visible.
+-- Called by external tab providers that need AuctionFrame to render their own content.
+local function uncloakLegacyAuctionFrame()
+ local f = _G.AuctionFrame
+ if not f then return end
+ restoreFrameTree(f, 0)
+ for i = 1, 16 do
+ local tab = _G["AuctionFrameTab" .. i]
+ if tab then restoreFrameTree(tab, 0) end
+ end
+ local named = { "AuctionFrameBrowse", "AuctionFrameBid", "AuctionFrameAuctions", "AuctionFrameMoneyFrame" }
+ for i = 1, #named do
+ local child = _G[named[i]]
+ if child then restoreFrameTree(child, 0) end
+ end
+end
+AH.UncloakLegacyAuctionFrame = uncloakLegacyAuctionFrame
+
+local function buildChrome(f)
+ local body = f:CreateTexture(nil, "BACKGROUND", nil, -8)
+ local rockPath = NE.tex and NE.tex.localFiles and NE.tex.localFiles[374155]
+ if rockPath then
+ body:SetTexture(rockPath, "REPEAT", "REPEAT")
+ else
+ body:SetTexture(374155, "REPEAT", "REPEAT")
+ end
+ body:SetHorizTile(true)
+ body:SetVertTile(true)
+ body:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -21)
+ body:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
+
+ local streaks = f:CreateTexture(nil, "BORDER")
+ if NE.tex and NE.tex.SetAtlas then
+ NE.tex.SetAtlas(streaks, "_UI-Frame-TopTileStreaks", false)
+ end
+ streaks:SetPoint("TOPLEFT", f, "TOPLEFT", 6, -21)
+ streaks:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -21)
+ streaks:SetHeight(43)
+ streaks:SetHorizTile(true)
+
+ local ns = CreateFrame("Frame", nil, f)
+ ns:SetAllPoints(f)
+ if NE.nineslice and NE.nineslice.ApplyLayout then
+ NE.nineslice.ApplyLayout(ns, "PortraitFrameTemplate")
+ end
+ f.NineSlice = ns
+
+ -- EnsureTitle, not SetTitle: this window builds its own chrome and never created a title
+ -- FontString, and SetTitle silently returns when it can't find one -- which is why the title bar
+ -- rendered empty. EnsureTitle makes the band + FontString (as f.Title), so the setTitleForMode
+ -- calls below find it from then on.
+ local title = AH_WINDOW_TITLE
+ if NE.panelchrome and NE.panelchrome.EnsureTitle then
+ NE.panelchrome.EnsureTitle(f, title)
+ elseif NE.panelchrome and NE.panelchrome.SetTitle then
+ NE.panelchrome.SetTitle(f, title)
+ elseif f.TitleText and f.TitleText.SetText then
+ f.TitleText:SetText(title)
+ end
+
+ local close = CreateFrame("Button", FRAME_NAME .. "CloseButton", f, "UIPanelCloseButton")
+ close:SetPoint("TOPRIGHT", f, "TOPRIGHT", 1, 0)
+ close:SetScript("OnClick", function()
+ if CloseAuctionHouse and CanSendAuctionQuery and CanSendAuctionQuery("list") then
+ CloseAuctionHouse()
+ end
+ AH.Hide()
+ end)
+ if NE.panelchrome and NE.panelchrome.ModernizeCloseButton then
+ NE.panelchrome.ModernizeCloseButton(close, { frameLevelBump = 10 })
+ end
+
+ local portrait = f.portrait or f.Portrait or f.PortraitTex
+ if portrait and SetPortraitTexture then
+ pcall(SetPortraitTexture, portrait, "npc")
+ elseif portrait and portrait.SetTexture then
+ portrait:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
+ end
+end
+
+local function setTitleForMode(mode)
+ local f = AH.frame
+ if not f then return end
+ local t = ""
+ if type(mode) == "string" and string.find(mode, "external:", 1, true) == 1 then
+ -- External tab: prefer the provider's own live title ("Auctionator+ - Buy", mirrored into
+ -- AH.atr._lastTitle by AuctionatorEmbed's SetText hook), falling back to the tab's label.
+ local key = string.gsub(mode, "^external:", "")
+ local def = f.ExternalTabDefs and f.ExternalTabDefs[key]
+ if def and def.providerId == "Auctionator" and AH.atr and AH.atr._lastTitle then
+ t = AH.atr._lastTitle
+ else
+ t = (def and def.text) or "Auctionator"
+ end
+ else
+ -- Every built-in tab (Buy / Sell / Auctions) keeps the window's own name; the tab strip
+ -- already says which one is active. Only an embedded external provider overrides it, above.
+ t = AH_WINDOW_TITLE
+ end
+ if NE.panelchrome and NE.panelchrome.SetTitle then
+ NE.panelchrome.SetTitle(f, t)
+ elseif f.TitleText and f.TitleText.SetText then
+ f.TitleText:SetText(t)
+ end
+end
+
+local function setMode(mode)
+ local f = AH.frame
+ if not f then return end
+
+ if not mode or (not BASE_MODES[mode] and not (type(mode) == "string" and string.find(mode, "external:", 1, true) == 1)) then
+ mode = "Buy"
+ end
+
+ -- External modes are only valid while their tab definition exists.
+ if type(mode) == "string" and string.find(mode, "external:", 1, true) == 1 then
+ local key = string.gsub(mode, "^external:", "")
+ if not (f.ExternalTabDefs and f.ExternalTabDefs[key]) then
+ mode = "Buy"
+ end
+ end
+
+ f._switchingModes = true
+ f._mode = mode
+
+ if f.ContentRoot and f.ContentRoot.GetChildren then
+ local children = { f.ContentRoot:GetChildren() }
+ for i = 1, #children do
+ local c = children[i]
+ if c and c.Hide then c:Hide() end
+ end
+ end
+
+ local isExternalMode = type(mode) == "string" and string.find(mode, "external:", 1, true) == 1
+
+ if f.Panes then
+ local selected = f.Panes[mode]
+ -- Only fall back to a base pane when the mode is genuinely unknown (not one of Buy/Sell/
+ -- Auctions). External modes (e.g. "external:Auctionator") are intentionally absent from
+ -- f.Panes -- they render via f.ExternalPane below -- so they must NOT be coerced into "Buy"
+ -- here, or every click on a mirrored Auctionator tab silently reopens the Buy pane instead.
+ if not selected and not isExternalMode then
+ selected = f.Panes.Buy or f.Panes.Sell or f.Panes.Auctions
+ if selected == f.Panes.Sell then mode = "Sell"
+ elseif selected == f.Panes.Auctions then mode = "Auctions"
+ else mode = "Buy" end
+ f._mode = mode
+ end
+ if selected then selected:Show() end
+ end
+
+ if f.ExternalPane then
+ if isExternalMode then
+ f.ExternalPane:Show()
+ end
+ end
+
+ -- Grow the shell to Auctionator's native footprint on its tabs, restore on the base tabs.
+ -- Done BEFORE the external onSelect (which reveals Auctionator's panel) so it lays out at the
+ -- final size. No-ops until AuctionatorEmbed has actually embedded (AH._atrEmbedded).
+ if AH.atr and AH.atr.SetWindowForExternal then
+ AH.atr.SetWindowForExternal(isExternalMode and AH._atrEmbedded and true or false)
+ end
+
+ if PanelTemplates_SetTab and f._tabIds and f._tabIds[mode] then
+ PanelTemplates_SetTab(f, f._tabIds[mode])
+ end
+
+ if isExternalMode then
+ local key = string.gsub(mode, "^external:", "")
+ local def = f.ExternalTabDefs and f.ExternalTabDefs[key]
+ if def and type(def.onSelect) == "function" then
+ def.onSelect({
+ frame = f,
+ mode = mode,
+ showHint = function(msg)
+ if not f.ExternalHint then return end
+ f.ExternalHint:SetText(msg or "")
+ end,
+ })
+ end
+ if f.ExternalHint then f.ExternalHint:Show() end
+ else
+ if f.ExternalHint then
+ f.ExternalHint:SetText("")
+ f.ExternalHint:Hide()
+ end
+ end
+
+ setTitleForMode(mode)
+ -- External modes hand off rendering to a provider (e.g. Auctionator); suppressing the legacy
+ -- AH frame here would undo whatever the provider's onSelect just showed -- UNLESS the
+ -- provider's UI has been reparented INTO this shell (AuctionatorEmbed sets AH._atrEmbedded),
+ -- in which case the legacy frame no longer hosts anything of the provider's and must stay
+ -- cloaked.
+ if not isExternalMode or AH._atrEmbedded then
+ suppressLegacyAuctionFrame()
+ end
+ f._switchingModes = false
+end
+AH.SetMode = setMode
+
+-- Player money readout, bottom-left of the window (retail AuctionHouseFrame.xml money inset).
+-- DOWNPORT: the reference's NE.money.Well used retail-only "ThinGoldEdgeTemplate" /
+-- "MoneyDisplayFrameTemplate", neither shipped on 3.3.5a. Use the same proven stock pattern
+-- CombinedBag.lua already relies on for its own money readout: native "SmallMoneyFrameTemplate"
+-- set to type "PLAYER", which self-initializes and self-updates on PLAYER_MONEY with no extra
+-- wiring. Wrapped in our own AttachInset recessed border for visual consistency with the rest
+-- of the window (categories/results panels use the same border).
+local function buildMoneyFrame(f)
+ local inset = CreateFrame("Frame", nil, f)
+ inset:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 2, 27)
+ inset:SetPoint("BOTTOMRIGHT", f, "BOTTOMLEFT", 167, 3)
+
+ -- AttachInset alone only draws the thin gold-trim border pieces (transparent center) -- every
+ -- other inset in this window (categories/results panels) additionally fills behind it with a
+ -- dark tint so the well itself reads as a solid recessed box, not just an outline over the rock
+ -- chrome. Same near-black tone used by the results header strip / filter panel for consistency.
+ local fill = inset:CreateTexture(nil, "BACKGROUND")
+ fill:SetTexture("Interface\\Buttons\\WHITE8X8")
+ fill:SetVertexColor(0.06, 0.06, 0.07, 0.95)
+ fill:SetAllPoints(inset)
+
+ if NE.nineslice and NE.nineslice.AttachInset then
+ pcall(NE.nineslice.AttachInset, inset, 0, 0, 0, 0)
+ end
+ f.MoneyFrameInset = inset
+
+ local money = CreateFrame("Frame", FRAME_NAME .. "MoneyFrame", inset, "SmallMoneyFrameTemplate")
+ money:SetPoint("RIGHT", inset, "RIGHT", -6, 0)
+ if MoneyFrame_SetType then pcall(MoneyFrame_SetType, money, "PLAYER") end
+ if MoneyFrame_SetMaxDisplayWidth then pcall(MoneyFrame_SetMaxDisplayWidth, money, 150) end
+ f.MoneyFrame = money
+end
+
+local function buildMainTabs(f)
+ local labels = {
+ AUCTION_HOUSE_BUY_TAB or "Buy",
+ AUCTION_HOUSE_SELL_TAB or "Sell",
+ AUCTION_HOUSE_AUCTIONS_SUB_TAB or "Auctions",
+ }
+
+ f._tabIds = {}
+ f.BaseTabNames = {}
+
+ for i = 1, #labels do
+ local name = FRAME_NAME .. "Tab" .. i
+ local tab = CreateFrame("Button", name, f, "CharacterFrameTabButtonTemplate")
+ tab:SetID(i)
+ tab:SetText(labels[i])
+
+ local mode = (i == 1 and "Buy") or (i == 2 and "Sell") or "Auctions"
+ tab._neMode = mode
+ tab:SetScript("OnClick", function(self)
+ setMode(self._neMode)
+ end)
+
+ f._tabIds[mode] = i
+ f.BaseTabNames[#f.BaseTabNames + 1] = name
+
+ if NE.tabs and NE.tabs.ReskinClassicTab then
+ NE.tabs.ReskinClassicTab(name)
+ end
+ end
+
+ if PanelTemplates_SetNumTabs then
+ PanelTemplates_SetNumTabs(f, #labels)
+ end
+
+ if NE.tabs and NE.tabs.SizeAndAnchorTabs then
+ NE.tabs.SizeAndAnchorTabs(f, f.BaseTabNames, { startX = 20, startY = 2, parentPoint = "BOTTOMLEFT" })
+ end
+end
+
+function AH.RefreshExternalTabs()
+ local f = AH.frame
+ if not f then return end
+
+ if f.ExternalTabs then
+ for i = 1, #f.ExternalTabs do
+ f.ExternalTabs[i]:Hide()
+ end
+ end
+
+ f.ExternalTabs = f.ExternalTabs or {}
+ f.ExternalTabDefs = {}
+
+ local ext = {}
+ if AH.bridge and AH.bridge.GetExternalTabs then
+ ext = AH.bridge.GetExternalTabs(f)
+ end
+
+ local nextId = #(f.BaseTabNames or {})
+ local allNames = {}
+ for i = 1, #(f.BaseTabNames or {}) do
+ allNames[#allNames + 1] = f.BaseTabNames[i]
+ end
+
+ for i = 1, #ext do
+ nextId = nextId + 1
+ local name = FRAME_NAME .. "Tab" .. nextId
+ local tab = _G[name] or f.ExternalTabs[i]
+ if not tab then
+ tab = CreateFrame("Button", name, f, "CharacterFrameTabButtonTemplate")
+ f.ExternalTabs[i] = tab
+ if NE.tabs and NE.tabs.ReskinClassicTab then
+ NE.tabs.ReskinClassicTab(name)
+ end
+ end
+
+ local mode = "external:" .. tostring(ext[i].key)
+ tab._neMode = mode
+ tab:SetID(nextId)
+ tab:SetText(ext[i].text)
+ -- Auctionator's own native tabs use its signature orange text; carry that through so the
+ -- embedded tabs read as distinct from the shell's base Buy/Sell/Auctions tabs (which they
+ -- otherwise duplicate by name).
+ if ext[i].providerId == "Auctionator" then
+ local fs = _G[name .. "Text"]
+ if fs and fs.SetTextColor then fs:SetTextColor(1, 0.55, 0.25) end
+ end
+ tab:SetScript("OnClick", function(self)
+ setMode(self._neMode)
+ end)
+ tab:Show()
+
+ f.ExternalTabDefs[ext[i].key] = ext[i]
+ f._tabIds[mode] = nextId
+ allNames[#allNames + 1] = name
+ end
+
+ if PanelTemplates_SetNumTabs then
+ PanelTemplates_SetNumTabs(f, nextId)
+ end
+ if PanelTemplates_UpdateTabs then
+ PanelTemplates_UpdateTabs(f)
+ end
+ if NE.tabs and NE.tabs.SizeAndAnchorTabs then
+ NE.tabs.SizeAndAnchorTabs(f, allNames, { startX = 20, startY = 2, parentPoint = "BOTTOMLEFT" })
+ end
+end
+
+local function buildPanes(f)
+ local root = CreateFrame("Frame", nil, f)
+ root:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -1)
+ root:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
+ f.ContentRoot = root
+
+ local function fallbackPane(modeName, errText)
+ local p = CreateFrame("Frame", nil, root)
+ p:SetAllPoints(root)
+ local t = p:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ t:SetPoint("CENTER", p, "CENTER", 0, 34)
+ t:SetWidth(700)
+ t:SetJustifyH("CENTER")
+ t:SetText(modeName .. " pane fallback active")
+ t:SetTextColor(1, 0.82, 0)
+
+ if errText and errText ~= "" then
+ local e = p:CreateFontString(nil, "OVERLAY", "GameFontDisable")
+ e:SetPoint("TOP", t, "BOTTOM", 0, -12)
+ e:SetWidth(700)
+ e:SetJustifyH("CENTER")
+ e:SetText(tostring(errText))
+ end
+
+ return p
+ end
+
+ local function safeBuild(builder, modeName)
+ if type(builder) ~= "function" then
+ return fallbackPane(modeName, "builder missing")
+ end
+ local ok, pane = pcall(builder, root)
+ if ok and pane then
+ return pane
+ end
+ return fallbackPane(modeName, pane)
+ end
+
+ f.Panes = {
+ Buy = safeBuild(AH.BuildBrowsePane, "Buy"),
+ Sell = safeBuild(AH.BuildSellPane, "Sell"),
+ Auctions = safeBuild(AH.BuildAuctionsPane, "Auctions"),
+ }
+
+ local function enforceSingleVisible(activePane)
+ if f._switchingModes then return end
+ for _, pane in pairs(f.Panes) do
+ if pane and pane ~= activePane then pane:Hide() end
+ end
+ end
+
+ for key, pane in pairs(f.Panes) do
+ if pane then
+ pane._neModeKey = key
+ pane:HookScript("OnShow", function(self)
+ enforceSingleVisible(self)
+ end)
+ end
+ end
+
+ for _, pane in pairs(f.Panes) do
+ if pane then pane:Hide() end
+ end
+
+ local external = CreateFrame("Frame", nil, root)
+ external:SetAllPoints(root)
+ external:Hide()
+ f.ExternalPane = external
+
+ local hint = external:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ hint:SetPoint("CENTER", external, "CENTER", 0, 20)
+ hint:SetText("")
+ hint:SetTextColor(1, 0.82, 0)
+ hint:Hide()
+ f.ExternalHint = hint
+end
+
+local function createWindow()
+ if AH.frame then return AH.frame end
+
+ local f = CreateFrame("Frame", FRAME_NAME, UIParent, "PortraitFrameTemplate")
+ -- The red 3-slice is the addon's standard button; Watch keeps this window's panel buttons
+ -- skinned as its panes are built (core/ButtonSkin.lua). Opt out per button with _neNoSkin.
+ if NE.buttonskin and NE.buttonskin.Watch then pcall(NE.buttonskin.Watch, f) end
+ f:SetSize(800, 538)
+ -- Default Blizzard UI panels (AuctionFrame included) open pinned to the LEFT side of the
+ -- screen, vertically centered -- not screen-CENTER. Match that instead of centering.
+ f:SetPoint("LEFT", UIParent, "LEFT", 16, 0)
+ f:SetFrameStrata("DIALOG")
+ f:SetMovable(true)
+ f:SetClampedToScreen(true)
+ f:EnableMouse(true)
+ f:RegisterForDrag("LeftButton")
+ f:SetScript("OnDragStart", function(self) self:StartMoving() end)
+ f:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
+ f:Hide()
+
+ AH.frame = f
+
+ buildChrome(f)
+ pcall(buildMoneyFrame, f) -- never let a money-widget failure blank the whole shell
+ buildMainTabs(f)
+ buildPanes(f)
+
+ -- Clear the Buy tab's search box/results whenever the WHOLE window closes (AUCTION_HOUSE_CLOSED,
+ -- ESC, the close button -- all end up calling f:Hide() one way or another), so the next
+ -- AUCTION_HOUSE_SHOW starts fresh instead of still displaying last session's search. A plain tab
+ -- switch away from Buy does NOT reach here (Buy's own OnHide handles that separately) -- this is
+ -- specifically for the top-level frame's own OnHide, which children don't inherit.
+ f:HookScript("OnHide", function()
+ local buy = f.Panes and f.Panes.Buy
+ if buy and buy.ResetSearch then pcall(buy.ResetSearch) end
+ end)
+
+ -- Per-window scaling via NE.scale (mode: ui / none / custom; options tab > Window Scaling).
+ -- DEFAULTS["auctionhouse"] is "ui" = a plain SetScale(1.0), i.e. exactly what this window did
+ -- before it had a setting.
+ if NE.scale and NE.scale.Apply then
+ if NE.scale.SetFrame then NE.scale.SetFrame("auctionhouse", f) end
+ NE.scale.Apply("auctionhouse")
+ end
+
+ if NE.FrameUtil and NE.FrameUtil.WirePanelSounds then
+ NE.FrameUtil.WirePanelSounds(f, SOUNDKIT.AUCTION_WINDOW_OPEN, SOUNDKIT.AUCTION_WINDOW_CLOSE)
+ end
+ if NE.FrameUtil and NE.FrameUtil.EscClose then
+ NE.FrameUtil.EscClose(FRAME_NAME)
+ end
+
+ AH.RefreshExternalTabs()
+ setMode("Buy")
+
+ return f
+end
+
+function AH.Show()
+ if not isModuleEnabled() then return end
+ local f = createWindow()
+ f:Show()
+ setMode("Buy")
+ local portrait = f.portrait or f.Portrait or f.PortraitTex
+ if portrait and SetPortraitTexture then
+ pcall(SetPortraitTexture, portrait, "npc")
+ end
+ suppressLegacyAuctionFrame()
+ suppressModernAuctionFrame()
+ if C_Timer and C_Timer.After then
+ C_Timer.After(0, suppressLegacyAuctionFrame)
+ C_Timer.After(0, suppressModernAuctionFrame)
+ C_Timer.After(0.05, suppressLegacyAuctionFrame)
+ C_Timer.After(0.05, suppressModernAuctionFrame)
+ end
+ AH.RefreshExternalTabs()
+ setMode("Buy")
+ -- Belt-and-suspenders: on some load orders Atr_Init (which creates Auctionator's own
+ -- AuctionFrameTab4+ Buy/Sell/More tabs) hasn't finished by the time AUCTION_HOUSE_SHOW reaches
+ -- us this same frame. Re-scan a couple frames later so the bridge upgrades from the generic
+ -- placeholder tab to the real native-mirrored ones without requiring the window to be reopened.
+ if C_Timer and C_Timer.After then
+ C_Timer.After(0, AH.RefreshExternalTabs)
+ C_Timer.After(0.2, AH.RefreshExternalTabs)
+ end
+end
+
+function AH.Hide()
+ if AH.frame then AH.frame:Hide() end
+end
+
+local eventFrame = CreateFrame("Frame")
+eventFrame:RegisterEvent("PLAYER_LOGIN")
+eventFrame:RegisterEvent("ADDON_LOADED")
+eventFrame:RegisterEvent("AUCTION_HOUSE_SHOW")
+eventFrame:RegisterEvent("AUCTION_HOUSE_CLOSED")
+
+local function onLogin()
+ local enabled = isModuleEnabled()
+ if enabled then
+ prewarmAuctionHouseTextures()
+ createWindow()
+ end
+ if NE.RegisterPanel then
+ NE.RegisterPanel({
+ id = MODULE,
+ title = AUCTION_HOUSE or "Auction House",
+ desc = "Modern visual shell for Buy/Sell/Auctions with optional Auctionator tab embedding.",
+ frame = AH.frame,
+ openFn = AH.Show,
+ closeFn = AH.Hide,
+ order = 50,
+ })
+ end
+end
+
+eventFrame:SetScript("OnEvent", function(_, event, arg1)
+ if event == "PLAYER_LOGIN" then
+ onLogin()
+ elseif event == "AUCTION_HOUSE_SHOW" then
+ if isModuleEnabled() then AH.Show() end
+ elseif event == "AUCTION_HOUSE_CLOSED" then
+ AH.Hide()
+ elseif event == "ADDON_LOADED" and arg1 == "Auctionator" then
+ if AH.RefreshExternalTabs then AH.RefreshExternalTabs() end
+ elseif event == "ADDON_LOADED" and arg1 == "Blizzard_AuctionUI" then
+ -- Auctionator only builds its own AuctionFrameTab4+ (Buy/Sell/More) once Blizzard_AuctionUI
+ -- itself finishes loading (Atr_Init runs off this addon's ADDON_LOADED, not Auctionator's own,
+ -- which fires much earlier at login before those tabs exist). Re-scan here so the bridge picks
+ -- up the native tabs instead of getting stuck on the single non-functional placeholder tab.
+ if AH.RefreshExternalTabs then AH.RefreshExternalTabs() end
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/BagSkin.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/BagSkin.lua
new file mode 100644
index 0000000..9833059
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/BagSkin.lua
@@ -0,0 +1,426 @@
+-- DragonUI_NewEra/modules/bags/BagSkin.lua — shared bag visual skin (asset reg + per-button skin
+-- + window background), used by BOTH the per-window restyle (Bags.lua) and the combined window
+-- (CombinedBag.lua) so the two render identically.
+--
+-- The defining "retail bags" look is the RECESSED SLOT CAVITY behind every item (the dark bordered
+-- square each icon sits in). 3.3.5a item buttons only carry a thin outline (UI-Quickslot2), so we
+-- hide it and draw the retail slot recess behind the icon. We use the target's OWN art
+-- (BagsItemSlot.blp, from the Retail Bags downport) — a filled-centre recess, so the window bg
+-- never bleeds through the slot. Every button also gets a quality ring (WhiteIconFrame, coloured by
+-- core/ItemButton) and a search-dim overlay; the window fill is the tiled backpack background.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+NE.bagskin = NE.bagskin or {}
+local BS = NE.bagskin
+
+-- ----------------------------------------------------------------------------
+-- Asset registration (guarded; NE.tex owns the API). The slot recess is a full-sheet atlas;
+-- the backpack background is a plain texture (no atlas — set directly by path).
+-- ----------------------------------------------------------------------------
+local P = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\Common\\"
+-- The target's OWN slot art (from the Retail Bags downport): a recessed square with a FILLED dark
+-- centre, so slots read clean and the window background never shows through them.
+BS.SLOT_PATH = P .. "BagsItemSlot.blp"
+BS.SLOT_LOCKED_PATH = P .. "BagsItemSlotClose.blp"
+-- Rarity-glow texture. The retail/modern "Interface\Common\WhiteIconFrame" is ABSENT on this client
+-- (SetTexture silently no-ops → an invisible ring, which is why the combined window showed no glow);
+-- UI-ActionButton-Border is a stock 3.3.5a soft-glow ring present on every client, ADD-blended and
+-- tinted by quality colour it reads as the rarity glow.
+BS.QUALITY_GLOW_PATH = "Interface\\Buttons\\UI-ActionButton-Border"
+-- Window interior fill. Default: the SHARED dark rock body fill (NE.chrome.ApplyBodyFill /
+-- PC.BODY_TINT) so the bag reads identically to the character panel and the rest of the window set —
+-- one tint, tuned in one place, no drift.
+BS.USE_SHARED_BODY_FILL = true
+-- Set the above false to restore the bag's old bespoke look: the soft "marble" cloud texture
+-- (UI-Background-Marble — near-black grey with gentle cloud variation) multiplied by a warm bronze
+-- vertex tint, which leaned the window warm to echo the tightly-packed warm slot recesses. Tunable.
+BS.BG_PATH = P .. "9900002-ui-background-marble.blp"
+BS.BG_TINT = { 1.0, 0.82, 0.58 } -- warm bronze wash over the grey marble (r,g,b multiply)
+
+-- ----------------------------------------------------------------------------
+-- Per-button skin. Idempotent. size = button edge (37 combined / native 37 per-window).
+-- * hides the thin classic slot outline (NormalTexture)
+-- * draws the recessed slot cavity on BACKGROUND, slightly larger than the button so adjacent
+-- slots read as separated recesses (matches the retail grid)
+-- * ensures an IconBorder region so core/ItemButton.ApplyQuality can colour a quality ring
+-- ----------------------------------------------------------------------------
+function BS.SkinButton(btn, size)
+ if not btn then return end
+ size = size or (btn.GetWidth and btn:GetWidth()) or 37
+
+ -- Hide the default outline so only our recess frames the slot.
+ local nt = btn.GetNormalTexture and btn:GetNormalTexture()
+ if nt then nt:SetTexture(nil) end
+
+ -- Recessed slot cavity (behind the icon) — filled-centre art so the window bg can't bleed through.
+ local recess = btn._neSlotRecess
+ if not recess then
+ recess = btn:CreateTexture(nil, "BACKGROUND", nil, -1)
+ btn._neSlotRecess = recess
+ end
+ recess:SetTexture(BS.SLOT_PATH)
+ recess:SetTexCoord(0, 1, 0, 1)
+ recess:ClearAllPoints()
+ recess:SetPoint("TOPLEFT", btn, "TOPLEFT", -2, 2)
+ recess:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 2, -2)
+ recess:Show()
+
+ -- Quality ring region (coloured later by ApplyQuality). Created at the TOP OVERLAY sublevel and
+ -- OVERHANGING the button by 3px so the coloured ring sits in the inter-slot gap — never hidden
+ -- behind the item icon (which the combined grid draws SetAllPoints over the whole button, which
+ -- was swallowing a slot-sized ring and is why the combined window showed no rarity glow).
+ if not btn.IconBorder then
+ local b = btn:CreateTexture(nil, "OVERLAY", nil, 7)
+ b:SetTexture(BS.QUALITY_GLOW_PATH)
+ b:SetBlendMode("ADD")
+ b:Hide()
+ btn.IconBorder = b
+ end
+ -- UI-ActionButton-Border keeps its glow INSET within the texture (transparent margin), so the ring
+ -- must be drawn a good bit LARGER than the slot for the visible glow to reach the slot edge. Overhang
+ -- ~35% of the slot size on every side.
+ local over = math.max(8, math.floor(size * 0.35 + 0.5))
+ btn.IconBorder:ClearAllPoints()
+ btn.IconBorder:SetPoint("TOPLEFT", btn, "TOPLEFT", -over, over)
+ btn.IconBorder:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", over, -over)
+
+ -- Search-dim overlay (shown when this slot doesn't match the active search text).
+ if not btn._neSearchDim then
+ local d = btn:CreateTexture(nil, "OVERLAY", nil, 5)
+ d:SetTexture(0, 0, 0, 0.7)
+ d:SetAllPoints(btn)
+ d:Hide()
+ btn._neSearchDim = d
+ end
+end
+
+-- Show/hide a slot's search dim. Also dims the quest-item border/glow (IconQuestTexture), which sits
+-- ABOVE the dim overlay and otherwise stays bright on filtered-out items. The grid re-shows/positions
+-- it on the next refresh (before this runs), so we only need to fade its alpha.
+function BS.SetSearchDim(btn, dim)
+ if not btn then return end
+ if btn._neSearchDim then
+ if dim then btn._neSearchDim:Show() else btn._neSearchDim:Hide() end
+ end
+ local qt = btn.IconQuestTexture or (btn.GetName and btn:GetName() and _G[btn:GetName() .. "IconQuestTexture"])
+ if qt then qt:SetAlpha(dim and 0.25 or 1) end
+ btn._neDim = dim and true or false
+ if BS.ApplyGlowState then BS.ApplyGlowState(btn) end -- fade the rarity glow to match the dimmed icon
+end
+
+-- "Can't use" detection. IsUsableItem does NOT catch armor/weapon PROFICIENCY (e.g. a Mail belt on a
+-- class that can't wear Mail, or a weapon skill the player lacks). The reliable signal is the item
+-- tooltip: Blizzard colours any UNMET requirement line (armor type, weapon skill, level, class,
+-- reputation) red. We scan a hidden tooltip for a red left-line. Cached by itemID; the cache is
+-- wiped when player state that changes usability changes (level/skill/talent/proficiency).
+local unusableCache = {}
+local usableScanTip
+local RED_REQ = function(r, g, b) return r and g and b and r > 0.9 and g < 0.2 and b < 0.2 end
+
+-- A line counts as an unmet-requirement red ONLY if it actually has text. GameTooltip:ClearLines()
+-- clears line text but NOT the font strings' colour, and the right column is shared/pooled — so a
+-- line the current item leaves empty can still carry a stale red from a previously-scanned unusable
+-- item (e.g. a mace), which would falsely redden the next item (a belt, a bag). Guard on non-empty text.
+local function lineIsRed(fs)
+ if not fs then return false end
+ local txt = fs:GetText()
+ if not txt or txt == "" then return false end
+ return RED_REQ(fs:GetTextColor())
+end
+
+local function scanUnusable(bagID, slot, itemID)
+ if itemID and unusableCache[itemID] ~= nil then return unusableCache[itemID] end
+ if not (bagID and slot) then return false end
+ if not usableScanTip then
+ usableScanTip = CreateFrame("GameTooltip", "NE_BagUsableScan", UIParent, "GameTooltipTemplate")
+ usableScanTip:SetOwner(UIParent, "ANCHOR_NONE")
+ end
+ usableScanTip:ClearLines()
+ local ok = pcall(usableScanTip.SetBagItem, usableScanTip, bagID, slot)
+ local red = false
+ if ok then
+ -- Scan BOTH columns: unmet level/class/reputation lines are red on the LEFT, but a weapon/armor
+ -- PROFICIENCY you lack (e.g. "Mace"/"Mail") is the item's subtype on the RIGHT — opposite the
+ -- equip-slot line. Checking only the left column missed maces the player can't wield.
+ for i = 2, (usableScanTip:NumLines() or 0) do -- skip line 1 (the quality-coloured name)
+ if lineIsRed(_G["NE_BagUsableScanTextLeft" .. i])
+ or lineIsRed(_G["NE_BagUsableScanTextRight" .. i]) then
+ red = true; break
+ end
+ end
+ end
+ if itemID then unusableCache[itemID] = red end
+ return red
+end
+
+-- Wipe the cache when the player's usability can change.
+do
+ local w = CreateFrame("Frame")
+ for _, e in ipairs({ "PLAYER_LEVEL_UP", "SKILL_LINES_CHANGED", "CHARACTER_POINTS_CHANGED",
+ "LEARNED_SPELL_IN_TAB", "PLAYER_ENTERING_WORLD" }) do
+ pcall(function() w:RegisterEvent(e) end)
+ end
+ w:SetScript("OnEvent", function() if wipe then wipe(unusableCache) else unusableCache = {} end end)
+end
+
+-- Red-tint the icon of an item the player can't use, when `enabled`. The item tooltip colours ANY
+-- unmet requirement line red — weapon/armor proficiency, class, required level, AND a recipe's
+-- required PROFESSION + skill level (also reputation, "already known", etc.) — so we scan the tooltip
+-- for a red requirement line (both columns, non-empty text only, cached per itemID). One check covers
+-- gear, level-gated food/drink, and recipes you can't learn (wrong/absent profession or skill too low).
+-- The cache wipes on level-up / SKILL_LINES_CHANGED / LEARNED_SPELL_IN_TAB, so tints re-evaluate when
+-- the player levels a profession or learns the recipe. Cleared to normal when disabled or usable.
+function BS.ApplyUsableTint(btn, bagID, slot, enabled)
+ if not btn then return end
+ local red = false
+ if enabled and bagID and slot and C_Container and C_Container.GetContainerItemInfo then
+ local info = C_Container.GetContainerItemInfo(bagID, slot)
+ if info and scanUnusable(bagID, slot, info.itemID) then red = true end
+ end
+ if SetItemButtonTextureVertexColor then
+ if red then SetItemButtonTextureVertexColor(btn, 1.0, 0.30, 0.30)
+ else SetItemButtonTextureVertexColor(btn, 1, 1, 1) end
+ else
+ local icon = btn.icon or (btn.GetName and _G[btn:GetName() .. "IconTexture"])
+ if icon then
+ if red then icon:SetVertexColor(1, 0.30, 0.30) else icon:SetVertexColor(1, 1, 1) end
+ end
+ end
+ btn._neRed = red
+ if BS.ApplyGlowState then BS.ApplyGlowState(btn) end -- tint the rarity glow the same when unusable
+end
+
+-- Quality colour lookup: prefer the brighter ITEM_QUALITY_COLORS, fall back to BAG_ITEM_QUALITY_COLORS.
+local function qualityColor(q)
+ if not q then return nil end
+ return (ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[q]) or (BAG_ITEM_QUALITY_COLORS and BAG_ITEM_QUALITY_COLORS[q])
+end
+
+-- Recompute the quality glow from every active filter so the ring matches the icon: base rarity
+-- colour, multiplied by the "can't use" red tint (same factor the icon gets) and faded when the slot
+-- is search-dimmed. State is stamped on the button by ApplyQuality/ApplyUsableTint/SetSearchDim.
+local function applyGlowState(btn)
+ local ib = btn and btn.IconBorder
+ if not ib then return end
+ local c = btn._neQualityColor
+ if not c then ib:Hide(); return end
+ local r, g, b = c.r, c.g, c.b
+ if btn._neRed then r, g, b = r * 1.0, g * 0.30, b * 0.30 end -- match the red unusable icon tint
+ ib:SetVertexColor(r, g, b)
+ ib:SetAlpha(btn._neDim and 0.30 or 1) -- match the search-dimmed icon
+ ib:Show()
+end
+BS.ApplyGlowState = applyGlowState
+
+-- Colour a button's quality ring (the WhiteIconFrame glow) from its container item. Self-contained —
+-- it colours btn.IconBorder DIRECTLY rather than delegating to NE.itembutton, so the combined grid's
+-- rings render even if that helper is unavailable/mistimed (they weren't showing before). The
+-- container `quality` is unreliable on 3.3.5a (often -1 until the item is queried — the same quirk
+-- that broke junk detection), so resolve from the item LINK via GetItemInfo when it's missing/negative.
+-- Glows uncommon+ (quality >= 2), matching retail (common/poor get no visible ring).
+function BS.ApplyQuality(btn, bagID, slot)
+ if not btn then return end
+ local border = btn.IconBorder or (btn.GetName and btn:GetName() and _G[btn:GetName() .. "IconBorder"])
+ if not border then return end
+
+ local quality
+ if bagID and slot and C_Container and C_Container.GetContainerItemInfo then
+ local info = C_Container.GetContainerItemInfo(bagID, slot)
+ if info then
+ quality = info.quality
+ local link = info.hyperlink or info.itemID
+ if (not quality or quality < 0) and link and GetItemInfo then
+ quality = select(3, GetItemInfo(link)) -- 3rd return = quality; reliable for held items
+ end
+ end
+ end
+
+ local c = quality and quality >= 2 and qualityColor(quality)
+ btn._neQualityColor = c or nil
+ if c then
+ border:SetTexture(BS.QUALITY_GLOW_PATH)
+ border:SetBlendMode("ADD")
+ end
+ applyGlowState(btn) -- colour/alpha honour the red + search-dim filters
+end
+
+-- ----------------------------------------------------------------------------
+-- Item level number on a bag slot — drawn by DRAGONUI's item level module (its modules/itemlevel
+-- .lua), not by us. It owns the font family/outline/size, the number's position, and the per-context
+-- checkboxes in Options -> Enhancements -> Item Level; NewEra shipping a second implementation just
+-- put two numbers on every icon (which is exactly what happened, and why ours was removed).
+--
+-- addon.UpdateItemLevelSlot(button, link, anchorTo, context) is its public per-button entry point —
+-- the same one Bagster uses for its own recycled slots, so this is the supported seam rather than a
+-- reach into its internals. It does ALL the gating itself (module enabled + the context's checkbox)
+-- and hides the text when the slot is empty or the setting is off, so there is nothing to test here
+-- and nothing to clean up when the player turns it off.
+--
+-- Silently absent on an older DragonUI without the module: no number, no error.
+-- ----------------------------------------------------------------------------
+-- The bag menu's item level checkbox IS the "Enable Item Level" checkbox in Options ->
+-- Enhancements -> Item Level. Not a mirror, not a copy: the same single flag
+-- (profile.modules.itemlevel.enabled), read and written through DragonUI's own accessors, with the
+-- same side effects its setFunc runs. Flip either one and the other is already showing the new
+-- state next time it's drawn, because there is only one value in existence.
+--
+-- Being the MASTER switch, it governs every frame the module draws on — this bag, the character
+-- panel, bank, merchant, loot, mail and the rest. DragonUI's per-frame checkboxes underneath it are
+-- untouched and keep working as the finer control.
+local ITEMLEVEL_MODULE = "itemlevel"
+
+-- False only when DragonUI has no item level module at all (an older build) — then there is no
+-- checkbox to be in sync with, and the menu entry greys out instead of pretending.
+function BS.CanToggleItemLevel()
+ local d = NE.dragon
+ return (d and d.RefreshItemLevel and d.IsModuleEnabled) and true or false
+end
+
+-- Exactly what the options checkbox's getFunc reads: addon:IsModuleEnabled("itemlevel").
+function BS.IsItemLevelShown()
+ if not BS.CanToggleItemLevel() then return false end
+ local d = NE.dragon
+ local ok, enabled = pcall(d.IsModuleEnabled, d, ITEMLEVEL_MODULE)
+ return (ok and enabled) and true or false
+end
+
+-- Where the flag lives. The options checkbox writes EnsureModuleTable("itemlevel"), which is
+-- addon.PanelControls:EnsureModuleTable — so when DragonUI_Options is loaded we call that method
+-- itself rather than reimplementing it. Only if the options addon isn't loaded (it's OptionalDeps +
+-- load-on-demand) do we walk the profile ourselves, which is all that method does anyway.
+local function itemLevelConfigTable()
+ local d = NE.dragon
+ if not d then return nil end
+
+ local C = d.PanelControls
+ if C and type(C.EnsureModuleTable) == "function" then
+ local ok, tbl = pcall(C.EnsureModuleTable, C, ITEMLEVEL_MODULE)
+ if ok and type(tbl) == "table" then return tbl end
+ end
+
+ if not (d.db and d.db.profile) then return nil end
+ local profile = d.db.profile
+ profile.modules = profile.modules or {}
+ profile.modules[ITEMLEVEL_MODULE] = profile.modules[ITEMLEVEL_MODULE] or {}
+ return profile.modules[ITEMLEVEL_MODULE]
+end
+
+-- The options checkbox's setFunc, step for step:
+-- EnsureModuleTable("itemlevel").enabled = val
+-- if val then addon.ApplyItemLevelSystem() end -- re-installs the module's hooks
+-- addon:RefreshItemLevel() -- repaints, or tears the system back down
+-- Anything less and the two controls would behave differently for the same flag, which is the whole
+-- thing this is meant to avoid.
+function BS.SetItemLevelShown(on)
+ local cfg = itemLevelConfigTable()
+ if not cfg then return end
+ cfg.enabled = on and true or false
+
+ -- RefreshItemLevel repaints the character slots for us: its UpdateAll -> UpdateAllCharacterSlots
+ -- walks the stock Character*Slot globals, which our panel REPARENTS rather than replaces, so they
+ -- are the very buttons on screen. Only the bag grid needs the extra nudge below, because those
+ -- buttons are ours and DragonUI has never heard of them.
+ local d = NE.dragon
+ if on and d.ApplyItemLevelSystem then pcall(d.ApplyItemLevelSystem) end
+ if d.RefreshItemLevel then pcall(d.RefreshItemLevel, d) end
+
+ -- Redraw the options window if it's sitting on the Enhancements tab, so its "Enable Item Level"
+ -- checkbox visibly follows this click instead of showing the old state until you switch tabs.
+ -- SelectTab on the CURRENT tab is DragonUI's own rebuild path and preserves the scroll offset —
+ -- its comment calls out "a toggle refreshing disabled states", which is exactly this.
+ local P = d.OptionsPanel
+ if P and P.SelectTab and P.currentTab == "enhancements"
+ and P.frame and P.frame.IsShown and P.frame:IsShown() then
+ pcall(P.SelectTab, P, "enhancements")
+ end
+
+ -- Repaint our own grid DIRECTLY rather than trusting the RefreshItemLevel hook to do it.
+ -- RefreshItemLevel hides every text it owns (ours included) and then re-runs only ITS update
+ -- paths; the hook in CombinedBag is what normally brings ours back, but this is the one caller
+ -- that must not depend on that hook having registered. Calling both is harmless — the repaint is
+ -- idempotent — and it means the menu entry works even if the hook never took.
+ if NE.combinedbag and NE.combinedbag.Refresh then pcall(NE.combinedbag.Refresh) end
+end
+
+-- ----------------------------------------------------------------------------
+-- ONE-TIME REPAIR of state earlier builds of this menu left behind.
+--
+-- Before it drove the master switch, this checkbox wrote DragonUI's PER-FRAME context flags —
+-- first "bags", then "bags" plus "character". Anyone who clicked it off in one of those builds
+-- still has those flags false in their saved profile, and the master switch it drives now cannot
+-- clear them: the module reports itself enabled while IsContextEnabled("bags") stays false, so the
+-- numbers are simply absent from the bag with nothing on screen to explain why.
+--
+-- Set the two flags we ever wrote back to DragonUI's own default (on), exactly once, tracked by a
+-- flag in OUR db so it can never fight a deliberate choice made afterwards in the options panel.
+-- Bails without marking itself done if the config isn't reachable yet, so it retries next login.
+-- ----------------------------------------------------------------------------
+local function repairStompedContexts()
+ if not NE.db or NE.db.itemLevelContextRepair then return end
+ local cfg = itemLevelConfigTable()
+ if not cfg then return end
+ NE.db.itemLevelContextRepair = true
+ cfg.bags, cfg.character = true, true
+ local d = NE.dragon
+ if d and d.RefreshItemLevel then pcall(d.RefreshItemLevel, d) end
+end
+
+local repairBoot = CreateFrame("Frame")
+repairBoot:RegisterEvent("PLAYER_LOGIN")
+repairBoot:SetScript("OnEvent", function(self)
+ pcall(repairStompedContexts)
+ self:UnregisterAllEvents()
+end)
+
+function BS.ApplyItemLevel(btn, bagID, slot)
+ if not btn then return end
+ local dragon = NE.dragon
+ if not (dragon and dragon.UpdateItemLevelSlot) then return end
+
+ -- Stock global, matching DragonUI's own container path — no compat shim in the way.
+ local link
+ if bagID and slot and GetContainerItemLink then
+ link = GetContainerItemLink(bagID, slot)
+ end
+
+ -- Bank bags are their own checkbox over there; mirror its ContainerContext() split exactly.
+ local context = (bagID and bagID >= 5 and bagID <= 11) and "bank" or "bags"
+ pcall(dragon.UpdateItemLevelSlot, btn, link, nil, context)
+end
+
+-- ----------------------------------------------------------------------------
+-- Window content background, inside the metal border. Applied on top of ApplyModernChrome's
+-- frame.Bg (which we retexture). Idempotent.
+-- ----------------------------------------------------------------------------
+function BS.ApplyWindowBackground(frame)
+ if not frame then return end
+
+ -- Shared dark rock fill — the same texture and PC.BODY_TINT the character panel body uses, so the
+ -- two windows match. ApplyBodyFill already handles the missing-art degrade (a solid near-black),
+ -- so a false return still leaves a DARK frame — don't fall through to the warm marble there, or a
+ -- client without the rock sheet would be the only one showing the old bronze bag.
+ if BS.USE_SHARED_BODY_FILL and NE.chrome and NE.chrome.ApplyBodyFill then
+ NE.chrome.ApplyBodyFill(frame)
+ return
+ end
+
+ local bg = frame.Bg
+ if not bg then
+ bg = frame:CreateTexture(nil, "BACKGROUND")
+ bg:SetPoint("TOPLEFT", 4, -4)
+ bg:SetPoint("BOTTOMRIGHT", -4, 4)
+ frame.Bg = bg
+ end
+ -- STRETCH the soft marble across the interior. DOWNPORT: tiling (SetHorizTile + wrap modes) turned
+ -- the fill black on 3.3.5a here; the plain-stretch path is what actually rendered. Marble is a soft
+ -- cloud with no hard pattern, so a stretch (downscale from 1024²) reads clean, not distorted.
+ if bg.SetHorizTile then bg:SetHorizTile(false) end
+ if bg.SetVertTile then bg:SetVertTile(false) end
+ bg:SetTexture(BS.BG_PATH)
+ bg:SetTexCoord(0, 1, 0, 1)
+ local t = BS.BG_TINT or { 1, 1, 1 }
+ bg:SetVertexColor(t[1], t[2], t[3]) -- warm the grey marble toward the retail bronze
+ bg:Show()
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/Bags.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/Bags.lua
new file mode 100644
index 0000000..8a40243
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/Bags.lua
@@ -0,0 +1,249 @@
+-- DragonUI_NewEra/modules/bags/Bags.lua — "Retail bags" style for the 3.3.5a bag windows.
+--
+-- DOWNPORT: distilled from NewEra (Classic 1.15) ContainerFrame/ContainerFrame.lua's restyle-in-
+-- place path, plus knowledge of the 3.3.5a stock ContainerFrame structure taken from the
+-- "Retail Bags" FrameXML downport. It RESTYLES the live Blizzard ContainerFrame1..N in place —
+-- it does NOT replace item handling, anchoring, open/close, tooltips or cooldowns (all native and
+-- left untouched). That keeps the module small, combat-safe, and impossible to break bag mechanics.
+--
+-- What it does, per bag window, matching the rest of DragonUI_NewEra's Dragonflight look:
+-- * hides the stock UI-Bag-Components chrome (BackgroundTop/Middle1/Middle2/Bottom/1Slot)
+-- * paints the shared dark rock Bg + applies the metal "HeldBagLayout" nineslice border
+-- * seats the bag's own portrait icon into the metal top-left corner cutout (portrait.ApplyCutout)
+-- * modernizes the close button to the RedButton-Exit family
+-- * restyles the bag name with the shared centred-gold title (NE.panelchrome.SetTitle), matching
+-- the combined bag + character/spellbook windows
+-- * colours a per-item quality border (the retail coloured ring) — 3.3.5a's stock
+-- ContainerFrame_Update never calls SetItemButtonQuality, so we drive it ourselves.
+-- * red-tints items the player can't use (weapon/armor proficiency + under-level food/drink),
+-- the same NE.bagskin.ApplyUsableTint the combined bag uses
+--
+-- DELIBERATELY OMITTED from the 1.15 original (each needs a retail-only system 3.3.5a lacks, so a
+-- faithful port is a separate, larger effort): the single combined-bags window (grid engine is
+-- present as core/ItemGrid.lua for a future pass), the currency/token border frame, the
+-- right-click bag Menu (filters / cleanup / combined toggle — needs the retail Menu API), the
+-- in-bag search box (needs BagSearchBoxTemplate), and the reskinned bag bar.
+--
+-- Reload-gated per Core/Modules.lua: disabled → not booted → stock Blizzard bags. We only ADD
+-- chrome over the native frames, so "disabled" is simply "stock bags", no teardown needed.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+local MODULE = "bags"
+NE.bags = NE.bags or {}
+local B = NE.bags
+
+local function log(msg)
+ if NE.Log then NE.Log("BAGS", msg) end
+end
+
+-- ----------------------------------------------------------------------------
+-- Constants / spec (3.3.5a stock ContainerFrame structure).
+-- ----------------------------------------------------------------------------
+local FRAME_COUNT = NUM_CONTAINER_FRAMES or 13 -- ContainerFrame1..N
+local MAX_ITEMS = MAX_CONTAINER_ITEMS or 36 -- $parentItem1..36
+
+-- Stock ARTWORK chrome textures that make up the classic bag frame — hidden so our metal border
+-- shows instead. (Named $parent; all built from Interface\ContainerFrame\UI-Bag-Components.)
+local CHROME_SUFFIXES = {
+ "BackgroundTop", "BackgroundMiddle1", "BackgroundMiddle2", "BackgroundBottom", "Background1Slot",
+}
+
+-- Portrait cutout geometry — MATCHES the combined bag exactly (size 60 seated at TOPLEFT{-5,8}) so
+-- the bag's icon fills the metal ring instead of floating small inside it. The native
+-- SetBagPortraitTexture still supplies the per-bag icon + circular crop; we only size/seat it.
+local PORTRAIT_OPTS = { size = 60, layer = "ARTWORK", anchor = { "TOPLEFT", -5, 8 }, maskInset = { 1, 0, -1, 2 } }
+
+-- The backpack (bag 0) has no per-bag inventory icon on 3.3.5a, so its portrait comes up blank.
+-- Give it the same icon the combined window uses so the main bag has a proper portrait.
+local BACKPACK_ICON = "Interface\\Icons\\Inv_misc_bag_08"
+
+-- ----------------------------------------------------------------------------
+-- Per-item skin: recessed slot cavity + quality ring (shared with the combined window via
+-- NE.bagskin so both modules render identically). Skipped in combat for the FIRST skin of a button
+-- (region creation), but re-colouring an already-skinned button is fine.
+-- ----------------------------------------------------------------------------
+local function styleItemButton(btn, bagID)
+ if not btn then return end
+ if not btn._neSlotRecess and InCombatLockdown() then return end
+ if NE.bagskin then
+ local slot = btn.GetID and btn:GetID()
+ NE.bagskin.SkinButton(btn, btn.GetWidth and btn:GetWidth() or 37)
+ NE.bagskin.ApplyQuality(btn, bagID, slot)
+ -- Red-tint items the player can't use — unmet weapon/armor proficiency + food/drink below its
+ -- required level — matching the combined bag. Always on here (individual bags have no options menu).
+ NE.bagskin.ApplyUsableTint(btn, bagID, slot, true)
+ end
+end
+
+-- Refresh every item button on a container frame (called after ContainerFrame_Update / generate).
+local function styleItems(frame)
+ if not frame or not frame.GetName then return end
+ local name = frame:GetName()
+ local bagID = frame:GetID()
+ local size = frame.size or MAX_ITEMS
+ for i = 1, size do
+ styleItemButton(_G[name .. "Item" .. i], bagID)
+ end
+end
+B.StyleItems = styleItems
+
+-- ----------------------------------------------------------------------------
+-- Frame chrome. Idempotent: the metal border / Bg persist once applied, so re-runs only re-hide
+-- the classic chrome (which ContainerFrame_GenerateFrame re-shows on each open) and re-seat trim.
+-- ----------------------------------------------------------------------------
+local function hideClassicChrome(frame)
+ local name = frame:GetName()
+ for _, suffix in ipairs(CHROME_SUFFIXES) do
+ local r = _G[name .. suffix]
+ if r and r.Hide then r:Hide() end
+ end
+end
+
+local function styleFrame(frame)
+ if not frame or not frame.GetName then return end
+ local name = frame:GetName()
+
+ -- Always re-hide classic chrome — GenerateFrame re-shows it on every bag open.
+ hideClassicChrome(frame)
+
+ if not frame._neBagStyled then
+ frame._neBagStyled = true
+
+ -- Dark rock Bg + metal nineslice border (shared toolkit). ApplyModernChrome builds frame.Bg +
+ -- frame.NineSlice; HeldBagLayout is the small-top-left-cutout metal variant used for held bags.
+ if NE.chrome and NE.chrome.ApplyModernChrome then
+ pcall(NE.chrome.ApplyModernChrome, frame, "HeldBagLayout")
+ end
+
+ -- Warm-dark backpack texture over the flat rock fill (matches the retail bag interior).
+ if NE.bagskin and NE.bagskin.ApplyWindowBackground then
+ pcall(NE.bagskin.ApplyWindowBackground, frame)
+ end
+
+ -- Portrait: seat the bag's own icon into the metal corner cutout.
+ local portrait = _G[name .. "Portrait"]
+ if portrait and NE.portrait and NE.portrait.ApplyCutout then
+ pcall(NE.portrait.ApplyCutout, portrait, frame, PORTRAIT_OPTS)
+ end
+
+ -- Modern RedButton-Exit close button (reskin the EXISTING one — never create a duplicate).
+ local close = _G[name .. "CloseButton"]
+ if close and NE.panelchrome and NE.panelchrome.ModernizeCloseButton then
+ pcall(NE.panelchrome.ModernizeCloseButton, close, { frameLevelBump = 10 })
+ end
+
+ -- Bag name → the shared panel-chrome title styling (GameFontNormal gold, centred in the title
+ -- band between the portrait and close button), identical to the combined bag + character/spellbook
+ -- windows. Pass text=nil so the bag keeps its own name and we only restyle/recentre it.
+ local nameFS = _G[name .. "Name"]
+ if nameFS then
+ if NE.panelchrome and NE.panelchrome.TitleBand and NE.panelchrome.SetTitle then
+ local band = NE.panelchrome.TitleBand(frame)
+ NE.panelchrome.SetTitle(frame, nil, nameFS, band)
+ else
+ nameFS:ClearAllPoints()
+ nameFS:SetPoint("TOPLEFT", frame, "TOPLEFT", 58, -10)
+ end
+ nameFS:SetDrawLayer("OVERLAY", 1)
+ end
+ end
+
+ -- Backpack portrait fallback. Runs every call (the frame is reused for different bags): whenever
+ -- this window is showing bag 0, stamp the bag icon into its otherwise-blank portrait; other bags
+ -- keep their own native icon.
+ if frame.GetID and frame:GetID() == 0 then
+ local portrait = _G[name .. "Portrait"]
+ if portrait then
+ if SetPortraitToTexture then SetPortraitToTexture(portrait, BACKPACK_ICON)
+ else portrait:SetTexture(BACKPACK_ICON) end
+ end
+ end
+end
+B.StyleFrame = styleFrame
+
+-- Restyle whatever bag windows are already open (used on boot / re-enable).
+local function styleOpen()
+ for i = 1, FRAME_COUNT do
+ local f = _G["ContainerFrame" .. i]
+ if f and f:IsShown() then
+ styleFrame(f)
+ styleItems(f)
+ end
+ end
+end
+B.StyleOpen = styleOpen
+
+-- ----------------------------------------------------------------------------
+-- Boot: hook the stock container pipeline. hooksecurefunc runs AFTER Blizzard's body, so our
+-- restyle re-asserts on top of every generate/update without interfering with item mechanics.
+-- ----------------------------------------------------------------------------
+local hooked = false
+local function installHooks()
+ if hooked then return end
+ hooked = true
+
+ if type(_G.ContainerFrame_GenerateFrame) == "function" then
+ hooksecurefunc("ContainerFrame_GenerateFrame", function(frame)
+ styleFrame(frame)
+ styleItems(frame)
+ end)
+ else
+ log("ContainerFrame_GenerateFrame missing; chrome will rely on OnShow hooks only")
+ end
+
+ if type(_G.ContainerFrame_Update) == "function" then
+ hooksecurefunc("ContainerFrame_Update", function(frame) styleItems(frame) end)
+ else
+ log("ContainerFrame_Update missing; item quality borders will not auto-refresh")
+ end
+
+ -- OnShow safety net: catches any frame shown without going through GenerateFrame.
+ for i = 1, FRAME_COUNT do
+ local f = _G["ContainerFrame" .. i]
+ if f and f.HookScript then
+ f:HookScript("OnShow", function(self) styleFrame(self); styleItems(self) end)
+ end
+ end
+end
+
+local function boot()
+ installHooks()
+ styleOpen() -- restyle anything already open (e.g. re-enable without a fresh open)
+end
+B.Boot = boot
+
+-- ----------------------------------------------------------------------------
+-- Register with Core/Modules.lua (reload-gated). conflictsWith the bag-replacement rivals so we
+-- yield to Bagnon/ArkInventory/etc. when present (same policy as NewEra).
+-- ----------------------------------------------------------------------------
+if NE.modules and NE.modules.Register then
+ NE.modules.Register{
+ name = MODULE,
+ default = false, -- superseded by the forced-on combined bag (CombinedBag.lua). Kept registered so
+ -- the per-window restyle can be re-enabled later; not booted by default.
+ label = NE.L["Retail bags"],
+ category = "Windows",
+ desc = NE.L["Restyle the bag windows with the Dragonflight metal frame, portrait, and item "
+ .. "quality borders. Disable to keep the stock Blizzard bags. Reload (/reload) to apply."],
+ events = { "PLAYER_LOGIN" },
+ onBoot = function() boot() end,
+ conflictsWith = NE.modules.RIVALS and NE.modules.RIVALS.BAGS or nil,
+ }
+else
+ log("NE.modules.Register absent; retail bags not booted")
+end
+
+-- ----------------------------------------------------------------------------
+-- QA harness entry (optional; guarded). open/close drive the native bag toggles.
+-- ----------------------------------------------------------------------------
+if NE.qa then
+ NE.qa.modules = NE.qa.modules or {}
+ table.insert(NE.qa.modules, {
+ name = NE.L["Retail bags"],
+ frame = _G.ContainerFrame1,
+ open = function() if OpenAllBags then OpenAllBags() elseif OpenBackpack then OpenBackpack() end end,
+ close = function() if CloseAllBags then CloseAllBags() end end,
+ })
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/CombinedBag.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/CombinedBag.lua
new file mode 100644
index 0000000..4c79e43
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/bags/CombinedBag.lua
@@ -0,0 +1,1799 @@
+-- DragonUI_NewEra/modules/bags/CombinedBag.lua — single all-in-one "retail combined bag" window.
+--
+-- DOWNPORT: distilled from NewEra (Classic 1.15) ContainerFrame/CombinedBag.lua, rebuilt on the
+-- already-downported grid engine (core/ItemGrid.lua = NE.itemgrid). One movable window shows every
+-- backpack slot (bags 0..NUM_BAG_SLOTS) in a 10-wide retail-ordered grid, framed with the same
+-- Dragonflight metal chrome the rest of the addon uses. The grid engine re-parents pooled
+-- ContainerFrameItemButtonTemplate buttons onto per-bag proxy frames so the NATIVE item handlers
+-- (click/drag/split/use/tooltip/cooldown) resolve the correct bag+slot — we never reimplement item
+-- mechanics.
+--
+-- When this module is enabled it becomes the bag UI: it takes over the bag-open globals
+-- (ToggleBackpack / ToggleAllBags / OpenAllBags / CloseAllBags / OpenBackpack / CloseBackpack) and
+-- suppresses the stock ContainerFrames on show. It therefore SUPERSEDES the per-window "Retail
+-- bags" restyle (modules/bags/Bags.lua) — enable one or the other. Default OFF (opt-in), since it
+-- changes bag-open behaviour; the per-window restyle is the lighter default.
+--
+-- DELIBERATELY OMITTED vs the 1.15 original (each needs a retail-only system 3.3.5a lacks): the
+-- right-click filter/cleanup/mode Menu (retail Menu API), the in-bag search box
+-- (BagSearchBoxTemplate), and the auto-sort button (3.3.5a has no native SortBags). Money frame IS
+-- kept (SmallMoneyFrameTemplate), and the bottom money band ports the retail currency/token bar
+-- (common-currencybox pills fed by 3.3.5a's GetBackpackCurrencyInfo + player money, right-docked).
+--
+-- Reload-gated per Core/Modules.lua: disabled → not booted → stock / per-window bags.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+
+local MODULE = "combinedbag"
+NE.combinedbag = NE.combinedbag or {}
+local CB = NE.combinedbag
+
+-- ItemGrid uses NE.containerframe for its shared helpers (ApplyLockedSlot etc.). Mark _hideLocked
+-- so the retail "buy more slots" authenticator phantom slots never appear on 3.3.5a.
+NE.containerframe = NE.containerframe or {}
+NE.containerframe._hideLocked = true
+
+local G = NE.itemgrid
+
+local function log(msg) if NE.Log then NE.Log("COMBINEDBAG", msg) end end
+
+-- ----------------------------------------------------------------------------
+-- Geometry (transcribed from NewEra's ContainerFrameCombinedBagsMixin values).
+-- ----------------------------------------------------------------------------
+-- Geometry measured from the target image.png (522px window): slot pitch ~45px, ~18-20px gutters
+-- to the window edges. So: 40px slots + 5px spacing = 45px pitch; LEFT_PADDING 18 + PADDING_WIDTH 36
+-- give matching ~18px left/right gutters (frameW = contentW + PADDING_WIDTH; right gutter =
+-- PADDING_WIDTH - LEFT_PADDING = 18).
+-- Measured from image.png: visible slot 38px, gap 7px, pitch 45px (slot/pitch 0.84). The recess
+-- draws at button+4px (SkinButton insets -2/+2), so ITEM_SIZE 34 → 38px visible slot; spacing 11 →
+-- 45px pitch and a 7px gap. Gutters ~18px via LEFT_PADDING 18 / PADDING_WIDTH 36.
+local NUM_BAG_SLOTS = NUM_BAG_SLOTS or 4 -- backpack(0) + bags 1..4
+local KEYRING_CONTAINER = KEYRING_CONTAINER or -2 -- the keyring is container -2 (its own key row)
+local COLUMNS = 10
+local ITEM_SIZE = 34 -- +4px recess overhang = 38px visible slot
+local ITEM_SPACING_X = 11 -- pitch 45, visible gap 7 (matches target)
+local ITEM_SPACING_Y = 11
+local PADDING_WIDTH = 36
+local PADDING_HEIGHT = 82
+local LEFT_PADDING = 18 -- left gutter to the metal frame
+local RIGHT_PADDING = PADDING_WIDTH - LEFT_PADDING -- matching right gutter (18)
+local TOP_HEADER = 72 -- chrome (title + search) above the items
+local MONEY_FRAME_H = 16
+
+-- Bottom money band (transcribed from retail's ContainerFrameTokenWatcherMixin + the NewEra port):
+-- a full-width band under the item grid, separated by a thin divider, carrying the watched-currency
+-- token pills on the LEFT and the player MoneyFrame on the RIGHT. Sizes tuned for the ~11px gutters
+-- the rest of the window uses.
+local BAND_H = 20 -- token/money row height
+local BAND_TOP_GAP = 12 -- last item row → divider
+local BAND_BOTTOM_GAP = 12 -- band → window bottom (matches the gutters)
+local BAND_RESERVE = BAND_TOP_GAP + BAND_H + BAND_BOTTOM_GAP -- bottom room the grid must leave
+
+-- common-currencybox 3-piece pill (fdid 4701880, coords from wago.tools via NewEra AtlasData). The
+-- BLP ships in Textures/Common (registered in Textures/Assets.lua); coords registered here so the
+-- whole token-bar feature stays inside the bags module.
+local TOKEN_ATLASES = {
+ ["common-currencybox-left"] = { file = 4701880, left = 0.031250, right = 0.531250, top = 0.289062, bottom = 0.554688, width = 16, height = 34 },
+ ["common-currencybox-right"] = { file = 4701880, left = 0.031250, right = 0.531250, top = 0.570312, bottom = 0.835938, width = 16, height = 34 },
+ ["_common-currencybox-center"] = { file = 4701880, left = 0.000000, right = 0.500000, top = 0.007812, bottom = 0.273438, width = 16, height = 34 },
+}
+if NE.tex and NE.tex.RegisterAtlases then NE.tex.RegisterAtlases(TOKEN_ATLASES) end
+local MAX_TOKENS = MAX_WATCHED_TOKENS or 3 -- 3.3.5a backpack tracks up to 3 currencies
+
+local PORTRAIT_BAG_ASSET = "Interface\\Icons\\Inv_misc_bag_08"
+-- Prefer WoW's localized COMBINED_BAG_TITLE global (absent on 3.3.5a) → English via NE.L.
+local TITLE_TEXT = (type(COMBINED_BAG_TITLE) == "string" and COMBINED_BAG_TITLE) or NE.L["All Bags"]
+
+local frame -- the window (built lazily)
+local grid -- NE.itemgrid instance over bags 0..NUM_BAG_SLOTS
+local keyGrid -- NE.itemgrid instance over the keyring (container -2), shown as an opt-in bottom row
+
+-- Keyring row layout: a small "Keys" label + one grid row under the main grid, above the money band.
+local KEYS_TOP_GAP = 10 -- gap between the last item row and the keys section
+local KEYS_LABEL_H = 15 -- height reserved for the "Keys" label above the key slots
+
+-- Opt-in: show the keyring as a row inside the combined window. OFF by default → the stock keyring
+-- frame opens normally when you click the keyring (see installIntercept).
+if CB._showKeys == nil then CB._showKeys = false end
+function CB.ShowKeys() return CB._showKeys and true or false end
+
+-- Opt-in: split specialty bags (quiver, ammo pouch, soul bag, profession bags) into their OWN labeled
+-- sections below the general grid — each with an UPPERCASE header (QUIVER, MINING BAG, …), mirroring
+-- the keyring row. OFF by default (the general grid then just clusters specialty items after general).
+if CB._separateBags == nil then CB._separateBags = false end
+function CB.SeparateBags() return CB._separateBags and true or false end
+
+-- Player's held bags grouped by TYPE: general bags (family 0, incl. the backpack) first, then
+-- specialty bags (quiver, soul, profession, etc.) grouped by family. Drives both the grid display
+-- order (items cluster by bag type) and the sort target assignment. bagFamily from
+-- GetContainerNumFreeSlots's 2nd return; item family from GetItemFamily.
+local function orderedBags()
+ local list = {}
+ for bag = 0, NUM_BAG_SLOTS do
+ local _, family = 0, 0
+ if GetContainerNumFreeSlots then _, family = GetContainerNumFreeSlots(bag) end
+ list[#list + 1] = { bag = bag, family = family or 0 }
+ end
+ table.sort(list, function(a, b)
+ if (a.family == 0) ~= (b.family == 0) then return a.family == 0 end -- general bags first
+ if a.family ~= b.family then return a.family < b.family end -- specialty grouped by family
+ return a.bag < b.bag
+ end)
+ return list
+end
+
+-- Does an item of family `itemFam` fit a bag of family `bagFam`? General bags (0) take anything;
+-- specialty bags only take matching families. Prevents e.g. regular items landing in a quiver.
+local function itemFitsBag(itemFam, bagFam)
+ if bagFam == 0 then return true end
+ return bit and bit.band and bit.band(itemFam or 0, bagFam) ~= 0
+end
+
+-- Sort scheme (persisted via NE.db.combinedbag). "category" = the smart default; others are simple.
+CB._sortMode = CB._sortMode or "category"
+-- Red-tint items the player can't use. Default on (opt-out). Persisted.
+if CB._redUnusable == nil then CB._redUnusable = true end
+-- Auto-sell grey (junk) items when a merchant opens. Default OFF (opt-in — it spends items). Persisted.
+if CB._autoSellJunk == nil then CB._autoSellJunk = false end
+-- Auto-empty & swap when equipping a bag over a non-empty one. Default on (opt-out). Persisted.
+if CB._autoEmptyBag == nil then CB._autoEmptyBag = true end
+
+-- Category rank for the smart sort (enUS fallback): weapons before armor, quest, consumables,
+-- materials, recipes, then everything else.
+local CAT_RANK = {
+ Weapon = 1, Armor = 2, Quest = 3, Consumable = 4,
+ ["Trade Goods"] = 5, Gem = 5, Projectile = 5, Quiver = 5, Reagent = 5, Recipe = 6,
+}
+-- Locale-independent category rank. GetItemInfo's itemType is LOCALISED, so keying CAT_RANK by the
+-- English string only ranks correctly on enUS. GetAuctionItemClasses() returns the class names in a
+-- FIXED index order on every locale — 1 Weapon, 2 Armor, 3 Container, 4 Consumable, 5 Glyph,
+-- 6 Trade Goods, 7 Projectile, 8 Quiver, 9 Recipe, 10 Gem, 11 Miscellaneous, 12 Quest — so we map
+-- each localised name to a rank by its index (built once, lazily). Falls back to the enUS table.
+local RANK_BY_CLASS_INDEX = { 1, 2, 6, 4, 5, 5, 5, 5, 6, 5, 7, 3 }
+local classRankMap
+local function classRank(itemType)
+ if not itemType then return 7 end
+ if not classRankMap then
+ classRankMap = {}
+ if GetAuctionItemClasses then
+ local names = { GetAuctionItemClasses() }
+ for i = 1, #names do classRankMap[names[i]] = RANK_BY_CLASS_INDEX[i] or 7 end
+ end
+ end
+ return classRankMap[itemType] or CAT_RANK[itemType] or 7
+end
+-- The Hearthstone is pinned to the very front of the category (smart) sort (rank below any CAT_RANK).
+local HEARTHSTONE_ID = 6948
+
+-- Canonical, deterministic sort key for an item (higher quality / item level first; usable before
+-- unusable; name then itemID break ties → the sort is stable and idempotent). Honours CB._sortMode.
+local function sortKey(link, itemID)
+ local name, _, quality, iLvl, _, itemType, itemSubType = GetItemInfo(link or itemID)
+ if not name then return string.format("~~|%08d", itemID or 0) end -- uncached → sort last
+ name, quality, iLvl = name:lower(), quality or 1, iLvl or 0
+ local mode = CB._sortMode
+ if mode == "name" then
+ return string.format("%s|%08d", name, itemID or 0)
+ elseif mode == "quality" then
+ return string.format("%02d|%s|%08d", 99 - quality, name, itemID or 0)
+ elseif mode == "ilvl" then
+ return string.format("%04d|%s|%08d", 9999 - math.min(iLvl, 9999), name, itemID or 0)
+ end
+ -- category (default): category > SUBTYPE > usable > item level > quality > name > id. Grouping by
+ -- subtype keeps like with like (all swords, all cloth, all potions together) — nicer in general AND
+ -- specialty bags. Hearthstone is pinned to rank 0 so it always lands in the first slot.
+ local cat = (itemID == HEARTHSTONE_ID) and 0 or classRank(itemType)
+ local usable = 1
+ if IsUsableItem then local u = IsUsableItem(link or itemID); usable = u and 0 or 1 end
+ return string.format("%d|%s|%d|%04d|%02d|%s|%08d",
+ cat, (itemSubType or ""):lower(), usable, 9999 - math.min(iLvl, 9999), 99 - quality, name, itemID or 0)
+end
+
+-- Small icon button for the header row (sort / options): dark backing, ADD highlight, tooltip.
+-- `spec` = { atlas = "name" } (a registered atlas, e.g. the grey cog "questlog-icon-setting"), OR
+-- { path = "Interface\\..." [, trim=true] [, desaturate=true] } (a plain texture/icon).
+-- atlasSize = true → draw the atlas at its NATIVE size, centred (spellbook-cog look), instead of
+-- stretching it to fill the button.
+-- noBg = true → skip the dark backing square (bare icon, like the spellbook cog).
+local function makeHeaderButton(parent, spec, tooltip, onClick)
+ local b = CreateFrame("Button", nil, parent)
+ b:SetFrameLevel((parent:GetFrameLevel() or 1) + 25)
+ if not spec.noBg then
+ local bg = b:CreateTexture(nil, "BACKGROUND")
+ bg:SetAllPoints(b)
+ bg:SetTexture(0, 0, 0, 0.4)
+ end
+ local icon = b:CreateTexture(nil, "ARTWORK")
+ local atlasOK = spec.atlas and NE.tex and NE.tex.SetAtlas
+ and NE.tex.SetAtlas(icon, spec.atlas, spec.atlasSize and true or false)
+ if spec.atlasSize and atlasOK then
+ icon:SetPoint("CENTER") -- native atlas size, centred (SetAtlas sized it)
+ else
+ icon:SetPoint("TOPLEFT", 2, -2)
+ icon:SetPoint("BOTTOMRIGHT", -2, 2)
+ end
+ if not atlasOK then
+ icon:SetTexture(spec.path)
+ if spec.trim ~= false then icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) end -- trim the icon's built-in border
+ end
+ if spec.desaturate and icon.SetDesaturated then icon:SetDesaturated(true) end
+ b.icon = icon
+ local hl = b:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetAllPoints(icon)
+ hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
+ hl:SetBlendMode("ADD")
+ b:SetScript("OnClick", onClick)
+ b:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_TOP")
+ GameTooltip:SetText(tooltip or "")
+ GameTooltip:Show()
+ end)
+ b:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ return b
+end
+
+-- ----------------------------------------------------------------------------
+-- Bottom money band: a thin engraved divider + a full-width band carrying the watched-currency
+-- token pills (left) and the player MoneyFrame (right). Built once, repopulated on refresh.
+-- ----------------------------------------------------------------------------
+-- One currency pill: [count][icon] on the money band. Pooled on band._pills.
+local function getTokenPill(band, index)
+ local pill = band._pills[index]
+ if pill then return pill end
+
+ pill = CreateFrame("Frame", nil, band)
+ pill:SetHeight(BAND_H)
+ pill:SetFrameLevel(band:GetFrameLevel() + 1)
+
+ -- No box border: the common-currencybox art renders with a green edge on this client, so we show
+ -- the currency as a bare icon + count on the band. capW is kept only as left/right text padding.
+ local capW = math.floor(16 * BAND_H / 34 + 0.5)
+
+ pill.icon = pill:CreateTexture(nil, "ARTWORK")
+ pill.icon:SetSize(14, 14)
+ pill.icon:SetPoint("RIGHT", pill, "RIGHT", -6, 0)
+ pill.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+
+ pill.count = pill:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
+ pill.count:SetPoint("RIGHT", pill.icon, "LEFT", -3, 0)
+ pill.count:SetJustifyH("RIGHT")
+
+ -- Hover → the native backpack-token tooltip + a "shift-right-click to stop watching" hint.
+ -- Shift+right-click → untrack this currency (mirrors the character Currency tab's watch checkbox).
+ pill:EnableMouse(true)
+ pill:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ if self._backpackIndex and GameTooltip.SetBackpackToken
+ and pcall(GameTooltip.SetBackpackToken, GameTooltip, self._backpackIndex) then
+ -- native tooltip set
+ else
+ GameTooltip:SetText(self._name or "", 1, 1, 1)
+ end
+ GameTooltip:AddLine(NE.L["Shift-right-click to stop watching"], 0.2, 1, 0.2)
+ GameTooltip:Show()
+ end)
+ pill:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ pill:SetScript("OnMouseUp", function(self, button)
+ if button == "RightButton" and IsShiftKeyDown and IsShiftKeyDown() then
+ CB.UntrackCurrency(self._itemID, self._name)
+ end
+ end)
+
+ pill._capW = capW
+ band._pills[index] = pill
+ return pill
+end
+
+local function buildMoneyBand(parent)
+ if parent._moneyBand then return parent._moneyBand end
+
+ -- Engraved divider between the item grid and the band (a dark line + a faint highlight under it).
+ local divShadow = parent:CreateTexture(nil, "ARTWORK")
+ divShadow:SetTexture(0, 0, 0, 0.55)
+ divShadow:SetHeight(1)
+ divShadow:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", LEFT_PADDING, BAND_H + BAND_BOTTOM_GAP + 3)
+ divShadow:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -RIGHT_PADDING, BAND_H + BAND_BOTTOM_GAP + 3)
+ local divHi = parent:CreateTexture(nil, "ARTWORK")
+ divHi:SetTexture(1, 1, 1, 0.06)
+ divHi:SetHeight(1)
+ divHi:SetPoint("TOPLEFT", divShadow, "BOTTOMLEFT", 0, 0)
+ divHi:SetPoint("TOPRIGHT", divShadow, "BOTTOMRIGHT", 0, 0)
+
+ local band = CreateFrame("Frame", "NE_CombinedBagMoneyBand", parent)
+ band:SetHeight(BAND_H)
+ band:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", LEFT_PADDING, BAND_BOTTOM_GAP)
+ band:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -RIGHT_PADDING, BAND_BOTTOM_GAP)
+ band:SetFrameLevel((parent:GetFrameLevel() or 1) + 6)
+ band._pills = {}
+
+ -- Player money, docked to the RIGHT of the band. Native SmallMoneyFrame (self-updates on PLAYER_MONEY).
+ local money = CreateFrame("Frame", "NE_CombinedBagMoneyFrame", band, "SmallMoneyFrameTemplate")
+ money:SetPoint("RIGHT", band, "RIGHT", -2, 0)
+ if MoneyFrame_SetType then pcall(MoneyFrame_SetType, money, "PLAYER") end
+ if MoneyFrame_SetMaxDisplayWidth then pcall(MoneyFrame_SetMaxDisplayWidth, money, 168) end
+ band.money = money
+ parent.moneyFrame = money
+
+ parent._moneyBand = band
+ return band
+end
+
+-- Repopulate the currency pills from the watched-token API (3.3.5a shape:
+-- name, count, extraCurrencyType, icon, itemID — icon at position 4). Lays them out left→right.
+local function updateMoneyBand(parent)
+ local band = parent._moneyBand
+ if not band then return end
+ if not GetBackpackCurrencyInfo then
+ for _, p in ipairs(band._pills) do p:Hide() end
+ return
+ end
+
+ local shown = 0
+ local prev
+ for i = 1, MAX_TOKENS do
+ local name, count, _, icon, itemID = GetBackpackCurrencyInfo(i)
+ if name and icon then
+ shown = shown + 1
+ local pill = getTokenPill(band, shown)
+ pill._name, pill._itemID, pill._backpackIndex = name, itemID, i
+ pill.icon:SetTexture(icon)
+ pill.count:SetText(BreakUpLargeNumbers and BreakUpLargeNumbers(count or 0) or (count or 0))
+ -- Width = left cap + count text + gap + icon + right cap + insets.
+ local w = pill._capW + (pill.count:GetStringWidth() or 0) + 3 + 14 + 6 + pill._capW
+ pill:SetWidth(math.max(w, 34))
+ pill:ClearAllPoints()
+ if prev then pill:SetPoint("LEFT", prev, "RIGHT", 6, 0)
+ else pill:SetPoint("LEFT", band, "LEFT", 0, 0) end
+ pill:Show()
+ prev = pill
+ end
+ end
+ for i = shown + 1, #band._pills do band._pills[i]:Hide() end
+end
+CB.UpdateMoneyBand = updateMoneyBand
+
+-- Stop watching a currency on the backpack. SetCurrencyBackpack indexes the CURRENCY LIST (not the
+-- backpack), and that list hides children under collapsed headers — so we expand every header, find
+-- the entry by itemID (fallback name), untrack it, then restore the headers we opened. Also refreshes
+-- the character Currency tab so its watch checkbox reflects the change.
+function CB.UntrackCurrency(itemID, name)
+ if not (SetCurrencyBackpack and GetCurrencyListSize and GetCurrencyListInfo) then return end
+
+ -- Expand all collapsed headers (remember them by name — list indices shift as we expand).
+ local reCollapse = {}
+ if ExpandCurrencyList then
+ local i, size = 1, (GetCurrencyListSize() or 0)
+ while i <= size do
+ local nm, isHeader, isExpanded = GetCurrencyListInfo(i)
+ if isHeader and not isExpanded then
+ reCollapse[#reCollapse + 1] = nm
+ pcall(ExpandCurrencyList, i, 1)
+ size = GetCurrencyListSize() or 0
+ end
+ i = i + 1
+ end
+ end
+
+ -- Find + untrack (itemID is the 9th return of GetCurrencyListInfo; match name as a fallback).
+ for j = 1, (GetCurrencyListSize() or 0) do
+ local nm, isHeader, _, _, _, _, _, _, iid = GetCurrencyListInfo(j)
+ if not isHeader and ((itemID and iid == itemID) or (name and nm == name)) then
+ pcall(SetCurrencyBackpack, j, 0)
+ break
+ end
+ end
+
+ -- Re-collapse the headers we opened (untracking doesn't reshape the list, so a name match is safe).
+ if ExpandCurrencyList then
+ for _, hn in ipairs(reCollapse) do
+ for j = 1, (GetCurrencyListSize() or 0) do
+ local nm, isHeader, isExpanded = GetCurrencyListInfo(j)
+ if isHeader and isExpanded and nm == hn then pcall(ExpandCurrencyList, j, 0); break end
+ end
+ end
+ end
+
+ CB.Refresh()
+ if NE.charpanel and NE.charpanel.RefreshCurrency then NE.charpanel.RefreshCurrency() end
+end
+
+-- ----------------------------------------------------------------------------
+-- Build the window chrome + grid once.
+-- ----------------------------------------------------------------------------
+local function buildChrome()
+ if frame then return frame end
+ if not G then log("NE.itemgrid missing (core/ItemGrid.lua not loaded); combined bag unavailable"); return nil end
+
+ frame = CreateFrame("Frame", "NE_CombinedBagFrame", UIParent)
+ -- The red 3-slice is the addon's standard button; Watch keeps this window's panel buttons
+ -- skinned as its panes are built (core/ButtonSkin.lua). Opt out per button with _neNoSkin.
+ if NE.buttonskin and NE.buttonskin.Watch then pcall(NE.buttonskin.Watch, frame) end
+ frame:SetFrameStrata("HIGH")
+ frame:SetToplevel(true)
+ frame:Hide()
+
+ -- Dark rock Bg + metal border, then the warm-dark backpack texture over the fill.
+ if NE.chrome and NE.chrome.ApplyModernChrome then
+ pcall(NE.chrome.ApplyModernChrome, frame, "HeldBagLayout")
+ end
+ if NE.bagskin and NE.bagskin.ApplyWindowBackground then
+ pcall(NE.bagskin.ApplyWindowBackground, frame)
+ end
+
+ -- Portrait (the generic bag icon) seated into the metal corner cutout.
+ local portrait = frame:CreateTexture(nil, "ARTWORK")
+ if NE.portrait and NE.portrait.ApplyCutout then
+ -- Fill the metal ring, centred on it — same size/anchor the character panel uses for this exact
+ -- PortraitMetal corner (size 60 seated at TOPLEFT{-5,8} → centred on the ring circle).
+ pcall(NE.portrait.ApplyCutout, portrait, frame,
+ { size = 60, layer = "ARTWORK", anchor = { "TOPLEFT", -5, 8 }, maskInset = { 1, 0, -1, 2 } })
+ end
+ -- Circular crop: SetPortraitToTexture zooms the square icon so the round metal ring reads circular
+ -- (3.3.5a has no real mask). Called AFTER ApplyCutout so its texcoord wins.
+ if SetPortraitToTexture then
+ SetPortraitToTexture(portrait, PORTRAIT_BAG_ASSET)
+ else
+ portrait:SetTexture(PORTRAIT_BAG_ASSET)
+ end
+ frame.portrait = portrait
+
+ -- Title — route through the shared panel-chrome title styling (NE.panelchrome.SetTitle) so the
+ -- header aligns EXACTLY like the character/spellbook windows: GameFontNormal gold, hosted on the
+ -- title band (between the portrait and close button) and anchored TOP/LEFT/RIGHT so it centres in
+ -- that span. (Previously a hand-rolled single -3 TOP anchor sat slightly off vs the other windows.)
+ local title
+ if NE.panelchrome and NE.panelchrome.TitleBand and NE.panelchrome.SetTitle then
+ local band = NE.panelchrome.TitleBand(frame)
+ title = band:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ frame.Title = title -- SetTitle resolves frame.Title on later calls
+ NE.panelchrome.SetTitle(frame, TITLE_TEXT, title, band)
+ else
+ title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ title:SetPoint("TOP", frame, "TOP", 0, -5)
+ title:SetJustifyH("CENTER")
+ title:SetText(TITLE_TEXT)
+ end
+ frame.title = title
+
+ -- Header buttons: Sort + Bag-options menu, on the RIGHT of the search row. Right edge aligns with
+ -- the item grid's right edge (LEFT_PADDING gutter); 7px gaps between them and the search box —
+ -- matching the item-slot gap. Vertically centred on the 18px search row (top -32, h 22, mid -43).
+ local HDR_BTN = 22
+ local ROW_MID = -43 -- vertical centre of the 24px search row (top -31) → keep both buttons on it
+ -- Options cog: the spellbook cog's exact size + look (16x18, native-size icon, no dark backing).
+ -- Left-click toggles the bag-options menu open/closed (CB.ToggleMenu).
+ local menuBtn = makeHeaderButton(frame,
+ { atlas = "questlog-icon-setting", atlasSize = true, noBg = true }, NE.L["Bag Options"],
+ function(self) CB.ToggleMenu(self) end)
+ menuBtn:SetSize(16, 18)
+ menuBtn:SetPoint("RIGHT", frame, "TOPRIGHT", -LEFT_PADDING, ROW_MID)
+ local sortBtn = makeHeaderButton(frame,
+ { path = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\Common\\inv_pet_broom.blp" },
+ NE.L["Sort Bags"], function() CB.SortBags() end)
+ sortBtn:SetSize(HDR_BTN, HDR_BTN)
+ sortBtn:SetPoint("RIGHT", menuBtn, "LEFT", -ITEM_SPACING_X + 4, 0) -- ~7px visual gap, same centre line
+ frame.sortButton, frame.menuButton = sortBtn, menuBtn
+
+ -- Search box — the spellbook's modern self-contained style: a bare EditBox with a tooltip-art
+ -- backdrop + magnifier + separate placeholder (NOT InputBoxTemplate/SearchBoxTemplate, which other
+ -- addons redefine and can break). Placeholder uses the localized SEARCH global.
+ local search = CreateFrame("EditBox", "NE_CombinedBagSearch", frame)
+ search:SetAutoFocus(false)
+ search:SetHeight(24)
+ search:SetPoint("TOPLEFT", frame, "TOPLEFT", 60, -31)
+ search:SetPoint("RIGHT", sortBtn, "LEFT", -ITEM_SPACING_X + 4, 0) -- leave room for the buttons, ~7px gap
+ search:SetFrameLevel((frame:GetFrameLevel() or 1) + 25)
+ search:SetFontObject(_G.ChatFontNormal or _G.GameFontHighlightSmall)
+ search:SetTextInsets(24, 8, 0, 0) -- room for the magnifier at the left
+ search:SetMaxLetters(40)
+ if search.SetBackdrop then
+ search:SetBackdrop({
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = true, tileSize = 16, edgeSize = 12, insets = { left = 4, right = 4, top = 4, bottom = 4 },
+ })
+ search:SetBackdropColor(0, 0, 0, 0.6)
+ search:SetBackdropBorderColor(0.6, 0.6, 0.6, 1)
+ end
+ local mag = search:CreateTexture(nil, "OVERLAY")
+ mag:SetSize(14, 14)
+ mag:SetPoint("LEFT", search, "LEFT", 7, 0)
+ mag:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
+ local ph = search:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
+ ph:SetPoint("LEFT", search, "LEFT", 24, 0)
+ ph:SetText((SEARCH or NE.L["Search"]) or "Search")
+ local function syncPlaceholder()
+ if (search:GetText() or "") == "" and not search:HasFocus() then ph:Show() else ph:Hide() end
+ end
+ search:SetScript("OnTextChanged", function(self)
+ CB._searchText = self:GetText() or ""
+ syncPlaceholder()
+ CB.Refresh()
+ end)
+ search:SetScript("OnEditFocusGained", syncPlaceholder)
+ search:SetScript("OnEditFocusLost", syncPlaceholder)
+ search:SetScript("OnEscapePressed", function(self) self:SetText(""); self:ClearFocus() end)
+ search:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
+ syncPlaceholder()
+ frame.search = search
+
+ -- Close button (build + modernize). OnClick hides the window.
+ local close = CreateFrame("Button", "NE_CombinedBagCloseButton", frame, "UIPanelCloseButton")
+ close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 1, 0)
+ close:SetScript("OnClick", function() CB.Hide() end)
+ if NE.panelchrome and NE.panelchrome.ModernizeCloseButton then
+ pcall(NE.panelchrome.ModernizeCloseButton, close, { frameLevelBump = 10 })
+ end
+ frame.closeButton = close
+
+ -- Bottom money band: divider + currency token pills (left) + player money (right).
+ buildMoneyBand(frame)
+
+ -- The grid over the held bags, ordered by TYPE (general first), filling top-left → bottom-right so
+ -- regular items cluster first and specialty bags (quiver/soul/profession) group after.
+ grid = G.New{
+ host = frame,
+ containers = function()
+ -- When "separate specialty bags" is on, the MAIN grid holds only general bags (family 0); the
+ -- specialty bags each get their own labeled section (see the spec sections in refresh()).
+ local t = {}
+ for _, bd in ipairs(orderedBags()) do
+ if (not CB.SeparateBags()) or bd.family == 0 then t[#t + 1] = bd.bag end
+ end
+ return t
+ end,
+ columns = COLUMNS,
+ itemSize = ITEM_SIZE,
+ spacingX = ITEM_SPACING_X,
+ spacingY = ITEM_SPACING_Y,
+ originX = LEFT_PADDING,
+ originY = -TOP_HEADER,
+ direction = "TLBR",
+ slotDescending = false,
+ namePrefix = "NE_CombinedItem",
+ frameLevel = function() return (frame:GetFrameLevel() or 1) + 5 end,
+ }
+ CB.grid = grid
+
+ -- Keyring row: a second grid over container -2, laid out under the main grid (positioned per refresh).
+ -- Its container list is empty unless the "Show keyring row" option is on, so it's a no-op when off.
+ keyGrid = G.New{
+ host = frame,
+ containers = function() return CB.ShowKeys() and { KEYRING_CONTAINER } or {} end,
+ slotCount = function(c)
+ if c == KEYRING_CONTAINER then
+ return (GetKeyRingSize and GetKeyRingSize())
+ or (C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerNumSlots(c)) or 0
+ end
+ return (C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerNumSlots(c)) or 0
+ end,
+ columns = COLUMNS,
+ itemSize = ITEM_SIZE,
+ spacingX = ITEM_SPACING_X,
+ spacingY = ITEM_SPACING_Y,
+ originX = LEFT_PADDING,
+ originY = -TOP_HEADER, -- repositioned each refresh, below the main grid
+ direction = "TLBR",
+ namePrefix = "NE_CombinedKey",
+ frameLevel = function() return (frame:GetFrameLevel() or 1) + 5 end,
+ }
+ CB.keyGrid = keyGrid
+
+ -- "Keys" label above the keyring row (shown only when the row has slots and the option is on).
+ local keysLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ keysLabel:SetText(string.upper(NE.L["Keys"] or "Keys")) -- UPPERCASE, matching the specialty-bag headers
+ keysLabel:SetTextColor(1, 0.82, 0)
+ keysLabel:Hide()
+ frame._keysLabel = keysLabel
+
+ -- Sorting cover: an opaque panel over the grid, shown while a sort runs, so the item shuffle is
+ -- invisible — the player sees "Sorting…" then the finished layout. Also blocks clicks mid-sort.
+ local cover = CreateFrame("Frame", nil, frame)
+ cover:SetPoint("TOPLEFT", frame, "TOPLEFT", 6, -(TOP_HEADER - 2))
+ cover:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -6, 6)
+ cover:SetFrameLevel((frame:GetFrameLevel() or 1) + 40)
+ cover:EnableMouse(true)
+ -- No opaque fill: during a sort we HIDE the item buttons/slots directly (see SortBags), so the bag's
+ -- own marble background shows through with just the spinner + label floating over it.
+
+ -- Center zone: spans [bottom of the search bar .. the currency divider], NOT the whole cover (which
+ -- also overlays the money band). Anchoring the spinner/label to THIS zone's center keeps them
+ -- centered in that band for any row count (2 rows or 18) — the top is pinned to the search bar's
+ -- bottom edge and the bottom to the divider (a fixed BAND_H + BAND_BOTTOM_GAP + 3 above the frame
+ -- bottom), so the midpoint tracks the real height difference instead of drifting low.
+ local centerZone = CreateFrame("Frame", nil, cover)
+ centerZone:SetPoint("LEFT", cover, "LEFT", 0, 0)
+ centerZone:SetPoint("RIGHT", cover, "RIGHT", 0, 0)
+ centerZone:SetPoint("TOP", search, "BOTTOM", 0, 0) -- top = bottom of the search bar
+ centerZone:SetPoint("BOTTOM", frame, "BOTTOM", 0, BAND_H + BAND_BOTTOM_GAP + 3) -- bottom = currency divider
+
+ -- Retail-style loading spinner (Stream* art ported from ezCollections): static gold background +
+ -- frame, with a gold circle + spark that rotate continuously via a looping Rotation animation.
+ local TEX = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\Common\\"
+ local spinner = CreateFrame("Frame", nil, cover)
+ spinner:SetSize(48, 48)
+ spinner:SetPoint("CENTER", centerZone, "CENTER", 0, 12)
+ local sbg = spinner:CreateTexture(nil, "BACKGROUND")
+ sbg:SetAllPoints(); sbg:SetTexture(TEX .. "StreamBackground.tga"); sbg:SetVertexColor(1, 0.82, 0)
+ local sframe = spinner:CreateTexture(nil, "ARTWORK")
+ sframe:SetAllPoints(); sframe:SetTexture(TEX .. "StreamFrame.tga")
+ local anim = CreateFrame("Frame", nil, spinner)
+ anim:SetAllPoints()
+ local scircle = anim:CreateTexture(nil, "BACKGROUND")
+ scircle:SetAllPoints(); scircle:SetTexture(TEX .. "StreamCircle.tga"); scircle:SetVertexColor(1, 0.82, 0)
+ local sspark = anim:CreateTexture(nil, "OVERLAY")
+ sspark:SetAllPoints(); sspark:SetTexture(TEX .. "StreamSpark.tga")
+ local ag = anim:CreateAnimationGroup()
+ ag:SetLooping("REPEAT")
+ local rot = ag:CreateAnimation("Rotation")
+ rot:SetDegrees(-360); rot:SetDuration(1); rot:SetOrder(1)
+ cover.spinnerAnim = ag
+
+ local clabel = cover:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
+ clabel:SetPoint("TOP", spinner, "BOTTOM", 0, -8)
+ clabel:SetText(NE.L["Sorting…"])
+ clabel:SetTextColor(1, 0.82, 0)
+ cover.label = clabel
+
+ -- Spin only while the cover is shown.
+ cover:SetScript("OnShow", function(self) if self.spinnerAnim then self.spinnerAnim:Play() end end)
+ cover:SetScript("OnHide", function(self) if self.spinnerAnim then self.spinnerAnim:Stop() end end)
+ cover:Hide()
+ frame.sortCover = cover
+
+ -- Movable + persisted position + ESC close.
+ if NE.FrameUtil then
+ if NE.FrameUtil.PersistWindowPosition then
+ NE.FrameUtil.PersistWindowPosition(frame, "combinedbag",
+ { point = "BOTTOMRIGHT", relPoint = "BOTTOMRIGHT", x = -25, y = 85 })
+ else
+ frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -25, 85)
+ end
+ if NE.FrameUtil.EscClose then NE.FrameUtil.EscClose("NE_CombinedBagFrame") end
+ else
+ frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -25, 85)
+ end
+
+ -- Per-window scaling via NE.scale (mode: ui / none / custom; options tab > Window Scaling). Same
+ -- "combinedbag" key PersistWindowPosition uses above. DEFAULTS["combinedbag"] is "ui" = a plain
+ -- SetScale(1.0), i.e. exactly what this window did before it had a setting. Applied AFTER the
+ -- position is restored so the anchor is already in place when the scale lands on it.
+ if NE.scale and NE.scale.Apply then
+ if NE.scale.SetFrame then NE.scale.SetFrame("combinedbag", frame) end
+ NE.scale.Apply("combinedbag")
+ end
+
+ return frame
+end
+CB.BuildChrome = buildChrome
+
+-- ----------------------------------------------------------------------------
+-- Refresh the grid + size the window to fit.
+-- ----------------------------------------------------------------------------
+-- Does the item in (bag,slot) match the search text? Empty slots never match while searching.
+function CB.SlotMatches(bagID, slot, text)
+ if not text or text == "" then return true end
+ if not (bagID and slot and C_Container and C_Container.GetContainerItemInfo) then return false end
+ local info = C_Container.GetContainerItemInfo(bagID, slot)
+ local key = info and (info.hyperlink or info.itemID)
+ if not key then return false end
+ local name = info.name
+ if not name and GetItemInfo then name = GetItemInfo(key) end
+ if not name then return true end -- name not cached yet: don't hide it
+ return name:lower():find(text:lower(), 1, true) ~= nil
+end
+
+-- ----------------------------------------------------------------------------
+-- Specialty-bag sections ("separate specialty bags" option). One labeled section per bag family the
+-- player carries (quiver, ammo pouch, soul bag, herb/mining/… profession bags), stacked below the
+-- general grid, mirroring the keyring row. Grids are created lazily and cached per family.
+-- ----------------------------------------------------------------------------
+-- UPPERCASE fallback names per bag family bit (used only if the bag's localized subtype isn't cached).
+local FAMILY_NAME = {
+ [1] = "QUIVER", [2] = "AMMO POUCH", [4] = "SOUL BAG",
+ [8] = "LEATHERWORKING BAG", [16] = "INSCRIPTION BAG", [32] = "HERB BAG",
+ [64] = "ENCHANTING BAG", [128] = "ENGINEERING BAG", [512] = "GEM BAG",
+ [1024] = "MINING BAG",
+}
+-- Ordered list of the specialty families the player currently has a bag for.
+local function activeSpecFamilies()
+ local seen, list = {}, {}
+ for _, bd in ipairs(orderedBags()) do
+ if bd.family ~= 0 and not seen[bd.family] then seen[bd.family] = true; list[#list + 1] = bd.family end
+ end
+ table.sort(list)
+ return list
+end
+-- Section header text: the bag's own localized subtype (e.g. "Mining Bag"), UPPERCASED; static fallback.
+local function familyLabel(family)
+ if ContainerIDToInventoryID and GetInventoryItemLink and GetItemInfo then
+ for _, bd in ipairs(orderedBags()) do
+ if bd.family == family then
+ local link = GetInventoryItemLink("player", ContainerIDToInventoryID(bd.bag))
+ local sub = link and select(7, GetItemInfo(link))
+ if sub and sub ~= "" then return string.upper(sub) end
+ end
+ end
+ end
+ return FAMILY_NAME[family] or "OTHER"
+end
+-- Lazily create + cache a { grid, label } section for a family. The grid holds that family's bags only
+-- while "separate specialty bags" is on (else its container list is empty → a no-op row).
+local specSections = {}
+local function getSpecSection(family)
+ local sec = specSections[family]
+ if sec then return sec end
+ local g = G.New{
+ host = frame,
+ containers = function()
+ if not CB.SeparateBags() then return {} end
+ local t = {}
+ for _, bd in ipairs(orderedBags()) do
+ if bd.family == family then t[#t + 1] = bd.bag end
+ end
+ return t
+ end,
+ columns = COLUMNS, itemSize = ITEM_SIZE, spacingX = ITEM_SPACING_X, spacingY = ITEM_SPACING_Y,
+ originX = LEFT_PADDING, originY = -TOP_HEADER, direction = "TLBR",
+ namePrefix = "NE_CombinedSpec" .. tostring(family),
+ frameLevel = function() return (frame:GetFrameLevel() or 1) + 5 end,
+ }
+ local label = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ label:SetTextColor(1, 0.82, 0)
+ label:Hide()
+ sec = { grid = g, label = label }
+ specSections[family] = sec
+ return sec
+end
+
+local function refresh()
+ if not frame or not frame:IsShown() or not grid then return end
+ if CB._sorting then return end -- frozen under the sort cover; one repaint runs when the sort ends
+ local _, _, contentW, contentH = grid:Refresh()
+
+ -- Skin every slot button the grid just laid out: recessed cavity + quality ring + search dim.
+ -- The grid stamps btn._bagID/_slotID, so we colour quality + test search from live container info.
+ local text = CB._searchText
+ local searching = text and text ~= ""
+ if NE.bagskin and grid.ForEachButton then
+ grid:ForEachButton(function(b)
+ NE.bagskin.SkinButton(b, ITEM_SIZE)
+ NE.bagskin.ApplyQuality(b, b._bagID, b._slotID)
+ NE.bagskin.ApplyUsableTint(b, b._bagID, b._slotID, CB._redUnusable)
+ NE.bagskin.SetSearchDim(b, searching and not CB.SlotMatches(b._bagID, b._slotID, text))
+ NE.bagskin.ApplyItemLevel(b, b._bagID, b._slotID)
+ end)
+ end
+
+ contentW = contentW or (COLUMNS * ITEM_SIZE + (COLUMNS - 1) * ITEM_SPACING_X)
+ contentH = contentH or 0
+
+ -- Specialty-bag sections (opt-in): stack a labeled grid per bag family below the general grid.
+ -- flowH tracks the running height below the header; maxW the widest section (drives window width).
+ local flowH, maxW = contentH, contentW
+ local activeFam = {}
+ if CB.SeparateBags() then
+ for _, family in ipairs(activeSpecFamilies()) do
+ activeFam[family] = true
+ local sec = getSpecSection(family)
+ local topY = -(TOP_HEADER + flowH + KEYS_TOP_GAP)
+ sec.grid.originY = topY - KEYS_LABEL_H
+ local _, rows, w, h = sec.grid:Refresh()
+ if NE.bagskin and sec.grid.ForEachButton then
+ sec.grid:ForEachButton(function(b)
+ NE.bagskin.SkinButton(b, ITEM_SIZE)
+ NE.bagskin.ApplyQuality(b, b._bagID, b._slotID)
+ NE.bagskin.ApplyUsableTint(b, b._bagID, b._slotID, CB._redUnusable)
+ NE.bagskin.SetSearchDim(b, searching and not CB.SlotMatches(b._bagID, b._slotID, text))
+ NE.bagskin.ApplyItemLevel(b, b._bagID, b._slotID)
+ end)
+ end
+ if (rows or 0) > 0 then
+ sec.label:SetText(familyLabel(family))
+ sec.label:ClearAllPoints()
+ sec.label:SetPoint("TOPLEFT", frame, "TOPLEFT", LEFT_PADDING, topY)
+ sec.label:Show()
+ flowH = flowH + KEYS_TOP_GAP + KEYS_LABEL_H + (h or 0)
+ maxW = math.max(maxW, w or 0)
+ else
+ sec.label:Hide()
+ end
+ end
+ end
+ -- Hide any cached section whose family is no longer active (or when the option is off) — its empty
+ -- container list makes the Refresh hide the slot buttons.
+ for family, sec in pairs(specSections) do
+ if not activeFam[family] then
+ sec.label:Hide()
+ sec.grid:Refresh()
+ end
+ end
+
+ -- Keyring row (opt-in): sit it just under the last section, with a "Keys" label above it. keyGrid's
+ -- container list is empty when the option is off, so this is a no-op (0 rows) then. originY is set
+ -- LIVE each refresh so the row tracks the grid stack's height.
+ local keysSectionH = 0
+ if keyGrid then
+ local keysTopY = -(TOP_HEADER + flowH + KEYS_TOP_GAP)
+ keyGrid.originY = keysTopY - KEYS_LABEL_H
+ local _, keyRows, _, keyH = keyGrid:Refresh()
+ if NE.bagskin and keyGrid.ForEachButton then
+ keyGrid:ForEachButton(function(b)
+ NE.bagskin.SkinButton(b, ITEM_SIZE)
+ NE.bagskin.ApplyQuality(b, b._bagID, b._slotID)
+ NE.bagskin.ApplyUsableTint(b, b._bagID, b._slotID, CB._redUnusable)
+ NE.bagskin.SetSearchDim(b, searching and not CB.SlotMatches(b._bagID, b._slotID, text))
+ NE.bagskin.ApplyItemLevel(b, b._bagID, b._slotID)
+ end)
+ end
+ if CB.ShowKeys() and (keyRows or 0) > 0 then
+ keysSectionH = KEYS_TOP_GAP + KEYS_LABEL_H + (keyH or 0)
+ if frame._keysLabel then
+ frame._keysLabel:ClearAllPoints()
+ frame._keysLabel:SetPoint("TOPLEFT", frame, "TOPLEFT", LEFT_PADDING, keysTopY)
+ frame._keysLabel:Show()
+ end
+ elseif frame._keysLabel then
+ frame._keysLabel:Hide()
+ end
+ end
+
+ -- Repopulate the currency pills, then reserve room below the grid (+ keys section) for the divider +
+ -- money band (BAND_RESERVE = top gap + band height + bottom gap) so the bottom breathes like retail.
+ updateMoneyBand(frame)
+ frame:SetSize(maxW + PADDING_WIDTH, flowH + TOP_HEADER + keysSectionH + BAND_RESERVE)
+end
+CB.Refresh = refresh
+
+-- Repaint when DragonUI's item level settings change. Its RefreshItemLevel hides EVERY text it owns
+-- (ours included — our slots' FontStrings live in its own registry) and then re-runs only ITS update
+-- paths, which don't know about this grid. Without this hook, turning the setting back on, or
+-- changing the font or position, leaves our slots blank until the next bag event repaints them.
+do
+ local dragon = NE.dragon
+ local function repaintIfOpen()
+ if frame and frame:IsShown() then pcall(refresh) end
+ end
+ if dragon and type(dragon.RefreshItemLevel) == "function" then
+ hooksecurefunc(dragon, "RefreshItemLevel", repaintIfOpen)
+ end
+ if dragon and type(dragon.RefreshItemLevelPosition) == "function" then
+ hooksecurefunc(dragon, "RefreshItemLevelPosition", repaintIfOpen)
+ end
+end
+
+-- ----------------------------------------------------------------------------
+-- Show / hide / toggle. Showing suppresses the stock container frames.
+-- ----------------------------------------------------------------------------
+local function hideStockFrames()
+ for i = 1, (NUM_CONTAINER_FRAMES or 13) do
+ local f = _G["ContainerFrame" .. i]
+ if f and f:IsShown() then f:Hide() end
+ end
+end
+
+function CB.Show()
+ if not buildChrome() then return end
+ local wasShown = frame:IsShown()
+ hideStockFrames()
+ -- Open with a clean search: clear the box + filter and drop focus so the placeholder shows again.
+ CB._searchText = ""
+ if frame.search then
+ frame.search:SetText("")
+ frame.search:ClearFocus()
+ end
+ frame:Show()
+ frame:Raise()
+ refresh()
+ -- Bag-open whoosh. The stock ContainerFrames play this on show, but the combined bag suppresses
+ -- them and replaces the open globals, so we have to play it ourselves (issue #12). Only on an
+ -- actual hidden->shown transition — a re-Show of an already-open bag (e.g. loot opening a
+ -- container while bags are up) must not re-trigger it.
+ if not wasShown and PlaySound then pcall(PlaySound, "igBackPackOpen") end
+end
+
+function CB.Hide()
+ if not (frame and frame:IsShown()) then
+ if frame then frame:Hide() end
+ return
+ end
+ frame:Hide()
+ if PlaySound then pcall(PlaySound, "igBackPackClose") end
+end
+
+function CB.IsShown()
+ return frame and frame:IsShown() and true or false
+end
+
+function CB.Toggle()
+ if CB.IsShown() then CB.Hide() else CB.Show() end
+end
+
+-- ----------------------------------------------------------------------------
+-- Sort. Best-effort, bag-TYPE aware:
+-- 1. Gather held items with their family (GetItemFamily) + a quality>name>id key (reversible).
+-- 2. Assign each item a target slot in a bag it FITS — specialty items prefer their specialty bag,
+-- everything else fills general bags. Never targets a bag that can't hold the item (so regular
+-- items never land in a quiver / profession bag).
+-- 3. Realize the assignment via swaps. The game itself rejects any invalid placement, so a busy
+-- server just leaves a few items unmoved rather than mis-filing them.
+-- Out-of-combat only; bails if any item is mid-transfer (locked) — re-click after it settles.
+-- ----------------------------------------------------------------------------
+-- One selection-sort PASS toward the deterministic target. Partitions slots into a general pool +
+-- one pool per specialty bag (so every swap stays within compatible bags), sorts each pool's items,
+-- then moves what it can — skipping LOCKED items (mid server round-trip) and their slots. Returns the
+-- number of slots still not matching their target, so the driver knows when to stop.
+-- PLAN the sort ONCE (offline, separate from the live bag): snapshot slots + items, partition into a
+-- general pool + one pool per specialty bag, sort each pool, and bake the itemID that should end up
+-- in each slot. Returns groups = { { slots = {{bag,slot}...}, want = { itemID-or-nil per slot } } }.
+-- The expensive work (GetItemInfo / sortKey / GetItemFamily) happens here, ONCE — not per pass.
+local function buildSortPlan()
+ local generalGroup = { slots = {}, family = 0, items = {} }
+ local groups, specGroups, famGroups = { generalGroup }, {}, {}
+ -- Pool specialty slots BY FAMILY (not per bag) so two quivers / two herb bags share one pool and an
+ -- item can fill either. General bags (family 0, incl. the backpack) form the general pool.
+ for _, bd in ipairs(orderedBags()) do
+ local n = C_Container.GetContainerNumSlots(bd.bag) or 0
+ if bd.family == 0 then
+ for s = 1, n do generalGroup.slots[#generalGroup.slots + 1] = { bag = bd.bag, slot = s } end
+ else
+ local g = famGroups[bd.family]
+ if not g then
+ g = { slots = {}, family = bd.family, items = {} }
+ famGroups[bd.family] = g; specGroups[#specGroups + 1] = g; groups[#groups + 1] = g
+ end
+ for s = 1, n do g.slots[#g.slots + 1] = { bag = bd.bag, slot = s } end
+ end
+ end
+
+ -- Assign every held item to its best home: the specialty pool it fits (arrows→quiver, herbs→herb
+ -- bag, soul shards→soul bag, profession mats→that profession's bag, …), else the general pool.
+ for _, g in ipairs(groups) do
+ for _, sl in ipairs(g.slots) do
+ local info = C_Container.GetContainerItemInfo(sl.bag, sl.slot)
+ if info and (info.itemID or info.hyperlink) then
+ local link = info.hyperlink or info.itemID
+ local fam = (GetItemFamily and GetItemFamily(link)) or 0
+ local target = generalGroup
+ if fam ~= 0 then
+ for _, sg in ipairs(specGroups) do
+ if itemFitsBag(fam, sg.family) then target = sg; break end
+ end
+ end
+ target.items[#target.items + 1] =
+ { id = info.itemID, count = info.stackCount or 1, key = sortKey(link, info.itemID) }
+ end
+ end
+ end
+
+ -- Ordering: by sort key, then FULLER STACKS FIRST so same-item stacks read 20,20,20,8 (not shuffled).
+ local function itemLess(a, b)
+ if a.key ~= b.key then
+ if CB._reverseSort then return a.key > b.key else return a.key < b.key end
+ end
+ return (a.count or 1) > (b.count or 1) -- partial stacks sink to the end
+ end
+
+ -- Specialty pools fill FIRST; anything that doesn't fit (a full quiver, etc.) spills into general so
+ -- nothing is left stranded — the tail (lowest-priority / smallest stacks) is what spills.
+ for _, g in ipairs(specGroups) do
+ table.sort(g.items, itemLess)
+ while #g.items > #g.slots do
+ generalGroup.items[#generalGroup.items + 1] = table.remove(g.items)
+ end
+ end
+
+ for _, g in ipairs(groups) do
+ table.sort(g.items, itemLess)
+ g.want = {}
+ -- Bake the desired itemID AND stack count per slot so the position pass can place the right stack.
+ for i = 1, #g.slots do
+ local it = g.items[i]
+ g.want[i] = it and { id = it.id, count = it.count } or nil
+ end
+ end
+ return groups
+end
+
+-- Stack consolidation. One pass: for every stackable item with more than one PARTIAL stack, pour the
+-- smallest partial into the largest so partial stacks combine and free whole slots (the position sort
+-- then packs the freed space). Moves are client-predicted + locked, so counts go stale after each pour
+-- — we do ONE pour per item-type per pass and re-read next pass; returns whether anything moved so the
+-- driver knows when stacks are fully consolidated. Same-item pours are always bag-family-legal.
+local function mergePass()
+ if not (GetItemInfo and PickupContainerItem and C_Container and C_Container.GetContainerNumSlots
+ and C_Container.GetContainerItemInfo) then return false end
+ if ClearCursor then ClearCursor() end
+ local stacks = {} -- itemID -> { {bag,slot,count,max,locked}, ... } — every non-full partial stack
+ for _, bd in ipairs(orderedBags()) do
+ local n = C_Container.GetContainerNumSlots(bd.bag) or 0
+ for s = 1, n do
+ local info = C_Container.GetContainerItemInfo(bd.bag, s)
+ if info and info.itemID then
+ local maxStack = select(8, GetItemInfo(info.hyperlink or info.itemID))
+ local count = info.stackCount or 1
+ if maxStack and maxStack > 1 and count < maxStack then
+ local t = stacks[info.itemID]; if not t then t = {}; stacks[info.itemID] = t end
+ t[#t + 1] = { bag = bd.bag, slot = s, count = count, max = maxStack, locked = info.isLocked }
+ end
+ end
+ end
+ end
+ local merged, pending = false, false
+ for _, list in pairs(stacks) do
+ if #list >= 2 then
+ local free = {}
+ for _, e in ipairs(list) do if not e.locked then free[#free + 1] = e end end
+ if #free >= 2 then
+ table.sort(free, function(a, b) return a.count > b.count end) -- largest first
+ local dst, src = free[1], free[#free] -- pour smallest → largest
+ PickupContainerItem(src.bag, src.slot)
+ PickupContainerItem(dst.bag, dst.slot)
+ if CursorHasItem and CursorHasItem() then ClearCursor() end -- overflow returns to src
+ merged = true
+ else
+ pending = true -- ≥2 partials but some are locked mid-move; retry after they settle
+ end
+ end
+ end
+ if ClearCursor then ClearCursor() end
+ return merged or pending
+end
+
+-- Route items into the correct BAG TYPE before arranging. The within-pool position sort only reorders
+-- inside a bag; it can't migrate an item from a general bag into its specialty bag — which is why
+-- specialty items were all ending up in the general bags. This pass does that migration: any item in
+-- the WRONG bag type (a specialty item sitting in a general bag, or a stray general item inside a
+-- specialty bag) is moved into a free slot of the bag family it belongs to (arrows→quiver, herbs→herb
+-- bag, soul shards→soul bag, …; an item with no matching specialty bag belongs in general). One move
+-- per mis-placed item per pass; iterate until nothing migrates, THEN plan + position arranges each bag.
+local function routePass()
+ if not (C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerItemInfo
+ and PickupContainerItem and GetItemFamily) then return false end
+ local bags = orderedBags()
+ local specFamilies, seen, freeByFamily = {}, {}, {}
+ for _, bd in ipairs(bags) do
+ if bd.family ~= 0 and not seen[bd.family] then seen[bd.family] = true; specFamilies[#specFamilies + 1] = bd.family end
+ freeByFamily[bd.family] = freeByFamily[bd.family] or {}
+ local n = C_Container.GetContainerNumSlots(bd.bag) or 0
+ for s = 1, n do
+ if not C_Container.GetContainerItemInfo(bd.bag, s) then
+ local f = freeByFamily[bd.family]; f[#f + 1] = { bag = bd.bag, slot = s }
+ end
+ end
+ end
+ -- The bag family an item belongs in: its fitting specialty family, else 0 (general).
+ local function targetFamily(link)
+ local fam = GetItemFamily(link) or 0
+ if fam ~= 0 then
+ for _, sf in ipairs(specFamilies) do
+ if itemFitsBag(fam, sf) then return sf end
+ end
+ end
+ return 0
+ end
+ local moved = false
+ if ClearCursor then ClearCursor() end
+ for _, bd in ipairs(bags) do
+ local n = C_Container.GetContainerNumSlots(bd.bag) or 0
+ for s = 1, n do
+ local info = C_Container.GetContainerItemInfo(bd.bag, s)
+ if info and (info.itemID or info.hyperlink) and not info.isLocked then
+ local tf = targetFamily(info.hyperlink or info.itemID)
+ if tf ~= bd.family then
+ local free = freeByFamily[tf]
+ if free and #free > 0 then
+ local fs = table.remove(free, 1) -- a free slot in the right bag type
+ PickupContainerItem(bd.bag, s)
+ PickupContainerItem(fs.bag, fs.slot)
+ if CursorHasItem and CursorHasItem() then ClearCursor() end
+ local og = freeByFamily[bd.family]; og[#og + 1] = { bag = bd.bag, slot = s } -- old slot now free
+ moved = true
+ end
+ end
+ end
+ end
+ end
+ if ClearCursor then ClearCursor() end
+ return moved
+end
+
+-- Sweep loose keys out of the regular bags into the keyring (container -2). Keys carry the keyring bag
+-- family (GetItemFamily → the keyring bit) but ALSO fit general bags, so they can sit loose; this moves
+-- any that aren't already in the keyring while free keyring slots remain. Runs as part of the sort.
+local function routeKeysToKeyRing()
+ if not (C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerItemInfo
+ and PickupContainerItem and GetItemFamily and GetContainerNumFreeSlots and bit and bit.band) then return false end
+ local keyN = (GetKeyRingSize and GetKeyRingSize()) or 0
+ if keyN <= 0 then return false end
+ local keyFamily = select(2, GetContainerNumFreeSlots(KEYRING_CONTAINER)) or 0
+ if keyFamily == 0 then keyFamily = 256 end -- keyring bag family (fallback if the API reports 0)
+
+ local function isKeyLink(link)
+ if not link then return false end
+ local fam = GetItemFamily(link) or 0
+ if bit.band(fam, keyFamily) ~= 0 then return true end
+ if GetItemInfo then return (select(6, GetItemInfo(link))) == "Key" end -- enUS type fallback
+ return false
+ end
+
+ -- Free keyring slots to fill.
+ local freeKey = {}
+ for s = 1, keyN do
+ if not C_Container.GetContainerItemInfo(KEYRING_CONTAINER, s) then freeKey[#freeKey + 1] = s end
+ end
+ if #freeKey == 0 then return false end
+
+ local moved = false
+ if ClearCursor then ClearCursor() end
+ for bag = 0, NUM_BAG_SLOTS do
+ local n = C_Container.GetContainerNumSlots(bag) or 0
+ for s = 1, n do
+ if #freeKey == 0 then break end
+ local info = C_Container.GetContainerItemInfo(bag, s)
+ if info and (info.itemID or info.hyperlink) and not info.isLocked
+ and isKeyLink(info.hyperlink or info.itemID) then
+ local dst = table.remove(freeKey, 1)
+ PickupContainerItem(bag, s)
+ PickupContainerItem(KEYRING_CONTAINER, dst)
+ if CursorHasItem and CursorHasItem() then ClearCursor() end
+ moved = true
+ end
+ end
+ end
+ if ClearCursor then ClearCursor() end
+ return moved
+end
+
+-- One pass toward the CACHED plan: compare each live slot to its baked target itemID and move what
+-- it can (skipping locked items). No re-sorting — just cheap itemID compares. Returns the count of
+-- slots not yet matching, so the driver knows when the bag has reached the planned layout.
+local function sortPassFromPlan(groups)
+ if not groups then return 0, 0 end
+ if ClearCursor then ClearCursor() end
+ local remaining = 0
+ local moved = 0 -- swaps actually issued this pass (drives the "nothing moved" convergence)
+ local locked = 0 -- slots waiting on an in-flight (locked) item — NOT yet settled
+ for _, g in ipairs(groups) do
+ local slots, want = g.slots, g.want
+ for i = 1, #slots do
+ local w = want[i]
+ local wantID, wantCount = w and w.id, w and w.count
+ local ci = C_Container.GetContainerItemInfo(slots[i].bag, slots[i].slot)
+ local curID = ci and ci.itemID or nil
+ local curCount = ci and ci.stackCount or nil
+ -- A slot is satisfied only if BOTH the item AND the stack size match — so same-item stacks of
+ -- different sizes still get ordered (fullest into the earliest slot, partial to the end).
+ if curID ~= wantID or curCount ~= wantCount then
+ if ci and ci.isLocked then
+ remaining = remaining + 1 -- can't move yet; retry next pass
+ locked = locked + 1 -- in-flight; a quiet pass here isn't convergence
+ elseif wantID then
+ -- Find the best forward stack of the wanted item: an exact stack-size match if possible,
+ -- otherwise the fullest available (earlier slots want the fuller stacks).
+ local bestJ, bestCount
+ for j = i + 1, #slots do
+ local cj = C_Container.GetContainerItemInfo(slots[j].bag, slots[j].slot)
+ if cj and cj.itemID == wantID and not cj.isLocked then
+ local cc = cj.stackCount or 1
+ if cc == wantCount then bestJ = j; break end
+ if not bestCount or cc > bestCount then bestJ, bestCount = j, cc end
+ end
+ end
+ if bestJ then
+ PickupContainerItem(slots[i].bag, slots[i].slot)
+ PickupContainerItem(slots[bestJ].bag, slots[bestJ].slot)
+ if CursorHasItem and CursorHasItem() then
+ PickupContainerItem(slots[i].bag, slots[i].slot) -- drop displaced item into slots[i]'s old slot
+ end
+ remaining = remaining + 1 -- swapped; may need another pass to fully settle the order
+ moved = moved + 1 -- something actually moved this pass
+ else
+ remaining = remaining + 1
+ end
+ end
+ end
+ end
+ end
+ if ClearCursor then ClearCursor() end
+ return remaining, moved, locked
+end
+
+-- Iteration driver: run a pass, let the moved items settle, run again. The positioning phase converges
+-- on STABILITY — it stops once two consecutive passes move NOTHING (or the bag is fully sorted), instead
+-- of a fixed pass count, so a small bag stops early and a huge one keeps going until it settles.
+-- Client-predicted moves lock items for a round-trip, so a big reorder needs several passes; this makes
+-- ONE click converge instead of many. SORT_MAX_ITERS is now only a safety guard against an item the
+-- server perpetually refuses to move (which would otherwise never register as "stable").
+local SORT_STABLE_PASSES = 2 -- stop after this many consecutive no-move passes
+local SORT_MAX_ITERS = 50 -- hard safety cap (guards against a never-settling move)
+local MERGE_MAX_ITERS = 20
+local ROUTE_MAX_ITERS = 20
+
+-- End the sort: drop the cached plan + cover and do the single, final repaint to the sorted layout.
+local function finishSort()
+ CB._sorting = false
+ CB._sortPlan = nil
+ CB._sortPhase = nil
+ if frame and frame.sortCover then frame.sortCover:Hide() end
+ refresh()
+end
+
+-- Two-phase driver: PHASE 1 consolidates partial stacks (mergePass) until stable, THEN plans + PHASE 2
+-- positions items into the planned layout. Merging first means the plan is built on the compacted bag,
+-- so freed slots are packed correctly. Each phase iterates (client-predicted moves lock items for a
+-- round-trip) and yields via C_Timer so the moves settle between passes.
+function CB._sortStep()
+ if not CB._sorting then return end
+ if InCombatLockdown() then finishSort(); return end
+
+ if CB._sortPhase == "merge" then
+ CB._mergeIter = (CB._mergeIter or 0) + 1
+ local didMerge = mergePass()
+ if didMerge and CB._mergeIter < MERGE_MAX_ITERS and C_Timer and C_Timer.After then
+ C_Timer.After(0.25, CB._sortStep) -- more partial stacks to combine
+ return
+ end
+ -- Stacks consolidated → migrate items into their correct bag TYPE next.
+ CB._sortPhase = "route"
+ CB._routeIter = 0
+ if C_Timer and C_Timer.After then C_Timer.After(0.25, CB._sortStep) else finishSort() end
+ return
+ end
+
+ if CB._sortPhase == "route" then
+ CB._routeIter = (CB._routeIter or 0) + 1
+ local didRoute = routePass()
+ local didKeys = routeKeysToKeyRing() -- move loose keys into the keyring
+ if (didRoute or didKeys) and CB._routeIter < ROUTE_MAX_ITERS and C_Timer and C_Timer.After then
+ C_Timer.After(0.25, CB._sortStep) -- more mis-placed items to migrate to their bags
+ return
+ end
+ -- Items now in the right bags → plan the layout on the routed bag and move to positioning.
+ CB._sortPlan = buildSortPlan()
+ CB._sortPhase = "position"
+ CB._sortIter = 0
+ CB._sortStable = 0
+ if C_Timer and C_Timer.After then C_Timer.After(0.25, CB._sortStep) else finishSort() end
+ return
+ end
+
+ -- PHASE 2: positioning. Converge on stability — stop after SORT_STABLE_PASSES consecutive passes that
+ -- move nothing (or once the bag exactly matches the plan). The hard cap is only a runaway guard.
+ CB._sortIter = (CB._sortIter or 0) + 1
+ local remaining, moved, locked = sortPassFromPlan(CB._sortPlan) -- moves items toward the CACHED plan; no repaint
+ -- A pass is "quiet" only if it moved nothing AND isn't still waiting on any in-flight (locked) item —
+ -- so a settle-delay pass on a laggy server doesn't get mistaken for convergence.
+ if (moved or 0) > 0 or (locked or 0) > 0 then
+ CB._sortStable = 0 -- moved, or still settling → reset the streak
+ else
+ CB._sortStable = (CB._sortStable or 0) + 1 -- a genuinely quiet pass → count toward convergence
+ end
+ if remaining == 0 or CB._sortStable >= SORT_STABLE_PASSES or CB._sortIter >= SORT_MAX_ITERS then
+ finishSort()
+ return
+ end
+ if C_Timer and C_Timer.After then
+ C_Timer.After(0.25, CB._sortStep) -- wait for the moved items to unlock, then continue
+ else
+ finishSort() -- no timer available → single pass only
+ end
+end
+
+function CB.SortBags()
+ if InCombatLockdown() or CB._sorting then return end
+ if not (C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerItemInfo
+ and PickupContainerItem) then return end
+ CB._sorting = true
+ CB._sortPhase = "merge" -- consolidate stacks first, then plan + position (see CB._sortStep)
+ CB._mergeIter = 0
+ CB._sortIter = 0
+ -- HIDE the item buttons/slots so the shuffle is invisible, then float the spinner over the empty bag.
+ if buildChrome() then
+ if grid and grid.ForEachButton then grid:ForEachButton(function(b) b:Hide() end) end
+ -- The cover has no opaque fill (we hide slots directly), so hide the keyring row + label too or
+ -- they show through the "Sorting…" cover.
+ if keyGrid and keyGrid.ForEachButton then keyGrid:ForEachButton(function(b) b:Hide() end) end
+ if frame._keysLabel then frame._keysLabel:Hide() end
+ -- Same for every separated specialty-bag section (grid slots + header label).
+ for _, sec in pairs(specSections) do
+ if sec.grid and sec.grid.ForEachButton then sec.grid:ForEachButton(function(b) b:Hide() end) end
+ if sec.label then sec.label:Hide() end
+ end
+ if frame.sortCover then
+ if frame.sortCover.label then frame.sortCover.label:SetText(NE.L["Sorting…"]) end
+ frame.sortCover:Raise(); frame.sortCover:Show()
+ end
+ end
+ CB._sortStep()
+end
+
+-- Sell every grey (Poor quality) item that has a sell value, if a merchant is open. UseContainerItem
+-- sells the item while the MerchantFrame is up (3.3.5a). Skips locked & no-value greys (quest greys).
+--
+-- 3.3.5a gotcha: C_Container.GetContainerItemInfo's `quality` is unreliable — the native container
+-- API returns -1 (or the ClassicAPI shim leaves it unresolved) until the item is queried, so gating
+-- on `info.quality == 0` silently matches nothing. We resolve quality (and the vendor sell price)
+-- from the item LINK via GetItemInfo instead, which is cached for anything sitting in the player's bags.
+function CB.SellJunk()
+ if not (MerchantFrame and MerchantFrame:IsShown()) then return end
+ if not (UseContainerItem and C_Container and C_Container.GetContainerItemInfo
+ and C_Container.GetContainerNumSlots) then return end
+ local sold = 0
+ for bag = 0, NUM_BAG_SLOTS do
+ local n = C_Container.GetContainerNumSlots(bag) or 0
+ for slot = 1, n do
+ local info = C_Container.GetContainerItemInfo(bag, slot)
+ if info and not info.isLocked then
+ local link = info.hyperlink
+ local quality, sellPrice = info.quality, nil
+ if link and GetItemInfo then
+ local _, _, q, _, _, _, _, _, _, _, price = GetItemInfo(link)
+ if (not quality or quality < 0) and q then quality = q end -- trust the link's quality
+ sellPrice = price
+ end
+ -- Grey (Poor) items only, and only if they actually have vendor value (skip quest greys).
+ if quality == 0 and (sellPrice == nil or sellPrice > 0) then
+ UseContainerItem(bag, slot)
+ sold = sold + 1
+ end
+ end
+ end
+ end
+ if sold > 0 and DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage(string.format(NE.L["Sold %d junk item(s)."], sold))
+ end
+end
+
+-- ============================================================================
+-- Auto-empty on bag equip. When the player drops a NEW bag onto an occupied, non-empty bag slot
+-- (which the game normally refuses — "that container is not empty"), we empty the old bag into free
+-- space in the OTHER bags, equip the new bag, stash the old (now empty) bag, then auto-sort.
+-- SAFETY: a pre-check (canEmptyBag) guarantees every item has a valid home BEFORE anything moves, so
+-- items are never destroyed; if they can't all fit, we abort and let the normal refusal stand.
+-- ============================================================================
+local CONTAINER_BAG_OFFSET = 19 -- inventory slot 20..23 ↔ container 1..4
+
+-- Dormant bag-swap trace helper. Inert unless CB._debug is set true (no user command sets it); kept
+-- inline so the swap logic stays annotated. Set CB._debug=true via /script to re-enable if debugging.
+local function dbg(...)
+ if CB._debug and DEFAULT_CHAT_FRAME then
+ local parts = {}
+ for i = 1, select("#", ...) do parts[i] = tostring((select(i, ...))) end
+ DEFAULT_CHAT_FRAME:AddMessage("|cff88ccffNEBags|r " .. table.concat(parts, " "))
+ end
+end
+
+local function containerUsed(container)
+ local n = C_Container.GetContainerNumSlots(container) or 0
+ local used = 0
+ for s = 1, n do
+ local info = C_Container.GetContainerItemInfo(container, s)
+ if info and (info.itemID or info.hyperlink) then used = used + 1 end
+ end
+ return used
+end
+
+-- Empty (nil-item) slots in every bag except `exclude`, each tagged with its bag family.
+local function freeSlotsExcluding(exclude)
+ local list = {}
+ for _, bd in ipairs(orderedBags()) do
+ if bd.bag ~= exclude then
+ local n = C_Container.GetContainerNumSlots(bd.bag) or 0
+ for s = 1, n do
+ if not C_Container.GetContainerItemInfo(bd.bag, s) then
+ list[#list + 1] = { bag = bd.bag, slot = s, family = bd.family }
+ end
+ end
+ end
+ end
+ return list
+end
+
+-- Can every item in `container` be placed into a fitting free slot elsewhere? (No move happens here.)
+local function canEmptyBag(container)
+ local free = freeSlotsExcluding(container)
+ local n = C_Container.GetContainerNumSlots(container) or 0
+ for s = 1, n do
+ local info = C_Container.GetContainerItemInfo(container, s)
+ if info and (info.itemID or info.hyperlink) then
+ local fam = (GetItemFamily and GetItemFamily(info.hyperlink or info.itemID)) or 0
+ local idx
+ for k, fs in ipairs(free) do
+ if not fs.used and itemFitsBag(fam, fs.family) then idx = k; break end
+ end
+ if not idx then return false end
+ free[idx].used = true
+ end
+ end
+ return true
+end
+
+-- One move-pass: relocate `container`'s (unlocked) items into fitting free slots elsewhere. Returns
+-- the count still inside so the driver keeps going until it's empty (locks resolve between passes).
+local function emptyBagPass(container)
+ local free = freeSlotsExcluding(container)
+ local n = C_Container.GetContainerNumSlots(container) or 0
+ if ClearCursor then ClearCursor() end
+ for s = 1, n do
+ local info = C_Container.GetContainerItemInfo(container, s)
+ if info and (info.itemID or info.hyperlink) and not info.isLocked then
+ local fam = (GetItemFamily and GetItemFamily(info.hyperlink or info.itemID)) or 0
+ for _, fs in ipairs(free) do
+ if not fs.used and itemFitsBag(fam, fs.family) then
+ fs.used = true
+ PickupContainerItem(container, s)
+ PickupContainerItem(fs.bag, fs.slot)
+ if CursorHasItem and CursorHasItem() then ClearCursor() end
+ break
+ end
+ end
+ end
+ end
+ if ClearCursor then ClearCursor() end
+ return containerUsed(container)
+end
+
+local function findItemByLink(link)
+ if not link then return nil end
+ for b = 0, NUM_BAG_SLOTS do
+ local n = C_Container.GetContainerNumSlots(b) or 0
+ for s = 1, n do
+ local info = C_Container.GetContainerItemInfo(b, s)
+ if info and info.hyperlink == link then return b, s end
+ end
+ end
+end
+
+local function firstFreeSlot()
+ for b = 0, NUM_BAG_SLOTS do
+ local n = C_Container.GetContainerNumSlots(b) or 0
+ for s = 1, n do
+ if not C_Container.GetContainerItemInfo(b, s) then return b, s end
+ end
+ end
+end
+
+-- End the swap: clear flags; either flow straight into a sort (keeping the cover up) or reveal.
+local function finishBagSwap(doSort)
+ CB._bagSwapping = false
+ CB._bagSwap = nil
+ if doSort then
+ CB._sorting = false -- let SortBags start (it keeps items hidden + cover up, then reveals)
+ CB.SortBags()
+ return
+ end
+ CB._sorting = false
+ if frame and frame.sortCover then
+ if frame.sortCover.label then frame.sortCover.label:SetText(NE.L["Sorting…"]) end
+ frame.sortCover:Hide()
+ end
+ refresh()
+end
+
+function CB._bagSwapStep()
+ local st = CB._bagSwap
+ if not st then finishBagSwap(false); return end
+ if InCombatLockdown() then finishBagSwap(false); return end
+ st.iter = (st.iter or 0) + 1
+ if st.iter > 60 then finishBagSwap(true); return end -- safety cap → at least tidy up with a sort
+
+ if st.phase == "empty" then
+ local left = emptyBagPass(st.container)
+ dbg(" empty pass", st.iter, "→ items left:", left)
+ if left == 0 then st.phase = "equip" end
+ if C_Timer and C_Timer.After then C_Timer.After(0.2, CB._bagSwapStep) else finishBagSwap(true) end
+ return
+ end
+
+ if st.phase == "equip" then
+ local bag, slot = findItemByLink(st.newBagLink)
+ dbg(" equip phase: found new bag at", bag, slot, "invSlot", st.invSlot)
+ if bag and PutItemInBag then
+ PickupContainerItem(bag, slot) -- pick up the new (empty) bag
+ PutItemInBag(st.invSlot) -- equip it; the old (now empty) bag lands on the cursor
+ if CursorHasItem and CursorHasItem() then
+ local fb, fs = firstFreeSlot() -- park the old empty bag in any free slot
+ if fb then PickupContainerItem(fb, fs) end
+ if CursorHasItem and CursorHasItem() then ClearCursor() end
+ end
+ end
+ st.phase = "done"
+ if C_Timer and C_Timer.After then C_Timer.After(0.25, CB._bagSwapStep) else finishBagSwap(true) end
+ return
+ end
+
+ finishBagSwap(true) -- "done": reveal + auto-sort
+end
+
+-- Called AFTER PutItemInBag with the bag link captured BEFORE the call (the cursor is cleared by a
+-- failed equip on this client, so we can't read it post-hoc). If the equip failed — i.e. the old bag
+-- in that slot is still non-empty — run the managed swap; the new bag is back in inventory (by link).
+function CB.HandleBagEquip(invSlot, link)
+ local container = (invSlot or 0) - CONTAINER_BAG_OFFSET
+ dbg("HandleBagEquip invSlot=", invSlot, "container=", container, "link=", link)
+ if not CB._autoEmptyBag then dbg(" skip: autoEmptyBag off"); return end
+ if InCombatLockdown() or CB._bagSwapping or CB._sorting then dbg(" skip: busy/combat"); return end
+ if not (C_Container and C_Container.GetContainerNumSlots and PickupContainerItem and PutItemInBag and link) then
+ dbg(" skip: missing API/link"); return end
+ if container < 1 or container > NUM_BAG_SLOTS then dbg(" skip: bad container"); return end
+ local _, _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
+ dbg(" equipLoc:", equipLoc)
+ if equipLoc ~= "INVTYPE_BAG" then dbg(" skip: not a bag"); return end
+ local used = containerUsed(container)
+ dbg(" old bag used slots:", used)
+ if used == 0 then dbg(" skip: equip succeeded / old bag empty"); return end -- nothing to move
+ if not findItemByLink(link) then dbg(" skip: new bag not found back in inventory"); return end
+ if not canEmptyBag(container) then
+ dbg(" ABORT: not enough free space")
+ if UIErrorsFrame then
+ UIErrorsFrame:AddMessage(NE.L["Not enough free space to swap that bag."], 1, 0.2, 0.2)
+ end
+ return
+ end
+ -- Begin the managed swap (cursor is already empty; the new bag sits back in inventory).
+ dbg(" >>> STARTING SWAP: emptying container", container)
+ CB._bagSwapping = true
+ CB._sorting = true
+ CB._bagSwap = { container = container, invSlot = invSlot, newBagLink = link, phase = "empty", iter = 0 }
+ if buildChrome() then
+ if grid and grid.ForEachButton then grid:ForEachButton(function(b) b:Hide() end) end
+ if frame.sortCover then
+ if frame.sortCover.label then frame.sortCover.label:SetText(NE.L["Swapping bag…"]) end
+ frame.sortCover:Raise(); frame.sortCover:Show()
+ end
+ end
+ CB._bagSwapStep()
+end
+
+-- Catch a failed "equip over a full bag". We WRAP the global PutItemInBag so we can read the cursor
+-- bag BEFORE the equip — on this client a failed equip clears the cursor, so hooksecurefunc (post-only)
+-- sees nothing. We capture the bag link, call the original, then hand off to HandleBagEquip which
+-- decides (from the target bag's contents) whether the equip failed and a managed swap is needed.
+local function installBagEquipHooks()
+ if CB._bagHooksInstalled then return end
+ CB._bagHooksInstalled = true
+ if type(_G.PutItemInBag) ~= "function" then dbg("PutItemInBag missing; bag-swap disabled"); return end
+ local orig = _G.PutItemInBag
+ _G.PutItemInBag = function(invSlot)
+ local link
+ if not (CB._bagSwapping or CB._sorting) and not InCombatLockdown()
+ and CursorHasItem and CursorHasItem() then
+ local ctype, _, clink = GetCursorInfo()
+ if ctype == "item" and clink then link = clink end
+ end
+ orig(invSlot) -- perform the real equip (may succeed or fail)
+ if link then CB.HandleBagEquip(invSlot, link) end
+ end
+ dbg("installed PutItemInBag wrapper")
+end
+CB.InstallBagEquipHooks = installBagEquipHooks
+
+-- Is our bag-options dropdown currently open? (DropDownList1 shown AND owned by our menu frame.)
+function CB.MenuIsOpen()
+ return CB._menuFrame and DropDownList1 and DropDownList1:IsShown()
+ and UIDROPDOWNMENU_OPEN_MENU == CB._menuFrame and true or false
+end
+
+-- Left-click the cog: toggle the menu — open it, or close it if it's already showing.
+function CB.ToggleMenu(anchor)
+ if CB.MenuIsOpen() then
+ if CloseDropDownMenus then CloseDropDownMenus() end
+ else
+ CB.OpenMenu(anchor)
+ end
+end
+
+-- The item level entry. It's DragonUI's setting, not ours (see BagSkin's BS.SetItemLevelShown) —
+-- this is a second CONTROL over one value, not a second copy of it, so it can't drift from the
+-- checkbox in Options -> Enhancements -> Item Level. Surfaced here because hunting through another
+-- addon's options tab to change how the bag looks is a poor trade for one line of menu.
+--
+-- When DragonUI's master "Enable Item Level" is off the per-context flag is inert, so the entry
+-- greys out and its tooltip says where the master lives — better than a switch that does nothing.
+local function itemLevelMenuItem()
+ local BS = NE.bagskin
+ local label = NE.L["Show item level on items"]
+ -- Greyed but still a checkbox, so the row doesn't reflow against its neighbours when it's inert.
+ if not (BS and BS.CanToggleItemLevel and BS.CanToggleItemLevel()) then
+ return {
+ text = label, checked = false, disabled = true,
+ tooltipOnButton = true, tooltipTitle = label,
+ tooltipText = NE.L["Turn on Item Level in DragonUI's options (Enhancements > Item Level) first."],
+ }
+ end
+ return {
+ text = label,
+ checked = BS.IsItemLevelShown(),
+ func = function() BS.SetItemLevelShown(not BS.IsItemLevelShown()) end,
+ -- It's the master switch, so say so: this is the same checkbox as "Enable Item Level" in
+ -- DragonUI's options and it governs every frame, not just the bag.
+ tooltipOnButton = true, tooltipTitle = label,
+ tooltipText = NE.L["The same setting as Enable Item Level in DragonUI's options (Enhancements > Item Level). Covers the character panel and every other frame too."],
+ }
+end
+
+-- Bag-options dropdown (native EasyMenu / UIDropDownMenu — present on 3.3.5a).
+function CB.OpenMenu(anchor)
+ if not EasyMenu then return end
+ if not CB._menuFrame then
+ CB._menuFrame = CreateFrame("Frame", "NE_CombinedBagMenu", UIParent, "UIDropDownMenuTemplate")
+ end
+ local function modeItem(mode, label)
+ return { text = label, checked = (CB._sortMode == mode),
+ func = function() CB._sortMode = mode; CB.SaveSortPrefs(); CB.SortBags() end }
+ end
+ local menu = {
+ { text = NE.L["Bag Options"], isTitle = true, notCheckable = true },
+ { text = NE.L["Sort Bags"], notCheckable = true, func = function() CB.SortBags() end },
+ { text = NE.L["Sort by"], isTitle = true, notCheckable = true },
+ modeItem("category", NE.L["Category (smart)"]),
+ modeItem("quality", NE.L["Quality"]),
+ modeItem("ilvl", NE.L["Item Level"]),
+ modeItem("name", NE.L["Name"]),
+ { text = NE.L["Reverse sort order"], checked = CB._reverseSort and true or false,
+ func = function() CB._reverseSort = not CB._reverseSort; CB.SaveSortPrefs() end },
+ { text = NE.L["Red-tint unusable items"], checked = CB._redUnusable and true or false,
+ func = function() CB._redUnusable = not CB._redUnusable; CB.SaveSortPrefs(); CB.Refresh() end },
+ itemLevelMenuItem(),
+ { text = NE.L["Show keyring row"], checked = CB._showKeys and true or false,
+ func = function() CB._showKeys = not CB._showKeys; CB.SaveSortPrefs(); CB.Refresh() end },
+ { text = NE.L["Separate specialty bags"], checked = CB._separateBags and true or false,
+ func = function() CB._separateBags = not CB._separateBags; CB.SaveSortPrefs(); CB.Refresh() end },
+ { text = NE.L["Auto-empty old bag when swapping"], checked = CB._autoEmptyBag and true or false,
+ func = function() CB._autoEmptyBag = not CB._autoEmptyBag; CB.SaveSortPrefs() end },
+ { text = NE.L["Merchant"], isTitle = true, notCheckable = true },
+ { text = NE.L["Auto-sell junk at merchants"], checked = CB._autoSellJunk and true or false,
+ func = function() CB._autoSellJunk = not CB._autoSellJunk; CB.SaveSortPrefs()
+ if CB._autoSellJunk then CB.SellJunk() end end }, -- sell now too if a merchant is already open
+ }
+ EasyMenu(menu, CB._menuFrame, anchor or "cursor", 0, 0, "MENU")
+end
+
+-- Persist / restore the sort scheme in our SavedVariables.
+function CB.SaveSortPrefs()
+ if not NE.db then return end
+ NE.db.combinedbag = NE.db.combinedbag or {}
+ NE.db.combinedbag.sortMode = CB._sortMode
+ NE.db.combinedbag.reverse = CB._reverseSort and true or false
+ NE.db.combinedbag.redUnusable = CB._redUnusable and true or false
+ NE.db.combinedbag.autoSellJunk = CB._autoSellJunk and true or false
+ NE.db.combinedbag.autoEmptyBag = CB._autoEmptyBag and true or false
+ NE.db.combinedbag.showKeys = CB._showKeys and true or false
+ NE.db.combinedbag.separateBags = CB._separateBags and true or false
+end
+function CB.LoadSortPrefs()
+ local c = NE.db and NE.db.combinedbag
+ if not c then return end
+ if c.sortMode then CB._sortMode = c.sortMode end
+ CB._reverseSort = c.reverse and true or false
+ if c.redUnusable ~= nil then CB._redUnusable = c.redUnusable and true or false end
+ if c.autoSellJunk ~= nil then CB._autoSellJunk = c.autoSellJunk and true or false end
+ if c.autoEmptyBag ~= nil then CB._autoEmptyBag = c.autoEmptyBag and true or false end
+ if c.showKeys ~= nil then CB._showKeys = c.showKeys and true or false end
+ if c.separateBags ~= nil then CB._separateBags = c.separateBags and true or false end
+end
+
+-- ----------------------------------------------------------------------------
+-- Take over the bag-open globals + suppress stock frames. Saved originals live on CB._old so a
+-- future teardown could restore them (we don't, since disable is reload-gated).
+-- ----------------------------------------------------------------------------
+local intercepted = false
+CB._old = CB._old or {}
+
+local function installIntercept()
+ if intercepted then return end
+ intercepted = true
+
+ local function take(name, fn)
+ if type(_G[name]) ~= "function" then return end
+ CB._old[name] = _G[name]
+ _G[name] = fn
+ end
+
+ take("ToggleBackpack", function() CB.Toggle() end)
+ take("ToggleAllBags", function() CB.Toggle() end)
+ take("OpenAllBags", function() CB.Show() end)
+ take("CloseAllBags", function() CB.Hide() end)
+ take("OpenBackpack", function() CB.Show() end)
+ take("CloseBackpack", function() CB.Hide() end)
+
+ -- Safety net for paths that open a ContainerFrame directly (loot, merchant, right-click a bag):
+ -- hide it and show the combined window instead.
+ for i = 1, (NUM_CONTAINER_FRAMES or 13) do
+ local f = _G["ContainerFrame" .. i]
+ if f and f.HookScript then
+ f:HookScript("OnShow", function(self)
+ if InCombatLockdown() then return end
+ -- Keyring (container -2): only take it over when the keys row is enabled; otherwise let the
+ -- stock keyring frame open so keys stay reachable when the row is toggled off.
+ if self.GetID and self:GetID() == KEYRING_CONTAINER and not CB.ShowKeys() then return end
+ self:Hide()
+ CB.Show()
+ end)
+ end
+ end
+end
+
+-- ----------------------------------------------------------------------------
+-- Live refresh watcher.
+-- ----------------------------------------------------------------------------
+local function installWatcher()
+ if CB._watcher then return end
+ local w = CreateFrame("Frame")
+ CB._watcher = w
+ for _, ev in ipairs({
+ "BAG_UPDATE", "BAG_UPDATE_DELAYED", "ITEM_LOCK_CHANGED", "BAG_UPDATE_COOLDOWN",
+ "PLAYER_MONEY", "CURRENCY_DISPLAY_UPDATE", "PLAYER_ENTERING_WORLD", "MERCHANT_SHOW", "MERCHANT_CLOSED",
+ "QUEST_ACCEPTED", "UNIT_QUEST_LOG_CHANGED", "PLAYER_REGEN_ENABLED",
+ "AUCTION_HOUSE_SHOW", "AUCTION_HOUSE_CLOSED",
+ }) do
+ pcall(function() w:RegisterEvent(ev) end)
+ end
+ w:SetScript("OnEvent", function(_, event)
+ if event == "PLAYER_REGEN_ENABLED" then
+ -- Combat ended: flush any bag re-parents that were queued during lockdown, then refresh.
+ if grid and grid.FlushPendingParents then grid:FlushPendingParents() end
+ elseif event == "AUCTION_HOUSE_SHOW" then
+ -- Stock merchant/bank auto-open bags via OpenAllBags() (which we intercept), but the 3.3.5 AH
+ -- does not -- so the combined bag never opened with the AH (issue #12). Mirror the merchant
+ -- behaviour here. Owner-gated: only auto-open when the bag wasn't already up, and remember we
+ -- did, so we only auto-close it on AUCTION_HOUSE_CLOSED if we were the ones who opened it.
+ if not InCombatLockdown() and not CB.IsShown() then
+ CB._ahAutoOpened = true
+ CB.Show()
+ end
+ elseif event == "AUCTION_HOUSE_CLOSED" then
+ if CB._ahAutoOpened then
+ CB._ahAutoOpened = nil
+ CB.Hide()
+ end
+ elseif event == "MERCHANT_SHOW" and CB._autoSellJunk and not InCombatLockdown() then
+ -- Defer one tick so the MerchantFrame is fully open + item quality is resolved before selling.
+ if C_Timer and C_Timer.After then
+ C_Timer.After(0.15, function() if not InCombatLockdown() then CB.SellJunk() end end)
+ else
+ CB.SellJunk() -- auto-sell greys on arrival at a vendor (opt-in)
+ end
+ end
+ refresh()
+ end)
+end
+
+-- ----------------------------------------------------------------------------
+-- Boot.
+-- ----------------------------------------------------------------------------
+local function boot()
+ CB.LoadSortPrefs()
+ installIntercept()
+ installWatcher()
+ installBagEquipHooks()
+end
+CB.Boot = boot
+
+if NE.modules and NE.modules.Register then
+ NE.modules.Register{
+ name = MODULE,
+ default = false, -- Default OFF: players opt in via the options toggle (integration/Options.lua).
+ -- When enabled it takes over bag opening and supersedes the per-window restyle.
+ label = NE.L["Combined bag (all-in-one)"],
+ category = "Windows",
+ desc = NE.L["One movable window showing every bag slot in a Dragonflight-style grid. Takes over "
+ .. "bag opening and replaces the per-window 'Retail bags' restyle. Reload (/reload) to apply."],
+ events = { "PLAYER_LOGIN" },
+ onBoot = function() boot() end,
+ conflictsWith = NE.modules.RIVALS and NE.modules.RIVALS.BAGS or nil,
+ }
+else
+ log("NE.modules.Register absent; combined bag not booted")
+end
+
+if NE.qa then
+ NE.qa.modules = NE.qa.modules or {}
+ table.insert(NE.qa.modules, {
+ name = "Combined bag",
+ frame = nil, -- built lazily; harness resolves NE_CombinedBagFrame on open
+ open = function() CB.Show() end,
+ close = function() CB.Hide() end,
+ })
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Assets.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Assets.lua
new file mode 100644
index 0000000..0709512
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Assets.lua
@@ -0,0 +1,150 @@
+-- DragonUI_NewEra/modules/character/Assets.lua — CharacterPanel art registration (Sprint 1, Agent F).
+--
+-- DOWNPORT: mirrors NewEra/CharacterPanel/Assets.lua, but
+-- (1) paths point at OUR addon (Textures\CharacterPanel\...), and
+-- (2) the atlas-name -> texcoord rects that NewEra read from its generated global NE_ATLAS are
+-- TRANSCRIBED here into NE.tex.atlases via NE.tex.RegisterAtlases (architect decision:
+-- coords live in NE.tex.atlases, NOT C_Texture.RegisterAtlas — the 3.3.5a client has no
+-- native atlas DB; see core/Texture.lua + Textures/Assets.lua ARCHITECT DECISION block).
+--
+-- This file ships ONLY the CharacterPanel-SPECIFIC sheets + the atlas families that Core does NOT
+-- already register. Shared chrome (UI-Frame-Metal nineslice, UI-Background-Rock, InsetFrame inner-
+-- border, tab sheet 4707839, RedButton close 4698972, minimal-scrollbar family) lives in Core's
+-- Textures/Assets.lua and is consumed from there — do NOT re-register it here (disabling this
+-- module must not drop shared art from Social/Battlefield/etc.).
+--
+-- Source coord rects transcribed from NewEra/Generated/AtlasData.lua (build 12.0.5.67451):
+-- ui-character-info--bg -> AtlasData.lua:13756-13770
+-- classicon- -> AtlasData.lua:2402-2414
+-- options_listexpand_* -> AtlasData.lua:519,8672-8674
+-- common-icon-{zoomin,...,undo} -> AtlasData.lua:2762-2779
+-- common-button-square-gray-* -> AtlasData.lua:2570-2571
+
+local NE = DragonUI_NewEra
+if not (NE and NE.tex and NE.tex.RegisterLocal) then return end
+
+local P = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\CharacterPanel\\"
+
+-- ============================================================================
+-- 1. fdid -> shipped BLP path (NE.tex.RegisterLocal)
+-- ============================================================================
+
+-- Class-themed PaperDoll backgrounds (UI-Character-Info--bg + Title/Line/ItemLevel bounce).
+NE.tex.RegisterLocal(1400895, P .. "1400895-character-info-classes-a.blp") -- 1024x1024 DXT5; Mage/Monk/Paladin/Priest/Rogue/Shaman/Warlock/Warrior
+NE.tex.RegisterLocal(1400896, P .. "1400896-character-info-classes-b.blp") -- 1024x512 DXT5; DeathKnight/DemonHunter/Druid/Hunter
+NE.tex.RegisterLocal(5882640, P .. "5882640-character-panel-background.blp") -- 1024x512 DXT5; retail overall pane bg
+
+-- Class-icon sheet (12 classes, 128x128 each). Portrait.lua swaps the portrait to the class icon
+-- on the PaperDoll tab (architect decision: CLASS icon, not spec icon — no spec system on 3.3.5a).
+-- DOWNPORT(user): class icons are CIRCULAR. The retail 1662186 sheet is SQUARE (NewEra masks it with
+-- CreateMaskTexture, which returns nil on 3.3.5a). So we baked a circular alpha mask into each class
+-- cell offline (DXT5→DDS decode → per-cell circle mask → TGA). Same layout, so the texcoords below
+-- are unchanged. Half-res (1024x512) RLE TGA; normalized texcoords are resolution-independent.
+NE.tex.RegisterLocal(1662186, P .. "1662186-classicon.blp") -- circular class icons (baked)
+
+-- Char-Paperdoll-{Parts,Horizontal,Vertical} — inner-border edges/corners + slot frames.
+NE.tex.RegisterLocal(410247, P .. "410247-charpaperdoll.blp") -- 32x16 DXT5; horizontal h-tile edges
+NE.tex.RegisterLocal(410248, P .. "410248-charpaperdoll.blp") -- 256x128 DXT5; parts (corners, slot frames, gap-fillers)
+NE.tex.RegisterLocal(410249, P .. "410249-charpaperdoll.blp") -- 16x32 DXT5; vertical v-tile edges
+
+-- Model controls (modern square buttons + icon overlays). NOTE: both these sheets are uncompressed
+-- ARGB8888 (BLP2 encoding 3) — large but 3.3.5a-loadable; see ASSETS.md validation notes.
+NE.tex.RegisterLocal(3534438, P .. "3534438-common-buttons-icons.blp") -- 1024x1024 DXT5; common-button-square-gray-{up,down}
+NE.tex.RegisterLocal(3487944, P .. "3487944-common-buttons-icons.blp") -- 2048x1024 ARGB8888; common-icon-* family
+
+-- Options_ListExpand — collapsible faction/skill header art (uncompressed ARGB8888).
+NE.tex.RegisterLocal(4571485, P .. "4571485-options-listexpand.blp") -- 128x128 ARGB8888
+
+-- UI-Character-ReputationBar end caps (ReputationBarTemplate).
+NE.tex.RegisterLocal(136567, P .. "136567-ui-character-reputationbar.blp") -- 256x64 DXT5
+-- NOTE: source ships NO 136570 sheet (contract listed it as "if present" — it is not). Skipped.
+
+-- Campaign header-icon sheet (Skill collapse/expand campaign_headericon_* atlases; ARGB8888).
+NE.tex.RegisterLocal(4499236, P .. "4499236-campaign-headericon.blp") -- 2048x1024 ARGB8888
+
+-- Race DressUpBackground quarters — stitched 4-per-race behind the 3D model.
+-- Path mirrors retail Interface\DressUpFrame\DressUpBackground-<1-4>.blp keyed by UnitRace.
+-- All 10 WOTLK races shipped (incl. Gnome/Troll, which Era's CASC lacks → maps Gnome->Dwarf, Troll->Orc).
+for _, fr in ipairs({
+ -- Alliance
+ {131089, "Dwarf1"}, {131090, "Dwarf2"}, {131091, "Dwarf3"}, {131092, "Dwarf4"},
+ {131093, "Human1"}, {131094, "Human2"}, {131095, "Human3"}, {131096, "Human4"},
+ {131097, "NightElf1"}, {131098, "NightElf2"}, {131099, "NightElf3"}, {131100, "NightElf4"},
+ {455998, "Gnome1"}, {455999, "Gnome2"}, {456000, "Gnome3"}, {456001, "Gnome4"},
+ {131085, "Draenei1"}, {131086, "Draenei2"}, {131087, "Draenei3"}, {131088, "Draenei4"},
+ -- Horde
+ {131101, "Orc1"}, {131102, "Orc2"}, {131103, "Orc3"}, {131104, "Orc4"},
+ {131105, "Scourge1"}, {131106, "Scourge2"}, {131107, "Scourge3"}, {131108, "Scourge4"},
+ {131109, "Tauren1"}, {131110, "Tauren2"}, {131111, "Tauren3"}, {131112, "Tauren4"},
+ {456006, "Troll1"}, {456007, "Troll2"}, {456008, "Troll3"}, {456009, "Troll4"},
+ {131081, "BloodElf1"}, {131082, "BloodElf2"}, {131083, "BloodElf3"}, {131084, "BloodElf4"},
+}) do
+ NE.tex.RegisterLocal(fr[1], P .. "RaceBackground\\" .. fr[1] .. "-DressUpBackground-" .. fr[2] .. ".blp")
+end
+
+-- ============================================================================
+-- 2. atlas-name -> texcoord rect (NE.tex.RegisterAtlases)
+-- CharacterPanel-specific atlases not already registered by Core.
+-- Rects transcribed verbatim from NewEra/Generated/AtlasData.lua (see header).
+-- ============================================================================
+
+-- Class-themed PaperDoll backgrounds (197x355 each).
+NE.tex.RegisterAtlases({
+ ["ui-character-info-deathknight-bg"] = { file=1400896, left=0.000977, right=0.193359, top=0.001953, bottom=0.695312, width=197, height=355 },
+ ["ui-character-info-demonhunter-bg"] = { file=1400896, left=0.195312, right=0.387695, top=0.001953, bottom=0.695312, width=197, height=355 },
+ ["ui-character-info-druid-bg"] = { file=1400896, left=0.389648, right=0.582031, top=0.001953, bottom=0.695312, width=197, height=355 },
+ ["ui-character-info-hunter-bg"] = { file=1400896, left=0.583984, right=0.776367, top=0.001953, bottom=0.695312, width=197, height=355 },
+ ["ui-character-info-mage-bg"] = { file=1400895, left=0.000977, right=0.193359, top=0.000977, bottom=0.347656, width=197, height=355 },
+ ["ui-character-info-monk-bg"] = { file=1400895, left=0.000977, right=0.193359, top=0.349609, bottom=0.696289, width=197, height=355 },
+ ["ui-character-info-paladin-bg"] = { file=1400895, left=0.195312, right=0.387695, top=0.000977, bottom=0.347656, width=197, height=355 },
+ ["ui-character-info-priest-bg"] = { file=1400895, left=0.195312, right=0.387695, top=0.349609, bottom=0.696289, width=197, height=355 },
+ ["ui-character-info-rogue-bg"] = { file=1400895, left=0.389648, right=0.582031, top=0.000977, bottom=0.347656, width=197, height=355 },
+ ["ui-character-info-shaman-bg"] = { file=1400895, left=0.389648, right=0.582031, top=0.349609, bottom=0.696289, width=197, height=355 },
+ ["ui-character-info-warlock-bg"] = { file=1400895, left=0.583984, right=0.776367, top=0.000977, bottom=0.347656, width=197, height=355 },
+ ["ui-character-info-warrior-bg"] = { file=1400895, left=0.778320, right=0.970703, top=0.000977, bottom=0.347656, width=197, height=355 },
+})
+
+-- Stat-sidebar HEADER + ROW backgrounds (sheet 1400895 — already shipped above). DOWNPORT: these
+-- coords weren't registered, so the Sidebar's SetAtlas missed and fell back to a brown tint on the
+-- section headers. Registering them shows the real Blizzard header/row art. (Coords: AtlasData 13760-13768.)
+NE.tex.RegisterAtlases({
+ ["ui-character-info-title"] = { file=1400895, left=0.000977, right=0.192383, top=0.698242, bottom=0.737305, width=196, height=40 },
+ ["ui-character-info-line-bounce"] = { file=1400895, left=0.000977, right=0.154297, top=0.769531, bottom=0.788086, width=157, height=19 },
+ ["ui-character-info-itemlevel-bounce"] = { file=1400895, left=0.000977, right=0.159180, top=0.739258, bottom=0.767578, width=162, height=29 },
+})
+
+-- Class icons (128x128 each). evoker has no 3.3.5a class, kept for parity (never selected).
+NE.tex.RegisterAtlases({
+ ["classicon-deathknight"] = { file=1662186, left=0.000488, right=0.062988, top=0.000977, bottom=0.125977, width=128, height=128 },
+ ["classicon-demonhunter"] = { file=1662186, left=0.000488, right=0.062988, top=0.127930, bottom=0.252930, width=128, height=128 },
+ ["classicon-druid"] = { file=1662186, left=0.000488, right=0.062988, top=0.254883, bottom=0.379883, width=128, height=128 },
+ ["classicon-evoker"] = { file=1662186, left=0.000488, right=0.062988, top=0.381836, bottom=0.506836, width=128, height=128 },
+ ["classicon-hunter"] = { file=1662186, left=0.000488, right=0.062988, top=0.508789, bottom=0.633789, width=128, height=128 },
+ ["classicon-mage"] = { file=1662186, left=0.000488, right=0.062988, top=0.635742, bottom=0.760742, width=128, height=128 },
+ ["classicon-monk"] = { file=1662186, left=0.000488, right=0.062988, top=0.762695, bottom=0.887695, width=128, height=128 },
+ ["classicon-paladin"] = { file=1662186, left=0.063965, right=0.126465, top=0.000977, bottom=0.125977, width=128, height=128 },
+ ["classicon-priest"] = { file=1662186, left=0.063965, right=0.126465, top=0.127930, bottom=0.252930, width=128, height=128 },
+ ["classicon-rogue"] = { file=1662186, left=0.063965, right=0.126465, top=0.254883, bottom=0.379883, width=128, height=128 },
+ ["classicon-shaman"] = { file=1662186, left=0.063965, right=0.126465, top=0.381836, bottom=0.506836, width=128, height=128 },
+ ["classicon-warlock"] = { file=1662186, left=0.063965, right=0.126465, top=0.508789, bottom=0.633789, width=128, height=128 },
+ ["classicon-warrior"] = { file=1662186, left=0.063965, right=0.126465, top=0.635742, bottom=0.760742, width=128, height=128 },
+})
+
+-- Collapsible header art (ReputationHeaderTemplate / Skills collapse).
+NE.tex.RegisterAtlases({
+ ["_options_listexpand_middle"] = { file=4571485, left=0.000000, right=0.007812, top=0.218750, bottom=0.421875, width=1, height=26 },
+ ["options_listexpand_left"] = { file=4571485, left=0.007812, right=0.101562, top=0.656250, bottom=0.859375, width=12, height=26 },
+ ["options_listexpand_right"] = { file=4571485, left=0.007812, right=0.226562, top=0.437500, bottom=0.640625, width=28, height=26 },
+ ["options_listexpand_right_expanded"]= { file=4571485, left=0.242188, right=0.460938, top=0.437500, bottom=0.640625, width=28, height=26 },
+})
+
+-- Model-control icon overlays (zoom/rotate/undo) + square button face.
+NE.tex.RegisterAtlases({
+ ["common-icon-rotateleft"] = { file=3487944, left=0.126465, right=0.175293, top=0.756836, bottom=0.854492, width=20, height=20 },
+ ["common-icon-rotateright"] = { file=3487944, left=0.126465, right=0.175293, top=0.856445, bottom=0.954102, width=20, height=20 },
+ ["common-icon-undo"] = { file=3487944, left=0.378418, right=0.503418, top=0.252930, bottom=0.502930, width=25, height=25 },
+ ["common-icon-zoomin"] = { file=3487944, left=0.504395, right=0.629395, top=0.000977, bottom=0.250977, width=25, height=25 },
+ ["common-icon-zoomout"] = { file=3487944, left=0.756348, right=0.881348, top=0.000977, bottom=0.250977, width=25, height=25 },
+ ["common-button-square-gray-down"]= { file=3534438, left=0.000977, right=0.250977, top=0.284180, bottom=0.534180, width=42, height=42 },
+ ["common-button-square-gray-up"] = { file=3534438, left=0.000977, right=0.250977, top=0.536133, bottom=0.786133, width=42, height=42 },
+})
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CONTRACT_S1.md b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CONTRACT_S1.md
new file mode 100644
index 0000000..941e5c1
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CONTRACT_S1.md
@@ -0,0 +1,151 @@
+# Sprint 1 — Character Panel: Build Contract
+
+Downport `NewEra/CharacterPanel/*` (Classic 1.15, ~7,600 LOC) → 3.3.5a, reskinning the real
+`CharacterFrame` in place. Sits on the Sprint-0 Core toolkit + compat shims. Read with the Sprint-0
+`/root/downport/DragonUI_NewEra/CONTRACTS.md` (§0 conventions still apply).
+
+Source: `/root/downport/DOWNPORT THIS/NewEra/CharacterPanel/`. Target: `modules/character/`.
+
+---
+
+## A0. ARCHITECTURE PIVOT (2026-06-20, user-directed — SUPERSEDES the reskin approach below)
+
+The reskin-in-place approach fought Blizzard's chrome/title/tabs/layout and looked broken. **We now
+build a CUSTOM DragonUI-owned frame and HIDE Blizzard's `CharacterFrame`, REPARENTING Blizzard's
+functional widgets (equipment slots + 3D model) into our frame for behavior.** Decisions:
+
+- **Custom frame.** `NE.charpanel.frame = CreateFrame("Frame","DragonUI_NewEra_Character",UIParent)`,
+ added to `UISpecialFrames` (ESC closes), sized to the DF character layout. All chrome/title/
+ portrait/close/tabs/insets/sidebar are OURS (built via the Core toolkit), never Blizzard's.
+- **Hide + intercept Blizzard.** Keep `CharacterFrame` hidden. Intercept the open path so the `C`
+ key / micro button opens OUR frame: `hooksecurefunc("ToggleCharacter", fn)` (hide CharacterFrame,
+ toggle ours to the requested tab) AND `CharacterFrame:HookScript("OnShow", hide-self+show-ours)`
+ to catch other open paths. Never taint: do show/hide only; reparent ONLY out of combat (at
+ PLAYER_LOGIN), guard with `InCombatLockdown()` + `CombatQueue`/`AfterCombat`.
+- **Reparent functional widgets (ONCE, out of combat).** `CharacterModelFrame` and the 19 slot
+ buttons (`CharacterHeadSlot`,`CharacterNeckSlot`,`CharacterShoulderSlot`,`CharacterShirtSlot`,
+ `CharacterChestSlot`,`CharacterWaistSlot`,`CharacterLegsSlot`,`CharacterFeetSlot`,
+ `CharacterWristSlot`,`CharacterHandsSlot`,`CharacterFinger0Slot`,`CharacterFinger1Slot`,
+ `CharacterTrinket0Slot`,`CharacterTrinket1Slot`,`CharacterBackSlot`,`CharacterMainHandSlot`,
+ `CharacterSecondaryHandSlot`,`CharacterRangedSlot`,`CharacterTabardSlot`) → `SetParent(Inset)` +
+ reposition to DF layout (two vertical columns + bottom weapon row, retail style). Their texture/
+ count/quality/click-to-equip logic is driven by Blizzard by GLOBAL NAME, so reparenting preserves
+ behavior. Do NOT reimplement them.
+- **What we build custom:** chrome (`NE.chrome.Apply` on our frame), class-icon portrait, DF close,
+ DF metal tabs (our own buttons that switch content panes), the stat sidebar, and the secondary-tab
+ panes (Reputation/Skills/Honor/Pet — read Blizzard data APIs, render our own rows). Stats are read
+ via the unchanged global APIs (`UnitStat`, `GetResistance`, etc.) and rendered in our sidebar.
+- **Much NewEra source still applies** — slot-frame decorations, inner border, model-area bg, model
+ controls, sidebar stat rendering, class backgrounds, collapsible tab rows — just host them in OUR
+ frame/Inset instead of Blizzard's. Reuse the visual logic; drop the chrome-suppression logic.
+
+The §A items below are updated to fit this: "reskin CharacterFrame" → "build/populate our custom
+frame". The integration (module toggle + options + QA) and all 3.3.5 gotchas (§B) are unchanged,
+except QA `open` shows OUR frame: `open=function() NE.charpanel.Toggle(true) end`.
+
+**REFERENCE — proven 3.3.5 mechanics: `/root/downport/DragonflightUICharacter/`** (the user's earlier
+WORKING custom-frame character panel, Interface 30300). MINE it for the hard 3.3.5 plumbing, which is
+already solved there: `CharacterFrame.lua` (hide stock CharacterFrame + `HookScript("OnShow",Hide)` +
+replace global `ToggleCharacter` with own `Toggle`, save old; `UISpecialFrames`; custom frame build;
+slot list + positioning), `StatsPanel.lua` (928 lines of working 3.3.5 stat reading/rendering — the
+real `UnitStat`/resistance/melee/ranged/spell getters), `EquipmentManager.lua` (ItemRack-model equip,
+3.3.5-safe). **Use DragonflightUICharacter for HOW (3.3.5 mechanics); use NewEra for WHAT (the DF look,
+class art, decorations, richer sidebar, secondary tabs).** It is a reference, NOT the base — the
+target look/scope is NewEra, built on our Core toolkit.
+
+## A. Architect decisions (locked — do not deviate without flagging)
+
+1. **Integration model.** The panel is our custom `NE.charpanel.frame` (Blizzard `CharacterFrame`
+ hidden). Wire it as:
+ - `NE.modules.Register{ name="character", default=true, onBoot=… }` (Core/Modules) — drives the reskin.
+ - Options + QA via a dedicated helper `NE.RegisterReskin(spec)` (Agent A adds it to `integration/`):
+ registers a DragonUI `ModuleRegistry` toggle + an options-tab entry + a QA entry whose
+ `open=function() ToggleCharacter("PaperDollFrame") end`, `close=function() HideUIPanel(CharacterFrame) end`.
+ **No mover** (CharacterFrame is a UIPanel).
+2. **WowScrollBox → named FauxScrollFrame.** 3.3.5a has NO `WowScrollBox`/`ScrollUtil`/`MinimalScrollBar`.
+ Everywhere the source uses them (Sidebar, Reputation, Skills, Honor lists), replace with a
+ **named** `FauxScrollFrameTemplate` (name REQUIRED — unnamed errors) + manual row pool + an
+ `OnVerticalScroll`/`FauxScrollFrame_Update` refresh. Row counts are bounded (~30-50), so
+ non-virtualized is fine. Reuse `NE.scrollbar.Reskin` (Slider path) for the bar skin.
+3. **Portrait = CLASS icon, not spec.** 3.3.5a has no retail specialization system. DO NOT port the
+ `C_SpecializationInfo` spec-icon mapping. Use the player's **class icon** from the class-icon
+ atlas (FDID 1662186, `classicon-`) via `CLASS_ICON_TCOORDS`/the registered atlas.
+4. **C_EquipmentSet → 3.3.5 global equip-manager.** 3.3.5a has a NATIVE equipment manager as GLOBAL
+ funcs (`GetNumEquipmentSets`, `GetEquipmentSetInfo`, `GetEquipmentSetInfoByName`,
+ `UseEquipmentSet`, `SaveEquipmentSet`, `DeleteEquipmentSet`, `GetEquipmentSetLocations`,
+ `EquipmentSetContainsCulledSlots`, `GetEquipmentSetItemIDs`) — NOT under `C_EquipmentSet`. Agent E
+ adds a `compat/C_EquipmentSet.lua` mapping the namespace to these globals (return-shape adapted),
+ and sets `NE.cap.equipmentSets` by probing `GetNumEquipmentSets`. Keep NewEra's custom-Lua backend
+ as the fallback if the globals are absent/flaky (private-server caveat — see project memory).
+5. **Graceful degradation is mandatory.** No sub-feature may break the panel. If a secondary tab
+ (Reputation/Skills/Honor/Pet) or the equip manager hits an unportable API, it must **leave that
+ tab's native Blizzard look intact** and `NE.Log` a warning — never error out of `Apply`/boot.
+ (Sprint 0 lesson: one nil deref aborted the whole panel.)
+6. **Phasing / DoD priority.** MUST-HAVE (the Sprint-1 DoD): the **PaperDoll tab** fully DF-styled —
+ chrome + restyled main tabs + model area + slot frames + slot-quality borders + class-icon
+ portrait + class-colored level text + the stat **sidebar**. SHOULD-HAVE (same sprint, but may
+ degrade): Reputation, Skills, Honor, PetPaperDoll tabs + Equipment Manager + per-slot flyout.
+
+## B. 3.3.5a gotcha checklist (apply proactively — these cost Sprint-0 round-trips)
+
+- `CreateMaskTexture` EXISTS but RETURNS NIL on this client → guard the return, not the method (`local ok,m=pcall(f.CreateMaskTexture,f); if ok and m and m.SetTexture then …`). Masks aren't needed (`SetPortraitTexture` is natively circular).
+- `Button:SetNormalTexture`/`SetPushedTexture`/etc. take a PATH STRING, not a texture object. Use `SetX(path)` then `GetX():SetTexCoord(...)`.
+- A child Button/Texture inherits the PARENT frame level → gets occluded by nineslice/title-band at higher levels. Raise it (`SetFrameLevel(base+N)`).
+- No `SetShown` (Show/Hide). `FauxScrollFrame` MUST be named. No `:SetMask`/`ScrollBox`/native `Texture:SetAtlas` (use `NE.tex.SetAtlas`).
+- 3.3.5 return-arg shifts: `GetCurrencyListInfo` icon=pos8, `GetFactionInfo` isHeader=pos9. `pcall` every stat/data getter in render loops.
+- LoD/data not ready at boot → defer with `C_Timer.After` / event hooks; build lazily, never assume a frame/texture exists.
+- Use `NE.tex.SetAtlas`/`NE.tex.Set` for all atlas art; `NE.dragon.Fonts` for fonts; `-- DOWNPORT:` comment every deviation.
+
+## C. Shared surface & anchors (Agent A owns creation; B–E consume)
+
+`NE.charpanel` table (created by Agent A in `CharacterPanel.lua`). Agent A MUST create and expose:
+- `CharacterFrame.Inset` (left content pane) and `CharacterFrame.InsetRight` (sidebar host) — built by
+ `NE.charpanel.BuildInset()` / `BuildInsetRight()` (from InsetFrames.lua, Agent A). If 3.3.5's
+ `CharacterFrame` already has `.Inset`, reuse it; else create defensively.
+- `NE.charpanel.ReassertLayout()` and `NE.charpanel.SelectSidebar(i)` — sidebar state (Agent C may
+ implement the body, but Agent A declares/stubs them so load order can't nil-deref).
+- The chrome applied via `NE.chrome.Apply(CharacterFrame, {layout="PortraitFrameTemplate", title=…})`
+ and main tabs restyled via `NE.tabs.ReskinClassicTab("CharacterFrameTab"..i)`.
+B–E anchor their content into `CharacterFrame.Inset` / `.InsetRight` / `NE_CharacterStatsPane` and
+attach to `NE.charpanel.*`. Nobody but A creates the Inset frames or the boot/register.
+
+## D. File ownership (two waves)
+
+**WAVE 1 (foundation — dispatched now):**
+- **Agent A — Shell/Nav/Integration:** `CharacterPanel.lua`, `InsetFrames.lua`, `TabButtons.lua`,
+ `CloseButton.lua`, `EditModeRegister.lua` + the `NE.RegisterReskin` helper in `integration/`.
+ Establishes the panel shell, chrome, main-tab reskin, boot/module-register, and the
+ `NE.charpanel` surface (incl. stubs for ReassertLayout/SelectSidebar). DoD: CharacterFrame opens
+ with DF chrome + restyled tabs + modern close button; no errors; registered in options + `/dnetest`.
+- **Agent F — Assets:** `modules/character/Assets.lua` + COPY the CharacterPanel art from
+ `NewEra/Art/CharacterPanel/` (+ `RaceBackground/`) into `Textures/CharacterPanel/`, register via
+ `NE.tex.RegisterLocal`, and ADD atlas-coord tables for any CharacterPanel-specific atlases not
+ already in Core (class-info BGs 1400895/1400896, panel bg 5882640, class icons 1662186, paperdoll
+ parts 410247/8/9, list-expand 4571485, rep/skill bars 136567/136570, model-control icons
+ 3534438/3487944, race BGs). Validate BLP headers. Deliver `Textures/CharacterPanel/ASSETS.md`.
+
+**WAVE 2 (content — dispatched after Wave 1 verified):**
+- **Agent B — PaperDoll/Model:** `PaperDoll.lua`, `ModelArea.lua`, `ModelControls.lua`,
+ `SlotFrames.lua`, `SlotQuality.lua`, `Portrait.lua` (CLASS icon), `LevelText.lua`, `InnerBorder.lua`.
+- **Agent C — Sidebar/Stats:** `Sidebar.lua`, `SidebarTabs.lua` (FauxScrollFrame fallback; the
+ PaperDoll stat sidebar — part of the MUST-HAVE DoD).
+- **Agent D — Secondary tabs:** `Reputation.lua`, `Skills.lua`, `Honor.lua`, `PetPaperDoll.lua`
+ (FauxScrollFrame fallback; graceful degradation per A.5).
+- **Agent E — Equipment:** `EquipmentFlyout.lua`, `EquipmentSets.lua`, `EquipmentManagerPane.lua` +
+ `compat/C_EquipmentSet.lua`.
+
+## E. Load order (TOC additions, after Sprint-0 files, before `core/_HelloDemo.lua`)
+
+`modules\character\Assets.lua` → `CharacterPanel.lua` → `InsetFrames.lua` → `InnerBorder.lua` →
+`ModelArea.lua` → `SlotFrames.lua` → `SlotQuality.lua` → `Portrait.lua` → `LevelText.lua` →
+`PaperDoll.lua` → `Sidebar.lua` → `SidebarTabs.lua` → `TabButtons.lua` → `ModelControls.lua` →
+`EquipmentFlyout.lua` → `EquipmentSets.lua` → `EquipmentManagerPane.lua` → `Reputation.lua` →
+`Skills.lua` → `Honor.lua` → `PetPaperDoll.lua` → `CloseButton.lua` → `EditModeRegister.lua`.
+(Architect owns the TOC; agents create files at exactly these paths.)
+
+## F. DoD verification (architect)
+
+Static gate green; `/dnetest` shows a "Character" entry that opens CharacterFrame without error;
+visual QA: open Character (`C`) → DF portrait-frame chrome, restyled metal tabs, DF close button,
+model + slot frames + quality borders, class-icon portrait, class-colored level text, stat sidebar.
+Secondary tabs either DF-styled or cleanly native (no errors). No taint on open/close in combat.
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CharacterPanel.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CharacterPanel.lua
new file mode 100644
index 0000000..44e5b7b
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CharacterPanel.lua
@@ -0,0 +1,791 @@
+-- DragonUI_NewEra/modules/character/CharacterPanel.lua — the Character panel ORCHESTRATOR.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): we do NOT reskin Blizzard's CharacterFrame in place. We build
+-- a CUSTOM DragonUI-owned frame (NE.charpanel.frame = "DragonUI_NewEra_Character"), HIDE Blizzard's
+-- CharacterFrame, and REPARENT Blizzard's functional widgets (the 19 equipment slot buttons +
+-- CharacterModelFrame) into ours so click-to-equip / texture / quality logic keeps working (Blizzard
+-- drives those by GLOBAL NAME, so reparenting preserves behaviour).
+--
+-- GEOMETRY (matches VISUAL_SPEC.md exactly, ported from NewEra/CharacterPanel/*):
+-- * Frame 338x424 COLLAPSED -> 548x424 EXPANDED (sidebar shown).
+-- * Inset (left content, FIXED 328x360) at Frame.TOPLEFT(4,-60), BOTTOMRIGHT->frame.BOTTOMLEFT(332,4).
+-- * InsetRight (sidebar host, auto-sized) at Inset.TOPRIGHT(1,0); hidden until expanded.
+-- * CharacterModelFrame resized to 231x320 at Inset.TOPLEFT(52,-66) (fixed, not box-filled).
+-- * 19 slots at the VANILLA PaperDollFrame.xml anchors (Head/Hands columns chain -4; weapons +5),
+-- translated relative to our Inset so they tight-flank the model exactly like NewEra.
+--
+-- HOW (proven 3.3.5a mechanics) is lifted from /root/downport/DragonflightUICharacter/CharacterFrame.lua:
+-- * CharacterFrame:HookScript("OnShow", function(s) s:Hide() end) -- keep stock frame hidden
+-- * save the global ToggleCharacter, replace it with our own that drives NE.charpanel.Toggle
+-- * tinsert(UISpecialFrames, "DragonUI_NewEra_Character") -- ESC closes ours
+-- WHAT (the DF look/scope) comes from NewEra + our Core toolkit (NE.chrome / NE.tabs / NE.tex).
+--
+-- GRACEFUL DEGRADATION (CONTRACT §A.5 / §B): every risky step (chrome, reparent, tab build) is
+-- pcall-wrapped and logged; onBoot never errors. Reparenting + repositioning happen ONLY OUT OF
+-- COMBAT, guarded by InCombatLockdown + NE.FrameUtil.AfterCombat.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local MODULE = "character"
+local FRAME_NAME = "DragonUI_NewEra_Character"
+
+-- ----------------------------------------------------------------------------
+-- Geometry constants (VISUAL_SPEC.md — replicate NewEra exactly).
+-- ----------------------------------------------------------------------------
+-- Frame.
+-- Match NewEra exactly: PANEL_DEFAULT_WIDTH=338 collapsed, 548 expanded. (We position slots/model
+-- relative to the Inset, so they fit 338 fine — the earlier 384 was wider than NewEra.)
+local FRAME_W_COLLAPSED = 338
+local FRAME_W_EXPANDED = 548
+local FRAME_H = 424
+
+-- Slot geometry. Vanilla PaperDollItemSlotButtonTemplate is a 37px button; metal frames around them
+-- are ~49x44 (left)/50x44 (right)/42x53 (bottom) and are drawn by Wave-2 (SlotFrames.lua). The slot
+-- CHAIN pitch is what we replicate here: vanilla chains each slot at the previous slot's BOTTOMLEFT
+-- with (0,-4), and the weapon row at the previous slot's TOPRIGHT with (5,0).
+local SLOT_VERT_GAP = -4 -- vanilla inter-slot vertical chain offset
+local SLOT_HORIZ_GAP = 5 -- vanilla inter-weapon horizontal chain offset
+
+-- DOWNPORT/REPORT: removed CP._dollBox box-fill of the model. Faithful NewEra sizes the model to a
+-- FIXED 231x320 at Inset.TOPLEFT(52,-66) (NOT box-filled / NOT centered), and the race bg uses native
+-- quarter sizes anchored to the model. ModelArea no longer reads CP._dollBox.
+local MODEL_W = 231 -- NewEra CharacterModelScene size
+local MODEL_H = 320
+-- DOWNPORT/REPORT: NewEra's model (52,-66) is relative to PaperDollFrame (≈ the FRAME). Our model is
+-- parented to the Inset (which is itself at frame -60,+4), so applying -66 directly dropped it ~60px
+-- too low (empty band above it). Subtract the inset offset: frame(52,-66) == Inset(48,-6).
+local MODEL_X = 48 -- Inset.TOPLEFT x (= frame x 52)
+local MODEL_Y = -6 -- Inset.TOPLEFT y (= frame y -66)
+
+-- Per-VISUAL_SPEC slot anchors (relative to the Inset):
+-- * Head at Inset.TOPLEFT(4,-2) — top of left column
+-- * Hands at Inset.TOPRIGHT(-4,-2) — top of right column
+-- * MainHand at Frame.BOTTOMLEFT(84,24)— bottom weapon row
+local LEFT_COL_X = 4 -- Inset.TOPLEFT x for Head
+local LEFT_COL_Y = -2 -- Inset.TOPLEFT y for Head
+local RIGHT_COL_X = -4 -- Inset.TOPRIGHT x for Hands
+local RIGHT_COL_Y = -2 -- Inset.TOPRIGHT y for Hands
+-- Weapon row: NewEra anchors CharacterMainHandSlot at Frame.BOTTOMLEFT(84,24).
+-- DOWNPORT/REPORT: center the weapon row UNDER THE MODEL (model frame-center ≈ 167.5; 3-slot row is
+-- ~121 wide → MainHand at 107). NewEra's 84 centers on the frame, leaving the row left of the model.
+local WEAPON_X = 107 -- Frame.BOTTOMLEFT x for MainHand (centered under the model)
+local WEAPON_Y = 24 -- Frame.BOTTOMLEFT y for MainHand
+
+-- The 19 equipment slot buttons split into the vanilla two-column + bottom-weapon layout.
+-- LEFT column (top->bottom): Head,Neck,Shoulder,Back,Chest,Shirt,Tabard,Wrist
+-- RIGHT column (top->bottom): Hands,Waist,Legs,Feet,Finger0,Finger1,Trinket0,Trinket1
+-- BOTTOM weapon row (left->right): MainHand,SecondaryHand,Ranged
+local SLOT_LEFT = {
+ "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot",
+ "CharacterChestSlot", "CharacterShirtSlot", "CharacterTabardSlot", "CharacterWristSlot",
+}
+local SLOT_RIGHT = {
+ "CharacterHandsSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot",
+ "CharacterFinger0Slot", "CharacterFinger1Slot", "CharacterTrinket0Slot", "CharacterTrinket1Slot",
+}
+local SLOT_BOTTOM = {
+ "CharacterMainHandSlot", "CharacterSecondaryHandSlot", "CharacterRangedSlot",
+}
+-- All reparented slot widgets, for the single reparent pass (model handled separately).
+local REPARENT_WIDGETS = {}
+for _, t in ipairs({ SLOT_LEFT, SLOT_RIGHT, SLOT_BOTTOM }) do
+ for _, n in ipairs(t) do REPARENT_WIDGETS[#REPARENT_WIDGETS + 1] = n end
+end
+
+-- ----------------------------------------------------------------------------
+-- Local logger + guard. DOWNPORT: NE.Log may be absent on a standalone load.
+-- ----------------------------------------------------------------------------
+local function log(msg)
+ if NE.Log then NE.Log("CHARPANEL", msg); return end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffffcc55DragonUI_NewEra|r [character]: " .. tostring(msg))
+ end
+end
+CP._log = log
+
+local function guard(label, fn)
+ local ok, err = pcall(fn)
+ if not ok then log(label .. " failed: " .. tostring(err)) end
+ return ok
+end
+
+-- ----------------------------------------------------------------------------
+-- Body background: stone (default) or dark.
+--
+-- chrome.Apply lays the shared grey-stone rock (FDID 374155) into f.Bg and PC.ApplyModernChrome
+-- tints it to PC.BODY_TINT (0.32) — a DOWNPORT fix for a first-paint colour flash on a different
+-- frame. Full brightness is what every other from-scratch standalone window in this addon shows
+-- (Guild, LFG, Professions, Auction House, Social, Collections, Cooldown Manager), so it's the
+-- default here too and this panel stops being the odd one out.
+--
+-- The tinted look is kept as an option rather than deleted because the argument for it is real and
+-- specific to this window: the character panel is nearly all Inset, so the stone survives only as a
+-- narrow gutter framing dark content, and which of the two reads better there is a matter of taste.
+-- The cog in the top-right corner (SettingsCog.lua) is the control; this is the setting.
+--
+-- Account-wide in NE.db.charPanel — it's a look, not a per-character choice.
+-- ----------------------------------------------------------------------------
+local function applyBodyBackground()
+ local f = CP.frame
+ if not (f and f.Bg and f.Bg.SetVertexColor) then return end
+ if CP.IsDarkBodyBackground() then
+ local t = (NE.panelchrome and NE.panelchrome.BODY_TINT) or { 0.32, 0.32, 0.32 }
+ f.Bg:SetVertexColor(t[1], t[2], t[3])
+ else
+ f.Bg:SetVertexColor(1, 1, 1)
+ end
+end
+CP.ApplyBodyBackground = applyBodyBackground
+
+function CP.IsDarkBodyBackground()
+ local t = NE.db and NE.db.charPanel
+ return (t and t.darkBody) and true or false
+end
+
+function CP.SetDarkBodyBackground(on)
+ if NE.db then
+ NE.db.charPanel = NE.db.charPanel or {}
+ NE.db.charPanel.darkBody = on and true or false
+ end
+ applyBodyBackground()
+end
+
+-- ----------------------------------------------------------------------------
+-- Class-icon portrait. CONTRACT §A0/§A.3: PaperDoll portrait is the CLASS icon
+-- (no spec system on 3.3.5a), drawn from the class-icon atlas (FDID 1662186,
+-- "classicon-") registered by Agent F's Assets.lua.
+-- ----------------------------------------------------------------------------
+local function applyClassPortrait(frame)
+ local p = frame and (frame.portrait or frame.PortraitTexture)
+ if not p then return end
+ local _, classFile = UnitClass("player")
+ classFile = classFile and classFile:lower() or "warrior"
+ local atlas = "classicon-" .. classFile
+ -- NE.tex.SetAtlas returns false (and logs a miss) if the sheet isn't shipped — fall back to the
+ -- native circular player portrait so the corner is never blank (graceful degrade).
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(p, atlas, false)) then
+ if SetPortraitTexture then pcall(SetPortraitTexture, p, "player") end
+ end
+end
+CP.ApplyClassPortrait = applyClassPortrait
+
+-- ----------------------------------------------------------------------------
+-- Build the custom frame shell (once). DF chrome + class portrait + DF close.
+-- Sized to the COLLAPSED width; SetSidebarExpanded() grows it.
+-- ----------------------------------------------------------------------------
+local function buildFrame()
+ if CP.frame then return CP.frame end
+
+ local f = CreateFrame("Frame", FRAME_NAME, UIParent)
+ -- The red 3-slice is the addon's standard button; Watch keeps this window's panel buttons
+ -- skinned as its panes are built (core/ButtonSkin.lua). Opt out per button with _neNoSkin.
+ if NE.buttonskin and NE.buttonskin.Watch then pcall(NE.buttonskin.Watch, f) end
+ f:SetSize(FRAME_W_EXPANDED, FRAME_H) -- unified panel width (all tabs); see setSidebarExpanded
+ f:SetPoint("CENTER", UIParent, "CENTER", 0, 60)
+ f:SetFrameStrata("HIGH")
+ f:SetToplevel(true)
+ f:EnableMouse(true)
+ f:SetMovable(true)
+ f:SetClampedToScreen(true)
+ f:Hide()
+ CP.frame = f
+ CP._sidebarExpanded = false
+
+ -- Per-window scaling via NE.scale (mode: ui / none / custom). Default "ui" = no change from the
+ -- previous behaviour (the panel followed the UI scale); re-applied on every show.
+ if NE.scale and NE.scale.Apply then
+ NE.scale.SetFrame("character", f)
+ NE.scale.Apply("character")
+ f:HookScript("OnShow", function() if NE.scale and NE.scale.Apply then NE.scale.Apply("character") end end)
+ end
+
+ -- ESC closes it (DOWNPORT: by global name; no SetShown anywhere — Show/Hide only).
+ if NE.FrameUtil and NE.FrameUtil.EscClose then
+ NE.FrameUtil.EscClose(FRAME_NAME)
+ else
+ tinsert(UISpecialFrames, FRAME_NAME)
+ end
+
+ -- Draggable title band so the frame can be repositioned (DFUIC pattern). Position persists
+ -- account-wide across /reload + sessions (and resets to the default centre if the screen
+ -- resolution changes, so a saved spot can never strand the window off-screen).
+ local drag = CreateFrame("Button", nil, f)
+ drag:SetPoint("TOPLEFT", 60, -2)
+ drag:SetPoint("TOPRIGHT", -30, -2)
+ drag:SetHeight(28)
+ if NE.FrameUtil and NE.FrameUtil.PersistWindowPosition then
+ NE.FrameUtil.PersistWindowPosition(f, "character",
+ { point = "CENTER", relPoint = "CENTER", x = 0, y = 60 }, drag)
+ else
+ drag:RegisterForDrag("LeftButton")
+ drag:SetScript("OnDragStart", function() f:StartMoving() end)
+ drag:SetScript("OnDragStop", function() f:StopMovingOrSizing() end)
+ end
+
+ -- DF portrait-frame chrome (nineslice + Rock bg + title). noPortrait=true: PanelChrome's default
+ -- portrait fill is the PLAYER portrait + a watcher that re-asserts it on UNIT_PORTRAIT_UPDATE,
+ -- which would fight our CLASS icon (§A.3). So we suppress its portrait entirely and OWN the
+ -- portrait region ourselves below.
+ guard("chrome.Apply", function()
+ if NE.chrome and NE.chrome.Apply then
+ NE.chrome.Apply(f, {
+ layout = "PortraitFrameTemplate",
+ -- Title = the character's NAME (matches the example: "Ashgaroth"). UnitName may be nil at
+ -- build time → fall back to "Character"; updateTitle() re-asserts the real name on login.
+ title = UnitName("player") or _G.CHARACTER or "Character",
+ noPortrait = true,
+ })
+ end
+ end)
+
+ -- Body background — the gutter between the title band and the Insets, plus the margins around
+ -- them, which is the only place chrome.Apply's rock sheet (FDID 374155) actually shows here.
+ guard("bodyBg", applyBodyBackground)
+
+ -- Own the portrait region: build the cutout texture + seat it in the metal corner, then fill it
+ -- with the CLASS icon (and keep it correct across model/portrait events).
+ guard("classPortrait", function()
+ if not f.portrait then
+ f.portrait = f:CreateTexture(nil, "ARTWORK")
+ end
+ if NE.portrait and NE.portrait.ApplyCutout then
+ -- Default size/anchor (60 @ TOPLEFT -5,8). The grey "square outline" was NOT the cutout gap —
+ -- it was opaque grey pixels in the inter-cell GAPS of the icon sheet that bilinear filtering
+ -- bled at the texcoord edges. Fixed by clearing the WHOLE sheet's alpha (gaps → transparent),
+ -- so size can stay at the correct 60.
+ NE.portrait.ApplyCutout(f.portrait, f)
+ end
+ applyClassPortrait(f)
+ f:HookScript("OnShow", function() applyClassPortrait(f) end)
+ if not f._neClassPortraitWatcher then
+ local w = CreateFrame("Frame", nil, f)
+ w:RegisterEvent("UNIT_PORTRAIT_UPDATE")
+ w:RegisterEvent("PLAYER_ENTERING_WORLD")
+ w:SetScript("OnEvent", function(_, _, unit)
+ if (not unit) or unit == "player" then applyClassPortrait(f) end
+ end)
+ f._neClassPortraitWatcher = w
+ end
+ end)
+
+ -- DF modern close button. NE.chrome.Apply already built+modernized f.CloseButton; CloseButton.lua
+ -- re-asserts it. Belt-and-suspenders here so the X exists even if that file failed to boot.
+ guard("closeButton", function()
+ if NE.panelchrome and NE.panelchrome.ModernizeCloseButton and f.CloseButton then
+ NE.panelchrome.ModernizeCloseButton(f.CloseButton, { frameLevelBump = 20 })
+ end
+ end)
+
+ -- Panel open/close sounds (igCharacterInfoOpen/Close), via a child watcher (never on the frame).
+ guard("sounds", function()
+ if NE.FrameUtil and NE.FrameUtil.WirePanelSounds then
+ NE.FrameUtil.WirePanelSounds(f, "igCharacterInfoOpen", "igCharacterInfoClose")
+ end
+ end)
+
+ return f
+end
+CP.BuildFrame = buildFrame
+
+-- ----------------------------------------------------------------------------
+-- Sidebar EXPAND / COLLAPSE. CONTRACT §C surface: the stats-sidebar agent (Wave 2) calls
+-- NE.charpanel.SetSidebarExpanded(bool). Collapsed = frame 338, InsetRight hidden. Expanded =
+-- frame 548, InsetRight shown. The left Inset is FIXED 328x360, so the model + slots NEVER move.
+-- DOWNPORT: width changes on our (non-protected) frame are combat-legal; only the slot/model
+-- REPARENT is gated out of combat. No SetShown on 3.3.5 — Show()/Hide() only.
+-- ----------------------------------------------------------------------------
+-- DOWNPORT/REPORT: per-tab frame/inset WIDTH (the state-machine fix). NewEra's
+-- InsetFrames.lua (setInsetForTab/TAB_WIDTHS) gives the three Era-only tabs FULL-WIDTH content with
+-- NO sidebar. Values read verbatim from NewEra: HonorFrame/ReputationFrame/SkillFrame = 445; the
+-- fixed left-content (PaperDoll) Inset width = 328 (frame.BOTTOMLEFT + 332 with BR_X). Character/Pet
+-- keep the fixed-width Inset + sidebar; the wide tabs stretch the Inset to the frame's right edge.
+local WIDE_TAB_FRAME_W = 445 -- NewEra TAB_WIDTHS.{Honor,Reputation,Skill}Frame
+local INSET_BR_RIGHT_X = -6 -- retail PANEL_INSET_RIGHT_OFFSET (Inset.BR -> frame.BOTTOMRIGHT)
+local INSET_BR_BOT_Y = 4 -- retail PANEL_INSET_BOTTOM_OFFSET
+-- Mirror InsetFrames.lua's fixed-width anchor numbers (kept local; we re-assert them when restoring
+-- the narrow paperdoll layout after a wide tab).
+local INSET_TOP_X = 4
+local INSET_TOP_Y = -60
+local INSET_FIXED_BR_X = 332 -- gives the fixed 328-wide Inset (frame.BOTTOMLEFT + 332)
+
+-- Re-anchor the Inset for the current tab: narrow/fixed (PaperDoll, Pet) vs wide/stretched (the three
+-- Era tabs). DOWNPORT: pure SetPoint/ClearAllPoints on our own non-protected Inset — combat-legal.
+local function applyInsetWidthForTab(wide)
+ local f = CP.frame
+ local inset = f and f.Inset
+ if not inset then return end
+ inset:ClearAllPoints()
+ inset:SetPoint("TOPLEFT", f, "TOPLEFT", INSET_TOP_X, INSET_TOP_Y)
+ if wide then
+ -- Stretch the Inset BOTTOMRIGHT to the frame's right edge so the full-width content pane
+ -- (SetAllPoints(Inset)) fills the wider frame.
+ inset:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", INSET_BR_RIGHT_X, INSET_BR_BOT_Y)
+ else
+ -- Fixed-width left content: BOTTOMRIGHT pinned to frame.BOTTOMLEFT so model/slots never shift
+ -- even when the frame expands for the sidebar.
+ inset:SetPoint("BOTTOMRIGHT", f, "BOTTOMLEFT", INSET_FIXED_BR_X, INSET_BR_BOT_Y)
+ end
+end
+CP.ApplyInsetWidthForTab = applyInsetWidthForTab
+
+-- DOWNPORT/REPORT: gather the paperdoll DECORATIONS that are NOT children of the model/slots (so they
+-- do NOT follow a slot/model Hide). SlotFrames.lua + InnerBorder.lua register their textures here.
+CP._paperdollDecor = CP._paperdollDecor or {}
+
+-- DOWNPORT/REPORT: ShowPaperDoll(show) — Show()/Hide() the WHOLE player-paperdoll group as one unit:
+-- CharacterModelFrame (race-bg + model controls are its children, so they follow it), the 19 slot
+-- buttons (their metal frames are children of the slots, so they follow), AND the decorations that
+-- are NOT children of the slots/model (inner gold border + the slot metal frames, registered into
+-- CP._paperdollDecor). Combat-safe — Show/Hide only on non-protected + Blizzard slot buttons; never
+-- reparents. Each step pcall-guarded so a single bad widget can't break a tab switch.
+local function showPaperDoll(show)
+ show = show and true or false
+ local model = _G.CharacterModelFrame
+ if model then pcall(show and model.Show or model.Hide, model) end
+ for _, name in ipairs(REPARENT_WIDGETS) do
+ local b = _G[name]
+ if b then pcall(show and b.Show or b.Hide, b) end
+ end
+ -- Decorations not parented to the slots/model (inner border pieces + slot metal frames).
+ for _, t in ipairs(CP._paperdollDecor) do
+ if t then pcall(show and t.Show or t.Hide, t) end
+ end
+end
+CP.ShowPaperDoll = showPaperDoll
+
+local function setSidebarExpanded(expanded)
+ local f = CP.frame or buildFrame()
+ if not f then return end
+ expanded = expanded and true or false
+
+ -- DOWNPORT/REPORT: the sidebar only exists on the paperdoll/pet tabs. If a wide (Era) tab is
+ -- active, force-collapse: the wide tab owns the full frame width and has no sidebar. This makes
+ -- SetSidebarExpanded resilient to being called after a secondary tab.
+ local wideTab = CP._wideTabActive and true or false
+ if wideTab then expanded = false end
+
+ -- CONTEXTUAL PET TAB CONTROL: Only keep the sidebar expanded if the player is looking at the
+ -- actual combat pet panel sheet. If on the Companions or Mounts sheet (i.e. PetPaperDollFramePetFrame
+ -- is hidden), force the sidebar to collapse so the collection grid remains clear.
+ if CP._activeTab == "Pet" then
+ local petSubFrame = _G.PetPaperDollFramePetFrame
+ if not (petSubFrame and petSubFrame:IsShown()) then
+ expanded = false
+ end
+ end
+
+ CP._sidebarExpanded = expanded
+
+ -- UNIFIED WIDTH: every tab uses the full panel width (FRAME_W_EXPANDED) so the window never resizes
+ -- on tab switch and the 6-tab row always fits. Secondary (wide) tabs stretch their Inset/content to
+ -- fill it; paperdoll tabs keep their fixed-width left Inset (model/slots) with the equipment-manager
+ -- sidebar (Character) or open space (Pet) on the right.
+ if wideTab then
+ f:SetWidth(FRAME_W_EXPANDED)
+ applyInsetWidthForTab(true)
+ else
+ f:SetWidth(FRAME_W_EXPANDED)
+ applyInsetWidthForTab(false)
+ end
+
+ local ir = f.InsetRight
+ if ir then
+ if expanded then ir:Show() else ir:Hide() end
+ end
+end
+CP.SetSidebarExpanded = setSidebarExpanded
+CP.IsSidebarExpanded = function() return CP._sidebarExpanded and true or false end
+
+-- Securely hook Blizzard's sub-tab layout switches so the sidebar collapses/expands dynamically
+local function hookPetUISubTabs()
+ if _G.PetPaperDollFrame_UpdateTabs and not CP._hookedPetTabsMain then
+ CP._hookedPetTabsMain = true
+ securehookfunc("PetPaperDollFrame_UpdateTabs", function()
+ if CP._activeTab == "Pet" then
+ setSidebarExpanded(CP._sidebarExpanded)
+ end
+ end)
+ end
+end
+
+-- DOWNPORT/REPORT: TabButtons.selectTab calls this on every tab change to drive the per-tab frame/
+-- inset width + paperdoll visibility + sidebar state. key == "Character" shows the player paperdoll;
+-- Pet hides it (PetPaperDoll drives its OWN DragonUI_NewEra_PetModel, not CharacterModelFrame); the
+-- three Era tabs go full-width with no model/slots/decor and no sidebar.
+local WIDE_TAB_KEYS = { Skills = true, Honor = true, Reputation = true, Currency = true }
+local function applyTabState(key)
+ local f = CP.frame
+ if not f then return end
+ local wide = WIDE_TAB_KEYS[key] and true or false
+ CP._wideTabActive = wide
+
+ -- Player paperdoll only on the Character tab.
+ pcall(showPaperDoll, key == "Character")
+
+ if wide then
+ -- Force-collapse the sidebar (hide InsetRight) and reset the saved expand flag, then go wide.
+ CP._sidebarExpanded = false
+ if f.InsetRight then f.InsetRight:Hide() end
+ setSidebarExpanded(false) -- wideTabActive=true path: sets WIDE width + stretched Inset
+ else
+ -- Narrow tab: restore the correct width. Character rests expanded (sidebar); Pet uses its own
+ -- expand path (handled by selectTab). Re-assert the fixed-width Inset either way.
+ setSidebarExpanded(CP._sidebarExpanded or false)
+ end
+end
+CP.ApplyTabState = applyTabState
+
+-- ----------------------------------------------------------------------------
+-- Hide + intercept Blizzard's CharacterFrame (DFUIC mechanics, CONTRACT §A0).
+-- * keep CharacterFrame hidden via an OnShow self-hide hook,
+-- * save the global ToggleCharacter and replace it with one that drives ours.
+-- Show/hide only — never reparent/teardown here (taint-free). Idempotent.
+-- ----------------------------------------------------------------------------
+local function interceptBlizzard()
+ if CP._intercepted then return end
+ CP._intercepted = true
+
+ local cf = _G.CharacterFrame
+ if cf and cf.HookScript then
+ -- DOWNPORT: do NOT UnregisterAllEvents on CharacterFrame — its events drive the slot/model/stat
+ -- updates that our reparented widgets still depend on. Just keep the FRAME hidden.
+ cf:HookScript("OnShow", function(self)
+ if not InCombatLockdown() then self:Hide() end
+ -- Open ours to mirror Blizzard's intent (a path that bypassed ToggleCharacter still works).
+ if not CP.frame or not CP.frame:IsShown() then CP.Toggle(true) end
+ end)
+ end
+
+ -- Replace the global ToggleCharacter (save the old). Our replacement maps Blizzard's tab arg to
+ -- ours and drives NE.charpanel.Toggle. DOWNPORT: combat-safe — Toggle no-ops the show in lockdown.
+ if type(_G.ToggleCharacter) == "function" and not CP._oldToggleCharacter then
+ CP._oldToggleCharacter = _G.ToggleCharacter
+ _G.ToggleCharacter = function(which)
+ CP.Toggle(nil, which) -- nil = toggle; `which` selects the tab
+ end
+ end
+end
+CP.InterceptBlizzard = interceptBlizzard
+
+-- ----------------------------------------------------------------------------
+-- Position the reparented widgets per the VANILLA PaperDollFrame.xml anchors, translated to OUR
+-- Inset. Idempotent — safe to re-run on every show (re-assert in case Blizzard re-anchored).
+-- ----------------------------------------------------------------------------
+local function positionSlots(host, modelFrame)
+ local f = CP.frame
+ if not host then return end
+
+ -- LEFT column: Head at Inset.TOPLEFT(4,-2), chain each slot at prev BOTTOMLEFT (0,-4).
+ local prev
+ for i, name in ipairs(SLOT_LEFT) do
+ local b = _G[name]
+ if b then
+ b:ClearAllPoints()
+ if i == 1 then
+ b:SetPoint("TOPLEFT", host, "TOPLEFT", LEFT_COL_X, LEFT_COL_Y)
+ else
+ b:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, SLOT_VERT_GAP)
+ end
+ prev = b
+ end
+ end
+
+ -- RIGHT column: Hands at Inset.TOPRIGHT(-4,-2), chain each slot at prev BOTTOMLEFT (0,-4).
+ prev = nil
+ for i, name in ipairs(SLOT_RIGHT) do
+ local b = _G[name]
+ if b then
+ b:ClearAllPoints()
+ if i == 1 then
+ -- Vanilla Hands anchors TOPLEFT to PaperDollFrame; relative to our Inset we want the slot's
+ -- TOPRIGHT pinned to the Inset's TOPRIGHT so the column hugs the right edge (VISUAL_SPEC).
+ b:SetPoint("TOPRIGHT", host, "TOPRIGHT", RIGHT_COL_X, RIGHT_COL_Y)
+ else
+ b:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, SLOT_VERT_GAP)
+ end
+ prev = b
+ end
+ end
+
+ -- BOTTOM weapon row: MainHand at Frame.BOTTOMLEFT(84,24); SecondaryHand/Ranged chain at prev
+ -- TOPRIGHT (5,0) — the vanilla weapon-row spacing.
+ prev = nil
+ for i, name in ipairs(SLOT_BOTTOM) do
+ local b = _G[name]
+ if b then
+ b:ClearAllPoints()
+ if i == 1 then
+ b:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", WEAPON_X, WEAPON_Y)
+ else
+ b:SetPoint("TOPLEFT", prev, "TOPRIGHT", SLOT_HORIZ_GAP, 0)
+ end
+ prev = b
+ end
+ end
+
+ -- DOWNPORT/REPORT: faithful NewEra — model is FIXED 231x320 at Inset.TOPLEFT(52,-66), NOT box-filled
+ -- and NOT centered. (Reverted the CP._dollBox fill.) Keep RefreshUnit/SetUnit (harmless reframe).
+ if modelFrame then
+ modelFrame:SetSize(MODEL_W, MODEL_H)
+ modelFrame:ClearAllPoints()
+ modelFrame:SetPoint("TOPLEFT", host, "TOPLEFT", MODEL_X, MODEL_Y)
+ if modelFrame.RefreshUnit then pcall(modelFrame.RefreshUnit, modelFrame)
+ elseif modelFrame.SetUnit then pcall(modelFrame.SetUnit, modelFrame, "player") end
+ end
+end
+CP.PositionSlots = positionSlots
+
+-- ----------------------------------------------------------------------------
+-- Reparent the functional widgets ONCE, OUT OF COMBAT (CONTRACT §A0 + §B).
+-- CharacterModelFrame + the 19 slot buttons -> our Inset, repositioned to the vanilla layout.
+-- Texture/quality/click logic is LEFT ALONE (Blizzard drives them by global name).
+-- ----------------------------------------------------------------------------
+local function doReparent()
+ local f = CP.frame
+ if not f then return end
+ -- Host = our Inset (left content pane). InsetFrames.lua builds it; fall back to the frame itself.
+ local host = (f.Inset) or f
+ local modelFrame = _G.CharacterModelFrame
+
+ -- Reparent slots.
+ for _, name in ipairs(REPARENT_WIDGETS) do
+ local b = _G[name]
+ if b and b.SetParent then
+ b:SetParent(host)
+ -- DOWNPORT: a reparented Blizzard child inherits OUR frame level; raise it above the inset
+ -- nineslice/bg so the slot art + quality border aren't occluded.
+ if b.SetFrameLevel then b:SetFrameLevel((host:GetFrameLevel() or 1) + 5) end
+ end
+ end
+ -- Reparent the 3D model + its rotate buttons.
+ if modelFrame and modelFrame.SetParent then
+ modelFrame:SetParent(host)
+ if modelFrame.SetFrameLevel then modelFrame:SetFrameLevel((host:GetFrameLevel() or 1) + 2) end
+ for _, rn in ipairs({ "CharacterModelFrameRotateLeftButton", "CharacterModelFrameRotateRightButton" }) do
+ local rb = _G[rn]
+ if rb and rb.SetParent then rb:SetParent(modelFrame) end
+ end
+ end
+
+ positionSlots(host, modelFrame)
+ CP._reparented = true
+end
+
+-- Run the reparent exactly once, deferring into out-of-combat if needed.
+local function reparentOnce()
+ if CP._reparented then return end
+ if InCombatLockdown() then
+ if NE.FrameUtil and NE.FrameUtil.AfterCombat then
+ NE.FrameUtil.AfterCombat(function() guard("reparent", doReparent) end)
+ end
+ return
+ end
+ guard("reparent", doReparent)
+end
+CP.ReparentWidgets = reparentOnce
+
+-- ----------------------------------------------------------------------------
+-- ReassertLayout — idempotent re-position of slots+model (cheap; safe out of combat). Called on
+-- every show + tab switch so a Blizzard re-anchor can't drift our layout. Combat-guarded: SetPoint
+-- on our reparented (non-protected-by-us) Blizzard children is legal out of combat only.
+-- ----------------------------------------------------------------------------
+local function reassertLayout()
+ if not CP._reparented then return end
+ if InCombatLockdown() then return end -- DOWNPORT: skip in lockdown; re-asserted on next show
+ local f = CP.frame
+ if not f then return end
+ local host = (f.Inset) or f
+ guard("reassertLayout", function() positionSlots(host, _G.CharacterModelFrame) end)
+end
+CP.ReassertLayout = reassertLayout
+
+-- ----------------------------------------------------------------------------
+-- Toggle (show/hide our frame), combat-safe + tab-aware. CONTRACT surface.
+-- Toggle(true) -> show
+-- Toggle(false) -> hide
+-- Toggle(nil) -> flip
+-- Toggle(_, "ReputationFrame"/...) -> show + select that tab
+-- DOWNPORT: maps Blizzard's tab-name arg ("PaperDollFrame"/"PetPaperDollFrame"/
+-- "ReputationFrame"/"SkillFrame"/"HonorFrame") to our SelectTab names.
+-- ----------------------------------------------------------------------------
+local BLIZ_TAB_TO_NAME = {
+ PaperDollFrame = "Character",
+ PetPaperDollFrame = "Pet",
+ ReputationFrame = "Reputation",
+ SkillFrame = "Skills",
+ HonorFrame = "Honor",
+ TokenFrame = "Currency",
+ CurrencyFrame = "Currency",
+}
+
+function CP.Toggle(show, whichTab)
+ local f = CP.frame or buildFrame()
+ if not f then return end
+
+ -- Decide target visibility.
+ local target
+ if show == true then target = true
+ elseif show == false then target = false
+ else target = not f:IsShown() end
+
+ if not target then
+ f:Hide()
+ return
+ end
+
+ -- DOWNPORT: showing is the only "risky" path in combat (a protected reparent may still be pending);
+ -- the Show itself is legal (our frame is not protected), but we must not reparent in lockdown.
+ f:Show()
+ reparentOnce()
+ reassertLayout()
+
+ local tabName = whichTab and BLIZ_TAB_TO_NAME[whichTab] or nil
+ if tabName and CP.SelectTab then guard("selectTab", function() CP.SelectTab(tabName) end) end
+end
+
+-- ----------------------------------------------------------------------------
+-- NE.charpanel public surface (CONTRACT §C). Implemented here: frame / Toggle / SetSidebarExpanded /
+-- ReassertLayout / PositionSlots / ReparentWidgets. The real BuildInset/BuildInsetRight come from
+-- InsetFrames.lua (loads after this file); SelectTab comes from TabButtons.lua; SelectSidebar is a
+-- Wave-2 (sidebar) stub. Declare no-op placeholders so load order can't nil-deref.
+-- ----------------------------------------------------------------------------
+CP.BuildInset = CP.BuildInset or function() end
+CP.BuildInsetRight = CP.BuildInsetRight or function() end
+CP.SelectTab = CP.SelectTab or function(_) end
+
+if not CP.SelectSidebar then CP.SelectSidebar = function(_) end end -- Wave-2 (sidebar) fills
+
+-- The canonical boot/re-entry: build the frame, intercept Blizzard, build insets/tabs, reparent.
+-- Set the title bar to the character's NAME (example shows "Ashgaroth"). Re-asserted on login since
+-- UnitName is nil at file/boot parse time.
+-- Set the window header text directly. Exposed so the Titles pane can update it OPTIMISTICALLY on
+-- click (UnitPVPName lags a frame behind SetCurrentTitle).
+local function setWindowTitle(text)
+ local f = CP.frame
+ if not f or not text then return end
+ local fs = f.Title or (f.TitleContainer and f.TitleContainer.TitleText)
+ if fs and fs.SetText then fs:SetText(text)
+ elseif NE.panelchrome and NE.panelchrome.SetTitle then pcall(NE.panelchrome.SetTitle, f, text) end
+end
+CP.SetWindowTitle = setWindowTitle
+
+local function updateTitle()
+ if not CP.frame then return end
+ -- DOWNPORT/REPORT: UnitPVPName returns the player's name WITH their current title applied (e.g.
+ -- "Magna the Explorer"); fall back to the plain name when there's no title.
+ local name = (UnitPVPName and UnitPVPName("player"))
+ if not name or name == "" then name = UnitName("player") end
+ if name then setWindowTitle(name) end
+end
+CP.UpdateTitle = updateTitle
+
+local function boot()
+ buildFrame()
+ guard("buildInset", function()
+ if CP.BuildInset then CP.BuildInset() end
+ if CP.BuildInsetRight then CP.BuildInsetRight() end
+ end)
+ guard("buildTabs", function() if CP.BuildTabs then CP.BuildTabs() end end)
+ guard("intercept", interceptBlizzard)
+ reparentOnce()
+ guard("title", updateTitle)
+ -- Resting state: the Character tab expands the stats sidebar (done by SelectTab). Honor the saved
+ -- flag otherwise.
+ guard("sidebarRest", function() setSidebarExpanded(CP._sidebarExpanded or false) end)
+ guard("reassert", reassertLayout)
+
+ -- Wire up sub-tab monitoring safely for Load-on-Demand modules
+ if IsAddOnLoaded("Blizzard_PetUI") then
+ hookPetUISubTabs()
+ else
+ local watcher = CreateFrame("Frame")
+ watcher:RegisterEvent("ADDON_LOADED")
+ watcher:SetScript("OnEvent", function(self, event, addonName)
+ if addonName == "Blizzard_PetUI" then
+ hookPetUISubTabs()
+ self:UnregisterAllEvents()
+ end
+ end)
+ end
+end
+CP.Boot = boot
+
+-- ----------------------------------------------------------------------------
+-- Boot via Core/Modules.lua. PLAYER_LOGIN builds + intercepts + reparents (out of combat);
+-- PLAYER_ENTERING_WORLD re-runs to catch any late-loaded tab content.
+-- ----------------------------------------------------------------------------
+if NE.modules and NE.modules.Register then
+ NE.modules.Register{
+ name = MODULE,
+ default = true,
+ label = "Character Panel",
+ desc = "The modern Dragonflight character window. Disable to keep the stock Blizzard frame.",
+ events = { "PLAYER_LOGIN", "PLAYER_ENTERING_WORLD" },
+ onBoot = function() boot() end,
+ }
+else
+ log("NE.modules.Register absent; character panel not booted")
+end
+
+-- ----------------------------------------------------------------------------
+-- DragonUI integration (CONTRACT §A.1) — INLINE here per Agent A's brief. Our panel is a CUSTOM
+-- frame (not a NE.RegisterPanel, no mover). open/close drive OUR Toggle (CONTRACT §A0 update).
+-- Each handshake is independently guarded (NE.dragon.* may be absent on a standalone load).
+-- ----------------------------------------------------------------------------
+local function registerWithDragon()
+ local title = "Character Panel"
+ local desc = "A modern Dragonflight character window (custom frame; the Blizzard frame is hidden)."
+
+ local moduleTable = {
+ ne_id = MODULE,
+ Enable = function() CP.Toggle(true) end,
+ Disable = function() CP.Toggle(false) end,
+ Refresh = function() reassertLayout() end,
+ }
+ local dragon = NE.dragon
+ if dragon then
+ local mr = dragon.ModuleRegistry
+ local registered = false
+ if mr and type(mr.Register) == "function" then
+ local ok = pcall(mr.Register, mr, "ne_" .. MODULE, moduleTable, title, desc, 20)
+ registered = ok
+ if not ok then log("ModuleRegistry:Register failed") end
+ end
+ if not registered and type(dragon.RegisterModule) == "function" then
+ local ok = pcall(dragon.RegisterModule, dragon, "ne_" .. MODULE, moduleTable, title, desc, 20)
+ if not ok then log("RegisterModule failed") end
+ end
+ end
+
+ -- (b) Options-tab entry.
+ NE.optionPanels = NE.optionPanels or {}
+ local already = false
+ for _, p in ipairs(NE.optionPanels) do if p.id == MODULE then already = true; break end end
+ if not already then
+ table.insert(NE.optionPanels, {
+ id = MODULE,
+ title = title,
+ desc = desc,
+ order = 20,
+ refresh = function() reassertLayout() end,
+ })
+ end
+
+ -- (c) QA harness entry (/dnetest). open/close drive OUR custom frame (CONTRACT §A0).
+ NE.qa = NE.qa or { modules = {} }
+ NE.qa.modules = NE.qa.modules or {}
+ local inQa = false
+ for _, m in ipairs(NE.qa.modules) do if m.name == "Character" then inQa = true; break end end
+ if not inQa then
+ table.insert(NE.qa.modules, {
+ name = "Character",
+ getFrame = function() return CP.frame end,
+ open = function() CP.Toggle(true) end,
+ close = function() CP.Toggle(false) end,
+ })
+ end
+end
+
+guard("registerWithDragon", registerWithDragon)
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CloseButton.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CloseButton.lua
new file mode 100644
index 0000000..f0b9174
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/CloseButton.lua
@@ -0,0 +1,44 @@
+-- DragonUI_NewEra/modules/character/CloseButton.lua — ensure OUR custom frame's close button is the
+-- DF red X (RedButton-Exit-2x family).
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): the close button belongs to OUR frame
+-- (NE.charpanel.frame = "DragonUI_NewEra_Character"), not Blizzard's CharacterFrame. NE.chrome.Apply
+-- (called from CharacterPanel.lua's buildFrame) already BUILDS + modernizes frame.CloseButton via
+-- core/PanelChrome.lua. This file is a thin, idempotent re-assert so:
+-- * the X is modernized even if NE.chrome.Apply's close-button step happened to no-op, and
+-- * it sits ABOVE the chrome stack (nineslice top-right corner + title band) — frameLevelBump.
+--
+-- PanelChrome.ModernizeCloseButton already applies the Sprint-0 lessons (SetNormalTexture takes a
+-- PATH not an object; degrades to native UIPanelCloseButton art if the atlas isn't shipped — never
+-- blank). All ops are non-protected. Idempotent (the helper guards with _neCloseModernized).
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local function modernize()
+ local f = CP.frame
+ if not f or not f.CloseButton then return end
+ if NE.panelchrome and NE.panelchrome.ModernizeCloseButton then
+ -- frameLevelBump lifts the X above the nineslice + title-band stack so it isn't occluded.
+ NE.panelchrome.ModernizeCloseButton(f.CloseButton, { frameLevelBump = 20 })
+ end
+ -- Make sure clicking it actually closes OUR frame (UIPanelCloseButton's default HideParent fires
+ -- on the parent, which is our frame — so this is belt-and-suspenders for a re-anchored button).
+ if not f.CloseButton._neCharClose then
+ f.CloseButton._neCharClose = true
+ f.CloseButton:HookScript("OnClick", function() if CP.Toggle then CP.Toggle(false) end end)
+ end
+end
+
+CP.ModernizeCloseButton = modernize
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ -- Ensure the frame (and thus its CloseButton) exists, then modernize.
+ if CP.BuildFrame then pcall(CP.BuildFrame) end
+ local ok, err = pcall(modernize)
+ if not ok and CP._log then CP._log("close button modernize failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Currency.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Currency.lua
new file mode 100644
index 0000000..e37cb85
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Currency.lua
@@ -0,0 +1,371 @@
+-- DragonUI_NewEra/modules/character/Currency.lua -- the Currency secondary tab.
+--
+-- Fills the EMPTY pane DragonUI_NewEra_CharacterPane_Currency (created by TabButtons.lua,
+-- parented to NE.charpanel.frame.Inset). Renders the token/currency list with collapsible
+-- category headers.
+--
+-- DOWNPORT (CONTRACT_S1 §B): 3.3.5a has no retail ScrollBox stack, so this tab uses a NAMED
+-- FauxScrollFrame + manual header/entry row pool like the other secondary tabs.
+--
+-- DATA: GetCurrencyListSize() + GetCurrencyListInfo(i). 3.3.5 tuple shape differs by client,
+-- but icon is at POSITION 8 on this branch. We read defensively and never hard-error.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local ROW_HEIGHT = 24
+
+local function log(msg)
+ if CP._log then CP._log("CURRENCY: " .. tostring(msg)); return end
+ if NE.Log then NE.Log("CURRENCY", msg); return end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffffcc55DragonUI_NewEra|r [currency]: " .. tostring(msg))
+ end
+end
+
+local function setAtlas(tex, name, useAtlasSize)
+ if tex and NE.tex and NE.tex.SetAtlas then return NE.tex.SetAtlas(tex, name, useAtlasSize) end
+ return false
+end
+
+local function L(global, fallback)
+ local v = _G[global]
+ if type(v) == "string" and v ~= "" then return v end
+ return fallback
+end
+
+local function buildHeader(parent)
+ local h = CreateFrame("Button", nil, parent)
+ h:SetHeight(ROW_HEIGHT)
+
+ h._bgLeft = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgLeft, "options_listexpand_left", true)
+ h._bgLeft:SetPoint("LEFT", h, "LEFT", 0, 0)
+
+ h._bgRight = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgRight, "options_listexpand_right", true)
+ h._bgRight:SetPoint("RIGHT", h, "RIGHT", 0, 0)
+
+ h._bgMiddle = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgMiddle, "_options_listexpand_middle", false)
+ h._bgMiddle:SetPoint("TOPLEFT", h._bgLeft, "TOPRIGHT")
+ h._bgMiddle:SetPoint("BOTTOMRIGHT", h._bgRight, "BOTTOMLEFT")
+
+ local hl = h:CreateTexture(nil, "HIGHLIGHT")
+ if hl.SetColorTexture then hl:SetColorTexture(1, 1, 1, 0.10) else hl:SetTexture(1, 1, 1, 0.10) end
+ hl:SetPoint("TOPLEFT", h._bgLeft, "TOPRIGHT")
+ hl:SetPoint("BOTTOMRIGHT", h._bgRight, "BOTTOMLEFT")
+
+ h._name = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h._name:SetPoint("LEFT", h, "LEFT", 12, 0)
+ h._name:SetJustifyH("LEFT")
+ h:RegisterForClicks("LeftButtonUp")
+
+ return h
+end
+
+local function updateHeader(h, info)
+ h._name:SetText(info.name or "")
+ setAtlas(h._bgRight, info.isExpanded and "options_listexpand_right_expanded" or "options_listexpand_right", true)
+ local idx = info.index
+ h:SetScript("OnClick", function()
+ if ExpandCurrencyList then
+ -- DOWNPORT: 3.3.5 ExpandCurrencyList takes (index, expandFlag).
+ pcall(ExpandCurrencyList, idx, info.isExpanded and 0 or 1)
+ end
+ if CP.RefreshCurrency then CP.RefreshCurrency() end
+ end)
+end
+
+-- How many currencies are currently watched on the backpack (native cap = MAX_WATCHED_TOKENS = 3).
+local function numWatched()
+ local n = 0
+ if GetBackpackCurrencyInfo then
+ for i = 1, (MAX_WATCHED_TOKENS or 3) do
+ if GetBackpackCurrencyInfo(i) then n = n + 1 end
+ end
+ end
+ return n
+end
+
+-- Toggle whether a currency shows on the backpack. Guards the 3-currency cap when turning one ON,
+-- then refreshes this tab AND the combined bag's bottom money band.
+local function toggleWatch(info)
+ if not (info and SetCurrencyBackpack) then return end
+ local want = not info.isWatched
+ if want and numWatched() >= (MAX_WATCHED_TOKENS or 3) then
+ if UIErrorsFrame then
+ UIErrorsFrame:AddMessage(
+ (L("TOO_MANY_WATCHED_TOKENS", "You can watch at most %d currencies on your backpack.")):format(MAX_WATCHED_TOKENS or 3),
+ 1, 0.2, 0.2)
+ end
+ return
+ end
+ pcall(SetCurrencyBackpack, info.index, want and 1 or 0)
+ CP.RefreshCurrency()
+ if NE.combinedbag and NE.combinedbag.Refresh then NE.combinedbag.Refresh() end
+end
+CP.ToggleCurrencyWatch = toggleWatch
+
+local function buildEntry(parent)
+ local e = CreateFrame("Button", nil, parent)
+ e:SetHeight(ROW_HEIGHT)
+
+ e._icon = e:CreateTexture(nil, "ARTWORK")
+ e._icon:SetSize(16, 16)
+ e._icon:SetPoint("LEFT", e, "LEFT", 12, 0)
+
+ e._name = e:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ e._name:SetPoint("LEFT", e._icon, "RIGHT", 8, 0)
+ e._name:SetJustifyH("LEFT")
+
+ -- "Show on backpack" checkbox at the far right; the count sits to its left.
+ e._watch = CreateFrame("CheckButton", nil, e, "UICheckButtonTemplate")
+ e._watch:SetSize(22, 22)
+ e._watch:SetPoint("RIGHT", e, "RIGHT", -6, 0)
+ e._watch:SetHitRectInsets(2, 2, 2, 2)
+ e._watch:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_LEFT")
+ GameTooltip:SetText(L("TOKEN_OPTIONS_TRACK", "Show on Backpack"), 1, 1, 1)
+ GameTooltip:AddLine(L("TOKEN_OPTIONS_TRACK_HINT", "Watch this currency at the bottom of your bags (up to 3)."), nil, nil, nil, true)
+ GameTooltip:Show()
+ end)
+ e._watch:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ e._count = e:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ e._count:SetPoint("RIGHT", e._watch, "LEFT", -6, 0)
+ e._count:SetJustifyH("RIGHT")
+
+ e._name:SetPoint("RIGHT", e._count, "LEFT", -8, 0)
+
+ local hl = e:CreateTexture(nil, "HIGHLIGHT")
+ if hl.SetColorTexture then hl:SetColorTexture(1, 1, 1, 0.10) else hl:SetTexture(1, 1, 1, 0.10) end
+ hl:SetAllPoints(e)
+
+ e:RegisterForClicks("LeftButtonUp")
+ return e
+end
+
+local function updateEntry(e, info)
+ e._name:SetText(info.name or "")
+ e._count:SetText(tostring(info.count or 0))
+
+ if info.icon and info.icon ~= "" then
+ e._icon:SetTexture(info.icon)
+ e._icon:Show()
+ else
+ e._icon:Hide()
+ end
+
+ -- Watch checkbox reflects the current state; clicking it (or the row) toggles tracking.
+ if e._watch then
+ e._watch:SetChecked(info.isWatched and true or false)
+ e._watch:SetScript("OnClick", function() toggleWatch(info) end)
+ end
+ e:SetScript("OnClick", function() toggleWatch(info) end)
+end
+
+local pane
+local scroll, content
+local headerPool, entryPool = {}, {}
+local flat = {}
+local NUM_VISIBLE = 0
+local emptyLabel
+
+local function getPane()
+ if pane then return pane end
+ pane = _G.DragonUI_NewEra_CharacterPane_Currency or (CP.EnsurePane and CP.EnsurePane("Currency"))
+ return pane
+end
+
+local function acquireHeader()
+ for _, h in ipairs(headerPool) do
+ if not h._inUse then h._inUse = true; h:Show(); return h end
+ end
+ local h = buildHeader(content)
+ h._inUse = true
+ headerPool[#headerPool + 1] = h
+ return h
+end
+
+local function acquireEntry()
+ for _, e in ipairs(entryPool) do
+ if not e._inUse then e._inUse = true; e:Show(); return e end
+ end
+ local e = buildEntry(content)
+ e._inUse = true
+ entryPool[#entryPool + 1] = e
+ return e
+end
+
+local function releaseAll()
+ for _, h in ipairs(headerPool) do h._inUse = false; h:Hide() end
+ for _, e in ipairs(entryPool) do e._inUse = false; e:Hide() end
+end
+
+local function parseCurrencyInfo(idx)
+ -- GetCurrencyListInfo: name, isHeader, isExpanded, isUnused, isWatched, count, extraType, icon, itemID
+ local ok, a, b, c, d, e, f, g, h, i = pcall(GetCurrencyListInfo, idx)
+ if not ok or not a then return nil end
+
+ local name = a
+ local isHeader = b and true or false
+ local isExpanded = c and true or false
+ local isUnused = d and true or false
+ local isWatched = e and true or false
+ local count = tonumber(f) or 0
+ local icon = h or g
+
+ return {
+ index = idx,
+ name = name,
+ isHeader = isHeader,
+ isExpanded = isExpanded,
+ isUnused = isUnused,
+ isWatched = isWatched,
+ count = count,
+ icon = icon,
+ itemID = i,
+ }
+end
+
+local function updateScroll()
+ if not (scroll and content) then return end
+ releaseAll()
+
+ local total = #flat
+ if emptyLabel then if total == 0 then emptyLabel:Show() else emptyLabel:Hide() end end
+
+ local nRows = NUM_VISIBLE > 0 and NUM_VISIBLE or 1
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(scroll, total, nRows, ROW_HEIGHT) end
+ if NE.scrollbar and NE.scrollbar.CenterIfNoBar then NE.scrollbar.CenterIfNoBar(scroll, total > nRows) end
+ local offset = (FauxScrollFrame_GetOffset and FauxScrollFrame_GetOffset(scroll)) or 0
+
+ local rowW = content:GetWidth() or 200
+ if rowW <= 0 then rowW = 200 end
+
+ for i = 1, nRows do
+ local data = flat[offset + i]
+ if data then
+ local row
+ if data.kind == "header" then
+ row = acquireHeader()
+ updateHeader(row, data.info)
+ else
+ row = acquireEntry()
+ updateEntry(row, data.info)
+ end
+
+ local indent = data.kind == "entry" and 8 or 0
+ row:ClearAllPoints()
+ row:SetWidth(rowW - indent)
+ row:SetPoint("TOPLEFT", content, "TOPLEFT", indent, -(i - 1) * ROW_HEIGHT)
+ row:Show()
+ end
+ end
+end
+
+local function refresh()
+ if not getPane() then return end
+ if not scroll then return end
+ flat = {}
+
+ if not (GetCurrencyListSize and GetCurrencyListInfo) then
+ log("GetCurrencyListSize/GetCurrencyListInfo unavailable -- Currency pane degraded")
+ updateScroll()
+ return
+ end
+
+ local ok, total = pcall(GetCurrencyListSize)
+ if not ok or not total then total = 0 end
+
+ for i = 1, total do
+ local info = parseCurrencyInfo(i)
+ if info and not info.isUnused then
+ flat[#flat + 1] = { kind = info.isHeader and "header" or "entry", info = info }
+ end
+ end
+
+ updateScroll()
+end
+
+CP.RefreshCurrency = function()
+ local ok, err = pcall(refresh)
+ if not ok then log("RefreshCurrency error: " .. tostring(err)) end
+end
+
+local function recomputeVisible()
+ if not scroll then return end
+ local h = scroll:GetHeight() or 0
+ NUM_VISIBLE = math.max(1, math.floor(h / ROW_HEIGHT))
+end
+
+local function build()
+ local host = getPane()
+ if not host then log("Currency pane host missing -- cannot build"); return false end
+ if scroll then return true end
+
+ local bg = host:CreateTexture(nil, "BACKGROUND")
+ if not setAtlas(bg, "character-panel-background", false) then bg:Hide() end
+ bg:SetPoint("TOPLEFT", host, "TOPLEFT", 2, -2)
+ bg:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -2, 2)
+
+ scroll = CreateFrame("ScrollFrame", "DragonUI_NewEra_CurrencyScroll", host, "FauxScrollFrameTemplate")
+ scroll:SetPoint("TOPLEFT", host, "TOPLEFT", 10, -12)
+ scroll:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -24, 10)
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ FauxScrollFrame_OnVerticalScroll(self, offset, ROW_HEIGHT, updateScroll)
+ end)
+ scroll:EnableMouseWheel(true)
+ scroll:SetScript("OnMouseWheel", function(self, delta)
+ local sb = _G[(self:GetName() or "") .. "ScrollBar"]
+ if not sb then return end
+ local mn, mx = sb:GetMinMaxValues()
+ local v = sb:GetValue() - delta * ROW_HEIGHT
+ if v < mn then v = mn elseif v > mx then v = mx end
+ sb:SetValue(v)
+ end)
+ scroll:HookScript("OnSizeChanged", function() recomputeVisible(); updateScroll() end)
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, scroll, { x = -8 }) end
+
+ content = CreateFrame("Frame", nil, host)
+ content:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
+ content:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
+ content:SetHeight(1)
+ content:SetFrameLevel(scroll:GetFrameLevel() + 5)
+
+ emptyLabel = host:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
+ emptyLabel:SetPoint("TOP", host, "TOP", 0, -40)
+ emptyLabel:SetWidth(200)
+ emptyLabel:SetText(L("CURRENCY", "Currency") .. " unavailable")
+ emptyLabel:Hide()
+
+ recomputeVisible()
+ return true
+end
+
+local function init()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ if not build() then return end
+ pcall(refresh)
+ local host = getPane()
+ if host and not host._neCurrencyShowHooked then
+ host._neCurrencyShowHooked = true
+ host:HookScript("OnShow", function() recomputeVisible(); pcall(refresh) end)
+ end
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:RegisterEvent("CURRENCY_DISPLAY_UPDATE")
+boot:RegisterEvent("KNOWN_CURRENCY_TYPES_UPDATE")
+boot:RegisterEvent("PLAYER_MONEY")
+boot:SetScript("OnEvent", function(_, event)
+ if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
+ if C_Timer and C_Timer.After then C_Timer.After(0, init) else init() end
+ else
+ CP.RefreshCurrency()
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EditModeRegister.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EditModeRegister.lua
new file mode 100644
index 0000000..66c60ea
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EditModeRegister.lua
@@ -0,0 +1,18 @@
+-- DragonUI_NewEra/modules/character/EditModeRegister.lua — INTENTIONALLY REGISTERS NOTHING.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): the character panel is now our CUSTOM frame
+-- (NE.charpanel.frame = "DragonUI_NewEra_Character"), opened/closed like a UIPanel window. Per the
+-- project's Edit Mode scope rule (Edit Mode is scoped to always-on HUD frames, NOT toggled windows
+-- like Merchant/Mail/Bank/Gossip/Character) and retail's own behaviour (panels live in window slots,
+-- not as EditModeSystems), the character panel must NOT get an Edit Mode handle.
+--
+-- 3.3.5a additionally has no native Edit Mode at all; even if a DragonUI Edit Mode shim exists, a
+-- handle over the closed panel's slot would render an empty mystery box. If panel scaling/position is
+-- ever wanted, surface it in the New Era options tab (CharacterPanel.lua already registers there),
+-- not Edit Mode. (The custom frame IS movable — drag the title band — see CharacterPanel.lua.)
+--
+-- This file is kept (rather than deleted) so the load order in the TOC stays stable and the decision
+-- is documented at the point a future maintainer would look for it.
+
+local NE = DragonUI_NewEra
+-- (no-op: registers nothing by design)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentFlyout.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentFlyout.lua
new file mode 100644
index 0000000..9dece72
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentFlyout.lua
@@ -0,0 +1,453 @@
+-- DragonUI_NewEra/modules/character/EquipmentFlyout.lua — per-slot popout flyout: a
+-- small arrow on each equipped paperdoll slot that opens a grid of bag items fitting
+-- that slot; click one to equip it. Ported from retail's EquipmentFlyout (which the
+-- 3.3.5 client does not load) + NewEra's port of it.
+--
+-- 3.3.5 DOWNPORTS vs the NewEra source:
+-- * `local NE = DragonUI_NewEra`; arrows are wired onto the (reparented) Blizzard
+-- slot buttons, which live in CP.frame.Inset now.
+-- * Flyout parents to CP.frame (not _G.CharacterFrame) so it inherits the panel
+-- scale; the frame falls back to UIParent if the panel isn't built yet.
+-- * Equip uses the ItemRack physical swap (PickupContainerItem/PickupInventoryItem)
+-- to match EquipmentSets.lua's backend (more reliable than EquipItemByName on
+-- private servers — project memory). Out-of-combat only.
+-- * C_Container routed via compat with positional-global fallbacks; GetItemInfoInstant
+-- guarded (falls back to GetItemInfo equip-location at arg 9).
+-- * No SetShown (Show/Hide); SetNormalTexture takes a path; pcall around getters.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local function log(msg) if CP._log then CP._log(msg) end end
+
+-- Geometry.
+local ITEMS_PER_ROW = 5
+local EFITEM_W, EFITEM_H = 37, 37
+local EFITEM_XOFF, EFITEM_YOFF = 4, -5
+local BORDER = 3
+local MAXBAG = _G.NUM_BAG_SLOTS or 4
+local ARROW_TEX = "Interface\\PaperDollInfoFrame\\UI-GearManager-FlyoutButton"
+
+-- Which bag-item equip locations fit each inventory slot.
+local SLOT_INVTYPES = {
+ [1] = { INVTYPE_HEAD = true },
+ [2] = { INVTYPE_NECK = true },
+ [3] = { INVTYPE_SHOULDER = true },
+ [4] = { INVTYPE_BODY = true },
+ [5] = { INVTYPE_CHEST = true, INVTYPE_ROBE = true },
+ [6] = { INVTYPE_WAIST = true },
+ [7] = { INVTYPE_LEGS = true },
+ [8] = { INVTYPE_FEET = true },
+ [9] = { INVTYPE_WRIST = true },
+ [10] = { INVTYPE_HAND = true },
+ [11] = { INVTYPE_FINGER = true },
+ [12] = { INVTYPE_FINGER = true },
+ [13] = { INVTYPE_TRINKET = true },
+ [14] = { INVTYPE_TRINKET = true },
+ [15] = { INVTYPE_CLOAK = true },
+ [16] = { INVTYPE_WEAPON = true, INVTYPE_2HWEAPON = true, INVTYPE_WEAPONMAINHAND = true },
+ [17] = { INVTYPE_WEAPON = true, INVTYPE_WEAPONOFFHAND = true, INVTYPE_SHIELD = true, INVTYPE_HOLDABLE = true },
+ [18] = { INVTYPE_RANGED = true, INVTYPE_RANGEDRIGHT = true, INVTYPE_THROWN = true, INVTYPE_RELIC = true },
+ [19] = { INVTYPE_TABARD = true },
+ [0] = { INVTYPE_AMMO = true },
+}
+
+-- Slots that get an arrow.
+local SLOT_NAMES = {
+ "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot", "CharacterBackSlot",
+ "CharacterChestSlot", "CharacterShirtSlot", "CharacterTabardSlot", "CharacterWristSlot",
+ "CharacterHandsSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot",
+ "CharacterFinger0Slot", "CharacterFinger1Slot", "CharacterTrinket0Slot", "CharacterTrinket1Slot",
+ "CharacterMainHandSlot", "CharacterSecondaryHandSlot", "CharacterRangedSlot",
+}
+
+-- The weapon row opens DOWNWARD; everything else opens to the side.
+local VERTICAL_FLYOUTS = { [16] = true, [17] = true, [18] = true }
+
+-- ----------------------------------------------------------------------------
+-- Container helpers — compat C_Container with positional-global fallback.
+-- ----------------------------------------------------------------------------
+local function bagNumSlots(bag)
+ if C_Container and C_Container.GetContainerNumSlots then return C_Container.GetContainerNumSlots(bag) or 0 end
+ return GetContainerNumSlots(bag) or 0
+end
+local function bagItemLink(bag, slot)
+ if C_Container and C_Container.GetContainerItemLink then return C_Container.GetContainerItemLink(bag, slot) end
+ return GetContainerItemLink(bag, slot)
+end
+local function bagItemID(bag, slot)
+ if C_Container and C_Container.GetContainerItemID then return C_Container.GetContainerItemID(bag, slot) end
+ local link = bagItemLink(bag, slot)
+ local id = link and link:match("item:(%d+)")
+ return id and tonumber(id) or nil
+end
+-- equip location string for an item (id or link).
+local function equipLocOf(itemOrLink)
+ if type(GetItemInfoInstant) == "function" then
+ local _, _, _, eloc = GetItemInfoInstant(itemOrLink)
+ if eloc then return eloc end
+ end
+ return select(9, GetItemInfo(itemOrLink))
+end
+
+-- ----------------------------------------------------------------------------
+-- Arrow facing.
+-- ----------------------------------------------------------------------------
+local function setArrowFacing(arrow, facing)
+ local n, h = arrow:GetNormalTexture(), arrow:GetHighlightTexture()
+ if not (n and h) then return end
+ if facing == "down" then
+ n:SetTexCoord(0.15625, 0.84375, 0.5, 0)
+ h:SetTexCoord(0.15625, 0.84375, 1, 0.5)
+ elseif facing == "up" then
+ n:SetTexCoord(0.15625, 0.84375, 0, 0.5)
+ h:SetTexCoord(0.15625, 0.84375, 0.5, 1)
+ elseif facing == "left" then
+ n:SetTexCoord(0.15625, 0, 0.84375, 0, 0.15625, 0.5, 0.84375, 0.5)
+ h:SetTexCoord(0.15625, 0.5, 0.84375, 0.5, 0.15625, 1, 0.84375, 1)
+ else
+ n:SetTexCoord(0.15625, 0.5, 0.84375, 0.5, 0.15625, 0, 0.84375, 0)
+ h:SetTexCoord(0.15625, 1, 0.84375, 1, 0.15625, 0.5, 0.84375, 0.5)
+ end
+end
+
+-- ----------------------------------------------------------------------------
+-- Usability check — set the item on a hidden scanning tooltip and look for a RED
+-- requirement line (level / class / proficiency all render red when unmet). A
+-- broken-item durability line is also red but still equippable, so skip it.
+-- ----------------------------------------------------------------------------
+local DUR_PREFIX = ((DURABILITY_TEMPLATE or "Durability %d / %d"):match("^(.-)%s*%%%d")) or "Durability"
+local scanTip
+local function isUsable(bag, slot)
+ if not scanTip then
+ scanTip = CreateFrame("GameTooltip", "NE_EquipFlyoutScanTooltip", nil, "GameTooltipTemplate")
+ scanTip:SetOwner(UIParent, "ANCHOR_NONE")
+ end
+ scanTip:ClearLines()
+ local ok = pcall(scanTip.SetBagItem, scanTip, bag, slot)
+ if not ok then return true end -- can't scan -> don't hide it
+ for i = 2, scanTip:NumLines() do
+ for _, side in ipairs({ "Left", "Right" }) do
+ local fs = _G["NE_EquipFlyoutScanTooltipText" .. side .. i]
+ if fs and fs:IsShown() then
+ local txt = fs:GetText()
+ local isDur = txt and txt:sub(1, #DUR_PREFIX) == DUR_PREFIX
+ if txt and not isDur then
+ local r, g, b = fs:GetTextColor()
+ if r and r > 0.8 and g < 0.3 and b < 0.3 then return false end
+ end
+ end
+ end
+ end
+ return true
+end
+
+-- ----------------------------------------------------------------------------
+-- Bag enumeration for a slot.
+-- ----------------------------------------------------------------------------
+local function anyUsableForSlot(slotID)
+ local accept = SLOT_INVTYPES[slotID]
+ if not accept then return false end
+ for bag = 0, MAXBAG do
+ local n = bagNumSlots(bag)
+ for s = 1, n do
+ local id = bagItemID(bag, s)
+ if id then
+ local eloc = equipLocOf(id)
+ if eloc and accept[eloc] and isUsable(bag, s) then return true end
+ end
+ end
+ end
+ return false
+end
+
+local function itemsForSlot(slotID)
+ local accept = SLOT_INVTYPES[slotID]
+ local out = {}
+ if not accept then return out end
+ for bag = 0, MAXBAG do
+ local n = bagNumSlots(bag)
+ for s = 1, n do
+ local id = bagItemID(bag, s)
+ if id then
+ local eloc = equipLocOf(id)
+ if eloc and accept[eloc] and isUsable(bag, s) then
+ local texture, count, quality, link
+ if C_Container and C_Container.GetContainerItemInfo then
+ local info = C_Container.GetContainerItemInfo(bag, s)
+ if info then
+ texture = info.iconFileID
+ count = info.stackCount
+ quality = info.quality
+ link = info.hyperlink
+ end
+ end
+ if not texture then
+ local tex, cnt, _, qual = GetContainerItemInfo(bag, s)
+ texture, count, quality = tex, cnt, qual
+ end
+ out[#out + 1] = {
+ bag = bag, slot = s, itemID = id,
+ link = link or bagItemLink(bag, s),
+ texture = texture,
+ count = count or 1,
+ quality = quality,
+ }
+ end
+ end
+ end
+ end
+ return out
+end
+
+-- ----------------------------------------------------------------------------
+-- The shared flyout frame.
+-- ----------------------------------------------------------------------------
+local flyout
+
+-- Equip a chosen item via the ItemRack physical swap (out of combat only).
+local function equipItem(btn)
+ local data = btn._data
+ local slotID = flyout and flyout._slotID
+ if not (data and slotID) then return end
+ if InCombatLockdown() then
+ if UIErrorsFrame then UIErrorsFrame:AddMessage(ERR_NOT_IN_COMBAT or "Can't do that in combat", 1, 0.1, 0.1, 1) end
+ return
+ end
+ if CursorHasItem() or SpellIsTargeting() then return end
+
+ -- 2H into main hand: clear the off-hand into the bag first.
+ if slotID == 16 then
+ local eloc = equipLocOf(data.itemID)
+ if eloc == "INVTYPE_2HWEAPON" and GetInventoryItemLink("player", 17) then
+ ClearCursor(); PickupInventoryItem(17); PutItemInBackpack()
+ ClearCursor()
+ end
+ end
+
+ ClearCursor()
+ if C_Container and C_Container.PickupContainerItem then
+ C_Container.PickupContainerItem(data.bag, data.slot)
+ else
+ PickupContainerItem(data.bag, data.slot)
+ end
+ PickupInventoryItem(slotID)
+ ClearCursor()
+ flyout:Hide()
+end
+
+local function makeFlyoutButton(i)
+ local bf = flyout.buttonFrame
+ local b = CreateFrame("Button", "NE_EquipFlyoutButton" .. i, bf, "ItemButtonTemplate")
+ b:SetSize(EFITEM_W, EFITEM_H)
+ if b.icon then b.icon:SetAllPoints(b) end
+ local nt = b:GetNormalTexture()
+ if nt then nt:SetTexture(nil); nt:Hide() end
+ b:RegisterForDrag("LeftButton")
+ b:SetScript("OnClick", equipItem)
+ b:SetScript("OnDragStart", function(self)
+ if self._data then
+ if C_Container and C_Container.PickupContainerItem then C_Container.PickupContainerItem(self._data.bag, self._data.slot)
+ else PickupContainerItem(self._data.bag, self._data.slot) end
+ end
+ end)
+ b:SetScript("OnEnter", function(self)
+ if not self._data then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetBagItem(self._data.bag, self._data.slot)
+ GameTooltip:Show()
+ end)
+ b:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ return b
+end
+
+local function buildFlyout()
+ if flyout then return flyout end
+ local parent = CP.frame or UIParent
+ flyout = CreateFrame("Frame", "NE_EquipmentFlyout", parent)
+ flyout:SetSize(43, 43)
+ flyout:SetFrameStrata("HIGH")
+ flyout:SetToplevel(true)
+ flyout:Hide()
+ flyout.buttons = {}
+
+ local bf = CreateFrame("Frame", "NE_EquipmentFlyoutButtons", flyout)
+ bf:EnableMouse(true)
+ bf:SetClampedToScreen(true)
+ -- Simple backdrop so the grid reads as a panel.
+ if bf.SetBackdrop then
+ bf:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8x8",
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = false, edgeSize = 14,
+ insets = { left = 3, right = 3, top = 3, bottom = 3 },
+ })
+ bf:SetBackdropColor(0, 0, 0, 0.85)
+ bf:SetBackdropBorderColor(0.6, 0.6, 0.6, 1)
+ end
+ flyout.buttonFrame = bf
+
+ -- Auto-hide when the mouse leaves the slot + arrow + flyout for a grace period.
+ flyout:SetScript("OnUpdate", function(self, elapsed)
+ local over = self:IsMouseOver() or self.buttonFrame:IsMouseOver()
+ or (self._slotBtn and self._slotBtn:IsMouseOver())
+ or (self._arrow and self._arrow:IsMouseOver())
+ if over then self._leaveT = 0
+ else
+ self._leaveT = (self._leaveT or 0) + elapsed
+ if self._leaveT > 0.25 then self:Hide() end
+ end
+ end)
+ flyout:SetScript("OnHide", function(self)
+ self._leaveT = 0
+ if self._arrow and self._arrow._closedFacing then setArrowFacing(self._arrow, self._arrow._closedFacing) end
+ self._slotID, self._slotBtn, self._arrow = nil, nil, nil
+ end)
+ return flyout
+end
+
+local function showFlyout(slotBtn, arrow)
+ buildFlyout()
+ local slotID = slotBtn:GetID()
+ local items = itemsForSlot(slotID)
+ if #items == 0 then flyout:Hide(); return end
+
+ flyout._slotID = slotID
+ flyout._slotBtn = slotBtn
+ flyout._arrow = arrow
+ flyout._leaveT = 0
+
+ local bf = flyout.buttonFrame
+ local n = #items
+ local rows = math.ceil(n / ITEMS_PER_ROW)
+ local perRow = math.min(n, ITEMS_PER_ROW)
+
+ local rel = arrow or slotBtn
+ flyout:ClearAllPoints()
+ flyout:SetFrameLevel(slotBtn:GetFrameLevel() + 5)
+ flyout:SetPoint("TOPLEFT", slotBtn, "TOPLEFT", -BORDER, BORDER)
+ bf:ClearAllPoints()
+ if VERTICAL_FLYOUTS[slotID] then
+ bf:SetPoint("TOPLEFT", rel, "BOTTOMLEFT", 0, 0)
+ else
+ bf:SetPoint("TOPLEFT", rel, "TOPRIGHT", 0, -3)
+ end
+ bf:SetFrameLevel(flyout:GetFrameLevel() + 1)
+
+ if arrow and arrow._openFacing then setArrowFacing(arrow, arrow._openFacing) end
+ bf:SetWidth((perRow * EFITEM_W) + ((perRow - 1) * EFITEM_XOFF) + BORDER * 2 + 6)
+ bf:SetHeight(EFITEM_H + ((rows - 1) * (EFITEM_H - EFITEM_YOFF)) + BORDER * 2 + 6)
+
+ for i = #flyout.buttons + 1, n do flyout.buttons[i] = makeFlyoutButton(i) end
+ for i = 1, #flyout.buttons do
+ local b = flyout.buttons[i]
+ if i <= n then
+ local data = items[i]
+ b._data = data
+ if SetItemButtonTexture then SetItemButtonTexture(b, data.texture) elseif b.icon then b.icon:SetTexture(data.texture) end
+ if SetItemButtonCount then SetItemButtonCount(b, data.count) end
+ if SetItemButtonQuality then SetItemButtonQuality(b, data.quality, data.itemID) end
+ b:ClearAllPoints()
+ local col = (i - 1) % ITEMS_PER_ROW
+ local row = math.floor((i - 1) / ITEMS_PER_ROW)
+ b:SetPoint("TOPLEFT", bf, "TOPLEFT",
+ BORDER + 3 + col * (EFITEM_W + EFITEM_XOFF),
+ -BORDER - 3 - row * (EFITEM_H - EFITEM_YOFF))
+ b:Show()
+ else
+ b:Hide()
+ end
+ end
+ flyout:Show()
+end
+
+-- ----------------------------------------------------------------------------
+-- Per-slot arrow buttons.
+-- ----------------------------------------------------------------------------
+local function attachArrow(slotBtn)
+ if slotBtn._neFlyoutArrow then return slotBtn._neFlyoutArrow end
+ local vertical = VERTICAL_FLYOUTS[slotBtn:GetID()] and true or false
+
+ local arrow = CreateFrame("Button", nil, slotBtn)
+ arrow._vertical = vertical
+ arrow._closedFacing = vertical and "down" or "right"
+ arrow._openFacing = vertical and "up" or "left"
+
+ if vertical then
+ arrow:SetSize(38, 16)
+ arrow:SetPoint("TOP", slotBtn, "BOTTOM", 0, 4)
+ else
+ arrow:SetSize(16, 38)
+ arrow:SetPoint("LEFT", slotBtn, "RIGHT", -8, 0)
+ end
+ -- SetNormalTexture/SetHighlightTexture take a PATH on 3.3.5.
+ arrow:SetNormalTexture(ARROW_TEX)
+ arrow:SetHighlightTexture(ARROW_TEX)
+ arrow:SetFrameLevel((slotBtn:GetFrameLevel() or 1) + 2)
+ setArrowFacing(arrow, arrow._closedFacing)
+ arrow:Hide()
+
+ arrow:SetScript("OnClick", function(self)
+ if flyout and flyout:IsShown() and flyout._slotBtn == slotBtn then
+ flyout:Hide()
+ else
+ showFlyout(slotBtn, self)
+ end
+ end)
+
+ slotBtn._neFlyoutArrow = arrow
+ return arrow
+end
+
+local function wireAllSlots()
+ for _, name in ipairs(SLOT_NAMES) do
+ local s = _G[name]
+ if s and not s._neFlyoutWired then
+ s._neFlyoutWired = true
+ attachArrow(s)
+ end
+ end
+end
+
+-- Show arrows only on slots that have a usable swap candidate.
+local function refreshArrows()
+ for _, name in ipairs(SLOT_NAMES) do
+ local s = _G[name]
+ if s and s._neFlyoutArrow then
+ if anyUsableForSlot(s:GetID()) then s._neFlyoutArrow:Show() else s._neFlyoutArrow:Hide() end
+ end
+ end
+end
+
+local function hideAllArrows()
+ if flyout then flyout:Hide() end
+ for _, name in ipairs(SLOT_NAMES) do
+ local s = _G[name]
+ if s and s._neFlyoutArrow then s._neFlyoutArrow:Hide() end
+ end
+end
+
+CP.WireEquipmentFlyout = wireAllSlots
+CP.ShowEquipmentFlyoutArrows = refreshArrows
+CP.HideEquipmentFlyoutArrows = hideAllArrows
+
+local function onEquipTab()
+ return CP._activeSidebar == 3 and CP.frame and CP.frame:IsShown()
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("BAG_UPDATE")
+boot:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+boot:SetScript("OnEvent", function(_, event)
+ if NE.modules and NE.modules.IsBooted and not NE.modules.IsBooted("character") then return end
+ if event == "PLAYER_LOGIN" then
+ wireAllSlots()
+ if CP.frame and CP.frame.HookScript then
+ CP.frame:HookScript("OnHide", hideAllArrows)
+ end
+ else
+ if onEquipTab() then refreshArrows() end
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentManagerPane.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentManagerPane.lua
new file mode 100644
index 0000000..4852c01
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentManagerPane.lua
@@ -0,0 +1,726 @@
+-- DragonUI_NewEra/modules/character/EquipmentManagerPane.lua — the Equipment
+-- Manager sidebar pane (set list + add/edit/delete + icon picker), ported from
+-- retail's PaperDollEquipmentManagerPane (and NewEra's port of it) to 3.3.5a.
+--
+-- Hosted in NE.charpanel.frame.InsetRight (the sidebar host that the stats pane
+-- also uses). The Sidebar agent's Tab3 (equipment) calls NE.charpanel.ShowEquipManager()
+-- to reveal this pane and HideEquipManager() to hide it; SelectSidebar(index) is also
+-- supported for retail-index callers (3 = equipment, anything else = hide).
+--
+-- All set data / equip logic goes through NE.equipsets (EquipmentSets.lua), which
+-- dispatches NATIVE vs CUSTOM — so this file is pure UI and never touches the backend
+-- choice.
+--
+-- 3.3.5 DOWNPORTS vs the NewEra source (which used retail-only widgets):
+-- * WowScrollBox / MinimalScrollBar / ScrollUtil -> a NAMED FauxScrollFrame
+-- (FauxScrollFrameTemplate REQUIRES a name on 3.3.5 or it errors) + a bounded
+-- row pool. Set counts are tiny (<=10), so non-virtualised is fine.
+-- * IconSelectorPopupFrameTemplate / IconDataProviderMixin (absent on 3.3.5)
+-- -> our own icon-grid popup driven by GetNumMacroIcons()/GetMacroIconInfo()
+-- (the 3.3.5-native gear-set icon source — same data PaperDollFrame uses).
+-- * No SetShown (Show/Hide only); SetNormalTexture takes a PATH; child frame
+-- levels raised so art isn't occluded; pcall around backend getters.
+-- * host = CP.frame.InsetRight, not _G.CharacterFrame.InsetRight.
+
+local NE = DragonUI_NewEra
+local L = NE:GetLocale()
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+local M = NE.equipsets
+
+local BTN_H = 40 -- gear-set row height
+local ICON_NEW = "Interface\\PaperDollInfoFrame\\Character-Plus"
+local ICON_QM = "Interface\\Icons\\INV_Misc_QuestionMark"
+
+local function log(msg) if CP._log then CP._log(msg) end end
+
+-- Localised-string-or-fallback helper.
+local function getLocaleString(global, fallback)
+ if type(global) == "string" and global ~= "" then
+ local v = _G[global]
+ if type(v) == "string" and v ~= "" then
+ return v
+ end
+ end
+ if type(L) == "table" and type(fallback) == "string" and fallback ~= "" then
+ local rawVal = rawget(L, fallback)
+ if type(rawVal) == "string" and rawVal ~= "" then
+ return rawVal
+ end
+ local ok, res = pcall(function() return L[fallback] end)
+ if ok and type(res) == "string" and res ~= "" then
+ return res
+ end
+ end
+ return fallback
+end
+
+if type(L) == "table" then
+ local mt = getmetatable(L) or {}
+ mt.__call = function(self, global, fallback)
+ return getLocaleString(global, fallback)
+ end
+ setmetatable(L, mt)
+else
+ L = function(global, fallback)
+ return getLocaleString(global, fallback)
+ end
+end
+
+local NORMAL = NORMAL_FONT_COLOR or { r = 1, g = 0.82, b = 0 }
+local RED = RED_FONT_COLOR or { r = 1, g = 0.1, b = 0.1 }
+local GREEN = GREEN_FONT_COLOR or { r = 0.1, g = 1, b = 0.1 }
+
+-- ----------------------------------------------------------------------------
+-- StaticPopups (delete confirm + save/overwrite confirm).
+-- ----------------------------------------------------------------------------
+-- Never assign the StaticPopupDialogs global itself: writing it taints the global
+-- for every reader, which blocks protected OnAccept handlers such as ReplaceEnchant.
+StaticPopupDialogs["NE_CONFIRM_DELETE_EQUIPMENT_SET"] = {
+ text = L("CONFIRM_DELETE_EQUIPMENT_SET", "Delete the equipment set '%s'?"),
+ button1 = YES or "Yes", button2 = NO or "No",
+ OnAccept = function(_, data) if M then M.Delete(data) end; if CP.RefreshEquipPane then CP.RefreshEquipPane() end end,
+ timeout = 0, hideOnEscape = 1, whileDead = 1, showAlert = 1,
+}
+StaticPopupDialogs["NE_CONFIRM_SAVE_EQUIPMENT_SET"] = {
+ text = L("CONFIRM_SAVE_EQUIPMENT_SET", "Overwrite the equipment set '%s' with your currently equipped items?"),
+ button1 = YES or "Yes", button2 = NO or "No",
+ OnAccept = function(_, data)
+ if M and data then M.Save(data.setID, data.icon) end
+ if CP.RefreshEquipPane then CP.RefreshEquipPane() end
+ end,
+ timeout = 0, hideOnEscape = 1, whileDead = 1, showAlert = 1,
+}
+
+-- ============================================================================
+-- Icon-picker popup. 3.3.5-native data: GetNumMacroIcons()/GetMacroIconInfo(i)
+-- give the same icon list PaperDollFrame's GearManagerDialogPopup uses. We render
+-- a named FauxScrollFrame grid (5 per row), an edit box for the set name, and
+-- Okay/Cancel. mode = "new" (create) or "edit" (rename existing setID).
+-- ============================================================================
+local ICONS_PER_ROW = 5
+local ICON_ROWS = 4
+local ICON_BTN = 36
+local ICON_PAD = 6
+local popup
+
+local function numMacroIcons()
+ if type(GetNumMacroIcons) == "function" then return GetNumMacroIcons() or 0 end
+ return 0
+end
+local function macroIcon(i)
+ if type(GetMacroIconInfo) == "function" then return GetMacroIconInfo(i) end
+ return ICON_QM
+end
+
+local function popupSelect(self, texture)
+ self.selectedTexture = texture
+ if self.selPreview then self.selPreview:SetTexture(texture or ICON_QM) end
+ -- DOWNPORT/REPORT: light the selected icon's border immediately (don't wait for the next scroll).
+ if self.iconBtns then
+ for _, b in ipairs(self.iconBtns) do
+ if b.sel then
+ if b._tex and b._tex == texture then b.sel:Show() else b.sel:Hide() end
+ end
+ end
+ end
+end
+
+local function popupUpdate()
+ local self = popup
+ if not self then return end
+ local total = numMacroIcons()
+ local offset = FauxScrollFrame_GetOffset(self.scroll) or 0
+ for i = 1, ICONS_PER_ROW * ICON_ROWS do
+ local btn = self.iconBtns[i]
+ local realIndex = (offset * ICONS_PER_ROW) + i
+ if realIndex <= total then
+ local tex = macroIcon(realIndex)
+ btn.icon:SetTexture(tex)
+ btn._tex = tex
+ btn:Show()
+ if self.selectedTexture and tex == self.selectedTexture then
+ btn.sel:Show()
+ else
+ btn.sel:Hide()
+ end
+ else
+ btn:Hide()
+ end
+ end
+ FauxScrollFrame_Update(self.scroll, math.ceil(total / ICONS_PER_ROW), ICON_ROWS, ICON_BTN + 4)
+end
+
+local function buildPopup()
+ if popup then return popup end
+ local parent = CP.frame or UIParent
+ local p = CreateFrame("Frame", "NE_GearManagerPopup", parent)
+ p:SetSize(ICONS_PER_ROW * (ICON_BTN + 4) + 40, ICON_ROWS * (ICON_BTN + 4) + 110)
+ p:SetFrameStrata("DIALOG")
+ p:SetToplevel(true)
+ p:ClearAllPoints()
+ -- DOWNPORT/REPORT: anchor the picker vertically CENTERED beside the panel (was hung off the top-right
+ -- corner, so it spilled downward past the frame — looked detached/messy).
+ p:SetPoint("LEFT", parent, "RIGHT", 8, 0)
+ p:EnableMouse(true)
+ p:Hide()
+
+ -- Backdrop (use a simple dialog backdrop available on 3.3.5).
+ if p.SetBackdrop then
+ p:SetBackdrop({
+ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
+ edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
+ tile = true, tileSize = 32, edgeSize = 32,
+ insets = { left = 11, right = 12, top = 12, bottom = 11 },
+ })
+ end
+
+ local header = p:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ header:SetPoint("TOP", p, "TOP", 0, -14)
+ header:SetText(L("GEARSETS_POPUP_TEXT", "Set Name:"))
+
+ -- Name edit box.
+ local eb = CreateFrame("EditBox", "NE_GearManagerPopupEditBox", p, "InputBoxTemplate")
+ eb:SetSize(p:GetWidth() - 60, 20)
+ eb:SetPoint("TOP", header, "BOTTOM", 0, -6)
+ eb:SetAutoFocus(false)
+ eb:SetMaxLetters(16) -- match the "(Max 16 Characters)" header
+ eb:SetScript("OnEscapePressed", function(self) self:GetParent():Hide() end)
+ p.editBox = eb
+
+ -- Named FauxScrollFrame for the icon grid (NAME REQUIRED on 3.3.5). DOWNPORT/REPORT: anchored cleanly
+ -- BELOW the edit box. The old anchor hung the grid off a separate preview icon's BOTTOMRIGHT with a
+ -- +40 nudge, which shoved a column past the popup edge (detached column) and left a gap under the
+ -- editbox. The in-grid selection border (btn.sel) shows the chosen icon, so no preview swatch needed.
+ local scroll = CreateFrame("ScrollFrame", "NE_GearManagerPopupScroll", p, "FauxScrollFrameTemplate")
+ scroll:SetSize(ICONS_PER_ROW * (ICON_BTN + 4), ICON_ROWS * (ICON_BTN + 4))
+ scroll:SetPoint("TOPLEFT", p, "TOPLEFT", 20, -70)
+ scroll:SetScript("OnVerticalScroll", function(self, off)
+ FauxScrollFrame_OnVerticalScroll(self, off, ICON_BTN + 4, popupUpdate)
+ end)
+ -- DOWNPORT: hand-built minimal scrollbar (Reskin's stock-slider re-skin didn't render).
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, scroll) end
+ p.scroll = scroll
+
+ -- DOWNPORT/REPORT: wheel scrolling for the icon grid (BuildCustom wires no wheel; the custom thumb
+ -- alone left it unscrollable). Nudge the hidden Faux slider value; it auto-clamps + fires the scroll.
+ scroll:EnableMouseWheel(true)
+ scroll:SetScript("OnMouseWheel", function(self, delta)
+ local sb = _G[(self:GetName() or "") .. "ScrollBar"]
+ if not sb then return end
+ local mn, mx = sb:GetMinMaxValues()
+ local v = sb:GetValue() - delta * (ICON_BTN + 4)
+ if v < mn then v = mn elseif v > mx then v = mx end
+ sb:SetValue(v)
+ end)
+
+ -- Icon buttons (fixed pool of ICONS_PER_ROW*ICON_ROWS). DOWNPORT/REPORT: parented to the SCROLL (not
+ -- the popup) so they sit ABOVE the mouse-enabled faux-scroll frame and actually receive clicks — as
+ -- children of `p` they were occluded by the scroll frame and clicks did nothing.
+ p.iconBtns = {}
+ for i = 1, ICONS_PER_ROW * ICON_ROWS do
+ local b = CreateFrame("Button", "NE_GearManagerPopupIcon" .. i, scroll)
+ b:SetSize(ICON_BTN, ICON_BTN)
+ local col = (i - 1) % ICONS_PER_ROW
+ local row = math.floor((i - 1) / ICONS_PER_ROW)
+ b:SetPoint("TOPLEFT", scroll, "TOPLEFT", col * (ICON_BTN + 4), -(row * (ICON_BTN + 4)))
+ b.icon = b:CreateTexture(nil, "ARTWORK")
+ b.icon:SetAllPoints(b)
+ b.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ b.sel = b:CreateTexture(nil, "OVERLAY")
+ b.sel:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
+ b.sel:SetBlendMode("ADD")
+ b.sel:SetSize(ICON_BTN + 32, ICON_BTN + 32)
+ b.sel:SetPoint("CENTER")
+ b.sel:Hide()
+ b:SetScript("OnClick", function(self) popupSelect(p, self._tex) end)
+ -- Hover highlight = button-sized (ButtonHilight-Square is near-solid, so scaling it up like the
+ -- transparent-margined selection border made it far too big). Auto-sized to the button.
+ b:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
+ p.iconBtns[i] = b
+ end
+
+ -- Okay / Cancel.
+ local okay = CreateFrame("Button", nil, p, "UIPanelButtonTemplate")
+ okay:SetSize(78, 22)
+ okay:SetText(L("OKAY", "Okay"))
+ okay:SetPoint("BOTTOMRIGHT", p, "BOTTOM", -2, 12)
+ okay:SetScript("OnClick", function()
+ local text = p.editBox:GetText()
+ if not text or text == "" then return end
+ local icon = p.selectedTexture
+ local existingID = M and M.GetSetID(text)
+
+ if p.mode == "edit" then
+ if existingID and text ~= p.origName then
+ if UIErrorsFrame then UIErrorsFrame:AddMessage(L("EQUIPMENT_SETS_CANT_RENAME", "A set with that name already exists."), 1, 0.1, 0.1, 1) end
+ return
+ end
+ M.Modify(p.setID, text, icon)
+ CP._equipPane.selectedSetID = p.setID
+ else
+ if existingID then
+ -- overwrite path (same name) — save into the existing set
+ M.Save(existingID, icon)
+ CP._equipPane.selectedSetID = existingID
+ else
+ if M.GetNumSets() >= M.MAX_SETS then
+ if UIErrorsFrame then UIErrorsFrame:AddMessage(L("EQUIPMENT_SETS_TOO_MANY", "You have too many equipment sets."), 1, 0.1, 0.1, 1) end
+ return
+ end
+ M.Create(text, icon)
+ CP._equipPane.selectedSetID = M.GetSetID(text)
+ end
+ end
+ p:Hide()
+ if CP.RefreshEquipPane then CP.RefreshEquipPane() end
+ end)
+
+ local cancel = CreateFrame("Button", nil, p, "UIPanelButtonTemplate")
+ cancel:SetSize(78, 22)
+ cancel:SetText(L("CANCEL", "Cancel"))
+ cancel:SetPoint("BOTTOMLEFT", p, "BOTTOM", 2, 12)
+ cancel:SetScript("OnClick", function() p:Hide() end)
+
+ -- ESC closes.
+ if NE.FrameUtil and NE.FrameUtil.EscClose then
+ NE.FrameUtil.EscClose("NE_GearManagerPopup")
+ else
+ tinsert(UISpecialFrames, "NE_GearManagerPopup")
+ end
+
+ popup = p
+ return p
+end
+
+local function openPopupNew()
+ local p = buildPopup()
+ if not p then return end
+ p.mode = "new"
+ p.setID, p.origName = nil, nil
+ p.selectedTexture = nil
+ p.editBox:SetText("")
+ -- pre-select the first icon for a non-blank preview
+ popupSelect(p, macroIcon(1))
+ p:Show()
+ popupUpdate()
+ p.editBox:SetFocus()
+end
+
+local function openPopupEdit(setID, name)
+ local p = buildPopup()
+ if not p then return end
+ p.mode = "edit"
+ p.setID = setID
+ p.origName = name
+ local _, icon = M.GetSetInfo(setID)
+ p.selectedTexture = icon
+ p.editBox:SetText(name or "")
+ popupSelect(p, icon or macroIcon(1))
+ p:Show()
+ popupUpdate()
+ p.editBox:SetFocus()
+ p.editBox:HighlightText()
+end
+
+-- ============================================================================
+-- Gear-set list row (port of GearSetButtonTemplate, simplified for 3.3.5).
+-- ============================================================================
+local function makeGearButton(parent)
+ local b = CreateFrame("Button", nil, parent)
+ b:SetHeight(BTN_H)
+ b:RegisterForClicks("LeftButtonUp")
+ b:RegisterForDrag("LeftButton")
+ b:SetScript("OnDragStart", function(self) if self.setID and M then M.Pickup(self.setID) end end)
+
+ -- Alternating stripe (BACKGROUND).
+ local stripe = b:CreateTexture(nil, "BACKGROUND")
+ stripe:SetPoint("TOPLEFT", 1, 0)
+ stripe:SetPoint("BOTTOMRIGHT", 0, 0)
+ stripe:Hide()
+ b.Stripe = stripe
+
+ -- Icon.
+ local icon = b:CreateTexture(nil, "ARTWORK")
+ icon:SetSize(34, 34)
+ icon:SetPoint("LEFT", 4, 0)
+ icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ b.icon = icon
+
+ -- Name.
+ local text = b:CreateFontString(nil, "ARTWORK", "GameFontNormalLeft")
+ text:SetPoint("LEFT", 42, 0)
+ -- DOWNPORT/REPORT: the narrow sidebar truncated "New Equipment Set" ("New Equipmen..."). Use the full
+ -- width (the hover-only edit/delete icons sit over the far-right end of a long name, which is fine).
+ text:SetPoint("RIGHT", -6, 0)
+ text:SetJustifyH("LEFT")
+ b.text = text
+
+ -- Equipped check (BORDER).
+ local check = b:CreateTexture(nil, "BORDER")
+ check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
+ check:SetSize(18, 18)
+ check:SetPoint("RIGHT", -4, 0)
+ check:Hide()
+ b.Check = check
+
+ -- Hover highlight.
+ local hl = b:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
+ hl:SetBlendMode("ADD")
+ hl:SetAlpha(0.4)
+ hl:SetAllPoints(b)
+
+ -- Selected bar (OVERLAY).
+ local selbar = b:CreateTexture(nil, "OVERLAY")
+ selbar:SetTexture("Interface\\FriendsFrame\\UI-FriendsFrame-HighlightBar")
+ selbar:SetTexCoord(0.2, 0.8, 0, 1)
+ selbar:SetAlpha(0.4)
+ selbar:SetBlendMode("ADD")
+ selbar:SetAllPoints(b)
+ selbar:Hide()
+ b.SelectedBar = selbar
+
+ -- Delete (X) — shown on hover.
+ local del = CreateFrame("Button", nil, b)
+ del:SetSize(14, 14)
+ del:SetPoint("BOTTOMRIGHT", -2, 2)
+ local delTex = del:CreateTexture(nil, "ARTWORK")
+ delTex:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up")
+ delTex:SetAllPoints(del)
+ delTex:SetAlpha(0.6)
+ del:SetScript("OnEnter", function(self) delTex:SetAlpha(1); GameTooltip:SetOwner(self, "ANCHOR_RIGHT"); GameTooltip:SetText(DELETE or "Delete"); GameTooltip:Show() end)
+ del:SetScript("OnLeave", function() delTex:SetAlpha(0.6); GameTooltip:Hide() end)
+ del:SetScript("OnClick", function(self)
+ local pb = self:GetParent()
+ if pb.setID then StaticPopup_Show("NE_CONFIRM_DELETE_EQUIPMENT_SET", pb.text:GetText(), nil, pb.setID) end
+ end)
+ del:Hide()
+ b.DeleteButton = del
+
+ -- Edit (gear) — left of delete, opens rename/icon popup.
+ local edit = CreateFrame("Button", nil, b)
+ edit:SetSize(16, 16)
+ edit:SetPoint("RIGHT", del, "LEFT", -1, 0)
+ local editTex = edit:CreateTexture(nil, "ARTWORK")
+ editTex:SetTexture("Interface\\WorldMap\\Gear_64Grey")
+ editTex:SetAllPoints(edit)
+ editTex:SetAlpha(0.6)
+ edit:SetScript("OnEnter", function(self) editTex:SetAlpha(1); GameTooltip:SetOwner(self, "ANCHOR_RIGHT"); GameTooltip:SetText(L("EQUIPMENT_SET_SETTINGS", "Edit")); GameTooltip:Show() end)
+ edit:SetScript("OnLeave", function() editTex:SetAlpha(0.6); GameTooltip:Hide() end)
+ edit:SetScript("OnClick", function(self)
+ local pb = self:GetParent()
+ if pb.setID then openPopupEdit(pb.setID, pb.text:GetText()) end
+ end)
+ edit:Hide()
+ b.EditButton = edit
+
+ -- Click / double-click / tooltip.
+ b:SetScript("OnClick", function(self)
+ if self.setID then
+ CP._equipPane.selectedSetID = self.setID
+ if M then M.LoadIgnoredFromSet(self.setID) end
+ CP.RefreshEquipPane()
+ else
+ CP._equipPane.selectedSetID = nil
+ openPopupNew()
+ CP.RefreshEquipPane()
+ end
+ end)
+ b:SetScript("OnDoubleClick", function(self)
+ if self.setID and M then M.Use(self.setID) end
+ end)
+ b:SetScript("OnEnter", function(self)
+ if not self.setID or not M then return end
+ local name, _, _, _, numItems, numEquipped, _, numLost = M.GetSetInfo(self.setID)
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText(name or "", 1, 1, 1)
+ GameTooltip:AddLine(string.format("%d / %d %s", numEquipped or 0, numItems or 0,
+ L("EQUIPMENT_SETS_EQUIPPED", "equipped")), 0.8, 0.8, 0.8)
+ if (numLost or 0) > 0 then
+ GameTooltip:AddLine(string.format("%d %s", numLost, L("EQUIPMENT_SETS_MISSING", "items missing")), 1, 0.1, 0.1)
+ end
+ GameTooltip:Show()
+ end)
+ b:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ return b
+end
+
+-- ============================================================================
+-- Pane construction (named FauxScrollFrame). Hosted in CP.frame.InsetRight.
+-- ============================================================================
+local function host()
+ return CP.frame and CP.frame.InsetRight
+end
+
+local function buildEquipPane()
+ if CP._equipPane then return CP._equipPane end
+ local ir = host()
+ if not ir then log("buildEquipPane: InsetRight not ready"); return nil end
+
+ local pane = CreateFrame("Frame", "NE_EquipmentManagerPane", ir)
+ pane:SetPoint("TOPLEFT", ir, "TOPLEFT", 3, -3)
+ pane:SetPoint("BOTTOMRIGHT", ir, "BOTTOMRIGHT", -3, 2)
+ pane:Hide()
+
+ -- Equip + Save buttons flush at the top.
+ -- DOWNPORT/REPORT: force a consistent font across enabled/disabled. The base DragonUI addon reskins
+ -- UIPanelButtonTemplate (red) and changes the enabled font, so enabled vs greyed looked different —
+ -- pin both states to the clean small fonts (the greyed look the user preferred).
+ local function pinBtnFont(btn)
+ if btn.SetNormalFontObject then pcall(btn.SetNormalFontObject, btn, "GameFontNormalSmall") end
+ if btn.SetDisabledFontObject then pcall(btn.SetDisabledFontObject, btn, "GameFontDisableSmall") end
+ if btn.SetHighlightFontObject then pcall(btn.SetHighlightFontObject, btn, "GameFontHighlightSmall") end
+ end
+
+ local equipBtn = CreateFrame("Button", "$parentEquipSet", pane, "UIPanelButtonTemplate")
+ equipBtn:SetSize(96, 22)
+ equipBtn:SetText(L("EQUIPSET_EQUIP", "Equip"))
+ equipBtn:SetPoint("TOPLEFT", pane, "TOPLEFT", 0, 0)
+ pinBtnFont(equipBtn)
+ equipBtn:SetScript("OnClick", function()
+ local id = pane.selectedSetID
+ if id and M then M.Use(id) end
+ end)
+ pane.EquipSet = equipBtn
+
+ local saveBtn = CreateFrame("Button", "$parentSaveSet", pane, "UIPanelButtonTemplate")
+ saveBtn:SetSize(96, 22)
+ saveBtn:SetText(L("SAVE", "Save"))
+ saveBtn:SetPoint("LEFT", equipBtn, "RIGHT", 4, 0)
+ pinBtnFont(saveBtn)
+ saveBtn:SetScript("OnClick", function()
+ local id = pane.selectedSetID
+ if not id or not M then return end
+ local name, icon = M.GetSetInfo(id)
+ StaticPopup_Show("NE_CONFIRM_SAVE_EQUIPMENT_SET", name, nil, { setID = id, icon = icon })
+ end)
+ pane.SaveSet = saveBtn
+
+ -- Named FauxScrollFrame list (NAME REQUIRED on 3.3.5).
+ local scroll = CreateFrame("ScrollFrame", "NE_EquipManagerScroll", pane, "FauxScrollFrameTemplate")
+ scroll:SetPoint("TOPLEFT", pane, "TOPLEFT", 0, -26)
+ scroll:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", -22, 2)
+ scroll:SetScript("OnVerticalScroll", function(self, off)
+ FauxScrollFrame_OnVerticalScroll(self, off, BTN_H, function() CP.RefreshEquipPane() end)
+ end)
+ -- DOWNPORT: hand-built minimal scrollbar (Reskin's stock-slider re-skin didn't render).
+ -- DOWNPORT/REPORT: push the bar FLUSH to the pane's right edge (x=-10) like the character panel —
+ -- the default inset put it over the (now full-width) rows, covering their equipped checkmarks.
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, scroll, { x = -10 }) end
+ pane._scroll = scroll
+
+ pane._btnPool = {}
+
+ -- Hover reveal of Delete/Edit.
+ pane:SetScript("OnUpdate", function(self)
+ if not self:IsVisible() then return end
+ for _, btn in ipairs(self._btnPool) do
+ if btn:IsShown() then
+ local over = btn:IsMouseOver() and btn.setID ~= nil
+ if over then btn.DeleteButton:Show(); btn.EditButton:Show()
+ else btn.DeleteButton:Hide(); btn.EditButton:Hide() end
+ end
+ end
+ end)
+
+ CP._equipPane = pane
+ return pane
+end
+
+-- ----------------------------------------------------------------------------
+-- Populate the list + drive Equip/Save enable state. Non-virtualised: we keep a
+-- pool sized to the visible rows and page it via FauxScrollFrame offset.
+-- ----------------------------------------------------------------------------
+local STRIPE = { r = 0.9, g = 0.9, b = 1 }
+
+local function refreshEquipPane()
+ local pane = CP._equipPane
+ if not pane or not M then return end
+ local scroll = pane._scroll
+
+ -- Validate selection + drive Equip/Save enable.
+ local selID = pane.selectedSetID
+ local isEquipped
+ if selID then
+ local _, _, sid, eq = M.GetSetInfo(selID)
+ if sid then isEquipped = eq else selID = nil; pane.selectedSetID = nil end
+ end
+ if selID and not isEquipped then
+ pane.EquipSet:Enable(); pane.SaveSet:Enable()
+ else
+ pane.EquipSet:Disable(); pane.SaveSet:Disable()
+ end
+
+ -- Row data: every set, then the "New Set" row (under cap).
+ local ids = M.GetSetIDs()
+ local rows = {}
+ for _, id in ipairs(ids) do rows[#rows + 1] = { setID = id } end
+ if #ids < M.MAX_SETS then rows[#rows + 1] = { addSetButton = true } end
+
+ local visible = math.max(1, math.floor((scroll:GetHeight() or (BTN_H * 6)) / BTN_H))
+ -- Grow the pool to the visible count. DOWNPORT/REPORT (the empty-list bug): parent the gear buttons
+ -- to the PANE, not the faux ScrollFrame. FauxScrollFrame_Update HIDES the scroll frame when the rows
+ -- fit (numItems <= numToDisplay) — with only the "New Set" row that's always true, so the buttons
+ -- (incl. the New-Set button) vanished and there was no way to create a set. Anchored to the scroll
+ -- rect below (which keeps its position even while hidden), raised above it so they stay clickable.
+ for i = #pane._btnPool + 1, visible do
+ local b = makeGearButton(pane)
+ if b.SetFrameLevel then b:SetFrameLevel((scroll:GetFrameLevel() or 1) + 5) end
+ pane._btnPool[i] = b
+ end
+
+ FauxScrollFrame_Update(scroll, #rows, visible, BTN_H)
+ local offset = FauxScrollFrame_GetOffset(scroll) or 0
+
+ for i = 1, #pane._btnPool do
+ local b = pane._btnPool[i]
+ local row = rows[offset + i]
+ if i <= visible and row then
+ b:ClearAllPoints()
+ -- DOWNPORT/REPORT: row spans to the PANE's right edge (not the scroll's), so the stripe/selection
+ -- background reaches the window edge / under where the scrollbar sits, instead of stopping short.
+ b:SetPoint("TOPLEFT", scroll, "TOPLEFT", 2, -((i - 1) * BTN_H))
+ b:SetPoint("RIGHT", pane, "RIGHT", -14, 0)
+ b:Show()
+
+ if row.addSetButton then
+ b.setID = nil
+ b.text:SetText(L("PAPERDOLL_NEWEQUIPMENTSET", "New Equipment Set"))
+ b.text:SetTextColor(GREEN.r, GREEN.g, GREEN.b)
+ -- DOWNPORT/REPORT: the "New Equipment Set" row has NO icon. ICON_NEW (Character-Plus) doesn't
+ -- exist on 3.3.5, so SetTexture left the pooled button showing the DELETED set's icon — clear +
+ -- hide it explicitly so a reused button never inherits a stale icon.
+ b.icon:SetTexture(nil); b.icon:Hide()
+ b.Check:Hide(); b.SelectedBar:Hide(); b.Stripe:Hide()
+ else
+ local name, texture, sid, equipped, _, _, _, numLost = M.GetSetInfo(row.setID)
+ b.setID = sid
+ b.text:SetText(name or "")
+ if (numLost or 0) > 0 then b.text:SetTextColor(RED.r, RED.g, RED.b)
+ else b.text:SetTextColor(NORMAL.r, NORMAL.g, NORMAL.b) end
+ b.icon:Show()
+ b.icon:SetTexture(texture or ICON_QM); b.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ if equipped then b.Check:Show() else b.Check:Hide() end
+ if pane.selectedSetID ~= nil and sid == pane.selectedSetID then b.SelectedBar:Show() else b.SelectedBar:Hide() end
+ if ((offset + i) % 2) == 0 then
+ b.Stripe:SetTexture(STRIPE.r, STRIPE.g, STRIPE.b, 0.1); b.Stripe:Show()
+ else
+ b.Stripe:Hide()
+ end
+ end
+ else
+ b:Hide()
+ end
+ end
+end
+CP.RefreshEquipPane = refreshEquipPane
+
+-- ----------------------------------------------------------------------------
+-- Show / hide the pane. The Sidebar agent's Tab3 (equipment) calls these.
+-- ----------------------------------------------------------------------------
+function CP.ShowEquipManager()
+ local pane = buildEquipPane()
+ if not pane then return end
+ -- Hide the stats pane if the sidebar agent exposed it.
+ if CP._sidebar and CP._sidebar.Hide then CP._sidebar:Hide() end
+ pane:Show()
+ refreshEquipPane()
+ CP._activeSidebar = 3
+ -- Equip-swap flyout arrows live on the equipment page only.
+ if CP.ShowEquipmentFlyoutArrows then CP.ShowEquipmentFlyoutArrows() end
+end
+
+function CP.HideEquipManager()
+ if CP._equipPane then CP._equipPane:Hide() end
+ if CP.HideEquipmentFlyoutArrows then CP.HideEquipmentFlyoutArrows() end
+end
+
+-- Retail-index compatibility (index 3 = equipment). Keeps NewEra callers working;
+-- the canonical entry points are ShowEquipManager/HideEquipManager.
+local _prevSelectSidebar = CP.SelectSidebar
+function CP.SelectSidebar(index)
+ CP._activeSidebar = index
+ if index == 3 then
+ CP.ShowEquipManager()
+ else
+ CP.HideEquipManager()
+ if CP._sidebar and CP._sidebar.Show then CP._sidebar:Show() end
+ if CP.RefreshSidebar then CP.RefreshSidebar() end
+ -- Defer to a previously-installed sidebar selector (stats sub-tabs) if any.
+ if _prevSelectSidebar and _prevSelectSidebar ~= CP.SelectSidebar then pcall(_prevSelectSidebar, index) end
+ end
+ if CP.SetSidebarTabSelected then pcall(CP.SetSidebarTabSelected, index) end
+end
+
+-- ----------------------------------------------------------------------------
+-- React to backend changes (set created/deleted/equipped, gear/bag changes).
+-- ----------------------------------------------------------------------------
+local function onBackendChanged()
+ if CP._equipPane and CP._equipPane:IsVisible() then refreshEquipPane() end
+end
+local function onSwapFinished(result, setID)
+ if result and CP._equipPane and CP._equipPane:IsVisible() then
+ if setID then CP._equipPane.selectedSetID = setID end
+ refreshEquipPane()
+ end
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsBooted and not NE.modules.IsBooted("character") then return end
+ if M then
+ M.RegisterChanged(onBackendChanged)
+ M.RegisterSwapFinished(onSwapFinished)
+ end
+end)
+
+-- ----------------------------------------------------------------------------
+-- /dnequip "Set Name" (alias /gearset) — equip a saved equipment set by name.
+-- The name may be quoted ("Bicc Set") or bare; matching is case-insensitive. If no set matches it
+-- errors and lists the sets you do have. Uses the same client-side backend (M.Use → physical swap).
+-- ----------------------------------------------------------------------------
+local function equipByName(msg)
+ msg = msg or ""
+ local name = msg:match('"(.-)"') or msg:match("'(.-)'")
+ if not name then name = msg:gsub("^%s+", ""):gsub("%s+$", "") end
+ local chat = DEFAULT_CHAT_FRAME
+ if name == "" then
+ chat:AddMessage('|cffffcc55DragonUI|r Usage: /dnequip "Set Name"')
+ return
+ end
+ if not M then chat:AddMessage("|cffff5555DragonUI|r Equipment manager not ready."); return end
+
+ local target = name:lower()
+ local found, names = nil, {}
+ for _, id in ipairs(M.GetSetIDs() or {}) do
+ local sname = M.GetSetInfo(id)
+ if sname then
+ names[#names + 1] = sname
+ if sname:lower() == target then found = id; break end
+ end
+ end
+
+ if found then
+ -- Already wearing it? GetSetInfo's 4th return is isEquipped (all of the set's items currently worn).
+ local sname, _, _, equipped = M.GetSetInfo(found)
+ if equipped then
+ chat:AddMessage(string.format('|cffffcc55DragonUI|r You already have "%s" equipped.', sname or name))
+ return
+ end
+ M.Use(found)
+ chat:AddMessage(string.format('|cff55ff55DragonUI|r Equipping set "%s".', sname or name))
+ else
+ local list = (#names > 0) and (" Your sets: " .. table.concat(names, ", ") .. ".")
+ or " You have no saved sets."
+ chat:AddMessage(string.format('|cffff5555DragonUI|r No equipment set named "%s".%s', name, list))
+ end
+end
+-- DOWNPORT/REPORT: NOT "/equip" or "/equipset" — BOTH are built-in WotLK secure macro commands
+-- (equip an item / equip a NATIVE set), processed by the secure command system before a SlashCmdList
+-- handler runs, so they never reach us (and look up native sets we don't have). Use unique names.
+SLASH_NEEQUIPSET1 = "/dnequip"
+SLASH_NEEQUIPSET2 = "/gearset"
+SlashCmdList["NEEQUIPSET"] = equipByName
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentSets.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentSets.lua
new file mode 100644
index 0000000..5c5c537
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/EquipmentSets.lua
@@ -0,0 +1,494 @@
+-- DragonUI_NewEra/modules/character/EquipmentSets.lua — Equipment-Manager backend.
+--
+-- ONE API surface (NE.equipsets) that mirrors retail's C_EquipmentSet return shapes
+-- so the pane (EquipmentManagerPane.lua) is identical regardless of which backend is
+-- live. Backend is RESOLVED at runtime, never guessed (project memory Law 1):
+--
+-- * NATIVE — when NE.cap.equipmentSets (compat/C_EquipmentSet.lua probed the
+-- 3.3.5 global equip family and it answered). Thin passthrough to the
+-- C_EquipmentSet adapter shim → the engine does the swap.
+-- * CUSTOM — the default on private servers (the native equip API is FLAKY there
+-- — project memory). Sets are stored per-character in NE.db via
+-- NE.CharKey(), and equip is the ItemRack-model physical swap
+-- (PickupContainerItem/PickupInventoryItem + a lock watcher) proven on
+-- 3.3.5a/Epoch in DragonflightUICharacter/EquipmentManager.lua. We do
+-- NOT trust the Blizzard equipment-set engine for the actual swap.
+--
+-- The pane consumes: GetSetIDs / GetNumSets / GetSetID / GetSetInfo / GetItemIDs /
+-- Create / Save / Modify / Delete / Use / Pickup / SeedDefaultIgnored / MAX_SETS +
+-- the ignore-slot scratch helpers + RegisterChanged / RegisterSwapFinished.
+--
+-- DOWNPORT notes vs NewEra source:
+-- * `local NE = DragonUI_NewEra` (not the `NE = NE or {}` global).
+-- * charKey() -> NE.CharKey().
+-- * Custom equip is the ItemRack physical-swap model, not EquipItemByName (more
+-- reliable on private servers; handles 2H→offhand clear; lock-aware retry).
+-- * C_Container is provided by compat/; NUM_BAG_SLOTS guarded (4 if absent).
+
+local NE = DragonUI_NewEra
+NE.equipsets = NE.equipsets or {}
+local M = NE.equipsets
+
+local function log(msg)
+ if NE.charpanel and NE.charpanel._log then NE.charpanel._log(msg) end
+end
+
+-- Constants. Vanilla equippable inventory slots: 1 Head … 19 Tabard (contiguous).
+local FIRST_SLOT, LAST_SLOT = 1, 19
+local NUM_SLOTS = LAST_SLOT
+local MAXBAG = _G.NUM_BAG_SLOTS or 4 -- bags 0..MAXBAG (backpack + 4)
+-- Retail ignores Shirt (4) and Tabard (19) on a new set.
+local DEFAULT_IGNORED = { [4] = true, [19] = true }
+M.MAX_SETS = _G.MAX_EQUIPMENT_SETS_PER_PLAYER or 10
+
+-- ----------------------------------------------------------------------------
+-- Backend resolution. NE.cap.equipmentSets is set by compat/C_EquipmentSet.lua.
+-- ----------------------------------------------------------------------------
+local function usingNative()
+ return NE.cap and NE.cap.equipmentSets == true and _G.C_EquipmentSet ~= nil
+end
+M.UsingNative = usingNative
+
+-- ----------------------------------------------------------------------------
+-- Change / swap notification. The pane registers callbacks; we fire on mutation
+-- and on inventory/bag updates so the equipped check + missing-item colouring stay
+-- live. In native mode we also relay the real engine events.
+-- ----------------------------------------------------------------------------
+local changedCallbacks, swapCallbacks = {}, {}
+function M.RegisterChanged(fn) changedCallbacks[#changedCallbacks + 1] = fn end
+function M.RegisterSwapFinished(fn) swapCallbacks[#swapCallbacks + 1] = fn end
+local function fireChanged() for _, fn in ipairs(changedCallbacks) do pcall(fn) end end
+local function fireSwap(result, setID) for _, fn in ipairs(swapCallbacks) do pcall(fn, result, setID) end end
+
+-- ----------------------------------------------------------------------------
+-- Container helpers — route through compat C_Container, with a positional-global
+-- fallback so the backend works even if compat hasn't shimmed everything.
+-- ----------------------------------------------------------------------------
+local function bagNumSlots(bag)
+ if C_Container and C_Container.GetContainerNumSlots then return C_Container.GetContainerNumSlots(bag) or 0 end
+ return GetContainerNumSlots(bag) or 0
+end
+local function bagItemLink(bag, slot)
+ if C_Container and C_Container.GetContainerItemLink then return C_Container.GetContainerItemLink(bag, slot) end
+ return GetContainerItemLink(bag, slot)
+end
+
+-- Item identity (ItemRack model): item string minus the trailing unique/level
+-- number, so the same gear matches across instances. Accepts links or ids.
+local function itemString(link)
+ if not link then return nil end
+ if type(link) == "number" then return tostring(link) end
+ return link:match("item:(.+):%-?%d+") or link:match("item:(%d+)")
+end
+local function itemIDFromLink(link)
+ if not link then return nil end
+ if type(link) == "number" then return link end
+ local id = link:match("item:(%d+)")
+ return id and tonumber(id) or nil
+end
+local function sameID(a, b)
+ if not a or not b then return false end
+ return a:match("^(%-?%d+)") == b:match("^(%-?%d+)")
+end
+local function slotItem(invSlot) return itemString(GetInventoryItemLink("player", invSlot)) end
+
+-- ----------------------------------------------------------------------------
+-- Per-character store: NE.db.equipmentSets[charKey] = { nextID, sets = { [id]=set } }.
+-- set = { id, name, icon, items = { [slot]=itemString }, ignored = { [slot]=bool } }
+-- ----------------------------------------------------------------------------
+local function store()
+ local db = NE.db
+ if not db then return nil end
+ db.equipmentSets = db.equipmentSets or {}
+ local key = NE.CharKey()
+ local s = db.equipmentSets[key]
+ if not s then s = { nextID = 1, sets = {} }; db.equipmentSets[key] = s end
+ return s
+end
+
+-- Transient "ignored slots for the next save" scratch state.
+local pendingIgnored = {}
+
+-- Capture currently-equipped gear (skipping ignored slots) as item strings.
+local function snapshotGear(ignored)
+ local items = {}
+ for slot = FIRST_SLOT, LAST_SLOT do
+ if not (ignored and ignored[slot]) then
+ local id = slotItem(slot)
+ if id then items[slot] = id end
+ end
+ end
+ return items
+end
+
+-- Find an item (exact, then base-id) in bags, skipping already-used sources.
+local function findInBags(idStr, used)
+ for pass = 1, 2 do
+ for bag = 0, MAXBAG do
+ local n = bagNumSlots(bag)
+ for slot = 1, n do
+ local key = bag * 100 + slot
+ if not (used and used[key]) then
+ local bi = itemString(bagItemLink(bag, slot))
+ if bi and ((pass == 1 and bi == idStr) or (pass == 2 and sameID(bi, idStr))) then
+ return bag, slot, key
+ end
+ end
+ end
+ end
+ end
+end
+
+local function findFreeBagSlot(used)
+ for bag = 0, MAXBAG do
+ local n = bagNumSlots(bag)
+ for slot = 1, n do
+ local key = bag * 100 + slot
+ if not (used and used[key]) and not bagItemLink(bag, slot) then
+ return bag, slot, key
+ end
+ end
+ end
+end
+
+-- ----------------------------------------------------------------------------
+-- Read API.
+-- ----------------------------------------------------------------------------
+function M.CanUse()
+ if usingNative() then return true end
+ return NE.db ~= nil
+end
+
+function M.GetSetIDs()
+ if usingNative() then return C_EquipmentSet.GetEquipmentSetIDs() or {} end
+ local s = store()
+ local ids = {}
+ if s then
+ for id in pairs(s.sets) do ids[#ids + 1] = id end
+ table.sort(ids)
+ end
+ return ids
+end
+
+function M.GetNumSets()
+ if usingNative() then return C_EquipmentSet.GetNumEquipmentSets() or 0 end
+ local s = store()
+ if not s then return 0 end
+ local n = 0
+ for _ in pairs(s.sets) do n = n + 1 end
+ return n
+end
+
+function M.GetSetID(name)
+ if usingNative() then return C_EquipmentSet.GetEquipmentSetID(name) end
+ local s = store()
+ if not s then return nil end
+ for id, set in pairs(s.sets) do
+ if set.name == name then return id end
+ end
+ return nil
+end
+
+-- name, icon, setID, isEquipped, numItems, numEquipped, numInInventory, numLost, numIgnored
+function M.GetSetInfo(setID)
+ if usingNative() then return C_EquipmentSet.GetEquipmentSetInfo(setID) end
+ local s = store()
+ local set = s and s.sets[setID]
+ if not set then return nil end
+
+ local numItems, numEquipped, numInInventory, numLost, numIgnored = 0, 0, 0, 0, 0
+ for slot = FIRST_SLOT, LAST_SLOT do
+ if set.ignored and set.ignored[slot] then numIgnored = numIgnored + 1 end
+ local want = set.items[slot]
+ if want then
+ numItems = numItems + 1
+ local cur = slotItem(slot)
+ if cur and sameID(cur, want) then
+ numEquipped = numEquipped + 1
+ elseif findInBags(want) then
+ numInInventory = numInInventory + 1
+ else
+ numLost = numLost + 1
+ end
+ end
+ end
+ local isEquipped = numItems > 0 and numEquipped == numItems
+ return set.name, set.icon, setID, isEquipped, numItems, numEquipped, numInInventory, numLost, numIgnored
+end
+
+function M.GetItemIDs(setID)
+ if usingNative() then return C_EquipmentSet.GetItemIDs(setID) or {} end
+ local s = store()
+ local set = s and s.sets[setID]
+ if not set then return {} end
+ local ids = {}
+ for slot = FIRST_SLOT, LAST_SLOT do ids[slot] = itemIDFromLink(set.items[slot] and ("item:" .. set.items[slot])) end
+ return ids
+end
+
+function M.ContainsLockedItems(setID)
+ if usingNative() then return C_EquipmentSet.EquipmentSetContainsLockedItems(setID) end
+ return false
+end
+
+-- ----------------------------------------------------------------------------
+-- Ignored-slot scratch state (mirrors C_EquipmentSet's save-ignore API).
+-- ----------------------------------------------------------------------------
+function M.ClearIgnored()
+ if usingNative() then C_EquipmentSet.ClearIgnoredSlotsForSave(); return end
+ wipe(pendingIgnored)
+end
+function M.IgnoreSlot(slot)
+ if usingNative() then C_EquipmentSet.IgnoreSlotForSave(slot); return end
+ pendingIgnored[slot] = true
+end
+function M.UnignoreSlot(slot)
+ if usingNative() then C_EquipmentSet.UnignoreSlotForSave(slot); return end
+ pendingIgnored[slot] = nil
+end
+function M.IsSlotIgnored(slot)
+ if usingNative() then return C_EquipmentSet.IsSlotIgnoredForSave(slot) end
+ return pendingIgnored[slot] == true
+end
+function M.GetIgnoredSlots(setID)
+ if usingNative() then return C_EquipmentSet.GetIgnoredSlots(setID) or {} end
+ local s = store()
+ local set = s and s.sets[setID]
+ local out = {}
+ if set and set.ignored then
+ for slot, v in pairs(set.ignored) do out[slot] = v and true or false end
+ end
+ return out
+end
+function M.LoadIgnoredFromSet(setID)
+ if usingNative() then return end
+ wipe(pendingIgnored)
+ local s = store()
+ local set = s and s.sets[setID]
+ if set and set.ignored then
+ for slot, v in pairs(set.ignored) do if v then pendingIgnored[slot] = true end end
+ end
+end
+function M.SeedDefaultIgnored()
+ if usingNative() then
+ if C_EquipmentSet.IgnoreSlotForSave then C_EquipmentSet.IgnoreSlotForSave(4); C_EquipmentSet.IgnoreSlotForSave(19) end
+ return
+ end
+ wipe(pendingIgnored)
+ for slot in pairs(DEFAULT_IGNORED) do pendingIgnored[slot] = true end
+end
+
+-- A sensible default icon for a freshly-saved set (chest, then head, then ?).
+local function defaultIcon()
+ return GetInventoryItemTexture("player", 5)
+ or GetInventoryItemTexture("player", 1)
+ or "Interface\\Icons\\INV_Misc_QuestionMark"
+end
+
+-- ----------------------------------------------------------------------------
+-- Mutations.
+-- ----------------------------------------------------------------------------
+function M.Create(name, icon)
+ if usingNative() then C_EquipmentSet.CreateEquipmentSet(name, icon); fireChanged(); return end
+ local s = store()
+ if not s or not name or name == "" then return end
+ local id = s.nextID
+ s.nextID = id + 1
+ local ignored = {}
+ for slot, v in pairs(pendingIgnored) do if v then ignored[slot] = true end end
+ s.sets[id] = {
+ id = id, name = name,
+ icon = icon or defaultIcon(),
+ items = snapshotGear(ignored),
+ ignored = ignored,
+ }
+ fireChanged()
+end
+
+function M.Save(setID, icon)
+ if usingNative() then C_EquipmentSet.SaveEquipmentSet(setID, icon); fireChanged(); return end
+ local s = store()
+ local set = s and s.sets[setID]
+ if not set then return end
+ local ignored = {}
+ for slot, v in pairs(pendingIgnored) do if v then ignored[slot] = true end end
+ set.items = snapshotGear(ignored)
+ set.ignored = ignored
+ if icon then set.icon = icon end
+ fireChanged()
+end
+
+function M.Modify(setID, newName, newIcon)
+ if usingNative() then C_EquipmentSet.ModifyEquipmentSet(setID, newName, newIcon); fireChanged(); return end
+ local s = store()
+ local set = s and s.sets[setID]
+ if not set then return end
+ if newName and newName ~= "" then set.name = newName end
+ if newIcon then set.icon = newIcon end
+ fireChanged()
+end
+
+function M.Delete(setID)
+ if usingNative() then C_EquipmentSet.DeleteEquipmentSet(setID); fireChanged(); return end
+ local s = store()
+ if s and s.sets[setID] then
+ s.sets[setID] = nil
+ fireChanged()
+ end
+end
+
+-- ----------------------------------------------------------------------------
+-- Equip a set (CUSTOM backend) — ItemRack physical-swap model.
+-- Out-of-combat only; equips are restricted in combat. A lock watcher runs further
+-- passes as item locks clear so multi-swaps (and the 2H→offhand clear) settle.
+-- ----------------------------------------------------------------------------
+local pendingEquipName -- name of the set mid-swap (lock watcher target)
+local pendingEquipSetID -- combat-queued equip
+local activeSetID -- last requested set id (for "is equipped" hints)
+
+local function anythingLocked()
+ for i = 1, NUM_SLOTS do
+ if IsInventoryItemLocked(i) then return true end
+ end
+ for bag = 0, MAXBAG do
+ local n = bagNumSlots(bag)
+ for slot = 1, n do
+ if select(3, GetContainerItemInfo(bag, slot)) then return true end
+ end
+ end
+ return false
+end
+
+local function swapBagToSlot(bag, slot, invSlot)
+ if CursorHasItem() or SpellIsTargeting() then return false end
+ ClearCursor()
+ PickupContainerItem(bag, slot)
+ PickupInventoryItem(invSlot)
+ return true
+end
+
+-- One pass of the swap for a set. Returns the number of swaps issued.
+local function doPass(set)
+ if not set or anythingLocked() then return 0 end
+ local used, swaps = {}, 0
+ for invSlot = FIRST_SLOT, LAST_SLOT do
+ if not (set.ignored and set.ignored[invSlot]) then
+ local target = set.items[invSlot]
+ if target and not sameID(slotItem(invSlot), target) then
+ local bag, slot = findInBags(target, used)
+ if bag then
+ -- 2H into main hand: clear the off-hand into a free bag slot first.
+ if invSlot == 16 then
+ local eloc = select(9, GetItemInfo(bagItemLink(bag, slot) or ""))
+ if eloc == "INVTYPE_2HWEAPON" and GetInventoryItemLink("player", 17) then
+ local fb, fs, fk = findFreeBagSlot(used)
+ if fb and not CursorHasItem() then
+ ClearCursor(); PickupInventoryItem(17); PickupContainerItem(fb, fs)
+ used[fk] = 1
+ end
+ end
+ end
+ if swapBagToSlot(bag, slot, invSlot) then
+ used[bag * 100 + slot] = 1
+ swaps = swaps + 1
+ end
+ end
+ end
+ end
+ end
+ return swaps
+end
+
+local function doEquipCustom(setID)
+ local s = store()
+ local set = s and s.sets[setID]
+ if not set then return end
+ activeSetID = setID
+ pendingEquipName = set.name
+ doPass(set) -- first pass; lock watcher finishes the rest
+end
+
+function M.Use(setID)
+ if usingNative() then
+ C_EquipmentSet.UseEquipmentSet(setID)
+ return
+ end
+ if InCombatLockdown() then
+ pendingEquipSetID = setID
+ if UIErrorsFrame then
+ UIErrorsFrame:AddMessage(ERR_NOT_IN_COMBAT or "Can't do that in combat", 1.0, 0.1, 0.1, 1.0)
+ end
+ return
+ end
+ doEquipCustom(setID)
+ ClearCursor()
+ fireSwap(true, setID)
+ fireChanged()
+end
+
+-- Put a set on the cursor (drop on an action bar for a one-click equip). Native
+-- only; the custom backend has no engine cursor type for this.
+function M.Pickup(setID)
+ if usingNative() and C_EquipmentSet.PickupEquipmentSet then
+ C_EquipmentSet.PickupEquipmentSet(setID)
+ return true
+ end
+ return false
+end
+
+function M.IsEquipped(setID)
+ local _, _, _, eq = M.GetSetInfo(setID)
+ return eq and true or false
+end
+
+-- ----------------------------------------------------------------------------
+-- Event plumbing.
+-- * lock watcher (ITEM_LOCK_CHANGED): run further custom swap passes.
+-- * PLAYER_REGEN_ENABLED: flush a combat-queued custom equip.
+-- * BAG_UPDATE / PLAYER_EQUIPMENT_CHANGED: keep the pane's live colouring fresh.
+-- * EQUIPMENT_SETS_CHANGED / EQUIPMENT_SWAP_FINISHED: relay native engine events.
+-- ----------------------------------------------------------------------------
+local ev = CreateFrame("Frame")
+ev:RegisterEvent("ITEM_LOCK_CHANGED")
+ev:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+ev:RegisterEvent("BAG_UPDATE")
+ev:RegisterEvent("PLAYER_REGEN_ENABLED")
+ev:RegisterEvent("EQUIPMENT_SETS_CHANGED")
+ev:RegisterEvent("EQUIPMENT_SWAP_FINISHED")
+ev:SetScript("OnEvent", function(_, event, ...)
+ if event == "ITEM_LOCK_CHANGED" then
+ if pendingEquipName and not usingNative() and not anythingLocked() then
+ local s = store()
+ local set = s and s.sets[activeSetID]
+ if set and set.name == pendingEquipName then
+ local swaps = doPass(set)
+ if swaps == 0 then
+ pendingEquipName = nil
+ fireSwap(true, activeSetID)
+ fireChanged()
+ end
+ else
+ pendingEquipName = nil
+ end
+ end
+ return
+ end
+ if event == "PLAYER_REGEN_ENABLED" then
+ if pendingEquipSetID and not usingNative() then
+ local id = pendingEquipSetID
+ pendingEquipSetID = nil
+ M.Use(id)
+ end
+ return
+ end
+ if event == "EQUIPMENT_SWAP_FINISHED" then
+ local result, name = ...
+ -- native passes a name; map to our synthesised id when possible.
+ local sid = name and M.GetSetID(name) or nil
+ fireSwap(result, sid)
+ fireChanged()
+ return
+ end
+ -- EQUIPMENT_SETS_CHANGED / PLAYER_EQUIPMENT_CHANGED / BAG_UPDATE.
+ fireChanged()
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Honor.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Honor.lua
new file mode 100644
index 0000000..0d1d4b6
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Honor.lua
@@ -0,0 +1,466 @@
+-- DragonUI_NewEra/modules/character/Honor.lua — the Honor (PvP) secondary tab.
+--
+-- Fills the EMPTY pane DragonUI_NewEra_CharacterPane_Honor (created by TabButtons.lua, parented
+-- to NE.charpanel.frame.Inset). PvP rank BADGE + progress bar + collapsible honor-stat sections
+-- (Today / Yesterday / This Week / Last Week / Lifetime), ported from
+-- NewEra/CharacterPanel/Honor.lua.
+--
+-- DOWNPORT (CONTRACT_S1 §A.2/§B): NewEra used a retail WowScrollBox factory (CP.CreateHonorPane)
+-- over the stock HonorFrame. 3.3.5a has no WowScrollBox, so we render DIRECTLY into our pane with
+-- a NAMED FauxScrollFrame (unnamed FauxScrollFrameTemplate ERRORS) over a flat header/row list.
+--
+-- DATA (3.3.5a HonorFrame.lua — these tuples DIFFER from NewEra's Classic source; transcribed
+-- from the live FrameXML):
+-- GetPVPSessionStats() -> hk, cp (NO dishonorable kills on WotLK)
+-- GetPVPYesterdayStats() -> hk, contribution
+-- GetPVPThisWeekStats() -> hk, contribution (exists; stock UI leaves it commented out)
+-- GetPVPLastWeekStats() -> hk, dk?, contribution, rank (guarded; may be absent on a realm)
+-- GetPVPLifetimeStats() -> hk, highestRank (NO lifetime DK on WotLK)
+-- UnitPVPRank("player"); GetPVPRankInfo(rank) -> rankName, rankNumber; GetPVPRankProgress() 0..1
+-- Badge = Interface\PvPRankBadges\PvPRank%02d. DOWNPORT: dishonorable-kill rows from the Classic
+-- schema are DROPPED (no WotLK API) rather than rendered as "--".
+--
+-- GRACEFUL DEGRADATION (§A.5): every getter is pcall-guarded; missing data → that row reads "--";
+-- if the whole PvP API is gone the pane shows an unranked hint and logs — NEVER errors.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local function log(msg)
+ if CP._log then CP._log("HONOR: " .. tostring(msg)); return end
+ if NE.Log then NE.Log("HONOR", msg); return end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffffcc55DragonUI_NewEra|r [honor]: " .. tostring(msg))
+ end
+end
+
+local function L(global, fallback)
+ local v = _G[global]
+ if type(v) == "string" and v ~= "" then return v end
+ return fallback
+end
+
+-- Geometry. DOWNPORT/REPORT: Honor content is DETERMINISTIC (rank block + exactly 5 sections × 2 rows),
+-- so it is sized to FIT the ~338px viewport when fully expanded (48 + 5*22 + 10*16 = 318) — no scroll
+-- needed, which sidesteps the no-clip overflow + absolute-scroll quirks entirely. Collapsing only frees
+-- more room. (Skills/Reputation have unbounded lists and keep real windowed scrolling.)
+local HEADER_H = 22
+local ROW_H = 16
+local RANK_H = 48 -- badge + progress-bar block (fits the 1–2 line unranked hint at full width)
+
+local FILL_TEX = "Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar"
+
+local function setAtlas(tex, name, useAtlasSize)
+ if tex and NE.tex and NE.tex.SetAtlas then return NE.tex.SetAtlas(tex, name, useAtlasSize) end
+ return false
+end
+
+-- Only numbers/strings reach SetText (a stray table tostring'd into a row = the breakage NewEra
+-- guarded against).
+local function disp(v)
+ local t = type(v)
+ if t == "number" or t == "string" then return tostring(v) end
+ return "--"
+end
+
+-- Stat section schema (WotLK-adapted: no dishonorable kills).
+local HK = L("HONORABLE_KILLS", "Honorable Kills")
+local CONTRIB = L("CONTRIBUTION_POINTS", "Honor Points")
+
+local SECTIONS = {
+ { title = L("HONOR_THIS_SESSION", "Today"), keys = { { HK, "sessionHK" }, { CONTRIB, "sessionCP" } } },
+ { title = L("HONOR_YESTERDAY", "Yesterday"), keys = { { HK, "yHK" }, { CONTRIB, "yCP" } } },
+ { title = L("HONOR_THISWEEK", "This Week"), keys = { { HK, "twHK" }, { CONTRIB, "twCP" } } },
+ { title = L("HONOR_LASTWEEK", "Last Week"), keys = { { HK, "lwHK" }, { CONTRIB, "lwCP" } } },
+ { title = L("HONOR_LIFETIME", "Lifetime"), keys = { { HK, "lHK" }, { L("HIGHEST_RANK", "Highest Rank"), "lRankName" } } },
+}
+
+-- ---------------------------------------------------------------------------
+-- Row builders.
+-- ---------------------------------------------------------------------------
+local function buildHeader(parent)
+ local h = CreateFrame("Button", nil, parent)
+ h:SetHeight(HEADER_H)
+
+ h._bgLeft = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgLeft, "options_listexpand_left", true)
+ h._bgLeft:SetPoint("LEFT", h, "LEFT", 0, 0)
+
+ h._bgRight = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgRight, "options_listexpand_right_expanded", true)
+ h._bgRight:SetPoint("RIGHT", h, "RIGHT", 0, 0)
+
+ h._bgMiddle = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgMiddle, "_options_listexpand_middle", false)
+ h._bgMiddle:SetPoint("TOPLEFT", h._bgLeft, "TOPRIGHT")
+ h._bgMiddle:SetPoint("BOTTOMRIGHT", h._bgRight, "BOTTOMLEFT")
+
+ local hl = h:CreateTexture(nil, "HIGHLIGHT")
+ if hl.SetColorTexture then hl:SetColorTexture(1, 1, 1, 0.10) else hl:SetTexture(1, 1, 1, 0.10) end
+ hl:SetPoint("TOPLEFT", h._bgLeft, "TOPRIGHT")
+ hl:SetPoint("BOTTOMRIGHT", h._bgRight, "BOTTOMLEFT")
+
+ h._name = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h._name:SetPoint("LEFT", h, "LEFT", 12, 0)
+ h._name:SetJustifyH("LEFT")
+
+ h:RegisterForClicks("LeftButtonUp")
+ return h
+end
+
+local function buildValueRow(parent)
+ local row = CreateFrame("Frame", nil, parent)
+ row:SetHeight(ROW_H)
+ row._label = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
+ row._label:SetPoint("LEFT", row, "LEFT", 18, 0)
+ row._label:SetJustifyH("LEFT")
+ row._value = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
+ row._value:SetPoint("RIGHT", row, "RIGHT", -10, 0)
+ row._value:SetJustifyH("RIGHT")
+ return row
+end
+
+-- The rank block: badge + title + progress bar (+ unranked hint).
+local function buildRankBlock(parent)
+ local r = CreateFrame("Frame", nil, parent)
+ r:SetHeight(RANK_H)
+
+ r.icon = r:CreateTexture(nil, "ARTWORK")
+ r.icon:SetSize(28, 28)
+ r.icon:SetPoint("TOPLEFT", r, "TOPLEFT", 14, -2)
+
+ r.title = r:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
+ r.title:SetPoint("LEFT", r.icon, "RIGHT", 8, 0)
+ r.title:SetJustifyH("LEFT")
+
+ local bar = CreateFrame("StatusBar", nil, r)
+ bar:SetHeight(13)
+ -- Anchor bar + hint to the BLOCK (not the icon): the icon is hidden when unranked, and anchoring to
+ -- a hidden region dragged the bar/hint up into an overlap with the next header.
+ bar:SetPoint("TOPLEFT", r, "TOPLEFT", 14, -32)
+ bar:SetPoint("RIGHT", r, "RIGHT", -12, 0)
+ bar:SetStatusBarTexture(FILL_TEX)
+ bar:SetMinMaxValues(0, 1)
+ bar:SetValue(0)
+ local well = bar:CreateTexture(nil, "BACKGROUND")
+ well:SetAllPoints(bar)
+ if well.SetColorTexture then well:SetColorTexture(0, 0, 0, 0.55) else well:SetTexture(0, 0, 0, 0.55) end
+ r.bar = bar
+
+ r.hint = r:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
+ r.hint:SetPoint("TOPLEFT", r, "TOPLEFT", 14, -22)
+ r.hint:SetPoint("RIGHT", r, "RIGHT", -12, 0)
+ r.hint:SetJustifyH("LEFT")
+ r.hint:SetWordWrap(true)
+ r.hint:SetText("Earn honorable kills in battlegrounds and world PvP to gain rank.")
+ r.hint:Hide()
+ return r
+end
+
+-- ---------------------------------------------------------------------------
+-- Data gather (every getter pcall-guarded; missing → nil → row "--").
+-- ---------------------------------------------------------------------------
+local function safe(fn, ...)
+ if type(fn) ~= "function" then return end
+ local r = { pcall(fn, ...) }
+ if r[1] then return select(2, unpack(r)) end
+end
+
+local function gatherStats()
+ local s = {}
+ s.sessionHK, s.sessionCP = safe(GetPVPSessionStats)
+ s.yHK, s.yCP = safe(GetPVPYesterdayStats)
+ s.twHK, s.twCP = safe(GetPVPThisWeekStats)
+ -- LastWeek tuple varies (hk, dk, contribution, rank) — take HK + contribution defensively.
+ local lwHK, lw2, lw3 = safe(GetPVPLastWeekStats)
+ s.lwHK = lwHK
+ s.lwCP = (lw3 ~= nil) and lw3 or lw2 -- contribution is arg2 or arg3 depending on dk presence
+ local lHK, highestRank = safe(GetPVPLifetimeStats)
+ s.lHK = lHK
+ local lRankName = highestRank and safe(GetPVPRankInfo, highestRank)
+ s.lRankName = lRankName or L("NONE", "None")
+ return s
+end
+
+-- ---------------------------------------------------------------------------
+-- Pane scaffold (named FauxScrollFrame; flat list of rank block + headers + rows).
+-- ---------------------------------------------------------------------------
+local pane, scroll, content, rankBlock
+local sectionHeaders = {} -- title -> header button
+local valueRows = {} -- key -> value row
+local expanded = {} -- title -> bool (default true)
+local flat = {}
+local NUM_VISIBLE = 0
+local unavailLabel
+
+local function getPane()
+ if pane then return pane end
+ pane = _G.DragonUI_NewEra_CharacterPane_Honor or (CP.EnsurePane and CP.EnsurePane("Honor"))
+ return pane
+end
+
+-- Variable row heights → we lay rows out absolutely from a running Y rather than a fixed slot.
+-- We still drive scroll with a fixed "scroll step" via FauxScrollFrame, mapping offset→a Y cursor.
+-- Simpler + robust: rebuild the flat list (each item carries its own height), then on scroll show
+-- the slice that fits. Because the honor content is small/bounded we use a single fixed step.
+local SCROLL_STEP = ROW_H
+
+local function rebuildFlat(stats)
+ flat = {}
+ -- Rank block is always first.
+ flat[#flat + 1] = { kind = "rank", h = RANK_H }
+ for _, sec in ipairs(SECTIONS) do
+ if expanded[sec.title] == nil then expanded[sec.title] = true end
+ flat[#flat + 1] = { kind = "header", title = sec.title, h = HEADER_H }
+ if expanded[sec.title] then
+ for _, kd in ipairs(sec.keys) do
+ flat[#flat + 1] = { kind = "row", label = kd[1], key = kd[2], h = ROW_H,
+ value = stats and disp(stats[kd[2]]) or "--" }
+ end
+ end
+ end
+end
+
+local function totalHeight()
+ local t = 0
+ for _, it in ipairs(flat) do t = t + it.h end
+ return t
+end
+
+-- Acquire/recycle widgets by stable identity (rank block is a singleton; headers keyed by title;
+-- value rows keyed by data key) so we never thrash CreateFrame.
+local function acquireHeader(title)
+ local h = sectionHeaders[title]
+ if not h then
+ h = buildHeader(content)
+ h:SetScript("OnClick", function()
+ expanded[title] = not expanded[title]
+ if CP.RefreshHonor then CP.RefreshHonor() end
+ end)
+ sectionHeaders[title] = h
+ end
+ h._name:SetText(title)
+ setAtlas(h._bgRight, expanded[title] and "options_listexpand_right_expanded" or "options_listexpand_right", true)
+ return h
+end
+
+local function acquireRow(key)
+ local row = valueRows[key]
+ if not row then row = buildValueRow(content); valueRows[key] = row end
+ return row
+end
+
+local function hideAllDynamic()
+ for _, h in pairs(sectionHeaders) do h:Hide() end
+ for _, row in pairs(valueRows) do row:Hide() end
+ if rankBlock then rankBlock:Hide() end
+end
+
+local function layout()
+ if not (scroll and content) then return end
+ hideAllDynamic()
+
+ if unavailLabel then unavailLabel:Hide() end
+
+ local rowW = content:GetWidth() or 200
+ if rowW <= 0 then rowW = 200 end
+
+ -- FauxScrollFrame here scrolls by pixels-as-steps: total "lines" = ceil(totalHeight/STEP).
+ local total = totalHeight()
+ local lineCount = math.ceil(total / SCROLL_STEP)
+ local visLines = NUM_VISIBLE > 0 and NUM_VISIBLE or 1
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(scroll, lineCount, visLines, SCROLL_STEP) end
+ if NE.scrollbar and NE.scrollbar.CenterIfNoBar then NE.scrollbar.CenterIfNoBar(scroll, lineCount > visLines) end
+ local offsetLines = (FauxScrollFrame_GetOffset and FauxScrollFrame_GetOffset(scroll)) or 0
+ local scrollPx = offsetLines * SCROLL_STEP
+
+ -- DOWNPORT/REPORT (overflow-onto-tabs bug): content runs taller than the viewport, and 3.3.5a frames
+ -- do NOT clip children — so rows past the bottom spilled below the box onto the tab buttons. CULL:
+ -- only show an item that fits ENTIRELY within the scroll viewport band [0 .. -viewH]; hide the rest
+ -- (revealed by scrolling). This is the windowing Skills/Reputation get for free from their fixed slots.
+ local viewH = scroll:GetHeight() or 0
+ -- Scroll DOWN must move content UP, so the first item's y grows POSITIVE (off the top) as scrollPx
+ -- increases — the earlier -scrollPx pushed content DOWN (gap above the rank block on scroll).
+ local y = scrollPx -- top of the first item relative to content top (positive = scrolled off top)
+ for _, it in ipairs(flat) do
+ local w
+ if it.kind == "rank" then
+ if not rankBlock then rankBlock = buildRankBlock(content); CP._honorRank = rankBlock end
+ w = rankBlock
+ elseif it.kind == "header" then
+ w = acquireHeader(it.title)
+ else
+ w = acquireRow(it.key)
+ w._label:SetText(it.label)
+ w._value:SetText(it.value or "--")
+ end
+ local top, bottom = y, y - it.h
+ if top <= 0.5 and bottom >= -viewH - 0.5 then
+ w:ClearAllPoints()
+ w:SetWidth(rowW)
+ w:SetPoint("TOPLEFT", content, "TOPLEFT", 0, y)
+ w:Show()
+ else
+ w:Hide()
+ end
+ y = y - it.h
+ end
+end
+
+-- Apply rank data to the (already-built) rank block.
+local function applyRank()
+ if not rankBlock then return end
+ local raw = safe(UnitPVPRank, "player")
+ local rankName, rankNumber = safe(GetPVPRankInfo, raw)
+ rankNumber = tonumber(rankNumber) or 0
+ local progress = safe(GetPVPRankProgress) or 0
+
+ -- Title sits beside the badge when ranked, or top-left (where the badge would be) when unranked.
+ local function anchorTitle(withIcon)
+ rankBlock.title:ClearAllPoints()
+ if withIcon then
+ rankBlock.title:SetPoint("LEFT", rankBlock.icon, "RIGHT", 8, 0)
+ else
+ rankBlock.title:SetPoint("TOPLEFT", rankBlock, "TOPLEFT", 14, -6)
+ end
+ end
+
+ if rankNumber > 0 then
+ rankBlock.icon:SetTexture(string.format("Interface\\PvPRankBadges\\PvPRank%02d", rankNumber))
+ rankBlock.icon:Show()
+ anchorTitle(true)
+ rankBlock.title:SetText(string.format("%s (%s %d)", disp(rankName), L("RANK", "Rank"), rankNumber))
+ rankBlock.bar:Show()
+ rankBlock.hint:Hide()
+ elseif progress and progress > 0 then
+ rankBlock.icon:Hide()
+ anchorTitle(false)
+ rankBlock.title:SetText(L("PVP_UNRANKED", "Unranked"))
+ rankBlock.bar:Show()
+ rankBlock.hint:Hide()
+ else
+ rankBlock.icon:Hide()
+ anchorTitle(false)
+ rankBlock.title:SetText(L("PVP_UNRANKED", "Unranked"))
+ rankBlock.bar:Hide()
+ rankBlock.hint:Show()
+ end
+
+ local fg = safe(UnitFactionGroup, "player")
+ if fg == "Alliance" then rankBlock.bar:SetStatusBarColor(0.05, 0.15, 0.36)
+ else rankBlock.bar:SetStatusBarColor(0.63, 0.09, 0.09) end
+ rankBlock.bar:SetValue(math.max(0, math.min(1, progress or 0)))
+end
+
+local function refresh()
+ if not getPane() then return end
+ if not scroll then return end
+
+ local apiOk = type(UnitPVPRank) == "function" and type(GetPVPSessionStats) == "function"
+ if not apiOk then
+ log("PvP API unavailable — Honor pane degraded")
+ flat = {}
+ hideAllDynamic()
+ if unavailLabel then unavailLabel:Show() end
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(scroll, 0, 1, SCROLL_STEP) end
+ if NE.scrollbar and NE.scrollbar.CenterIfNoBar then NE.scrollbar.CenterIfNoBar(scroll, false) end
+ return
+ end
+
+ local stats = gatherStats()
+ rebuildFlat(stats)
+ -- Ensure the rank block exists before layout positions it, then fill it.
+ if not rankBlock then rankBlock = buildRankBlock(content); CP._honorRank = rankBlock end
+ layout()
+ applyRank()
+end
+-- DOWNPORT/REPORT: surface (don't swallow) a refresh error so a broken Honor pane is diagnosable.
+CP.RefreshHonor = function()
+ local ok, err = pcall(refresh)
+ if not ok then log("RefreshHonor error: " .. tostring(err)) end
+end
+
+local function recomputeVisible()
+ if not scroll then return end
+ local h = scroll:GetHeight() or 0
+ NUM_VISIBLE = math.max(1, math.floor(h / SCROLL_STEP))
+end
+
+local function build()
+ local host = getPane()
+ if not host then log("Honor pane host missing — cannot build"); return false end
+ if scroll then return true end
+
+ local bg = host:CreateTexture(nil, "BACKGROUND")
+ if not setAtlas(bg, "character-panel-background", false) then bg:Hide() end
+ bg:SetPoint("TOPLEFT", host, "TOPLEFT", 2, -2)
+ bg:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -2, 2)
+
+ scroll = CreateFrame("ScrollFrame", "DragonUI_NewEra_HonorScroll", host, "FauxScrollFrameTemplate")
+ -- DOWNPORT/REPORT: viewport inset to sit INSIDE the engraved inset box (top/left margin), leaving a
+ -- right gutter for the scrollbar (pushed out to the frame border by the negative BuildCustom x below).
+ scroll:SetPoint("TOPLEFT", host, "TOPLEFT", 10, -12)
+ scroll:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -24, 10)
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ FauxScrollFrame_OnVerticalScroll(self, offset, SCROLL_STEP, layout)
+ end)
+ -- DOWNPORT/REPORT: wheel scrolling. BuildCustom hides the stock slider but Faux still drives it, so
+ -- nudging the (hidden) slider's value scrolls + auto-clamps to its range. Without this the culled
+ -- bottom rows were unreachable.
+ scroll:EnableMouseWheel(true)
+ scroll:SetScript("OnMouseWheel", function(self, delta)
+ local sb = _G[(self:GetName() or "") .. "ScrollBar"]
+ if not sb then return end
+ local mn, mx = sb:GetMinMaxValues()
+ local v = sb:GetValue() - delta * SCROLL_STEP
+ if v < mn then v = mn elseif v > mx then v = mx end
+ sb:SetValue(v)
+ end)
+ scroll:HookScript("OnSizeChanged", function() recomputeVisible(); layout() end)
+ -- DOWNPORT: hand-built minimal scrollbar (Reskin's stock-slider re-skin didn't render).
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, scroll, { x = -8 }) end
+
+ -- DOWNPORT/REPORT (the empty-pane bug): parent content to the PANE, not the faux ScrollFrame.
+ -- FauxScrollFrame_Update hides the scroll frame when the list fits (no scroll needed), which would
+ -- take content + rows with it. Honor renders today only because its list overflows; a low-content
+ -- player (no honor data) would hit the same vanish. Anchor over the scroll rect, raise above it.
+ content = CreateFrame("Frame", nil, host)
+ content:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
+ content:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
+ content:SetHeight(1)
+ content:SetFrameLevel(scroll:GetFrameLevel() + 5)
+
+ unavailLabel = host:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
+ unavailLabel:SetPoint("TOP", host, "TOP", 0, -40)
+ unavailLabel:SetWidth(200)
+ unavailLabel:SetText(L("HONOR", "Honor") .. " unavailable")
+ unavailLabel:Hide()
+
+ recomputeVisible()
+ return true
+end
+
+local function init()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ if not build() then return end
+ pcall(refresh)
+ local host = getPane()
+ if host and not host._neHonorShowHooked then
+ host._neHonorShowHooked = true
+ host:HookScript("OnShow", function() recomputeVisible(); pcall(refresh) end)
+ end
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:RegisterEvent("PLAYER_PVP_KILLS_CHANGED")
+boot:RegisterEvent("PLAYER_PVP_RANK_CHANGED")
+boot:SetScript("OnEvent", function(_, event)
+ if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
+ if C_Timer and C_Timer.After then C_Timer.After(0, init) else init() end
+ else
+ CP.RefreshHonor()
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/InnerBorder.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/InnerBorder.lua
new file mode 100644
index 0000000..a0403ac
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/InnerBorder.lua
@@ -0,0 +1,169 @@
+-- DragonUI_NewEra/modules/character/InnerBorder.lua — gold inner border wrapping the model/slot
+-- viewport (4 corners + 4 tiled edges) + the full-width gold divider line above the weapon row.
+-- Ported from NewEra/CharacterPanel/InnerBorder.lua.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): parented to OUR Inset (NE.charpanel.Inset), not Blizzard's
+-- CharacterFrame.Inset. The Inset is FIXED 338x364, so the retail-faithful corner offsets translate
+-- directly. Borders are OVERLAY textures on the Inset; they only make sense on the Character tab, so
+-- we Show/Hide them from CP.SelectTab (our foundation's tab switch hook) — no taint risk since these
+-- are OUR widgets, not Blizzard's protected fields.
+--
+-- ART (FDIDs 410247/410248/410249, registered in Assets.lua), with canonical-path fallback:
+-- Corners (7x7): Char-Paperdoll-Parts (410248)
+-- Horiz edges (h-tile): Char-Paperdoll-Horizontal (410247)
+-- Vert edges (v-tile): Char-Paperdoll-Vertical (410249)
+-- Texcoords transcribed verbatim from retail/NewEra CharacterFrame.xml.
+--
+-- GRACEFUL DEGRADATION (§B): build is pcall-guarded; missing art renders blank, never errors.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local MODULE = "character"
+
+local function log(msg) if CP._log then CP._log(msg) elseif NE.Log then NE.Log("CHARPANEL", msg) end end
+
+-- Art sources: prefer the shipped local BLP (Assets.lua), else the canonical 3.3.5a client path.
+local function src(fdid, path)
+ return (NE.tex and NE.tex.localFiles and NE.tex.localFiles[fdid]) or path
+end
+local BLP_PARTS = src(410248, "Interface\\CharacterFrame\\Char-Paperdoll-Parts")
+local BLP_HORIZONTAL = src(410247, "Interface\\CharacterFrame\\Char-Paperdoll-Horizontal")
+local BLP_VERTICAL = src(410249, "Interface\\CharacterFrame\\Char-Paperdoll-Vertical")
+
+local CORNERS = {
+ UpperLeft = { 0.40625, 0.43359375, 0.80468750, 0.85937500 },
+ UpperRight = { 0.40625, 0.43359375, 0.73437500, 0.78906250 },
+ LowerLeft = { 0.40625, 0.43359375, 0.66406250, 0.71875000 },
+ LowerRight = { 0.40625, 0.43359375, 0.59375000, 0.64843750 },
+}
+local EDGE_TOP = { 0.00000, 1.00000, 0.50000, 0.81250 }
+local EDGE_BOTTOM = { 0.00000, 1.00000, 0.06250, 0.37500 }
+local EDGE_LEFT = { 0.06250, 0.37500, 0.00000, 1.00000 }
+local EDGE_RIGHT = { 0.50000, 0.81250, 0.00000, 1.00000 }
+
+-- The created textures, kept on CP so the tab-visibility toggle can reach them.
+CP._innerBorderPieces = CP._innerBorderPieces or {}
+-- DOWNPORT/REPORT: shared paperdoll-decor list (also populated by SlotFrames.lua). The inner-border
+-- pieces are children of the INSET, not the model/slots, so they do NOT follow a model/slot Hide —
+-- they MUST be registered here so CP.ShowPaperDoll can hide them on non-Character tabs.
+CP._paperdollDecor = CP._paperdollDecor or {}
+
+-- Register a freshly-built border texture into the decor list (once).
+local function registerDecor(t)
+ if t then table.insert(CP._paperdollDecor, t) end
+end
+
+local function buildBorder()
+ local f = CP.frame
+ local inset = f and f.Inset
+ if not inset then log("InnerBorder: Inset not built yet"); return end
+ if inset._neInnerBorderBuilt then return end
+ inset._neInnerBorderBuilt = true
+
+ local pieces = CP._innerBorderPieces
+
+ -- Corner: 7x7 from Char-Paperdoll-Parts, anchored relative to the Inset.
+ local function corner(name, texcoord, point, relPoint, x, y)
+ local t = inset:CreateTexture(nil, "OVERLAY")
+ t:SetTexture(BLP_PARTS)
+ t:SetSize(7, 7)
+ t:SetTexCoord(texcoord[1], texcoord[2], texcoord[3], texcoord[4])
+ t:SetPoint(point, inset, relPoint, x, y)
+ pieces[name] = t
+ return t
+ end
+
+ -- Retail-faithful corner offsets (NewEra CharacterFrame.xml). Inset is FIXED 338x364.
+ local tl = corner("TopLeft", CORNERS.UpperLeft, "TOPLEFT", "TOPLEFT", 46, -4)
+ local tr = corner("TopRight", CORNERS.UpperRight, "TOPRIGHT", "TOPRIGHT", -47, -4)
+ local bl = corner("BottomLeft", CORNERS.LowerLeft, "BOTTOMLEFT", "BOTTOMLEFT", 46, 31)
+ local br = corner("BottomRight", CORNERS.LowerRight, "BOTTOMRIGHT", "BOTTOMRIGHT", -47, 31)
+
+ -- Vertical edges (5px wide, v-tiled) connecting the corners.
+ local left = inset:CreateTexture(nil, "OVERLAY")
+ left:SetTexture(BLP_VERTICAL); left:SetVertTile(true)
+ left:SetTexCoord(EDGE_LEFT[1], EDGE_LEFT[2], EDGE_LEFT[3], EDGE_LEFT[4])
+ left:SetWidth(5)
+ left:SetPoint("TOPLEFT", tl, "BOTTOMLEFT", -1, 0)
+ left:SetPoint("BOTTOMLEFT", bl, "TOPLEFT", -1, 0)
+ pieces.Left = left
+
+ local right = inset:CreateTexture(nil, "OVERLAY")
+ right:SetTexture(BLP_VERTICAL); right:SetVertTile(true)
+ right:SetTexCoord(EDGE_RIGHT[1], EDGE_RIGHT[2], EDGE_RIGHT[3], EDGE_RIGHT[4])
+ right:SetWidth(5)
+ right:SetPoint("TOPRIGHT", tr, "BOTTOMRIGHT", 1, 0)
+ right:SetPoint("BOTTOMRIGHT", br, "TOPRIGHT", 1, 0)
+ pieces.Right = right
+
+ -- Horizontal edges (5px tall, h-tiled).
+ local top = inset:CreateTexture(nil, "OVERLAY")
+ top:SetTexture(BLP_HORIZONTAL); top:SetHorizTile(true)
+ top:SetTexCoord(EDGE_TOP[1], EDGE_TOP[2], EDGE_TOP[3], EDGE_TOP[4])
+ top:SetHeight(5)
+ top:SetPoint("TOPLEFT", tl, "TOPRIGHT", 0, 1)
+ top:SetPoint("TOPRIGHT", tr, "TOPLEFT", 0, 1)
+ pieces.Top = top
+
+ local bottom = inset:CreateTexture(nil, "OVERLAY")
+ bottom:SetTexture(BLP_HORIZONTAL); bottom:SetHorizTile(true)
+ bottom:SetTexCoord(EDGE_BOTTOM[1], EDGE_BOTTOM[2], EDGE_BOTTOM[3], EDGE_BOTTOM[4])
+ bottom:SetHeight(5)
+ bottom:SetPoint("BOTTOMLEFT", bl, "BOTTOMRIGHT", 0, -1)
+ bottom:SetPoint("BOTTOMRIGHT", br, "BOTTOMLEFT", 0, -1)
+ pieces.Bottom = bottom
+
+ -- The full-width gold divider: the SECOND horizontal line that runs across the whole Inset width at
+ -- y=+27 above the bottom — the separator above the weapon row crossing under the slot columns.
+ local bottom2 = inset:CreateTexture(nil, "OVERLAY")
+ bottom2:SetTexture(BLP_HORIZONTAL); bottom2:SetHorizTile(true)
+ bottom2:SetTexCoord(EDGE_BOTTOM[1], EDGE_BOTTOM[2], EDGE_BOTTOM[3], EDGE_BOTTOM[4])
+ bottom2:SetHeight(5)
+ bottom2:SetPoint("BOTTOMLEFT", inset, "BOTTOMLEFT", 0, 27)
+ bottom2:SetPoint("BOTTOMRIGHT", inset, "BOTTOMRIGHT", 0, 27)
+ pieces.Bottom2 = bottom2
+
+ -- DOWNPORT/REPORT: register every border piece into the shared paperdoll-decor list so
+ -- CP.ShowPaperDoll hides them on non-Character tabs (they are Inset children, not model/slot
+ -- children, so they would otherwise stay visible).
+ for _, t in pairs(pieces) do registerDecor(t) end
+end
+
+-- Show/hide the border by active tab. Borders frame the PaperDoll model+slots, so they only show on
+-- the Character tab. DOWNPORT: no taint risk (our own textures); no SetShown — Show()/Hide() only.
+local function setBorderVisibility(visible)
+ for _, t in pairs(CP._innerBorderPieces) do
+ if t then if visible then t:Show() else t:Hide() end end
+ end
+end
+CP.SetInnerBorderVisible = setBorderVisibility
+
+-- Hook the foundation's tab switch so the border tracks the active tab. Wrapped once.
+local function hookTabVisibility()
+ if CP._innerBorderTabHooked then return end
+ if type(CP.SelectTab) ~= "function" then return end
+ CP._innerBorderTabHooked = true
+ hooksecurefunc(CP, "SelectTab", function(name)
+ setBorderVisibility(name == nil or name == "Character")
+ end)
+ -- Initial state: visible if the resting tab is Character (the foundation default).
+ setBorderVisibility((CP._activeTab or "Character") == "Character")
+end
+
+CP.BuildInnerBorder = buildBorder
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled(MODULE) then return end
+ local ok, err = pcall(function()
+ -- Ensure the Inset exists (the foundation builds it at PLAYER_LOGIN; build defensively).
+ if CP.frame and not CP.frame.Inset and CP.BuildInset then CP.BuildInset() end
+ buildBorder()
+ hookTabVisibility()
+ end)
+ if not ok then log("InnerBorder boot failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/InsetFrames.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/InsetFrames.lua
new file mode 100644
index 0000000..bd1c0d2
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/InsetFrames.lua
@@ -0,0 +1,109 @@
+-- DragonUI_NewEra/modules/character/InsetFrames.lua — the Inset + InsetRight content/sidebar hosts.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): these are children of OUR custom frame
+-- (NE.charpanel.frame = "DragonUI_NewEra_Character"), NOT Blizzard's CharacterFrame.
+--
+-- GEOMETRY (DOWNPORT/REPORT — faithful NewEra/CharacterPanel/InsetFrames.lua):
+-- frame.Inset — left content pane, FIXED 328x360 via TWO anchors: TOPLEFT(4,-60) +
+-- BOTTOMRIGHT to frame.BOTTOMLEFT + (332,4). Holds the reparented 3D model +
+-- equipment slots + per-tab content panes. Fixed WIDTH (BOTTOMRIGHT anchored to
+-- frame.BOTTOMLEFT) so it does NOT resize when the sidebar expands — model/slots
+-- stay put. Mirrors retail UpdateSize pinning Inset to PANEL_DEFAULT_WIDTH.
+-- frame.InsetRight — sidebar host, auto-sizing via TWO anchors: TOPLEFT = Inset.TOPRIGHT+(1,0),
+-- BOTTOMRIGHT = frame.BOTTOMRIGHT+(-4,4). ~1px collapsed, ~211px when expanded.
+-- HIDDEN until the panel expands for the sidebar (SetSidebarExpanded shows it).
+--
+-- Each gets an InsetFrameTemplate nineslice (thin gold inner border) + a marble inner bg, matching
+-- retail's InsetFrameTemplate. All ops are CreateFrame / CreateTexture / SetPoint — combat-legal.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+-- VISUAL_SPEC / NewEra geometry constants.
+-- DOWNPORT/REPORT: faithful NewEra port — Inset is FIXED-width via TWO anchors
+-- (TOPLEFT(4,-60) + BOTTOMRIGHT to frame.BOTTOMLEFT + (332,4)) = 328 wide x 360 tall,
+-- not a SetSize. This keeps the Inset width fixed even when the frame expands.
+local INSET_LEFT_X = 4 -- Frame.TOPLEFT x for the Inset
+local INSET_TOP_Y = -60 -- DOWNPORT/REPORT: revert to NewEra PANEL_INSET_ATTIC_OFFSET (-60); was -50
+local INSET_BR_X = 332 -- DOWNPORT/REPORT: BOTTOMRIGHT-to-frame.BOTTOMLEFT x (gives 328 fixed width)
+local INSET_BR_Y = 4 -- DOWNPORT/REPORT: BOTTOMRIGHT-to-frame.BOTTOMLEFT y
+local INSETRIGHT_X = 1 -- Inset.TOPRIGHT x for InsetRight (retail (1,0))
+local INSETRIGHT_Y = 0 -- Inset.TOPRIGHT y for InsetRight
+
+local function log(msg) if CP._log then CP._log(msg) end end
+
+-- Decorate an inset: marble Bg (BACKGROUND -5) + InsetFrameTemplate inner nineslice (thin gold trim).
+-- DOWNPORT: marble path is the stock 3.3.5a UI-Background-Marble. Idempotent.
+local function decorateInset(inset)
+ if not inset or inset._neDecorated then return end
+
+ local bg = inset:CreateTexture(nil, "BACKGROUND", nil, -5)
+ bg:SetTexture("Interface\\FrameGeneral\\UI-Background-Marble", "REPEAT", "REPEAT")
+ bg:SetHorizTile(true)
+ bg:SetVertTile(true)
+ bg:SetAllPoints(inset)
+ inset.Bg = bg
+
+ if NE.nineslice and NE.nineslice.ApplyLayout then
+ NE.nineslice.ApplyLayout(inset, "InsetFrameTemplate")
+ end
+
+ inset._neDecorated = true
+end
+CP.DecorateInset = decorateInset
+
+-- Build frame.Inset (left content pane). FIXED 338x364 at Frame.TOPLEFT(4,-60).
+-- DOWNPORT: anchor ONLY the TOPLEFT + SetSize (not a BOTTOMRIGHT anchor) so the Inset width is fixed
+-- and never tracks the frame's width when the sidebar expands.
+local function buildInset()
+ local f = CP.frame or (CP.BuildFrame and CP.BuildFrame())
+ if not f then log("buildInset: frame not built yet"); return nil end
+ if f.Inset then decorateInset(f.Inset); return f.Inset end
+
+ local inset = CreateFrame("Frame", "DragonUI_NewEra_CharacterInset", f)
+ -- DOWNPORT/REPORT: faithful NewEra — two anchors (TOPLEFT + BOTTOMRIGHT-to-frame.BOTTOMLEFT),
+ -- NOT SetSize. Fixed 328 wide x 360 tall; does not grow when the frame expands.
+ inset:SetPoint("TOPLEFT", f, "TOPLEFT", INSET_LEFT_X, INSET_TOP_Y)
+ inset:SetPoint("BOTTOMRIGHT", f, "BOTTOMLEFT", INSET_BR_X, INSET_BR_Y)
+ f.Inset = inset
+
+ decorateInset(inset)
+ return inset
+end
+
+-- Build frame.InsetRight (sidebar host). 209x364 at Inset.TOPRIGHT(1,0). Hidden by default; shown by
+-- NE.charpanel.SetSidebarExpanded (Wave 2 fills the body).
+local function buildInsetRight()
+ local f = CP.frame
+ if not f then log("buildInsetRight: frame not built yet"); return nil end
+ if f.InsetRight then decorateInset(f.InsetRight); return f.InsetRight end
+ local inset = f.Inset
+ if not inset then log("buildInsetRight: Inset not built yet"); return nil end
+
+ local insetRight = CreateFrame("Frame", "DragonUI_NewEra_CharacterInsetRight", f)
+ -- DOWNPORT/REPORT: faithful NewEra — two anchors (auto-sizing): TOPLEFT = Inset.TOPRIGHT+(1,0),
+ -- BOTTOMRIGHT = frame.BOTTOMRIGHT+(-4,4). ~1px collapsed, ~211px when frame=548. NOT SetSize.
+ insetRight:SetPoint("TOPLEFT", inset, "TOPRIGHT", INSETRIGHT_X, INSETRIGHT_Y)
+ insetRight:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 4)
+ insetRight:Hide() -- DOWNPORT: no SetShown on 3.3.5a; Hide()/Show() only. Shown on expand.
+ f.InsetRight = insetRight
+
+ decorateInset(insetRight)
+ return insetRight
+end
+
+-- Publish the surface (overwrites the CharacterPanel.lua no-op placeholders).
+CP.BuildInset = buildInset
+CP.BuildInsetRight = buildInsetRight
+
+-- Boot: build at PLAYER_LOGIN so SlotFrames/Sidebar (Wave 2) + the reparent pass can depend on the
+-- Insets existing. Gated on the module being enabled. DOWNPORT: build defensively — CharacterPanel
+-- .boot also calls BuildInset, but an enabled module always wants its anchors regardless of ordering.
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ local ok, err = pcall(function() buildInset(); buildInsetRight() end)
+ if not ok then log("inset build failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/LevelText.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/LevelText.lua
new file mode 100644
index 0000000..a586de8
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/LevelText.lua
@@ -0,0 +1,135 @@
+-- DragonUI_NewEra/modules/character/LevelText.lua — class-colored "Level N Race Class" text on the
+-- PaperDoll tab, with the retail two-line guild rule. Ported from NewEra/CharacterPanel/LevelText.lua.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): the vanilla CharacterLevelText/CharacterGuildText are layer
+-- FontStrings on the now-HIDDEN PaperDollFrame, so they never render in our custom frame. DOWNPORT:
+-- instead of reparenting Blizzard layer regions (fiddly + drags name/title), we own OUR OWN level +
+-- guild FontStrings on the Inset's Character pane and keep them synced to the same data + the same
+-- events 3.3.5a re-formats on (PaperDollFrame_SetLevel / PaperDollFrame_SetGuild). We ALSO recolor the
+-- stock CharacterLevelText (harmless if hidden) so any future reparent stays class-colored.
+--
+-- Class color via NE.color.WrapClass (RAID_CLASS_COLORS-backed). Two-line rule: when the guild line
+-- shows, the level line lifts to keep the pair centered as a block (retail PaperDollFrame_SetLevel).
+--
+-- GRACEFUL DEGRADATION (§B): every getter pcall-safe; if the Inset isn't built yet we just skip and
+-- re-run on the next event. Never errors out of boot.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local MODULE = "character"
+
+local function log(msg) if CP._log then CP._log(msg) elseif NE.Log then NE.Log("CHARPANEL", msg) end end
+
+-- Build the "Level N Race ClassColored" string. Shared so the stock + our own FontStrings match.
+local function buildColoredLevelText()
+ local level = UnitLevel("player") or 1
+ local raceName = UnitRace("player") or ""
+ local classDisplay, classFile = UnitClass("player")
+ classDisplay = classDisplay or ""
+ local coloredClass = (NE.color and NE.color.WrapClass)
+ and NE.color.WrapClass(classFile, classDisplay) or classDisplay
+ return string.format("Level %d %s %s", level, raceName, coloredClass)
+end
+
+-- Our own guild string (GUILD_TITLE_TEMPLATE = "%s of <%s>"). Returns nil if not guilded.
+local function buildGuildText()
+ local guildName, title = GetGuildInfo("player")
+ if not guildName then return nil end
+ local tmpl = _G.GUILD_TITLE_TEMPLATE or "%s of %s"
+ return string.format(tmpl, title or "", guildName)
+end
+
+-- Level-line Y: retail's two-line rule — lift to -36 when the guild line shows beneath, else -42.
+local function levelTextY(guilded)
+ return guilded and -36 or -42
+end
+
+-- Lazily create our level + guild FontStrings. DOWNPORT/REPORT: faithful NewEra — centered on the
+-- FRAME top (CENTER,frame.TOP,(0,-36/-42)). The character NAME is shown in the title bar
+-- (CharacterPanel.updateTitle); the level line sits centered at the frame top, guild just below it.
+local function ensureOwnFontStrings()
+ if CP._levelFS and CP._guildFS then return true end
+ local f = CP.frame
+ if not (f and f.Inset) then return false end
+
+ if not CP._levelFS then
+ local fs = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ CP._levelFS = fs
+ end
+ if not CP._guildFS then
+ local fs = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ CP._guildFS = fs
+ end
+ return true
+end
+
+-- DOWNPORT/REPORT: faithful NewEra — the level line is centered on the FRAME TOP at CENTER(0, Y),
+-- where Y = -36 when a guild line shows beneath, else -42 (retail PaperDollFrame_SetLevel two-line
+-- rule). Reverted the prior "anchor under the title FS" change. The character NAME stays in the title
+-- bar (CharacterPanel.updateTitle); guild, if any, sits just below the level line.
+local function anchorHeader(guilded)
+ local f = CP.frame
+ if not (f and CP._levelFS) then return end
+ CP._levelFS:ClearAllPoints()
+ CP._guildFS:ClearAllPoints()
+ CP._levelFS:SetPoint("CENTER", f, "TOP", 0, levelTextY(guilded))
+ if guilded then
+ CP._guildFS:SetPoint("TOP", CP._levelFS, "BOTTOM", 0, -2)
+ end
+end
+
+local function applyLevelText()
+ -- (1) Recolor the stock CharacterLevelText if present (covers a future reparent).
+ if _G.CharacterLevelText and _G.CharacterLevelText.SetText then
+ pcall(function() _G.CharacterLevelText:SetText(buildColoredLevelText()) end)
+ end
+
+ -- (2) Drive OUR OWN FontStrings — the ones that actually render in the custom frame.
+ if not ensureOwnFontStrings() then return end
+
+ local levelFS, guildFS = CP._levelFS, CP._guildFS
+ levelFS:SetText(buildColoredLevelText())
+
+ local guildStr = buildGuildText()
+ if guildStr then
+ guildFS:SetText(guildStr)
+ guildFS:Show()
+ else
+ guildFS:SetText("")
+ guildFS:Hide()
+ end
+
+ -- Anchor the lines in the header band above the Inset (centered over the model).
+ anchorHeader(guildStr ~= nil)
+end
+
+CP.ApplyLevelText = applyLevelText
+
+-- Once-only hook guards (session upvalues).
+local setLevelHooked, setGuildHooked
+
+local function installHooks()
+ if _G.PaperDollFrame_SetLevel and not setLevelHooked then
+ setLevelHooked = true
+ hooksecurefunc("PaperDollFrame_SetLevel", function() pcall(applyLevelText) end)
+ end
+ if _G.PaperDollFrame_SetGuild and not setGuildHooked then
+ setGuildHooked = true
+ hooksecurefunc("PaperDollFrame_SetGuild", function() pcall(applyLevelText) end)
+ end
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:RegisterEvent("PLAYER_LEVEL_UP")
+boot:RegisterEvent("PLAYER_GUILD_UPDATE")
+boot:RegisterEvent("UNIT_LEVEL")
+boot:SetScript("OnEvent", function(_, event, unit)
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled(MODULE) then return end
+ if event == "UNIT_LEVEL" and unit and unit ~= "player" then return end
+ local ok, err = pcall(function() installHooks(); applyLevelText() end)
+ if not ok then log("LevelText boot failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/ModelArea.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/ModelArea.lua
new file mode 100644
index 0000000..ca6b03e
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/ModelArea.lua
@@ -0,0 +1,196 @@
+-- DragonUI_NewEra/modules/character/ModelArea.lua — race-keyed 4-part background behind the 3D model
+-- (desaturated + dark overlay). Ported from NewEra/CharacterPanel/ModelArea.lua.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): the foundation (CharacterPanel.lua) already REPARENTED
+-- CharacterModelFrame into our Inset and sized it to 231x320 at Inset.TOPLEFT(52,-66). So unlike
+-- NewEra we DO NOT resize/anchor the model here — we ONLY stitch the race background behind it.
+-- DOWNPORT: dropped NewEra's resizeModelFrame() (the foundation owns model geometry).
+-- DOWNPORT/REPORT: race bg reverted to NATIVE retail quarter sizes anchored to the model's TOPLEFT
+-- (231 wide = same as the model), NOT box-filled to the inter-column gap.
+--
+-- The 4 quarter textures are CHILD textures of the model frame at BACKGROUND layer, so they render
+-- behind the 3D model. Keyed by UnitRace("player") -> race-bg FDIDs (registered in Assets.lua),
+-- desaturated, with a per-race black overlay for the muted retail look.
+--
+-- GRACEFUL DEGRADATION (§B): every step pcall-guarded; missing art -> blank bg, never errors.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local MODULE = "character"
+
+local function log(msg) if CP._log then CP._log(msg) elseif NE.Log then NE.Log("CHARPANEL", msg) end end
+
+-- Race file name -> 4 quarter BLP FDIDs (registered local in Assets.lua). Matches NewEra's map.
+local RACE_BG_FDIDS = {
+ Dwarf = {131089, 131090, 131091, 131092},
+ Human = {131093, 131094, 131095, 131096},
+ NightElf = {131097, 131098, 131099, 131100},
+ Orc = {131101, 131102, 131103, 131104},
+ Scourge = {131105, 131106, 131107, 131108},
+ Tauren = {131109, 131110, 131111, 131112},
+ Gnome = {455998, 455999, 456000, 456001},
+ Troll = {456006, 456007, 456008, 456009},
+ Draenei = {131085, 131086, 131087, 131088},
+ BloodElf = {131081, 131082, 131083, 131084},
+}
+
+-- Per-race overlay alpha (retail PaperDollFrame.lua). Default (Human/Dwarf/Tauren/Gnome) -> 0.7.
+local RACE_OVERLAY_ALPHA = {
+ BLOODELF = 0.8, NIGHTELF = 0.6, SCOURGE = 0.3,
+ TROLL = 0.6, ORC = 0.6, WORGEN = 0.5, GOBLIN = 0.6,
+}
+
+-- CUSTOM RACES: private servers ship races this addon has no art for, and UnitRace's file name for
+-- them is whatever the server's ChrRaces DBC says ("HighElf", "Vulpera", "Naga", ...). Rather than
+-- letting those fall through to a hardcoded Orc backdrop regardless of faction, resolve in 3 steps:
+-- 1. exact/normalized match against RACE_BG_FDIDS (so "Blood Elf"/"bloodelf" still hit BloodElf),
+-- 2. an alias for the post-WotLK + common custom races whose look maps cleanly onto shipped art,
+-- 3. a faction default — Alliance -> Human, Horde -> Orc, neutral/unknown -> Human.
+-- Extend RACE_ALIASES for a server-specific race; the faction default keeps it sane until you do.
+local RACE_ALIASES = {
+ worgen = "Human", goblin = "Orc", pandaren = "Human",
+ highelf = "BloodElf", voidelf = "BloodElf", nightborne = "BloodElf",
+ darkirondwarf = "Dwarf", lightforgeddraenei = "Draenei", magharorc = "Orc",
+ zandalaritroll = "Troll", highmountaintauren = "Tauren", forsaken = "Scourge",
+ undead = "Scourge", mechagnome = "Gnome", kultiran = "Human", vulpera = "Orc",
+}
+local FACTION_DEFAULT_RACE = { Alliance = "Human", Horde = "Orc" }
+
+-- Lowercased, punctuation-stripped index of the shipped race keys, built once.
+local LOGGED_UNKNOWN = {}
+local NORMALIZED_RACES = {}
+for key in pairs(RACE_BG_FDIDS) do NORMALIZED_RACES[key:lower()] = key end
+
+local function normalizeRace(raceFileName)
+ if type(raceFileName) ~= "string" then return nil end
+ return (raceFileName:lower():gsub("[^a-z0-9]", ""))
+end
+
+-- The shipped race key to draw for a (possibly unknown/custom) race file name. Never returns nil.
+local function resolveRaceKey(raceFileName)
+ if RACE_BG_FDIDS[raceFileName] then return raceFileName end
+
+ local norm = normalizeRace(raceFileName)
+ if norm then
+ if NORMALIZED_RACES[norm] then return NORMALIZED_RACES[norm] end
+ if RACE_ALIASES[norm] then return RACE_ALIASES[norm] end
+ end
+
+ -- DOWNPORT/§B: UnitFactionGroup can be nil (or "Neutral" on a Pandaren-like custom race) before
+ -- the player data is loaded — pcall it and fall back to Human so we always draw *something*.
+ local ok, faction = pcall(UnitFactionGroup, "player")
+ local fallback = (ok and faction and FACTION_DEFAULT_RACE[faction]) or "Human"
+ -- Log each unknown race once (resolveRaceKey runs per apply, and per quarters+alpha lookup).
+ local seenKey = tostring(raceFileName)
+ if not LOGGED_UNKNOWN[seenKey] then
+ LOGGED_UNKNOWN[seenKey] = true
+ log("no race background for '" .. seenKey .. "', using " .. fallback)
+ end
+ return fallback
+end
+
+local function resolveFdid(fdid)
+ return (NE.tex and NE.tex.localFiles and NE.tex.localFiles[fdid]) or fdid
+end
+
+-- 4 quarter texture sources for a race file name (unknown/custom races -> faction default).
+local function quarterSourcesFor(raceFileName)
+ local fdids = RACE_BG_FDIDS[resolveRaceKey(raceFileName)]
+ return resolveFdid(fdids[1]), resolveFdid(fdids[2]), resolveFdid(fdids[3]), resolveFdid(fdids[4])
+end
+
+-- Overlay alpha for a race file name, keyed off the RESOLVED art (so a custom race drawn on Orc art
+-- gets Orc's 0.6, not the 0.7 default that would over-darken it).
+local function overlayAlphaFor(raceFileName)
+ return RACE_OVERLAY_ALPHA[resolveRaceKey(raceFileName):upper()] or 0.7
+end
+
+-- Build the 4 stitched BG textures + the black overlay (once), as children of the (already-sized)
+-- model frame. Dimensions/texcoords verbatim from retail XML — the BG grid extends below the model
+-- viewport behind the weapon row, as retail does.
+-- DOWNPORT/REPORT: faithful NewEra — NATIVE retail quarter sizes anchored to the MODEL's TOPLEFT, so
+-- the bg is 231 wide = the same width as the model (which the foundation sized to 231x320). Reverted
+-- the prior box-fill math + BOX_INSET constants. Quarters: bgTL 212x245 @ model.TOPLEFT(0,0); bgTR
+-- 19x245 @ bgTL.TOPRIGHT(0,0); bgBL 212x128 @ bgTL.BOTTOMLEFT(0,0); bgBR 19x128 @ bgTL.BOTTOMRIGHT
+-- (0,0). Overlay TOPLEFT @ bgTL.TOPLEFT(0,0) / BOTTOMRIGHT @ bgBR.BOTTOMRIGHT(0,52).
+local function buildBackground()
+ local model = _G.CharacterModelFrame
+ if not model or model._neRaceBgBuilt then return end
+
+ -- DOWNPORT/REPORT: native quarter dimensions, anchored to the model frame's TOPLEFT (231 wide grid).
+ local bgTL = model:CreateTexture(nil, "BACKGROUND")
+ bgTL:SetSize(212, 245)
+ bgTL:SetPoint("TOPLEFT", model, "TOPLEFT", 0, 0)
+ bgTL:SetTexCoord(0.171875, 1, 0.0392156862745098, 1)
+
+ local bgTR = model:CreateTexture(nil, "BACKGROUND")
+ bgTR:SetSize(19, 245)
+ bgTR:SetPoint("TOPLEFT", bgTL, "TOPRIGHT", 0, 0)
+ bgTR:SetTexCoord(0, 0.296875, 0.0392156862745098, 1)
+
+ local bgBL = model:CreateTexture(nil, "BACKGROUND")
+ bgBL:SetSize(212, 128)
+ bgBL:SetPoint("TOPLEFT", bgTL, "BOTTOMLEFT", 0, 0)
+ bgBL:SetTexCoord(0.171875, 1, 0, 1)
+
+ local bgBR = model:CreateTexture(nil, "BACKGROUND")
+ bgBR:SetSize(19, 128)
+ bgBR:SetPoint("TOPLEFT", bgTL, "BOTTOMRIGHT", 0, 0)
+ bgBR:SetTexCoord(0, 0.296875, 0, 1)
+
+ -- Black overlay (BORDER layer = above BACKGROUND, below the model) for the race tint. Retail extends
+ -- the overlay 52px past the bg bottom to cover the seam below the model viewport.
+ local overlay = model:CreateTexture(nil, "BORDER")
+ -- DOWNPORT: 3.3.5a textures have no SetColorTexture; SetTexture(r,g,b) fills a solid color.
+ if overlay.SetColorTexture then overlay:SetColorTexture(0, 0, 0) else overlay:SetTexture(0, 0, 0) end
+ overlay:SetPoint("TOPLEFT", bgTL, "TOPLEFT", 0, 0)
+ overlay:SetPoint("BOTTOMRIGHT", bgBR, "BOTTOMRIGHT", 0, 52)
+
+ model._neBgTL, model._neBgTR, model._neBgBL, model._neBgBR = bgTL, bgTR, bgBL, bgBR
+ model._neBgOverlay = overlay
+ model._neRaceBgBuilt = true
+end
+
+local function applyRaceBackground()
+ buildBackground()
+ local model = _G.CharacterModelFrame
+ if not model or not model._neRaceBgBuilt then return end
+
+ -- DOWNPORT: do NOT bail when raceFile is nil (custom-race servers can return nil here, and a
+ -- nil-race player used to get bare untextured quarters). resolveRaceKey handles nil -> faction
+ -- default, so we always paint a real backdrop.
+ local _, raceFile = UnitRace("player")
+
+ local q1, q2, q3, q4 = quarterSourcesFor(raceFile)
+ model._neBgTL:SetTexture(q1)
+ model._neBgTR:SetTexture(q2)
+ model._neBgBL:SetTexture(q3)
+ model._neBgBR:SetTexture(q4)
+
+ -- Desaturate (retail's PaperDollBgDesaturate). Combined with the overlay -> muted atmospheric look.
+ -- DOWNPORT: SetDesaturated returns false if the shader is unsupported; harmless (stays full color).
+ for _, t in ipairs({ model._neBgTL, model._neBgTR, model._neBgBL, model._neBgBR }) do
+ if t.SetDesaturated then pcall(t.SetDesaturated, t, true) end
+ end
+
+ model._neBgOverlay:SetAlpha(overlayAlphaFor(raceFile))
+end
+
+CP.ApplyRaceBackground = applyRaceBackground
+
+-- Reusable lookup (for a future InspectFrame port): 4 quarter sources + overlay alpha for a race file.
+CP.RaceBgQuarters = function(raceFile)
+ local q1, q2, q3, q4 = quarterSourcesFor(raceFile)
+ return q1, q2, q3, q4, overlayAlphaFor(raceFile)
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled(MODULE) then return end
+ local ok, err = pcall(applyRaceBackground)
+ if not ok then log("ModelArea boot failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/ModelControls.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/ModelControls.lua
new file mode 100644
index 0000000..2cbd300
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/ModelControls.lua
@@ -0,0 +1,314 @@
+-- DragonUI_NewEra/modules/character/ModelControls.lua — the modern DF model control bar (zoom in/out,
+-- rotate L/R, reset) at the top of the 3D model. Ported from NewEra/CharacterPanel/ModelControls.lua.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): the model lives in our Inset (reparented + sized by the
+-- foundation). The bar is a child of CharacterModelFrame; "is the panel shown" = our custom frame +
+-- the Character tab being active.
+--
+-- 3.3.5a DOWNPORT (the hard part — NewEra targeted Era which had Model_OnMouseWheel + a controlFrame
+-- rotation hook; 3.3.5a has NEITHER):
+-- * ZOOM: 3.3.5a has NO Model_OnMouseWheel. PlayerModel exposes Get/SetPosition(x,y,z) where x is
+-- the camera depth axis — moving it toward the camera zooms IN. We implement zoom by nudging the
+-- x position, clamped to a sane range, plus wheel-zoom over the model. (SetCamDistanceScale exists
+-- but is undocumented/uneven across cores; SetPosition is reliable.)
+-- * ROTATE: 3.3.5a Model_OnUpdate reads the rotate buttons BY GLOBAL NAME
+-- (_G[model:GetName().."RotateLeftButton"]) — there is NO model.controlFrame indirection. So we
+-- cannot redirect it to our buttons. Instead we hide the vanilla wooden rotate buttons (a hidden
+-- button is never PUSHED, so Model_OnUpdate no-ops on them) and drive Model_RotateLeft/Right
+-- ourselves from our buttons' own OnUpdate while held.
+-- * No NE.squelch helper here -> hide vanilla buttons directly (pcall-guarded).
+-- * BTN_HPADDING/sizes/hover-reveal/alpha 0.5 are preserved from NewEra.
+--
+-- GRACEFUL DEGRADATION (§B): missing atlas art -> button face/icon blank but still clickable; every
+-- model call pcall-guarded; OnUpdate watcher tolerates a torn-down model. Never errors out of boot.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local MODULE = "character"
+
+local function log(msg) if CP._log then CP._log(msg) elseif NE.Log then NE.Log("CHARPANEL", msg) end end
+
+local BTN_SIZE = 32
+local BTN_HPADDING = -6 -- retail buttonHorizontalPadding (buttons overlap by 6px)
+local ROTATE_REPEAT = 0.03 -- radians per tick while a rotate button is held (Model_RotateLeft step)
+local ZOOM_REPEAT_INTERVAL = 0.05 -- seconds between zoom ticks while held
+local ZOOM_STEP = 0.18 -- x-position delta per zoom tick (depth axis)
+-- DOWNPORT: symmetric clamp around the default (x=0) so zoom works BOTH ways; the old [0,3.5] clamp
+-- pinned the model at the closest position so "+"/zoom-in did nothing. Direction is flipped below.
+local ZOOM_MIN = -3.0 -- farthest pull-back (zoom out)
+local ZOOM_MAX = 3.0 -- closest (zoom in)
+
+local DEFAULT_ALPHA = 0.5 -- retail ModelSceneControlFrame default alpha
+
+-- AdjustPointsOffset is a Dragonflight Region method; ClassicAPI adds it to FRAMES but not Textures,
+-- and these button icons are Textures (hence "attempt to call method 'AdjustPointsOffset' (a nil
+-- value)"). Use ClassicAPI's method when the region has it, else replicate it (shift every anchor
+-- point by dx,dy) — same logic ClassicAPI uses. Drives the 1px button-press nudge.
+local function adjustOffset(region, dx, dy)
+ if not (region and region.GetNumPoints) then return end
+ if region.AdjustPointsOffset then return region:AdjustPointsOffset(dx, dy) end
+ for i = 1, region:GetNumPoints() do
+ local point, relTo, relPoint, x, y = region:GetPoint(i)
+ if point then region:SetPoint(point, relTo, relPoint, (x or 0) + dx, (y or 0) + dy) end
+ end
+end
+
+-- ----------------------------------------------------------------------------
+-- Zoom helpers — 3.3.5a SetPosition(x,y,z): x is the depth (zoom) axis.
+-- ----------------------------------------------------------------------------
+local function zoomModel(model, direction)
+ if not (model and model.GetPosition and model.SetPosition) then return end
+ local ok, x, y, z = pcall(model.GetPosition, model)
+ if not ok or not x then return end
+ -- DOWNPORT: direction>0 = zoom IN. The previous code did `x - dir*step`, which (per in-game test)
+ -- zoomed the WRONG way; flipped to `x + dir*step` so "+" / scroll-up zoom in. Clamp [MIN,MAX].
+ x = x + direction * ZOOM_STEP
+ if x < ZOOM_MIN then x = ZOOM_MIN elseif x > ZOOM_MAX then x = ZOOM_MAX end
+ pcall(model.SetPosition, model, x, y or 0, z or 0)
+end
+
+local function resetModel(model)
+ if not model then return end
+ -- Rotation back to the vanilla default (Model_OnLoad sets 0.61).
+ if model.SetRotation then pcall(model.SetRotation, model, 0.61); model.rotation = 0.61 end
+ if model.SetPosition then pcall(model.SetPosition, model, 0, 0, 0) end
+ if model.SetCamDistanceScale then pcall(model.SetCamDistanceScale, model, 1) end
+ if model.SetPortraitZoom then pcall(model.SetPortraitZoom, model, 0) end
+end
+
+-- ----------------------------------------------------------------------------
+-- Button factory. Square gray face + centered 16x16 icon + ADD-blend hover glow + press offset.
+-- ----------------------------------------------------------------------------
+local function makeButton(parent, iconAtlas, onClick)
+ local btn = CreateFrame("Button", nil, parent)
+ btn:SetSize(BTN_SIZE, BTN_SIZE)
+ btn:SetHitRectInsets(4, 4, 4, 4)
+
+ -- Icon: 16x16 centered, OVERLAY.
+ local icon = btn:CreateTexture(nil, "OVERLAY")
+ icon:SetSize(16, 16)
+ icon:SetPoint("CENTER")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(icon, iconAtlas, false) end
+ btn.Icon = icon
+
+ -- Normal face: common-button-square-gray-up. DOWNPORT: SetNormalTexture on 3.3.5a accepts EITHER a
+ -- path or a texture object; we pass a texture object we've atlas-set (matches NewEra) — but if the
+ -- atlas is missing, NE.tex.SetAtlas left it blank, which is fine (button still clickable).
+ local normal = btn:CreateTexture(nil, "BACKGROUND")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(normal, "common-button-square-gray-up", false) end
+ normal:SetAllPoints(btn)
+ btn:SetNormalTexture(normal)
+
+ -- Pushed face: common-button-square-gray-down, offset (1,-1).
+ local pushed = btn:CreateTexture(nil, "BACKGROUND")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(pushed, "common-button-square-gray-down", false) end
+ pushed:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -1)
+ pushed:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 1, -1)
+ btn:SetPushedTexture(pushed)
+
+ -- Highlight: the icon atlas drawn ADD at 0.4 over the icon bounds -> subtle "icon glows" hover.
+ local hl = btn:CreateTexture(nil, "HIGHLIGHT")
+ if NE.tex and NE.tex.SetAtlas then NE.tex.SetAtlas(hl, iconAtlas, false) end
+ hl:SetPoint("TOPLEFT", icon, "TOPLEFT", 0, 0)
+ hl:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0, 0)
+ hl:SetBlendMode("ADD")
+ hl:SetAlpha(0.4)
+ btn:SetHighlightTexture(hl)
+
+ btn:SetScript("OnMouseDown", function(self) adjustOffset(self.Icon, 1, -1) end)
+ btn:SetScript("OnMouseUp", function(self) adjustOffset(self.Icon, -1, 1) end)
+ if onClick then btn:SetScript("OnClick", onClick) end
+ return btn
+end
+
+-- Attach hold-to-zoom: OnMouseDown fires one step + installs an OnUpdate accumulator; OnMouseUp clears.
+local function attachZoomHold(btn, model, direction)
+ btn:SetScript("OnMouseDown", function(self)
+ adjustOffset(self.Icon, 1, -1)
+ zoomModel(model, direction)
+ self._acc = 0
+ self:SetScript("OnUpdate", function(s, elapsed)
+ s._acc = (s._acc or 0) + elapsed
+ if s._acc >= ZOOM_REPEAT_INTERVAL then
+ s._acc = 0
+ zoomModel(model, direction)
+ end
+ end)
+ end)
+ btn:SetScript("OnMouseUp", function(self)
+ adjustOffset(self.Icon, -1, 1)
+ self:SetScript("OnUpdate", nil)
+ end)
+end
+
+-- Attach hold-to-rotate: drive Model_RotateLeft/Right ourselves each tick while held. DOWNPORT: 3.3.5a
+-- Model_OnUpdate can't be redirected to our buttons (it reads vanilla buttons by name), so we own the
+-- rotation drive here. A quick tap still produces one rotation step on MouseDown.
+local function attachRotateHold(btn, model, dir) -- dir: "L" or "R"
+ local fn = (dir == "L") and _G.Model_RotateLeft or _G.Model_RotateRight
+ btn:SetScript("OnMouseDown", function(self)
+ adjustOffset(self.Icon, 1, -1)
+ if fn then pcall(fn, model, ROTATE_REPEAT) end
+ self._acc = 0
+ self:SetScript("OnUpdate", function(s, elapsed)
+ s._acc = (s._acc or 0) + elapsed
+ -- ~30 ticks/sec feel; Model_RotateLeft itself plays a sound, so step at a modest rate.
+ if s._acc >= 0.03 then
+ s._acc = 0
+ if fn then pcall(fn, model, ROTATE_REPEAT) end
+ end
+ end)
+ end)
+ btn:SetScript("OnMouseUp", function(self)
+ adjustOffset(self.Icon, -1, 1)
+ self:SetScript("OnUpdate", nil)
+ end)
+end
+
+-- buildControlBar(model, opts). Defaults target the CharacterPanel model.
+local function buildControlBar(model, opts)
+ model = model or _G.CharacterModelFrame
+ if not model or model._neControlBar then return end
+ opts = opts or {}
+
+ -- Wheel-zoom over the model (3.3.5a wires no wheel handler on CharacterModelFrame). delta>0 = in.
+ if not model._neWheelZoom then
+ model._neWheelZoom = true
+ model:EnableMouseWheel(true)
+ model:HookScript("OnMouseWheel", function(self, delta) zoomModel(self, delta) end)
+ end
+
+ -- Click-drag: LEFT = rotate, RIGHT = pan (move the character within the frame). DOWNPORT: we own
+ -- both so right-drag panning works (vanilla only does left-drag rotate). Cursor-delta driven.
+ if not model._neDragWired then
+ model._neDragWired = true
+ model:EnableMouse(true)
+ model:RegisterForDrag("LeftButton", "RightButton")
+ model:SetScript("OnMouseDown", function(self, button)
+ if button ~= "LeftButton" and button ~= "RightButton" then return end
+ self._dragBtn = button
+ self._dragLX, self._dragLY = GetCursorPosition()
+ end)
+ model:SetScript("OnMouseUp", function(self) self._dragBtn = nil end)
+ model:HookScript("OnUpdate", function(self)
+ if not self._dragBtn then return end
+ -- DOWNPORT/REPORT (janky release): the physical button is the source of truth. OnMouseUp is MISSED
+ -- when the cursor leaves the model mid-drag (release happens off-frame), so _dragBtn stuck and the
+ -- doll kept rotating/panning. Self-terminate the instant the button is no longer held, anywhere.
+ local ok, down = pcall(IsMouseButtonDown, self._dragBtn)
+ if not (ok and down) then self._dragBtn = nil; return end
+ local mx, my = GetCursorPosition()
+ local dx = mx - (self._dragLX or mx)
+ local dy = my - (self._dragLY or my)
+ self._dragLX, self._dragLY = mx, my
+ if self._dragBtn == "LeftButton" then
+ -- Horizontal drag should match the rotate button direction rather than invert it.
+ local rot = (self.rotation or 0) + dx * 0.012
+ if self.SetRotation then pcall(self.SetRotation, self, rot); self.rotation = rot end
+ elseif self._dragBtn == "RightButton" then
+ local ok, px, py, pz = pcall(self.GetPosition, self)
+ if ok and px and self.SetPosition then
+ -- DOWNPORT/REPORT: right-drag pan follows the cursor. Horizontal was inverted (now +dx);
+ -- vertical was already correct (+dy) — over-swapping it once inverted up/down, restored here.
+ pcall(self.SetPosition, self, px, (py or 0) + dx * 0.004, (pz or 0) + dy * 0.004)
+ end
+ end
+ end)
+ end
+
+ local rotateButtons = opts.rotateButtons or {
+ "CharacterModelFrameRotateLeftButton",
+ "CharacterModelFrameRotateRightButton",
+ }
+ local panelCheck = opts.panelCheck or function()
+ return CP.frame and CP.frame:IsShown()
+ and (CP._activeTab == nil or CP._activeTab == "Character")
+ end
+
+ -- Hide the vanilla wooden rotate buttons. DOWNPORT: no NE.squelch -> Hide() directly. A hidden
+ -- button is never in the PUSHED state, so the vanilla Model_OnUpdate rotation no-ops on them; our
+ -- own buttons drive rotation instead.
+ for _, name in ipairs(rotateButtons) do
+ local b = _G[name]
+ if b then pcall(function() b:Hide(); if b.SetAlpha then b:SetAlpha(0) end end) end
+ end
+
+ -- Bar container. Width = 2 + 5*(32-6) = 132. Hover-reveal; alpha 0.5 default (per retail).
+ -- Unnamed: nothing here looks the frame up by its old hardcoded global name, and a fixed literal
+ -- name would collide (CreateFrame errors reusing an existing global) the moment this helper is
+ -- called for a SECOND model — which the Collections window now does (Mounts + Pet Journal each
+ -- get their own model/control bar).
+ local bar = CreateFrame("Frame", nil, model)
+ bar:SetSize(2 + 5 * (BTN_SIZE + BTN_HPADDING), BTN_SIZE)
+ bar:SetPoint("TOP", model, "TOP", 0, -4)
+ bar:SetAlpha(DEFAULT_ALPHA)
+ bar:Hide()
+ model._neControlBar = bar
+
+ local function placeButton(btn, prev)
+ btn:ClearAllPoints()
+ if prev then btn:SetPoint("LEFT", prev, "RIGHT", BTN_HPADDING, 0)
+ else btn:SetPoint("LEFT", bar, "LEFT", 0, 0) end
+ end
+
+ local btnZoomIn = makeButton(bar, "common-icon-zoomin", nil)
+ attachZoomHold(btnZoomIn, model, 1)
+ placeButton(btnZoomIn, nil)
+
+ local btnZoomOut = makeButton(bar, "common-icon-zoomout", nil)
+ attachZoomHold(btnZoomOut, model, -1)
+ placeButton(btnZoomOut, btnZoomIn)
+
+ local btnRotL = makeButton(bar, "common-icon-rotateleft", nil)
+ attachRotateHold(btnRotL, model, "L")
+ placeButton(btnRotL, btnZoomOut)
+
+ local btnRotR = makeButton(bar, "common-icon-rotateright", nil)
+ attachRotateHold(btnRotR, model, "R")
+ placeButton(btnRotR, btnRotL)
+
+ local btnReset = makeButton(bar, "common-icon-undo", function() resetModel(model) end)
+ placeButton(btnReset, btnRotR)
+
+ -- Hover-reveal via cursor-rect polling (PlayerModel:IsMouseOver is unreliable on 3.3.5a). Bar shows
+ -- at alpha 1 while the cursor is over the model OR the bar; hides entirely otherwise. Only while the
+ -- panel + Character tab are shown.
+ local function isMouseInside(frame)
+ if not (frame and frame:IsVisible()) then return false end
+ local x, y = GetCursorPosition()
+ local s = frame:GetEffectiveScale() or 1
+ if s <= 0 then return false end
+ local mx, my = x / s, y / s
+ local left, bottom = frame:GetLeft(), frame:GetBottom()
+ local right, top = frame:GetRight(), frame:GetTop()
+ if not (left and bottom and right and top) then return false end
+ return mx >= left and mx <= right and my >= bottom and my <= top
+ end
+
+ local watcher = CreateFrame("Frame")
+ watcher:SetScript("OnUpdate", function()
+ if not panelCheck() then
+ if bar:IsShown() then bar:Hide() end
+ return
+ end
+ if isMouseInside(model) or isMouseInside(bar) then
+ if not bar:IsShown() then bar:Show(); bar:SetAlpha(1) end
+ else
+ if bar:IsShown() then bar:Hide(); bar:SetAlpha(DEFAULT_ALPHA) end
+ end
+ end)
+ model._neControlWatcher = watcher
+end
+
+CP.BuildModelControls = buildControlBar
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled(MODULE) then return end
+ local ok, err = pcall(buildControlBar)
+ if not ok then log("ModelControls boot failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/PaperDoll.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/PaperDoll.lua
new file mode 100644
index 0000000..f1ebc24
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/PaperDoll.lua
@@ -0,0 +1,79 @@
+-- DragonUI_NewEra/modules/character/PaperDoll.lua — the class-themed background behind the stats pane.
+--
+-- ARCHITECTURE (CONTRACT_S1 §A0 / VISUAL_SPEC): mirrors retail's CharacterStatsPane.ClassBackground —
+-- a per-class backdrop (197x355, ui-character-info--bg) anchored TOPLEFT(0,0) of
+-- NE_CharacterStatsPane. The atlases (FDID 1400895 / 1400896) + their rects are registered by Agent F's
+-- Assets.lua; we just pick the player's class atlas and seat it.
+--
+-- Ported from NewEra/CharacterPanel/PaperDoll.lua. The pane is built lazily by Sidebar.lua (on first
+-- sidebar build); buildSidebar calls CP.ApplyClassBackground the moment NE_CharacterStatsPane exists,
+-- and a login-time retry covers the already-built / late-load case.
+--
+-- 3.3.5 GOTCHAS (§B): no SetShown (Show/Hide); NE.tex.SetAtlas returns false if the class-bg sheet
+-- isn't shipped → we simply skip (the marble inset bg from InsetFrames.lua remains; graceful).
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local function log(msg)
+ if CP._log then CP._log(msg); return end
+ if NE.Log then NE.Log("PAPERDOLL", msg) end
+end
+
+-- classfile (UnitClass) -> atlas name. All 12 are registered in Assets.lua; 3.3.5 only has the 9
+-- vanilla+wrath classes, but resolving the rest is harmless (a custom server could add them).
+local CLASS_BG_ATLAS = {
+ DRUID = "ui-character-info-druid-bg",
+ HUNTER = "ui-character-info-hunter-bg",
+ MAGE = "ui-character-info-mage-bg",
+ PALADIN = "ui-character-info-paladin-bg",
+ PRIEST = "ui-character-info-priest-bg",
+ ROGUE = "ui-character-info-rogue-bg",
+ SHAMAN = "ui-character-info-shaman-bg",
+ WARLOCK = "ui-character-info-warlock-bg",
+ WARRIOR = "ui-character-info-warrior-bg",
+ DEATHKNIGHT = "ui-character-info-deathknight-bg",
+ DEMONHUNTER = "ui-character-info-demonhunter-bg",
+ MONK = "ui-character-info-monk-bg",
+}
+
+-- Apply the class background to NE_CharacterStatsPane (built by Sidebar.lua).
+local function applyClassBackground()
+ local pane = _G.NE_CharacterStatsPane or (CP._sidebar)
+ if not pane then return end
+ local _, classFile = UnitClass("player")
+ if not classFile then return end
+ local atlas = CLASS_BG_ATLAS[classFile]
+ if not atlas then log("no class bg atlas for " .. tostring(classFile)); return end
+
+ local bg = pane._neClassBg
+ if not bg then
+ -- Below the stat rows + marble inset, above nothing. Retail anchors TOPLEFT(0,0), useAtlasSize.
+ bg = pane:CreateTexture(nil, "BACKGROUND", nil, -3)
+ bg:SetPoint("TOPLEFT", pane, "TOPLEFT", 0, 0)
+ bg:SetSize(197, 355)
+ bg:SetAlpha(0.55) -- DOWNPORT: dim slightly so the stat text stays legible over the art
+ pane._neClassBg = bg
+ end
+
+ if NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(bg, atlas, false) then
+ bg:Show()
+ else
+ -- Atlas not shipped → leave the marble inset bg; hide our (blank) texture so it can't blank art.
+ bg:Hide()
+ end
+end
+CP.ApplyClassBackground = applyClassBackground
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ applyClassBackground()
+ -- Retry shortly in case Sidebar.lua hasn't built the pane yet (PaperDoll loads before Sidebar).
+ if not (_G.NE_CharacterStatsPane and _G.NE_CharacterStatsPane._neClassBg) then
+ if C_Timer and C_Timer.After then C_Timer.After(0.5, applyClassBackground) end
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/PetPaperDoll.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/PetPaperDoll.lua
new file mode 100644
index 0000000..b78d400
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/PetPaperDoll.lua
@@ -0,0 +1,591 @@
+-- DragonUI_NewEra/modules/character/PetPaperDoll.lua — the Pet secondary tab.
+--
+-- Fills the EMPTY pane DragonUI_NewEra_CharacterPane_Pet (created by TabButtons.lua, parented to
+-- NE.charpanel.frame.Inset). Pet MODEL + pet INFO (name / level+family / happiness / loyalty /
+-- diet) + an XP bar + a scrollable pet-STATS list — the player-paperdoll idiom applied to the pet,
+-- ported in spirit from NewEra/CharacterPanel/PetPaperDoll.lua.
+--
+-- DOWNPORT (CONTRACT_S1 §A0/§A.2/§B): NewEra reparented the stock PetModelFrame into a class-themed
+-- 3D scene driven by the stock PetPaperDollFrame_Update, and used WowScrollBox for the right stats
+-- sidebar. Our custom panel HIDES the stock CharacterFrame, so the stock pet-update path does NOT
+-- run — we therefore DRIVE the model ourselves (model:SetUnit("pet")) and read pet data directly.
+-- Stats render in a NAMED FauxScrollFrame (unnamed FauxScrollFrameTemplate ERRORS).
+--
+-- DATA (3.3.5a): UnitExists/UnitName/UnitLevel/UnitCreatureFamily("pet"), GetPetHappiness,
+-- GetPetLoyalty, GetPetFoodTypes, GetPetExperience, GetPetTrainingPoints, UnitStat/UnitArmor/
+-- UnitResistance/UnitDamage/UnitAttackPower/UnitAttackSpeed/UnitHealthMax/UnitPowerMax("pet").
+-- Many are HUNTER-pet-only (happiness/loyalty/diet/training) and are nil for warlock minions —
+-- every one is guarded.
+--
+-- GRACEFUL DEGRADATION (§A.5): NO pet → the pane shows a centered "You do not have a pet." line and
+-- hides the model/stats; every getter is pcall-guarded — NEVER errors.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local function log(msg)
+ if CP._log then CP._log("PET: " .. tostring(msg)); return end
+ if NE.Log then NE.Log("PET", msg); return end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffffcc55DragonUI_NewEra|r [pet]: " .. tostring(msg))
+ end
+end
+
+local function L(global, fallback)
+ local v = _G[global]
+ if type(v) == "string" and v ~= "" then return v end
+ return fallback
+end
+
+local function setAtlas(tex, name, useAtlasSize)
+ if tex and NE.tex and NE.tex.SetAtlas then return NE.tex.SetAtlas(tex, name, useAtlasSize) end
+ return false
+end
+
+local function safe(fn, ...)
+ if type(fn) ~= "function" then return end
+ local r = { pcall(fn, ...) }
+ if r[1] then return select(2, unpack(r)) end
+end
+
+-- Geometry. The pane covers the Inset (~330 wide collapsed). Model on top, info overlay, XP bar,
+-- then a scrollable stats list filling the rest.
+local MODEL_H = 200
+local XP_H = 14
+local STAT_ROW = 16
+local FILL_TEX = "Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar"
+local XP_COLOR = { 0.58, 0, 0.55 } -- stock pet-XP purple
+
+-- UI-PetHappiness cells (PetFrame_SetHappiness): 1=unhappy 2=content 3=happy.
+local HAPPINESS_TC = {
+ [1] = { 0.375, 0.5625 }, [2] = { 0.1875, 0.375 }, [3] = { 0, 0.1875 },
+}
+
+-- ---------------------------------------------------------------------------
+-- Pet stat definitions (label + a guarded compute() -> value, tooltipTitle, tooltipLines).
+-- Resistance school indices: Fire=2 Nature=3 Frost=4 Shadow=5 Arcane=6.
+-- ---------------------------------------------------------------------------
+local function statPrimary(idx, labelText)
+ return function()
+ local _, effective = UnitStat("pet", idx)
+ effective = effective or 0
+ return tostring(effective), labelText .. " " .. effective
+ end
+end
+
+local function statResist(schoolIdx, schoolLabel)
+ return function()
+ local base, resistance = UnitResistance("pet", schoolIdx)
+ resistance = resistance or 0
+ return tostring(resistance), schoolLabel .. " " .. L("RESISTANCE", "Resistance") .. ": " .. resistance,
+ { "Reduces " .. schoolLabel:lower() .. " damage taken." }
+ end
+end
+
+local DEF_HEALTH = { label = L("HEALTH", "Health"), compute = function()
+ local v = UnitHealthMax("pet") or 0
+ return tostring(v), L("HEALTH", "Health") .. " " .. v
+end }
+local DEF_POWER = { label = L("MANA", "Mana"),
+ relabel = function() local _, t = UnitPowerType("pet"); return _G[t or ""] or L("MANA", "Mana") end,
+ compute = function()
+ local v = UnitPowerMax("pet") or 0
+ local _, t = UnitPowerType("pet")
+ return tostring(v), (_G[t or ""] or L("MANA", "Mana")) .. " " .. v
+ end }
+local DEF_TRAINING = { label = L("PET_TRAIN_BUTTON", "Training Points"), compute = function()
+ local total, spent = GetPetTrainingPoints()
+ total, spent = total or 0, spent or 0
+ local unspent = total - spent
+ return tostring(unspent), L("PET_TRAIN_BUTTON", "Training Points") .. ": " .. unspent,
+ { string.format("Total: %d", total), string.format("Spent: %d", spent) }
+end }
+local DEF_DAMAGE = { label = L("DAMAGE", "Damage"), compute = function()
+ local minDmg, maxDmg = UnitDamage("pet")
+ local low = math.max(math.floor(minDmg or 0), 1)
+ local high = math.max(math.ceil(maxDmg or 0), 1)
+ local value = low .. " - " .. high
+ return value, L("DAMAGE", "Damage") .. " " .. value
+end }
+local DEF_AP = { label = L("ATTACK_POWER", "Attack Power"), compute = function()
+ local base, pos, neg = UnitAttackPower("pet")
+ local eff = (base or 0) + (pos or 0) + (neg or 0)
+ return tostring(eff), L("ATTACK_POWER", "Attack Power") .. ": " .. eff
+end }
+local DEF_ATTACK_SPEED = { label = L("ATTACK_SPEED", "Attack Speed"), compute = function()
+ local speed = UnitAttackSpeed("pet") or 0
+ local value = string.format("%.2f", speed)
+ return value, L("ATTACK_SPEED", "Attack Speed") .. " " .. value
+end }
+local DEF_ARMOR = { label = L("ARMOR", "Armor"), compute = function()
+ local _, effective = UnitArmor("pet")
+ effective = effective or 0
+ return tostring(math.floor(effective)), L("ARMOR", "Armor") .. " " .. math.floor(effective)
+end }
+
+local DEF_RESISTS = {
+ { label = L("DAMAGE_SCHOOL6", "Arcane"), compute = statResist(6, L("DAMAGE_SCHOOL6", "Arcane")) },
+ { label = L("DAMAGE_SCHOOL2", "Fire"), compute = statResist(2, L("DAMAGE_SCHOOL2", "Fire")) },
+ { label = L("DAMAGE_SCHOOL4", "Frost"), compute = statResist(4, L("DAMAGE_SCHOOL4", "Frost")) },
+ { label = L("DAMAGE_SCHOOL3", "Nature"), compute = statResist(3, L("DAMAGE_SCHOOL3", "Nature")) },
+ { label = L("DAMAGE_SCHOOL5", "Shadow"), compute = statResist(5, L("DAMAGE_SCHOOL5", "Shadow")) },
+}
+
+local function isHunter() return (select(2, UnitClass("player"))) == "HUNTER" end
+
+-- Build the flat stat list (headers + rows), hunter-only Training row included for hunters.
+local function buildStatDefs()
+ local sections = {
+ { title = L("GENERAL", "General"), defs = (function()
+ local t = { DEF_HEALTH, DEF_POWER }
+ if isHunter() then t[#t + 1] = DEF_TRAINING end
+ return t
+ end)() },
+ { title = L("STAT_CATEGORY_ATTRIBUTES", "Attributes"), defs = {
+ { label = L("SPELL_STAT1_NAME", "Strength"), compute = statPrimary(1, L("SPELL_STAT1_NAME", "Strength") .. ":") },
+ { label = L("SPELL_STAT2_NAME", "Agility"), compute = statPrimary(2, L("SPELL_STAT2_NAME", "Agility") .. ":") },
+ { label = L("SPELL_STAT3_NAME", "Stamina"), compute = statPrimary(3, L("SPELL_STAT3_NAME", "Stamina") .. ":") },
+ { label = L("SPELL_STAT4_NAME", "Intellect"), compute = statPrimary(4, L("SPELL_STAT4_NAME", "Intellect") .. ":") },
+ { label = L("SPELL_STAT5_NAME", "Spirit"), compute = statPrimary(5, L("SPELL_STAT5_NAME", "Spirit") .. ":") },
+ } },
+ { title = L("MELEE", "Attack"), defs = { DEF_DAMAGE, DEF_AP, DEF_ATTACK_SPEED } },
+ { title = L("DEFENSE", "Defense"), defs = { DEF_ARMOR } },
+ { title = L("RESISTANCE", "Resistances"), defs = DEF_RESISTS },
+ }
+ return sections
+end
+
+-- ---------------------------------------------------------------------------
+-- Pane state.
+-- ---------------------------------------------------------------------------
+local pane, scene, model, info, happy, xpBar
+local nameText, levelText, loyaltyText, dietText
+local statScroll, statContent
+local statHeaders, statRows = {}, {} -- recycled by index
+local flat = {} -- { kind="header"/"row", ... }
+local NUM_VISIBLE = 0
+local noPetLabel
+
+local function getPane()
+ if pane then return pane end
+ pane = _G.DragonUI_NewEra_CharacterPane_Pet or (CP.EnsurePane and CP.EnsurePane("Pet"))
+ return pane
+end
+
+-- ---------------------------------------------------------------------------
+-- Build the model scene + info overlay + XP bar (top half of the pane).
+-- ---------------------------------------------------------------------------
+local function buildScene(host)
+ if scene then return end
+ scene = CreateFrame("Frame", "DragonUI_NewEra_PetScene", host)
+ -- DOWNPORT/REPORT: the PET STATS now live in the InsetRight sidebar (CP.ExpandPetSidebar), so the
+ -- scene fills the WHOLE Inset pane (model + name/level/loyalty/diet + XP bar) instead of just the
+ -- top MODEL_H band above an in-pane stats list. Anchor BOTTOMRIGHT to the pane.
+ scene:SetPoint("TOPLEFT", host, "TOPLEFT", 4, -4)
+ scene:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -4, 4)
+
+ -- Class-themed backdrop (degrades silently if not shipped).
+ local bg = scene:CreateTexture(nil, "BACKGROUND")
+ local _, classFile = UnitClass("player")
+ if not setAtlas(bg, "ui-character-info-" .. (classFile and classFile:lower() or "hunter") .. "-bg", false) then
+ if bg.SetColorTexture then bg:SetColorTexture(0.06, 0.06, 0.08, 0.6) else bg:SetTexture(0.06, 0.06, 0.08, 0.6) end
+ end
+ bg:SetAllPoints(scene)
+
+ -- The 3D pet model. Reparent the stock PetModelFrame if present (its global name keeps any stock
+ -- driving intact); else create our own PlayerModel. We drive SetUnit("pet") ourselves on refresh.
+ local m = _G.PetModelFrame
+ if m and m.SetParent then
+ m:SetParent(scene)
+ else
+ m = CreateFrame("PlayerModel", "DragonUI_NewEra_PetModel", scene)
+ end
+ m:ClearAllPoints()
+ m:SetPoint("TOPLEFT", scene, "TOPLEFT", 6, -28)
+ m:SetPoint("BOTTOMRIGHT", scene, "BOTTOMRIGHT", -6, 6)
+ m:SetFrameLevel(scene:GetFrameLevel() + 1)
+ model = m
+
+ -- DOWNPORT/REPORT (Issue C): the stock 3.3.5a pet model frame ships wooden rotate buttons. When we
+ -- reparent PetModelFrame they get orphaned / float at the screen origin. HIDE the stray vanilla
+ -- rotate buttons (both the pet-model's and — belt-and-suspenders — the character-model's, since the
+ -- player paperdoll is hidden on this tab). A hidden button is never PUSHED, so stock Model_OnUpdate
+ -- no-ops on it. The pet model has NO controls (static display) — see below.
+ for _, name in ipairs({
+ "PetModelFrameRotateLeftButton", "PetModelFrameRotateRightButton",
+ "PetModelRotateLeftButton", "PetModelRotateRightButton",
+ "PetPaperDollFrameRotateLeftButton", "PetPaperDollFrameRotateRightButton",
+ "CharacterModelFrameRotateLeftButton", "CharacterModelFrameRotateRightButton",
+ }) do
+ local b = _G[name]
+ if b then pcall(function() b:Hide(); if b.SetAlpha then b:SetAlpha(0) end end) end
+ end
+
+ -- DOWNPORT/REPORT: the pet model is a STATIC display — no zoom, no rotate (per design). We do NOT
+ -- attach CP.BuildModelControls, and we disable mouse + wheel on the model so there is no drag-rotate
+ -- or wheel-zoom either.
+ if m.EnableMouse then pcall(m.EnableMouse, m, false) end
+ if m.EnableMouseWheel then pcall(m.EnableMouseWheel, m, false) end
+
+ -- Info overlay (above the model): happiness face + pet name.
+ info = CreateFrame("Frame", nil, scene)
+ info:SetPoint("TOPLEFT", scene, "TOPLEFT", 8, -4)
+ info:SetPoint("TOPRIGHT", scene, "TOPRIGHT", -8, -4)
+ info:SetHeight(22)
+ info:SetFrameLevel(scene:GetFrameLevel() + 4)
+
+ happy = CreateFrame("Frame", nil, info)
+ happy:SetSize(20, 19)
+ happy:SetPoint("LEFT", info, "LEFT", 0, 0)
+ happy:EnableMouse(true)
+ local ht = happy:CreateTexture(nil, "ARTWORK")
+ ht:SetAllPoints(happy)
+ ht:SetTexture("Interface\\PetPaperDollFrame\\UI-PetHappiness")
+ happy.tex = ht
+ happy:SetScript("OnEnter", function(self)
+ if not self.tooltip then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText(self.tooltip, 1, 1, 1)
+ GameTooltip:Show()
+ end)
+ happy:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ happy:Hide()
+
+ nameText = info:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
+ nameText:SetPoint("LEFT", info, "LEFT", 0, 0)
+ nameText:SetJustifyH("LEFT")
+ nameText:SetShadowColor(0, 0, 0, 1); nameText:SetShadowOffset(1, -1)
+
+ levelText = scene:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ levelText:SetPoint("TOPLEFT", info, "BOTTOMLEFT", 0, -2)
+ levelText:SetJustifyH("LEFT")
+ levelText:SetShadowColor(0, 0, 0, 1); levelText:SetShadowOffset(1, -1)
+
+ loyaltyText = scene:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ loyaltyText:SetPoint("TOPLEFT", levelText, "BOTTOMLEFT", 0, -2)
+ loyaltyText:SetJustifyH("LEFT")
+ loyaltyText:SetShadowColor(0, 0, 0, 1); loyaltyText:SetShadowOffset(1, -1)
+
+ dietText = scene:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ dietText:SetPoint("BOTTOMLEFT", scene, "BOTTOMLEFT", 8, XP_H + 6)
+ dietText:SetPoint("RIGHT", scene, "RIGHT", -8, 0)
+ dietText:SetJustifyH("LEFT")
+ dietText:SetWordWrap(true)
+ dietText:SetShadowColor(0, 0, 0, 1); dietText:SetShadowOffset(1, -1)
+
+ -- XP bar along the scene bottom.
+ xpBar = CreateFrame("StatusBar", "DragonUI_NewEra_PetXPBar", scene)
+ xpBar:SetHeight(XP_H)
+ xpBar:SetPoint("BOTTOMLEFT", scene, "BOTTOMLEFT", 8, 4)
+ xpBar:SetPoint("BOTTOMRIGHT", scene, "BOTTOMRIGHT", -8, 4)
+ xpBar:SetFrameLevel(scene:GetFrameLevel() + 5)
+ xpBar:SetStatusBarTexture(FILL_TEX)
+ local xt = xpBar:GetStatusBarTexture()
+ if xt then xt:SetDrawLayer("BORDER") end
+ xpBar:SetStatusBarColor(XP_COLOR[1], XP_COLOR[2], XP_COLOR[3])
+ xpBar:SetMinMaxValues(0, 1); xpBar:SetValue(0)
+ local xbg = xpBar:CreateTexture(nil, "BACKGROUND")
+ xbg:SetAllPoints(xpBar)
+ if xbg.SetColorTexture then xbg:SetColorTexture(0, 0, 0, 0.8) else xbg:SetTexture(0, 0, 0, 0.8) end
+ xpBar._text = xpBar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ xpBar._text:SetPoint("CENTER", xpBar, "CENTER", 0, 0)
+end
+
+-- ---------------------------------------------------------------------------
+-- Build the stats list (bottom half, named FauxScrollFrame).
+-- ---------------------------------------------------------------------------
+local function buildStatRow(parent)
+ local row = CreateFrame("Frame", nil, parent)
+ row:SetHeight(STAT_ROW)
+ local zebra = row:CreateTexture(nil, "BACKGROUND")
+ zebra:SetAllPoints(row)
+ if zebra.SetColorTexture then zebra:SetColorTexture(1, 1, 1, 0.04) else zebra:SetTexture(1, 1, 1, 0.04) end
+ row._zebra = zebra
+ row._label = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
+ row._label:SetPoint("LEFT", row, "LEFT", 10, 0)
+ row._value = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
+ row._value:SetPoint("RIGHT", row, "RIGHT", -8, 0)
+ row:EnableMouse(true)
+ row:SetScript("OnEnter", function(self)
+ if not self._tipTitle then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText(self._tipTitle, 1, 1, 1, 1, true)
+ if self._tipLines then
+ for _, ln in ipairs(self._tipLines) do
+ if ln and ln ~= "" then GameTooltip:AddLine(ln, 1, 0.82, 0.1, true) end
+ end
+ end
+ GameTooltip:Show()
+ end)
+ row:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ return row
+end
+
+local function buildStatHeader(parent)
+ local h = CreateFrame("Frame", nil, parent)
+ h:SetHeight(STAT_ROW + 4)
+ local hb = h:CreateTexture(nil, "BACKGROUND")
+ hb:SetAllPoints(h)
+ if not setAtlas(hb, "UI-Character-Info-Title", false) then
+ if hb.SetColorTexture then hb:SetColorTexture(0.2, 0.16, 0.1, 0.5) else hb:SetTexture(0.2, 0.16, 0.1, 0.5) end
+ end
+ h._name = h:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
+ h._name:SetPoint("CENTER", h, "CENTER", 0, 0)
+ return h
+end
+
+local function acquireHeader(i)
+ if not statHeaders[i] then statHeaders[i] = buildStatHeader(statContent) end
+ return statHeaders[i]
+end
+local function acquireRow(i)
+ if not statRows[i] then statRows[i] = buildStatRow(statContent) end
+ return statRows[i]
+end
+
+local function hideAllStatWidgets()
+ for _, h in ipairs(statHeaders) do h:Hide() end
+ for _, r in ipairs(statRows) do r:Hide() end
+end
+
+-- Rebuild the flat list from the stat sections, computing each value (guarded).
+local function rebuildStatFlat()
+ flat = {}
+ if not UnitExists("pet") then return end
+ for _, sec in ipairs(buildStatDefs()) do
+ flat[#flat + 1] = { kind = "header", title = sec.title }
+ local n = 0
+ for _, def in ipairs(sec.defs) do
+ n = n + 1
+ local label = def.label
+ if def.relabel then label = safe(def.relabel) or label end
+ local value, tipTitle, tipLines = "--", nil, nil
+ local ok, v, t, lines = pcall(def.compute)
+ if ok then value, tipTitle, tipLines = v or "--", t, lines end
+ flat[#flat + 1] = { kind = "row", label = label, value = value, even = (n % 2 == 0),
+ tipTitle = tipTitle, tipLines = tipLines }
+ end
+ end
+end
+
+local function updateStatScroll()
+ if not (statScroll and statContent) then return end
+ hideAllStatWidgets()
+ local total = #flat
+ local nRows = NUM_VISIBLE > 0 and NUM_VISIBLE or 1
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(statScroll, total, nRows, STAT_ROW) end
+ local offset = (FauxScrollFrame_GetOffset and FauxScrollFrame_GetOffset(statScroll)) or 0
+
+ local rowW = statContent:GetWidth() or 200
+ if rowW <= 0 then rowW = 200 end
+
+ local hUsed, rUsed = 0, 0
+ for i = 1, nRows do
+ local data = flat[offset + i]
+ if data then
+ local w
+ if data.kind == "header" then
+ hUsed = hUsed + 1
+ w = acquireHeader(hUsed)
+ w._name:SetText(data.title)
+ else
+ rUsed = rUsed + 1
+ w = acquireRow(rUsed)
+ w._label:SetText(data.label)
+ w._value:SetText(data.value or "--")
+ if data.even then w._zebra:Show() else w._zebra:Hide() end
+ w._tipTitle = data.tipTitle
+ w._tipLines = data.tipLines
+ end
+ w:ClearAllPoints()
+ w:SetWidth(rowW)
+ w:SetPoint("TOPLEFT", statContent, "TOPLEFT", 0, -(i - 1) * STAT_ROW)
+ end
+ end
+end
+
+local function recomputeVisible()
+ if not statScroll then return end
+ local h = statScroll:GetHeight() or 0
+ NUM_VISIBLE = math.max(1, math.floor(h / STAT_ROW))
+end
+
+local function buildStatsList(host)
+ if statScroll then return end
+ statScroll = CreateFrame("ScrollFrame", "DragonUI_NewEra_PetStatsScroll", host, "FauxScrollFrameTemplate")
+ statScroll:SetPoint("TOPLEFT", host, "TOPLEFT", 6, -(MODEL_H + 12))
+ statScroll:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -26, 6)
+ statScroll:SetScript("OnVerticalScroll", function(self, offset)
+ FauxScrollFrame_OnVerticalScroll(self, offset, STAT_ROW, updateStatScroll)
+ end)
+ statScroll:HookScript("OnSizeChanged", function() recomputeVisible(); updateStatScroll() end)
+ -- DOWNPORT: hand-built minimal scrollbar (Reskin's stock-slider re-skin didn't render).
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, statScroll, { x = 6 }) end
+
+ statContent = CreateFrame("Frame", nil, statScroll)
+ statContent:SetPoint("TOPLEFT", statScroll, "TOPLEFT", 0, 0)
+ statContent:SetPoint("RIGHT", statScroll, "RIGHT", 0, 0)
+ statContent:SetHeight(1)
+ recomputeVisible()
+end
+
+-- ---------------------------------------------------------------------------
+-- Refresh: drive the model + fill info + XP + stats. NO pet → degrade.
+-- ---------------------------------------------------------------------------
+local collectionModeActive = false
+
+local function refresh()
+ if not getPane() then return end
+ if not scene then return end
+ if collectionModeActive then return end
+
+ local hasPet = UnitExists("pet")
+ if not hasPet then
+ if scene then scene:Hide() end
+ if statScroll then statScroll:Hide() end
+ flat = {}
+ hideAllStatWidgets()
+ if noPetLabel then noPetLabel:Show() end
+ -- DOWNPORT/REPORT: keep the sidebar pet stats in sync (they read "--" with no pet).
+ if CP._sidebarPetMode and CP.RefreshSidebar then pcall(CP.RefreshSidebar) end
+ return
+ end
+ if noPetLabel then noPetLabel:Hide() end
+ scene:Show()
+ if statScroll then statScroll:Hide() end -- DOWNPORT/REPORT: in-pane stats retired; sidebar owns them
+
+ -- Model. Re-frame only when the pet actually changes (refresh fires often for happiness/xp; a
+ -- per-refresh SetUnit would reset any manual zoom every tick).
+ if model then
+ local guid = UnitGUID and UnitGUID("pet")
+ if model._nePetGUID ~= guid then
+ model._nePetGUID = guid
+ if model.SetUnit then pcall(model.SetUnit, model, "pet") end
+ -- DOWNPORT/REPORT: do NOT call SetCamera(0) — camera index 0 is the creature's PORTRAIT camera
+ -- (zoomed to the head), which made the pet "far too zoomed in". SetUnit alone gives the full-body
+ -- fit; clear any leftover portrait-zoom / depth so the resting framing is the whole pet.
+ if model.SetPortraitZoom then pcall(model.SetPortraitZoom, model, 0) end
+ if model.SetPosition then pcall(model.SetPosition, model, 0, 0, 0) end
+ if model.SetCamDistanceScale then pcall(model.SetCamDistanceScale, model, 1) end
+ end
+ end
+
+ -- Name.
+ local name = UnitName("pet")
+ if name and name ~= (UNKNOWNOBJECT or "Unknown") then nameText:SetText(name) end
+
+ -- Happiness (hunter pets only).
+ local h = safe(GetPetHappiness)
+ if h and HAPPINESS_TC[h] then
+ happy.tex:SetTexCoord(HAPPINESS_TC[h][1], HAPPINESS_TC[h][2], 0, 0.359375)
+ happy.tooltip = _G["PET_HAPPINESS" .. h] or L("HAPPINESS", "Happiness")
+ happy:Show()
+ nameText:ClearAllPoints()
+ nameText:SetPoint("LEFT", happy, "RIGHT", 6, 0)
+ else
+ happy:Hide()
+ nameText:ClearAllPoints()
+ nameText:SetPoint("LEFT", info, "LEFT", 0, 0)
+ end
+
+ -- Level + family.
+ local family = safe(UnitCreatureFamily, "pet")
+ local lvl = UnitLevel("pet") or 0
+ if family then
+ levelText:SetText(string.format(L("UNIT_LEVEL_TEMPLATE", "Level %d"), lvl) .. " " .. family)
+ else
+ levelText:SetText(string.format(L("UNIT_LEVEL_TEMPLATE", "Level %d"), lvl))
+ end
+
+ -- Loyalty / diet (hunter only).
+ local loyalty = safe(GetPetLoyalty)
+ loyaltyText:SetText(loyalty or "")
+ local foods = { safe(GetPetFoodTypes) }
+ if foods[1] and BuildListString then
+ dietText:SetText((L("PET_DIET_TEMPLATE", "Diet: %s")):format(BuildListString(unpack(foods))))
+ else
+ dietText:SetText("")
+ end
+
+ -- XP bar (warlock minions report 0/0 → hide).
+ local cur, nextXP = safe(GetPetExperience)
+ cur, nextXP = cur or 0, nextXP or 0
+ if nextXP > 0 then
+ xpBar:Show()
+ xpBar:SetMinMaxValues(0, nextXP)
+ xpBar:SetValue(cur)
+ xpBar._text:SetText(string.format("%s %d / %d (%d%%)", L("XP", "XP"), cur, nextXP,
+ math.floor(cur / nextXP * 100)))
+ else
+ xpBar:Hide()
+ end
+
+ -- Stats now render in the InsetRight sidebar (CP.ExpandPetSidebar). Refresh it when in pet mode.
+ if CP._sidebarPetMode and CP.RefreshSidebar then pcall(CP.RefreshSidebar) end
+end
+CP.RefreshPet = function() pcall(refresh) end
+
+-- Exposed for Companions.lua: the "Mounts & Companions" browser shares this same pane (per the
+-- server's merged mount/companion model). When it's active we hide the live pet model/XP bar and
+-- skip our own refresh entirely so the two views never fight over the pane; SetPetCollectionMode(false)
+-- hands the pane straight back and re-runs a normal refresh.
+function CP.SetPetCollectionMode(active)
+ collectionModeActive = active and true or false
+ if collectionModeActive then
+ if scene then scene:Hide() end
+ if statScroll then statScroll:Hide() end
+ if noPetLabel then noPetLabel:Hide() end
+ else
+ pcall(refresh)
+ end
+end
+
+local function build()
+ local host = getPane()
+ if not host then log("Pet pane host missing — cannot build"); return false end
+ if scene then return true end
+
+ buildScene(host)
+ -- DOWNPORT/REPORT: pet stats moved to the InsetRight sidebar (CP.ExpandPetSidebar). The in-pane
+ -- FauxScrollFrame stats list is no longer built here (it would duplicate the sidebar's pet stats).
+ -- buildStatsList(host) intentionally not called.
+
+ noPetLabel = host:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
+ noPetLabel:SetPoint("CENTER", host, "CENTER", 0, 0)
+ noPetLabel:SetText(L("NO_PET", "You do not have a pet."))
+ noPetLabel:Hide()
+ return true
+end
+
+local function init()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ if not build() then return end
+ pcall(refresh)
+ local host = getPane()
+ if host and not host._nePetShowHooked then
+ host._nePetShowHooked = true
+ host:HookScript("OnShow", function() recomputeVisible(); pcall(refresh) end)
+ end
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:RegisterEvent("UNIT_PET")
+boot:RegisterEvent("PET_BAR_UPDATE")
+boot:RegisterEvent("UNIT_HAPPINESS")
+boot:RegisterEvent("UNIT_NAME_UPDATE")
+boot:RegisterEvent("UNIT_PET_EXPERIENCE")
+boot:SetScript("OnEvent", function(_, event, arg1)
+ if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
+ if C_Timer and C_Timer.After then C_Timer.After(0, init) else init() end
+ else
+ -- Unit events: only react to player/pet.
+ if event == "UNIT_PET" or event == "UNIT_HAPPINESS" or event == "UNIT_NAME_UPDATE"
+ or event == "UNIT_PET_EXPERIENCE" then
+ if arg1 and arg1 ~= "player" and arg1 ~= "pet" then return end
+ end
+ CP.RefreshPet()
+ end
+end)
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Reputation.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Reputation.lua
new file mode 100644
index 0000000..99fcfcb
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Reputation.lua
@@ -0,0 +1,411 @@
+-- DragonUI_NewEra/modules/character/Reputation.lua — the Reputation secondary tab.
+--
+-- Fills the EMPTY pane DragonUI_NewEra_CharacterPane_Reputation (created by TabButtons.lua,
+-- parented to NE.charpanel.frame.Inset). Collapsible faction-group HEADERS + per-faction
+-- standing BARS, ported from NewEra/CharacterPanel/Reputation.lua.
+--
+-- DOWNPORT (CONTRACT_S1 §A.2/§B): NewEra used retail WowScrollBox + ScrollUtil + a custom
+-- NE_ReputationFrame overlaying the stock ReputationFrame. 3.3.5a has neither WowScrollBox nor
+-- a meaningful stock ReputationFrame inside our custom panel, so we render DIRECTLY into our
+-- pane with a NAMED FauxScrollFrame (FauxScrollFrameTemplate's OnLoad concatenates
+-- self:GetName().."ScrollBar" → an UNNAMED scroll frame ERRORS) + a recycled row pool, exactly
+-- like the proven DragonflightUICharacter StatsPanel/EquipmentManager pattern.
+--
+-- DATA: GetNumFactions() + GetFactionInfo(i). 3.3.5a tuple (isHeader is at POSITION 9 — §B):
+-- name, description, standingID, barMin, barMax, barValue, atWarWith, canToggleAtWar,
+-- isHeader, isCollapsed, hasRep, isWatched, isChild
+-- Bar tint per standing via the global FACTION_BAR_COLORS[standingID]. Headers toggle via
+-- ExpandFactionHeader(i)/CollapseFactionHeader(i) (which renumber the list → we re-read).
+--
+-- GRACEFUL DEGRADATION (§A.5): every getter is pcall-guarded; if GetNumFactions/GetFactionInfo
+-- are absent the pane shows a single "unavailable" line and logs a warning — NEVER errors.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+-- ---------------------------------------------------------------------------
+-- Local logger (graceful — NE.Log may be absent on a standalone load).
+-- ---------------------------------------------------------------------------
+local function log(msg)
+ if CP._log then CP._log("REPUTATION: " .. tostring(msg)); return end
+ if NE.Log then NE.Log("REPUTATION", msg); return end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffffcc55DragonUI_NewEra|r [reputation]: " .. tostring(msg))
+ end
+end
+
+-- ---------------------------------------------------------------------------
+-- Geometry (mirrors NewEra Reputation.lua's row rhythm).
+-- ---------------------------------------------------------------------------
+local ROW_HEIGHT = 24 -- one FauxScroll row slot (header + entry share the slot height)
+local BAR_W = 132
+local BAR_H = 13
+local LEFT_W = 78 -- left cap (native rep-bar cap, scaled to BAR_W)
+local RIGHT_W = 54 -- right cap (78+54 = BAR_W)
+
+-- Native 3.3.5a rep-bar art (Interface\PaperDollInfoFrame\...). Caps come off the
+-- ReputationBar sheet; the grey fill is the Skills-Bar (tinted via SetStatusBarColor).
+-- DOWNPORT: FDID 136570 does NOT exist on 3.3.5a — use the native Skills-Bar PATH, never that fdid.
+local CAP_TEX = "Interface\\PaperDollInfoFrame\\UI-Character-ReputationBar"
+local FILL_TEX = "Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar"
+-- Prefer the locally-shipped rep-bar BLP (FDID 136567) if registered, else the native path.
+local function capSource()
+ return (NE.tex and NE.tex.localFiles and NE.tex.localFiles[136567]) or CAP_TEX
+end
+
+-- Native stock rep-bar cap texcoords (ReputationFrame.xml LeftTexture/RightTexture).
+local LEFT_TC = { 0.691, 1.0, 0.047, 0.281 }
+local RIGHT_TC = { 0.0, 0.164, 0.3906, 0.625 }
+
+local function setAtlas(tex, name, useAtlasSize)
+ if tex and NE.tex and NE.tex.SetAtlas then return NE.tex.SetAtlas(tex, name, useAtlasSize) end
+ return false
+end
+
+-- ---------------------------------------------------------------------------
+-- Collapsible header row (Options_ListExpand 3-piece + collapse chevron on the right).
+-- DOWNPORT: NewEra used NE.listheader.Build (a Core helper that doesn't exist here) — built
+-- inline. If the list-expand atlas isn't shipped the pieces degrade to untextured (still clickable).
+-- ---------------------------------------------------------------------------
+local function buildHeader(parent)
+ local h = CreateFrame("Button", nil, parent)
+ h:SetHeight(ROW_HEIGHT)
+
+ h._bgLeft = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgLeft, "options_listexpand_left", true)
+ h._bgLeft:SetPoint("LEFT", h, "LEFT", 0, 0)
+
+ h._bgRight = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgRight, "options_listexpand_right", true)
+ h._bgRight:SetPoint("RIGHT", h, "RIGHT", 0, 0)
+
+ h._bgMiddle = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgMiddle, "_options_listexpand_middle", false)
+ h._bgMiddle:SetPoint("TOPLEFT", h._bgLeft, "TOPRIGHT")
+ h._bgMiddle:SetPoint("BOTTOMRIGHT", h._bgRight, "BOTTOMLEFT")
+
+ local hl = h:CreateTexture(nil, "HIGHLIGHT")
+ if hl.SetColorTexture then hl:SetColorTexture(1, 1, 1, 0.10) else hl:SetTexture(1, 1, 1, 0.10) end
+ hl:SetPoint("TOPLEFT", h._bgLeft, "TOPRIGHT")
+ hl:SetPoint("BOTTOMRIGHT", h._bgRight, "BOTTOMLEFT")
+
+ h._name = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h._name:SetPoint("LEFT", h, "LEFT", 12, 0)
+ h._name:SetJustifyH("LEFT")
+
+ h:RegisterForClicks("LeftButtonUp")
+ return h
+end
+
+local function updateHeader(h, info)
+ h._name:SetText(info.name or "")
+ local cap = info.isCollapsed and "options_listexpand_right" or "options_listexpand_right_expanded"
+ setAtlas(h._bgRight, cap, true)
+ local idx = info.factionIndex
+ h:SetScript("OnClick", function()
+ -- 3.3.5a renumbers the faction list on expand/collapse → re-refresh after.
+ if info.isCollapsed then
+ if ExpandFactionHeader then pcall(ExpandFactionHeader, idx) end
+ else
+ if CollapseFactionHeader then pcall(CollapseFactionHeader, idx) end
+ end
+ if CP.RefreshReputation then CP.RefreshReputation() end
+ end)
+end
+
+-- ---------------------------------------------------------------------------
+-- Faction standing entry row (name + tinted standing bar w/ native cap chrome).
+-- ---------------------------------------------------------------------------
+local function buildEntry(parent)
+ local e = CreateFrame("Button", nil, parent)
+ e:SetHeight(ROW_HEIGHT)
+
+ e._name = e:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ e._name:SetPoint("LEFT", e, "LEFT", 12, 0)
+ e._name:SetJustifyH("LEFT")
+
+ local bar = CreateFrame("StatusBar", nil, e)
+ bar:SetSize(BAR_W, BAR_H)
+ bar:SetPoint("RIGHT", e, "RIGHT", -4, 0)
+ bar:SetStatusBarTexture(FILL_TEX)
+ -- Force the fill DOWN to BORDER so the ARTWORK caps draw above it (stock rep-bar behaviour).
+ local fillTex = bar:GetStatusBarTexture()
+ if fillTex then fillTex:SetDrawLayer("BORDER") end
+ bar:SetMinMaxValues(0, 1)
+ bar:SetValue(1)
+
+ local barBg = bar:CreateTexture(nil, "BACKGROUND")
+ barBg:SetAllPoints(bar)
+ if barBg.SetColorTexture then barBg:SetColorTexture(0, 0, 0, 1) else barBg:SetTexture(0, 0, 0, 1) end
+
+ -- DOWNPORT/REPORT: brown ReputationBar end-caps removed (clean bar). A 1px dark OUTLINE frames the
+ -- bar WITHOUT covering the fill — the earlier solid overlay sat on the BORDER-layer fill and hid the
+ -- colored progress ("appearing below the black background"). Outline = 4 edge lines at OVERLAY only.
+ local function barEdge()
+ local t = bar:CreateTexture(nil, "OVERLAY")
+ if t.SetColorTexture then t:SetColorTexture(0, 0, 0, 0.9) else t:SetTexture(0, 0, 0, 0.9) end
+ return t
+ end
+ local eT = barEdge(); eT:SetPoint("TOPLEFT", bar, "TOPLEFT", -1, 1); eT:SetPoint("TOPRIGHT", bar, "TOPRIGHT", 1, 1); eT:SetHeight(1)
+ local eB = barEdge(); eB:SetPoint("BOTTOMLEFT", bar, "BOTTOMLEFT", -1, -1); eB:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 1, -1); eB:SetHeight(1)
+ local eL = barEdge(); eL:SetPoint("TOPLEFT", bar, "TOPLEFT", -1, 1); eL:SetPoint("BOTTOMLEFT", bar, "BOTTOMLEFT", -1, -1); eL:SetWidth(1)
+ local eR = barEdge(); eR:SetPoint("TOPRIGHT", bar, "TOPRIGHT", 1, 1); eR:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 1, -1); eR:SetWidth(1)
+
+ bar._text = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ bar._text:SetPoint("CENTER", bar, "CENTER", 0, 0)
+
+ -- Right-anchor the name to the bar's left edge so a long faction name truncates cleanly.
+ e._name:SetPoint("RIGHT", bar, "LEFT", -8, 0)
+
+ e._bar = bar
+
+ local hl = e:CreateTexture(nil, "HIGHLIGHT")
+ if hl.SetColorTexture then hl:SetColorTexture(1, 1, 1, 0.10) else hl:SetTexture(1, 1, 1, 0.10) end
+ hl:SetAllPoints(e)
+
+ e:RegisterForClicks("LeftButtonUp")
+
+ -- Hover swaps the bar text from standing label → numeric "value / max".
+ e:HookScript("OnEnter", function(self)
+ if self._bar and self._bar._progressText then self._bar._text:SetText(self._bar._progressText) end
+ end)
+ e:HookScript("OnLeave", function(self)
+ if self._bar and self._bar._standingText then self._bar._text:SetText(self._bar._standingText) end
+ end)
+ return e
+end
+
+local function updateEntry(e, info)
+ e._name:SetText(info.name or "")
+
+ local bar = e._bar
+ local barMax = (info.barMax or 0) - (info.barMin or 0)
+ local barVal = (info.barValue or 0) - (info.barMin or 0)
+ if barMax <= 0 then barMax = 1; barVal = 0 end
+ bar:SetMinMaxValues(0, barMax)
+ bar:SetValue(barVal)
+
+ local color = _G.FACTION_BAR_COLORS and _G.FACTION_BAR_COLORS[info.standingID or 4]
+ if color then bar:SetStatusBarColor(color.r, color.g, color.b) end
+
+ local standingText = ""
+ local sid = info.standingID or 4
+ if _G["FACTION_STANDING_LABEL" .. sid] then standingText = _G["FACTION_STANDING_LABEL" .. sid] end
+ bar._standingText = standingText
+ bar._progressText = string.format("%d / %d", barVal, barMax)
+ bar._text:SetText(standingText)
+
+ -- Click → select this faction (drives the stock detail/watch system, if present).
+ local idx = info.factionIndex
+ e:SetScript("OnClick", function()
+ if SetSelectedFaction then pcall(SetSelectedFaction, idx) end
+ end)
+end
+
+-- ---------------------------------------------------------------------------
+-- Build the pane scaffold: a named FauxScrollFrame + recycled header/entry pools.
+-- ---------------------------------------------------------------------------
+local pane -- DragonUI_NewEra_CharacterPane_Reputation
+local scroll, content
+local headerPool, entryPool = {}, {}
+local flat = {} -- ordered list of { kind="header"/"entry", info=... }
+local NUM_VISIBLE = 0
+
+local function getPane()
+ if pane then return pane end
+ pane = _G.DragonUI_NewEra_CharacterPane_Reputation
+ or (CP.EnsurePane and CP.EnsurePane("Reputation"))
+ return pane
+end
+
+local function acquireHeader()
+ for _, h in ipairs(headerPool) do
+ if not h._inUse then h._inUse = true; h:Show(); return h end
+ end
+ local h = buildHeader(content)
+ headerPool[#headerPool + 1] = h
+ h._inUse = true
+ return h
+end
+
+local function acquireEntry()
+ for _, en in ipairs(entryPool) do
+ if not en._inUse then en._inUse = true; en:Show(); return en end
+ end
+ local en = buildEntry(content)
+ entryPool[#entryPool + 1] = en
+ en._inUse = true
+ return en
+end
+
+local function releaseAll()
+ for _, h in ipairs(headerPool) do h._inUse = false; h:Hide() end
+ for _, en in ipairs(entryPool) do en._inUse = false; en:Hide() end
+end
+
+local emptyLabel
+
+-- Paint the visible window of rows for the current scroll offset.
+local function updateScroll()
+ if not (scroll and content) then return end
+ content:Show()
+ releaseAll()
+
+ local total = #flat
+ if emptyLabel then if total == 0 then emptyLabel:Show() else emptyLabel:Hide() end end
+
+ local nRows = NUM_VISIBLE > 0 and NUM_VISIBLE or 1
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(scroll, total, nRows, ROW_HEIGHT) end
+ if NE.scrollbar and NE.scrollbar.CenterIfNoBar then NE.scrollbar.CenterIfNoBar(scroll, total > nRows) end
+ local offset = (FauxScrollFrame_GetOffset and FauxScrollFrame_GetOffset(scroll)) or 0
+
+ local rowW = (content:GetWidth() or 200)
+ if rowW <= 0 then rowW = 200 end
+
+ for i = 1, nRows do
+ local data = flat[offset + i]
+ if data then
+ local row
+ if data.kind == "header" then
+ row = acquireHeader()
+ updateHeader(row, data.info)
+ else
+ row = acquireEntry()
+ updateEntry(row, data.info)
+ end
+ local indent = data.info.isChild and 12 or 0
+ row:ClearAllPoints()
+ row:SetWidth(rowW - indent)
+ row:SetPoint("TOPLEFT", content, "TOPLEFT", indent, -(i - 1) * ROW_HEIGHT)
+ row:Show()
+ end
+ end
+end
+
+-- Rebuild `flat` from the live faction list, then repaint.
+local function refresh()
+ if not getPane() then return end
+ if not scroll then return end -- build() not done yet
+ flat = {}
+
+ if not (GetNumFactions and GetFactionInfo) then
+ log("GetNumFactions/GetFactionInfo unavailable — Reputation pane degraded")
+ updateScroll()
+ return
+ end
+
+ local ok, total = pcall(GetNumFactions)
+ if not ok or not total then total = 0 end
+
+ for i = 1, total do
+ -- §B: pcall every getter; isHeader is at POSITION 9.
+ local ok2, name, description, standingID, barMin, barMax, barValue,
+ atWarWith, canToggleAtWar, isHeader, isCollapsed, hasRep, isWatched, isChild = pcall(GetFactionInfo, i)
+ if ok2 and name then
+ local info = {
+ factionIndex = i,
+ name = name, description = description, standingID = standingID,
+ barMin = barMin, barMax = barMax, barValue = barValue,
+ isHeader = isHeader, isCollapsed = isCollapsed, isChild = isChild,
+ }
+ flat[#flat + 1] = { kind = isHeader and "header" or "entry", info = info }
+ end
+ end
+
+ updateScroll()
+end
+-- DOWNPORT/REPORT: surface (don't swallow) a refresh error so an empty Reputation pane is diagnosable.
+CP.RefreshReputation = function()
+ local ok, err = pcall(refresh)
+ if not ok then log("RefreshReputation error: " .. tostring(err)) end
+end
+
+local function recomputeVisible()
+ if not scroll then return end
+ local h = scroll:GetHeight() or 0
+ NUM_VISIBLE = math.max(1, math.floor(h / ROW_HEIGHT))
+end
+
+local function build()
+ local host = getPane()
+ if not host then log("Reputation pane host missing — cannot build"); return false end
+ if scroll then return true end
+
+ -- Class-themed content background (matches the other secondary tabs); degrades silently.
+ local bg = host:CreateTexture(nil, "BACKGROUND")
+ if not setAtlas(bg, "character-panel-background", false) then bg:Hide() end
+ bg:SetPoint("TOPLEFT", host, "TOPLEFT", 2, -2)
+ bg:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -2, 2)
+
+ -- NAMED FauxScrollFrame (§B: name REQUIRED — template OnLoad uses self:GetName()).
+ scroll = CreateFrame("ScrollFrame", "DragonUI_NewEra_RepScroll", host, "FauxScrollFrameTemplate")
+ -- DOWNPORT/REPORT: viewport inset to sit INSIDE the engraved inset box (top/left margin), leaving a
+ -- right gutter for the scrollbar (pushed out to the frame border by the negative BuildCustom x below).
+ scroll:SetPoint("TOPLEFT", host, "TOPLEFT", 10, -12)
+ scroll:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -24, 10)
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ FauxScrollFrame_OnVerticalScroll(self, offset, ROW_HEIGHT, updateScroll)
+ end)
+ -- DOWNPORT/REPORT: wheel scrolling via the (hidden) Faux slider value; auto-clamps to its range.
+ scroll:EnableMouseWheel(true)
+ scroll:SetScript("OnMouseWheel", function(self, delta)
+ local sb = _G[(self:GetName() or "") .. "ScrollBar"]
+ if not sb then return end
+ local mn, mx = sb:GetMinMaxValues()
+ local v = sb:GetValue() - delta * ROW_HEIGHT
+ if v < mn then v = mn elseif v > mx then v = mx end
+ sb:SetValue(v)
+ end)
+ scroll:HookScript("OnSizeChanged", function() recomputeVisible(); updateScroll() end)
+ -- Reskin the classic slider to the DF minimal bar (no-op art if the sheet isn't shipped).
+ -- DOWNPORT: hand-built minimal scrollbar (Reskin's stock-slider re-skin didn't render).
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, scroll, { x = -8 }) end
+
+ -- DOWNPORT/REPORT (the empty-pane bug): parent content to the PANE, not the faux ScrollFrame.
+ -- FauxScrollFrame_Update hides the scroll frame when the list fits (numItems <= numToDisplay),
+ -- which dragged content + every row down with it. Anchor over the scroll rect, raise above it so
+ -- faction header expand/collapse buttons stay clickable.
+ content = CreateFrame("Frame", nil, host)
+ content:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
+ content:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
+ content:SetHeight(1)
+ content:SetFrameLevel(scroll:GetFrameLevel() + 5)
+
+ emptyLabel = host:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
+ emptyLabel:SetPoint("TOP", host, "TOP", 0, -40)
+ emptyLabel:SetWidth(200)
+ emptyLabel:SetText(_G.REPUTATION and (_G.REPUTATION .. " unavailable") or "No reputations.")
+ emptyLabel:Hide()
+
+ recomputeVisible()
+ return true
+end
+
+-- ---------------------------------------------------------------------------
+-- Boot: build lazily, refresh on faction events + whenever the pane is shown.
+-- ---------------------------------------------------------------------------
+local function init()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ if not build() then return end
+ pcall(refresh)
+ -- Re-refresh on pane show (Inset width may differ; faction list may have changed).
+ local host = getPane()
+ if host and not host._neRepShowHooked then
+ host._neRepShowHooked = true
+ host:HookScript("OnShow", function() recomputeVisible(); pcall(refresh) end)
+ end
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:RegisterEvent("UPDATE_FACTION")
+boot:SetScript("OnEvent", function(_, event)
+ if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
+ -- Defer one frame so TabButtons.lua has created the pane.
+ if C_Timer and C_Timer.After then C_Timer.After(0, init) else init() end
+ else
+ CP.RefreshReputation()
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SettingsCog.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SettingsCog.lua
new file mode 100644
index 0000000..0cd748f
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SettingsCog.lua
@@ -0,0 +1,114 @@
+-- DragonUI_NewEra/modules/character/SettingsCog.lua — the settings gear in the character panel's
+-- top-right corner, and the menu of display options it opens.
+--
+-- Art + build follow the spellbook/professions/Cooldown Manager cogs (the "questlog-icon-setting"
+-- atlas with the stock Interface\Buttons\UI-OptionsButton as the 3.3.5a fallback), so every cog in
+-- the addon is the same gear. The menu goes through NE.menu, which gives it toggle-on-second-click
+-- for free — the same path modules/cooldownviewer/SettingsMenu.lua's cog uses.
+--
+-- PLACEMENT: seated UNDER the close X (which PanelChrome anchors at TOPRIGHT(1,0) at 24x24), in the
+-- empty gutter right of the sidebar tab strip. The cog's frame level clears the whole chrome stack
+-- the same way the close button does — the nineslice corner (level+1) and the title band (level+11)
+-- both paint over this corner otherwise.
+--
+-- ADDING AN OPTION: append to menuGenerator(). Anything with a getter/setter pair on CP works; keep
+-- the SETTING itself in the module that owns the feature (as the body-background pair lives in
+-- CharacterPanel.lua, which owns f.Bg) so this file stays presentation-only.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local MODULE = "character"
+local L = NE.L or setmetatable({}, { __index = function(_, k) return k end })
+
+local function log(msg) if CP._log then CP._log(msg) elseif NE.Log then NE.Log("CHARPANEL", msg) end end
+
+local COG_SIZE = 6
+-- Offsets from the close button's BOTTOMRIGHT (owner steer: the gear sits UNDER the X, not beside
+-- it). x pulls the gear in off the frame's right border so the two don't crowd; y is the gap below
+-- the 24px X. Anchoring off the button rather than the frame corner keeps the pair together if the
+-- chrome ever moves the X. Fallback below uses the frame corner with the same resulting offsets.
+local COG_X = -9
+local COG_Y = -6
+
+-- ----------------------------------------------------------------------------
+-- The menu. Rebuilt from the generator on every open, so the radio marks are always current.
+-- ----------------------------------------------------------------------------
+local function menuGenerator(_, root)
+ root:CreateTitle(L["Background"])
+ root:CreateRadio(L["Stone"],
+ function() return not CP.IsDarkBodyBackground() end,
+ function() CP.SetDarkBodyBackground(false) end)
+ root:CreateRadio(L["Dark"],
+ function() return CP.IsDarkBodyBackground() end,
+ function() CP.SetDarkBodyBackground(true) end)
+end
+
+local function toggleMenu(cog)
+ if not (NE.menu and NE.menu.ToggleAnchored) then
+ log("NE.menu unavailable; the settings cog has nothing to open")
+ return
+ end
+ NE.menu.ToggleAnchored(menuGenerator, cog,
+ { point = "TOPRIGHT", relativePoint = "BOTTOMRIGHT", x = 0, y = -2 })
+end
+
+-- ----------------------------------------------------------------------------
+-- The gear button.
+-- ----------------------------------------------------------------------------
+local function buildCog()
+ local f = CP.frame
+ if not f then return nil end
+ if CP._settingsCog then return CP._settingsCog end
+
+ local cog = CreateFrame("Button", "NE_CharacterSettingsCog", f)
+ cog:SetSize(COG_SIZE, COG_SIZE)
+
+ cog.Icon = cog:CreateTexture(nil, "ARTWORK")
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(cog.Icon, "questlog-icon-setting", true)) then
+ cog.Icon:SetTexture("Interface\\Buttons\\UI-OptionsButton"); cog.Icon:SetSize(16, 16)
+ end
+ cog.Icon:SetPoint("CENTER")
+
+ cog.Hi = cog:CreateTexture(nil, "HIGHLIGHT")
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(cog.Hi, "questlog-icon-setting", true)) then
+ cog.Hi:SetTexture("Interface\\Buttons\\UI-OptionsButton"); cog.Hi:SetSize(16, 16)
+ end
+ cog.Hi:SetPoint("CENTER"); cog.Hi:SetBlendMode("ADD"); cog.Hi:SetAlpha(0.4)
+
+ -- Above the nineslice corner + title band, matching how CloseButton.lua lifts the X.
+ cog:SetFrameLevel(((f.GetFrameLevel and f:GetFrameLevel()) or 1) + 20)
+ local x = f.CloseButton
+ if x then
+ cog:SetPoint("TOPRIGHT", x, "BOTTOMRIGHT", COG_X, COG_Y)
+ else
+ -- No X to hang off (chrome degraded): same spot measured from the corner instead. The close
+ -- button is 24x24 at TOPRIGHT(1,0), so its BOTTOMRIGHT is (1, -24) in frame coords.
+ cog:SetPoint("TOPRIGHT", f, "TOPRIGHT", COG_X + 1, COG_Y - 24)
+ end
+
+ cog:RegisterForClicks("LeftButtonUp")
+ cog:SetScript("OnClick", function(self) toggleMenu(self) end)
+ cog:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText(L["Character panel settings"], 1, 1, 1)
+ GameTooltip:Show()
+ end)
+ cog:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ CP._settingsCog = cog
+ return cog
+end
+
+CP.BuildSettingsCog = buildCog
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled(MODULE) then return end
+ -- The frame owns the corner we anchor into; build it first (idempotent), same as CloseButton.lua.
+ if CP.BuildFrame then pcall(CP.BuildFrame) end
+ local ok, err = pcall(buildCog)
+ if not ok then log("settings cog build failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Sidebar.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Sidebar.lua
new file mode 100644
index 0000000..9ca5263
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Sidebar.lua
@@ -0,0 +1,1600 @@
+-- DragonUI_NewEra/modules/character/Sidebar.lua — the collapsible right-side STATS sidebar.
+--
+-- ARCHITECTURE (CONTRACT_S1 §A0 / VISUAL_SPEC): the sidebar is the signature DF feature. It hosts
+-- NE_CharacterStatsPane inside NE.charpanel.InsetRight (209x364, hidden until expanded). Toggling the
+-- sidebar open/closed calls NE.charpanel.SetSidebarExpanded(true/false) (foundation, CharacterPanel.lua),
+-- which GROWS the frame 338->548 and shows/hides InsetRight. We replace the foundation's SelectSidebar
+-- and ReassertLayout stubs.
+--
+-- WHAT (the DF look) — ported faithfully from NewEra/CharacterPanel/Sidebar.lua: 7 collapsible stat
+-- sections (General / Attributes / Melee / Ranged / Spell / Defense / Resistances), 197x40 headers,
+-- 187x15 rows with an alternating background, a scrollable pane.
+--
+-- HOW (proven 3.3.5a mechanics) — lifted from /root/downport/DragonflightUICharacter/StatsPanel.lua,
+-- the user's WORKING 3.3.5 stats panel (928 lines). Those are the REAL getters/return-shapes for this
+-- client: UnitStat / UnitArmor(base,eff,_,pos,neg) / UnitAttackPower / UnitRangedAttackPower /
+-- GetCritChance / UnitResistance / GetCombatRating / GetManaRegen, etc. Every getter is pcall'd; a
+-- missing API renders "--" and never errors (CONTRACT §A.5 / §B graceful degradation).
+--
+-- DOWNPORT/REPORT — PIXEL-SCROLL LAYOUT (faithful NewEra, which used VARIABLE row heights via a retail
+-- WowScrollBox absent on 3.3.5a): we build ALL header+stat rows ONCE into a tall content frame at their
+-- cumulative Y positions (header 40 + 2 gap, rows 15 each) inside a plain ScrollFrame, and scroll by
+-- PIXELS. The custom scrollbar drives the ScrollFrame's SetVerticalScroll via NE.scrollbar.
+-- BuildCustomPixel. Collapse-on-header-click hides a section's rows and re-runs the cumulative layout.
+-- Shift+left-drag on a header reorders the blocks (see the reorder section); both the collapse state
+-- and the block order persist per character in NE.db.charStats.
+--
+-- 3.3.5 GOTCHAS (§B) applied here:
+-- * No SetShown — Show()/Hide() only.
+-- * NE.tex.SetAtlas returns false on a missing sheet — the UI-Character-Info-Title atlas isn't
+-- shipped on 3.3.5a, so the header falls back to a solid tint (DOWNPORT). The alternating row bg
+-- stays a FAINT NEUTRAL tint (user preference — NOT the brown line-bounce atlas).
+-- * pcall every stat getter inside the render/tooltip loops.
+
+local NE = DragonUI_NewEra
+local L = NE:GetLocale()
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local format, min, max, floor, abs = string.format, math.min, math.max, math.floor, math.abs
+
+-- ----------------------------------------------------------------------------
+-- Local logger (mirrors CharacterPanel.lua's). DOWNPORT: NE.Log may be absent standalone.
+-- ----------------------------------------------------------------------------
+local function log(msg)
+ if CP._log then CP._log(msg); return end
+ if NE.Log then NE.Log("SIDEBAR", msg); return end
+end
+
+local function getLocaleString(global, fallback)
+ if type(global) == "string" and global ~= "" then
+ local v = _G[global]
+ if type(v) == "string" and v ~= "" then
+ return v
+ end
+ end
+ if type(L) == "table" and type(fallback) == "string" and fallback ~= "" then
+ local rawVal = rawget(L, fallback)
+ if type(rawVal) == "string" and rawVal ~= "" then
+ return rawVal
+ end
+ local ok, res = pcall(function() return L[fallback] end)
+ if ok and type(res) == "string" and res ~= "" then
+ return res
+ end
+ end
+ return fallback
+end
+
+if type(L) == "table" then
+ local mt = getmetatable(L) or {}
+ mt.__call = function(self, global, fallback)
+ return getLocaleString(global, fallback)
+ end
+ setmetatable(L, mt)
+else
+ L = function(global, fallback)
+ return getLocaleString(global, fallback)
+ end
+end
+
+-- ----------------------------------------------------------------------------
+-- Geometry — DOWNPORT/REPORT: faithful NewEra exact dims.
+-- ----------------------------------------------------------------------------
+local PANE_INSET = 3 -- NE_CharacterStatsPane sits InsetRight.TOPLEFT(3,-3)/BOTTOMRIGHT(-3,2)
+local HEADER_W = 197 -- NewEra section header 197x40
+local HEADER_H = 40 -- DOWNPORT/REPORT: revert to NewEra's 40 (was 22)
+local ROW_W = 187 -- NewEra stat row 187x15
+local ROW_H = 15
+local HEADER_GAP = 2 -- 2px gap below each header before its first row
+local FIRST_ROW_INSET = -5 -- first row after a header: TOPRIGHT(-5,0) (row 187 vs header 197)
+local SCROLLBAR_W = 8 -- custom pixel scrollbar width
+local SCROLLBAR_GAP = SCROLLBAR_W + 2 -- room for the bar on the right of the viewport
+-- Item Level block = the 197x40 header box + a strip beneath it for the number (like a stat row).
+local ITEMLEVEL_ROW_H = HEADER_H + 22
+
+-- ----------------------------------------------------------------------------
+-- Stat formatting helpers (ported from DUIC StatsPanel.lua — the proven 3.3.5 forms).
+-- ----------------------------------------------------------------------------
+local HL = HIGHLIGHT_FONT_COLOR_CODE or "|cffffffff"
+local GRN = GREEN_FONT_COLOR_CODE or "|cff20ff20"
+local RED = RED_FONT_COLOR_CODE or "|cffff2020"
+local CLOSE= FONT_COLOR_CODE_CLOSE or "|r"
+
+-- Buffed primary-stat formatter (DUIC PaperDollFormatStat): returns colored value + tooltip title.
+local function formatStat(name, base, posBuff, negBuff)
+ base = base or 0
+ posBuff = posBuff or 0
+ negBuff = negBuff or 0
+ local effective = max(0, base + posBuff + negBuff)
+ local text = HL .. name .. " " .. effective
+ local frameText
+ if posBuff == 0 and negBuff == 0 then
+ text = text .. CLOSE
+ frameText = tostring(effective)
+ else
+ if posBuff > 0 or negBuff < 0 then text = text .. " (" .. base .. CLOSE end
+ if posBuff > 0 then text = text .. CLOSE .. GRN .. "+" .. posBuff .. CLOSE end
+ if negBuff < 0 then text = text .. RED .. " " .. negBuff .. CLOSE end
+ if posBuff > 0 or negBuff < 0 then text = text .. HL .. ")" .. CLOSE end
+ if negBuff < 0 then
+ frameText = RED .. effective .. CLOSE
+ else
+ frameText = GRN .. effective .. CLOSE
+ end
+ end
+ return frameText, text
+end
+
+-- Safe combat-rating reads (these globals/funcs exist on 3.3.5 but pcall keeps us bulletproof).
+local function rating(cr)
+ local ok, v = pcall(GetCombatRating, cr)
+ return (ok and v) or 0
+end
+local function ratingBonus(cr)
+ local ok, v = pcall(GetCombatRatingBonus, cr)
+ return (ok and v) or 0
+end
+-- Stock shows melee/ranged haste inside the Attack Speed tooltip (PaperDollFrame_SetAttackSpeed),
+-- not as its own row. Mirror that: "Haste rating N (X% haste)".
+local function hasteLine(cr)
+ return format(_G.CR_HASTE_RATING_TOOLTIP or "Haste rating %d (%.2f%% haste)", rating(cr), ratingBonus(cr))
+end
+
+-- Self-contained melee hand maths (DUIC ComputeHand — replaces the retail combat dummy).
+local function computeHand(minDamage, maxDamage, physPos, physNeg, percent, speed)
+ minDamage = minDamage or 0; maxDamage = maxDamage or 0
+ physPos = physPos or 0; physNeg = physNeg or 0; percent = percent or 1
+ local displayMin = max(floor(minDamage), 1)
+ local displayMax = max(floor(maxDamage), 1)
+ local realMin = (minDamage / percent) - physPos - physNeg
+ local realMax = (maxDamage / percent) - physPos - physNeg
+ local baseDamage = (realMin + realMax) * 0.5
+ local fullDamage = (baseDamage + physPos + physNeg) * percent
+ local totalBonus = fullDamage - baseDamage
+ local dps = (speed and speed > 0) and (max(fullDamage, 1) / speed) or 0
+ local dmgText
+ if totalBonus == 0 then
+ dmgText = displayMin .. " - " .. displayMax
+ else
+ local color = (totalBonus > 0) and "|cff20ff20" or "|cffff2020"
+ dmgText = color .. displayMin .. " - " .. displayMax .. "|r"
+ end
+ return dmgText, dps
+end
+
+-- ----------------------------------------------------------------------------
+-- Stat element registry. Each element: { id, name, func() -> (value, tipTitle, tipBody, tipLines) }.
+-- Mirrors NewEra's 7-section layout; getters are the DUIC-proven 3.3.5 ones.
+-- tipLines (optional) is a { {left=, right=, white=} ... } list for multi-line tooltips.
+-- ----------------------------------------------------------------------------
+local MAGIC_AP = _G.ATTACK_POWER_MAGIC_NUMBER or 14
+
+-- DOWNPORT/REPORT: IsRangedWeapon() was unreliable (a Hunter with a bow still read N/A). Match the
+-- stock PaperDollFrame_SetRangedAttack rule instead: show ranged stats whenever the ranged slot holds
+-- an item and the class is NOT a relic user (idol/libram/totem/sigil have no ranged damage).
+local function hasRanged()
+ local slot = (INVSLOT_RANGED or 18)
+ local ok, tex = pcall(function() return GetInventoryItemTexture and GetInventoryItemTexture("player", slot) end)
+ if not (ok and tex) then return false end
+ local relicOk, relic = pcall(function() return UnitHasRelicSlot and UnitHasRelicSlot("player") end)
+ if relicOk and relic then return false end
+ return true
+end
+
+-- ----------------------------------------------------------------------------
+-- Average item level (retail's PaperDollFrame ItemLevelFrame, pinned above the General category).
+-- 3.3.5a has no GetAverageItemLevel API, so we compute it ourselves: 17 gear slots (retail's own
+-- formula excludes Shirt/Tabard — they don't count), GetInventoryItemLink + GetItemInfo (4th return
+-- = itemLevel) per slot, pcall'd — an uncached link just contributes 0 until GetItemInfo caches it,
+-- matching how every other stat getter in this file degrades (§B).
+-- ----------------------------------------------------------------------------
+local ILVL_SLOTS = {
+ INVSLOT_HEAD or 1, INVSLOT_NECK or 2, INVSLOT_SHOULDER or 3, INVSLOT_BACK or 15,
+ INVSLOT_CHEST or 5, INVSLOT_WRIST or 9, INVSLOT_HAND or 10, INVSLOT_WAIST or 6,
+ INVSLOT_LEGS or 7, INVSLOT_FEET or 8, INVSLOT_FINGER1 or 11, INVSLOT_FINGER2 or 12,
+ INVSLOT_TRINKET1 or 13, INVSLOT_TRINKET2 or 14, INVSLOT_MAINHAND or 16,
+ INVSLOT_OFFHAND or 17, INVSLOT_RANGED or 18,
+}
+
+-- Average item level across those 17 slots (empty slots count as 0, matching retail's formula), plus
+-- the average QUALITY of the slots that ARE filled — retail colors the number by an ilvl/content
+-- tier we have no data for on 3.3.5a, so we approximate with the standard item-quality palette
+-- instead (ITEM_QUALITY_COLORS, the same table item borders/links use everywhere else in the client).
+local function averageItemLevel()
+ local total, qTotal, qCount = 0, 0, 0
+ for _, slotId in ipairs(ILVL_SLOTS) do
+ local link = GetInventoryItemLink("player", slotId)
+ if link then
+ local ok, _, _, quality, ilvl = pcall(GetItemInfo, link)
+ if ok then
+ if ilvl then total = total + ilvl end
+ if quality then qTotal = qTotal + quality; qCount = qCount + 1 end
+ end
+ end
+ end
+ local avgIlvl = total / #ILVL_SLOTS
+ local avgQuality = (qCount > 0) and floor(qTotal / qCount + 0.5) or nil
+ return avgIlvl, avgQuality
+end
+
+-- ----------------------------------------------------------------------------
+-- Movement speed, as a percentage of normal running speed (retail's convention: 100% = base run,
+-- epic ground mount 200%, epic flight 380%).
+--
+-- DOWNPORT: 3.3.5a exposes ONLY `GetUnitSpeed(unit)` -> the unit's CURRENT speed in yards/second.
+-- Retail's 4-return form (currentSpeed, runSpeed, flightSpeed, swimSpeed) that PaperDollFrame's own
+-- movement-speed stat reads is Cata+, so there is no way to ask this client "what WOULD my run speed
+-- be" — standing at the character sheet genuinely reads 0.
+--
+-- So we sample while moving, tagged with the movement CONTEXT the sample was taken in (mounted /
+-- flying / swimming / shapeshift form), and report the fastest RECENT sample for the current context.
+-- Deliberately NOT the live reading: walking is a flat 2.5 yd/s (36%) and backpedalling 4.5 (64%),
+-- and this row answers "what is my movement speed stat" — 100 / 120 / 80 — not "how fast am I
+-- physically travelling this instant". Walk-speed samples are dropped outright (WALK_FLOOR) and
+-- taking the max means backpedalling can't pull the figure down while you're also moving forward.
+-- Mount up and the on-foot 100% isn't shown as if it were current — samples are per context.
+-- With no sample at all we assume 100%: normal run speed is the right default, and it self-corrects
+-- on your first step.
+-- ----------------------------------------------------------------------------
+local BASE_SPEED = 7 -- yards/second at 100%. DOWNPORT: BASE_MOVEMENT_SPEED is Cata+.
+local SPEED_ID = "movespeed"
+-- Walking is a flat 2.5 yd/s (36%). Below this we assume walking rather than a slow effect: the
+-- slowest real snares land around 50-60%, and a full root reports speed 0 (never sampled at all).
+local WALK_FLOOR = 40
+-- Seconds after which a stored measurement stops outranking a newer, slower one. This is what lets
+-- an expired Sprint decay back to 100% WITHOUT invalidating on UNIT_AURA — an aura-driven reset
+-- fires constantly in combat and would leave the row bouncing to whatever you did in the instant
+-- after each proc, backpedalling included.
+local SPEED_WINDOW = 6
+local speedSamples = {} -- movement context -> { pct = fastest % measured, t = when }
+
+local function speedContext()
+ local form = (GetShapeshiftForm and GetShapeshiftForm()) or 0
+ return format("%d-%d-%d-%d",
+ IsMounted and IsMounted() and 1 or 0,
+ IsFlying and IsFlying() and 1 or 0,
+ IsSwimming and IsSwimming() and 1 or 0,
+ tonumber(form) or 0)
+end
+
+-- Returns percent-of-base, measured. measured=false means nothing has been sampled in this context
+-- yet and the 100% baseline is being assumed.
+-- Samples are kept PER context rather than in one slot, so dismounting restores the on-foot figure
+-- immediately instead of falling back to the assumed baseline until you take another step.
+local function movementSpeedPct()
+ local cur = (GetUnitSpeed and GetUnitSpeed("player")) or 0
+ local ctx = speedContext()
+ local pct = cur / BASE_SPEED * 100
+ if pct >= WALK_FLOOR then
+ local now = (GetTime and GetTime()) or 0
+ local best = speedSamples[ctx]
+ -- Take the sample if it's faster, or if what we're holding has gone stale (a buff that expired).
+ if not best or pct >= best.pct or (now - best.t) > SPEED_WINDOW then
+ speedSamples[ctx] = { pct = pct, t = now }
+ end
+ end
+ local best = speedSamples[ctx]
+ if best then return best.pct, true end
+ return 100, false
+end
+
+local function movementSpeedText()
+ return format("%d%%", floor(movementSpeedPct() + 0.5))
+end
+
+-- ----------------------------------------------------------------------------
+-- Equipment durability as a percentage: total current / total maximum across everything equipped.
+-- GetInventoryItemDurability returns nil for items with no durability at all (rings, neck, trinkets,
+-- shirt, tabard, and heirlooms) — those must be SKIPPED, not counted as 0, or a fully-kitted
+-- character would read far below its real condition. Nothing durable equipped -> nil -> "--".
+-- Also reports the single worst-condition slot, which is the number that actually matters (an
+-- average of 90% still means something is about to break at 5%).
+-- ----------------------------------------------------------------------------
+local function equipmentDurability()
+ local cur, maxTotal, worst = 0, 0, nil
+ for slot = 1, 19 do -- INVSLOT_HEAD .. INVSLOT_TABARD; non-durable slots return nil and are skipped
+ local ok, c, m = pcall(GetInventoryItemDurability, slot)
+ if ok and c and m and m > 0 then
+ cur, maxTotal = cur + c, maxTotal + m
+ local pct = c / m * 100
+ if not worst or pct < worst then worst = pct end
+ end
+ end
+ if maxTotal == 0 then return nil end
+ return cur / maxTotal * 100, cur, maxTotal, worst
+end
+
+-- 7 sections, in the locked NewEra order. Each `stats` is a list of element defs.
+local SECTIONS = {
+ {
+ id = "general", title = L("STAT_CATEGORY_GENERAL", L("GENERAL", "General")),
+ stats = {
+ { id = "health", name = L("HEALTH", "Health"), func = function()
+ local hp = UnitHealthMax("player") or 0
+ return tostring(hp), (L("HEALTH", "Health") .. " " .. hp),
+ L("MAX_HEALTH_TOOLTIP", "Maximum Health. If your health reaches zero, you will die.")
+ end },
+ { id = "power", name = (function()
+ local _, token = UnitPowerType("player")
+ return (token and _G[token]) or L("MANA", "Mana")
+ end)(), func = function()
+ local pmax = UnitPowerMax("player") or 0
+ local _, token = UnitPowerType("player")
+ local plabel = (token and _G[token]) or L("MANA", "Mana")
+ return tostring(pmax), (plabel .. " " .. pmax),
+ format(L["Your maximum %s."], plabel)
+ end },
+ { id = SPEED_ID, name = L("STAT_MOVEMENT_SPEED", "Movement Speed"), func = function()
+ local label = L("STAT_MOVEMENT_SPEED", "Movement Speed")
+ local pct, measured = movementSpeedPct()
+ local val = format("%d%%", floor(pct + 0.5))
+ return val, (label .. " " .. val), format(
+ "Your running speed. 100%% is normal (%d yards/second); mounts and speed effects raise it,"
+ .. " slows lower it. Walking and backpedalling are ignored."
+ .. (measured and "" or "\nAssuming the 100%% baseline — move a step to measure it."),
+ BASE_SPEED)
+ end },
+ { id = "durability", name = L("DURABILITY", "Durability"), func = function()
+ local label = L("DURABILITY", "Durability")
+ local pct, cur, maxTotal, worst = equipmentDurability()
+ if not pct then
+ return "--", label, "You have nothing equipped that can take durability damage."
+ end
+ local val = format("%d%%", floor(pct + 0.5))
+ return val, (label .. " " .. val), format(
+ "Condition of your equipment: %d of %d durability across every equipped item that has it."
+ .. "\nWorst slot: %d%%."
+ .. "\nItems with no durability (rings, neck, trinkets, shirt, tabard) are not counted.",
+ cur, maxTotal, floor(worst + 0.5))
+ end },
+ },
+ },
+
+ {
+ id = "attributes", title = L("STAT_CATEGORY_ATTRIBUTES", "Attributes"),
+ stats = (function()
+ -- DOWNPORT/REPORT: the stock DEFAULT_STATi_TOOLTIP globals are FORMAT strings ("Increases Mana
+ -- by %d\nIncreases Spell Critical Hit by %.2f%%") whose placeholders need per-class computed
+ -- values we don't have here — passed through raw they printed literal "%d"/"%.2f%%". Use accurate
+ -- static descriptions instead so the tooltip body is correct with no unfilled placeholders.
+ local ATTR_DESC = {
+ "Increases attack power for some classes and the damage you block with a shield.",
+ "Increases critical strike chance, dodge chance, armor, and (for some classes) attack power.",
+ "Increases your maximum health.",
+ "Increases your maximum mana and your spell critical strike chance.",
+ "Increases your health and mana regeneration while out of combat.",
+ }
+ local t = {}
+ for i = 1, 5 do
+ local statName = _G["SPELL_STAT" .. i .. "_NAME"] or ("Stat" .. i)
+ t[i] = {
+ id = "stat" .. i, name = statName,
+ func = function()
+ local base, eff, posBuff, negBuff = UnitStat("player", i)
+ local frameText, tip = formatStat(statName, base, posBuff, negBuff)
+ return frameText, tip, L(ATTR_DESC[i], ATTR_DESC[i])
+ end,
+ }
+ end
+ return t
+ end)(),
+ },
+
+ {
+ id = "melee", title = L("MELEE", "Melee"),
+ stats = {
+ { id = "mh_damage", name = L("DAMAGE", "Damage"), func = function()
+ local speed, offSpeed = UnitAttackSpeed("player")
+ local minD, maxD, minO, maxO, physPos, physNeg, percent = UnitDamage("player")
+ local mhText, mhDps = computeHand(minD, maxD, physPos, physNeg, percent, speed)
+ local lines = {
+ { left = L("INVTYPE_WEAPONMAINHAND", "Main Hand") },
+ { left = L("ATTACK_SPEED_SECONDS", "Speed"), right = format("%.2f", speed or 0) },
+ { left = L("DAMAGE", "Damage"), right = mhText },
+ { left = L("DAMAGE_PER_SECOND", "DPS"), right = format("%.1f", mhDps) },
+ }
+ if offSpeed then
+ local ohText, ohDps = computeHand(minO, maxO, physPos, physNeg, percent, offSpeed)
+ lines[#lines + 1] = { left = " " }
+ lines[#lines + 1] = { left = L("INVTYPE_WEAPONOFFHAND", "Off Hand"), white = true }
+ lines[#lines + 1] = { left = L("ATTACK_SPEED_SECONDS", "Speed"), right = format("%.2f", offSpeed) }
+ lines[#lines + 1] = { left = L("DAMAGE", "Damage"), right = ohText }
+ lines[#lines + 1] = { left = L("DAMAGE_PER_SECOND", "DPS"), right = format("%.1f", ohDps) }
+ end
+ return mhText, nil, nil, lines
+ end },
+ { id = "mh_dps", name = L("STAT_DPS_SHORT", "DPS"), func = function()
+ local speed, offSpeed = UnitAttackSpeed("player")
+ local minD, maxD, minO, maxO, physPos, physNeg, percent = UnitDamage("player")
+ local _, mhDps = computeHand(minD, maxD, physPos, physNeg, percent, speed)
+ local frameText = format("%.1f", mhDps)
+ local lines = { { left = L("DAMAGE_PER_SECOND", "Damage Per Second") },
+ { left = L("INVTYPE_WEAPONMAINHAND", "Main Hand"), right = frameText } }
+ if offSpeed then
+ local _, ohDps = computeHand(minO, maxO, physPos, physNeg, percent, offSpeed)
+ lines[#lines + 1] = { left = L("INVTYPE_WEAPONOFFHAND", "Off Hand"), right = format("%.1f", ohDps) }
+ end
+ return frameText, nil, nil, lines
+ end },
+ { id = "ap", name = L("ATTACK_POWER", "Attack Power"), func = function()
+ local base, posBuff, negBuff = UnitAttackPower("player")
+ local frameText, tip = formatStat(L("MELEE_ATTACK_POWER", "Attack Power"), base, posBuff, negBuff)
+ local body = format(L("MELEE_ATTACK_POWER_TOOLTIP",
+ "Increases your damage with melee weapons by %.1f damage per second."),
+ max((base or 0) + (posBuff or 0) + (negBuff or 0), 0) / MAGIC_AP)
+ return frameText, tip, body
+ end },
+ { id = "attackspeed", name = L("ATTACK_SPEED", "Attack Speed"), func = function()
+ local speed, offSpeed = UnitAttackSpeed("player")
+ local frameText = format("%.2f", speed or 0)
+ if offSpeed then frameText = frameText .. " / " .. format("%.2f", offSpeed) end
+ return frameText, (L("ATTACK_SPEED", "Attack Speed") .. " " .. frameText),
+ L("Seconds between melee swings.", "Seconds between melee swings.") .. "\n" .. hasteLine(CR_HASTE_MELEE or 18)
+ end },
+ { id = "hit", name = L("COMBAT_RATING_NAME6", "Hit Rating"), func = function()
+ local r = rating(CR_HIT_MELEE)
+ return tostring(r), (L("COMBAT_RATING_NAME6", "Hit") .. " " .. r),
+ format(L["Improves your chance to hit by %.2f%%."], ratingBonus(CR_HIT_MELEE))
+ end },
+ { id = "crit", name = L("MELEE_CRIT_CHANCE", "Crit Chance"), func = function()
+ local crit = (GetCritChance and GetCritChance()) or 0
+ local str = format("%.2f%%", crit)
+ return str, (L("MELEE_CRIT_CHANCE", "Crit Chance") .. " " .. str),
+ L["Chance of melee attacks dealing extra damage."]
+ end },
+ { id = "arp", name = L["Armor Penetration"], func = function()
+ local r = rating(CR_ARMOR_PENETRATION)
+ local ok, pct = pcall(GetArmorPenetration)
+ return tostring(r), (L["Armor Penetration"] .. " " .. r),
+ format(L["Reduces the target's armor by up to %.2f%%."], (ok and pct) or 0)
+ end },
+ { id = "expertise", name = L("STAT_EXPERTISE", "Expertise"), func = function()
+ local ok, exp, offExp = pcall(GetExpertise)
+ if not ok then return "--" end
+ local speed, offSpeed = UnitAttackSpeed("player")
+ local r = offSpeed and (exp .. " / " .. (offExp or 0)) or tostring(exp)
+ return r, (L("STAT_EXPERTISE", "Expertise") .. " " .. r),
+ L["Reduces the chance your attacks are dodged or parried."]
+ end },
+ },
+ },
+
+ {
+ id = "ranged", title = L("RANGED", "Ranged"),
+ stats = {
+ { id = "r_damage", name = L("DAMAGE", "Damage"), func = function()
+ if not hasRanged() then return L("NOT_APPLICABLE", "N/A") end
+ local speed, lowD, hiD = UnitRangedDamage("player")
+ local lo = max(floor(lowD or 0), 1)
+ local hi = max(floor(hiD or 0), 1)
+ local txt = lo .. " - " .. hi
+ local dps = (speed and speed > 0) and (((lowD or 0) + (hiD or 0)) / 2 / speed) or 0
+ local lines = {
+ { left = L("RANGED", "Ranged") },
+ { left = L("ATTACK_SPEED_SECONDS", "Speed"), right = format("%.2f", speed or 0) },
+ { left = L("DAMAGE", "Damage"), right = txt },
+ { left = L("DAMAGE_PER_SECOND", "DPS"), right = format("%.1f", dps) },
+ }
+ return txt, nil, nil, lines
+ end },
+ { id = "r_dps", name = L("STAT_DPS_SHORT", "DPS"), func = function()
+ if not hasRanged() then return L("NOT_APPLICABLE", "N/A") end
+ local speed, lowD, hiD = UnitRangedDamage("player")
+ local dps = (speed and speed > 0) and (((lowD or 0) + (hiD or 0)) / 2 / speed) or 0
+ return format("%.1f", dps)
+ end },
+ { id = "r_ap", name = L("ATTACK_POWER", "Attack Power"), func = function()
+ if not hasRanged() then return L("NOT_APPLICABLE", "N/A") end
+ if HasWandEquipped and HasWandEquipped() then return L("NOT_APPLICABLE", "N/A") end
+ local base, posBuff, negBuff = UnitRangedAttackPower("player")
+ local frameText, tip = formatStat(L("RANGED_ATTACK_POWER", "Ranged Attack Power"), base, posBuff, negBuff)
+ local body = format(L("RANGED_ATTACK_POWER_TOOLTIP",
+ "Increases your damage with ranged weapons by %.1f damage per second."),
+ max((base or 0) + (posBuff or 0) + (negBuff or 0), 0) / MAGIC_AP)
+ return frameText, tip, body
+ end },
+ { id = "r_speed", name = L("ATTACK_SPEED", "Attack Speed"), func = function()
+ if not hasRanged() then return L("NOT_APPLICABLE", "N/A") end
+ local speed = UnitRangedDamage("player")
+ local ft = format("%.2f", speed or 0)
+ return ft, (L("ATTACK_SPEED", "Attack Speed") .. " " .. ft),
+ hasteLine(CR_HASTE_RANGED or 19)
+ end },
+ { id = "r_hit", name = L("STAT_HIT_CHANCE", "Hit Chance"), func = function()
+ if not hasRanged() then return L("NOT_APPLICABLE", "N/A") end
+ local hit = (GetHitModifier and GetHitModifier()) or 0
+ return format("%.2f%%", hit), L("STAT_HIT_CHANCE", "Hit Chance"), L["Reduces your chance to miss."]
+ end },
+ { id = "r_crit", name = L("STAT_CRITICAL_STRIKE", "Crit Chance"), func = function()
+ if not hasRanged() then return L("NOT_APPLICABLE", "N/A") end
+ local crit = (GetRangedCritChance and GetRangedCritChance()) or 0
+ return format("%.2f%%", crit), (L("CRIT_CHANCE", "Crit Chance") .. format(" %.2f%%", crit)),
+ L["Chance of ranged attacks dealing extra damage."]
+ end },
+ },
+ },
+
+ {
+ id = "spell", title = L("STAT_CATEGORY_SPELL", L("MAGIC_SCHOOLS", "Spell")),
+ stats = {
+ { id = "s_damage", name = L("BONUS_DAMAGE", "Spell Power"), func = function()
+ local holy = 2
+ local minMod = (GetSpellBonusDamage and GetSpellBonusDamage(holy)) or 0
+ local maxSchool = MAX_SPELL_SCHOOLS or 7
+ for i = holy + 1, maxSchool do
+ local b = (GetSpellBonusDamage and GetSpellBonusDamage(i)) or 0
+ minMod = min(minMod, b)
+ end
+ return tostring(minMod), (L("BONUS_DAMAGE", "Spell Power") .. " " .. minMod),
+ L["Lowest bonus damage across schools. Hover the paper-doll stat for a breakdown."]
+ end },
+ { id = "s_healing", name = L("BONUS_HEALING", "Bonus Healing"), func = function()
+ local heal = (GetSpellBonusHealing and GetSpellBonusHealing()) or 0
+ return format(" %d", heal), (L("BONUS_HEALING", "Bonus Healing") .. " " .. heal),
+ L["Bonus power applied to healing spells."]
+ end },
+ { id = "s_hit", name = L("COMBAT_RATING_NAME8", "Spell Hit"), func = function()
+ local r = rating(CR_HIT_SPELL)
+ local pen = (GetSpellPenetration and GetSpellPenetration()) or 0
+ -- Faithful to the stock Spell Hit tooltip (PaperDollFrame.lua:342, issue #11): the spell
+ -- penetration line always shows, even at 0. Reuse Blizzard's own localized format string
+ -- when present so the text is byte-identical to default; fall back to a literal otherwise.
+ local fmt = _G.CR_HIT_SPELL_TOOLTIP
+ local body
+ if type(fmt) == "string" then
+ body = format(fmt, UnitLevel("player") or 0, ratingBonus(CR_HIT_SPELL), pen, pen)
+ else
+ body = format("Improves your chance to hit with spells by %.2f%%.\n\nSpell Penetration %d (Reduces enemy resistances by %d)",
+ ratingBonus(CR_HIT_SPELL), pen, pen)
+ end
+ return tostring(r), (L("COMBAT_RATING_NAME8", "Spell Hit") .. " " .. r), body
+ end },
+ { id = "s_regen", name = L("MANA_REGEN", "Mana Regen"), func = function()
+ if not (UnitHasMana and UnitHasMana("player")) then return L("NOT_APPLICABLE", "N/A") end
+ local ok, base, casting = pcall(GetManaRegen)
+ if not ok then return "--" end
+ base = floor((base or 0) * 5.0)
+ casting = floor((casting or 0) * 5.0)
+ return tostring(base), (L("MANA_REGEN", "Mana Regen") .. " " .. base),
+ format(L["Not casting: %d / 5s\nWhile casting: %d / 5s"], base, casting)
+ end },
+ { id = "s_crit", name = L("SPELL_CRIT_CHANCE", "Spell Crit"), func = function()
+ local holy = 2
+ local minCrit = (GetSpellCritChance and GetSpellCritChance(holy)) or 0
+ local maxSchool = MAX_SPELL_SCHOOLS or 7
+ for i = holy + 1, maxSchool do
+ local c = (GetSpellCritChance and GetSpellCritChance(i)) or 0
+ minCrit = min(minCrit, c)
+ end
+ local str = format("%.2f%%", minCrit)
+ return str, (L("SPELL_CRIT_CHANCE", "Spell Crit") .. " " .. str),
+ L["Chance of spells dealing extra damage."]
+ end },
+ { id = "s_haste", name = L["Haste Rating"], func = function()
+ local cr = CR_HASTE_SPELL or 20
+ local r = rating(cr)
+ return tostring(r), (L["Haste Rating"] .. " " .. r),
+ format(_G.SPELL_HASTE_TOOLTIP or "Increases the speed that your spells cast by %.2f%%.", ratingBonus(cr))
+ end },
+ },
+ },
+
+ {
+ id = "defense", title = L("DEFENSE", "Defense"),
+ stats = {
+ { id = "armor", name = L("ARMOR", "Armor"), func = function()
+ local base, eff, _, posBuff, negBuff = UnitArmor("player")
+ local frameText, tip = formatStat(L("ARMOR", "Armor"), base, posBuff, negBuff)
+ local body
+ if PaperDollFrame_GetArmorReduction then
+ local ok, reduction = pcall(PaperDollFrame_GetArmorReduction, eff, UnitLevel("player"))
+ if ok and DEFAULT_STATARMOR_TOOLTIP then body = format(DEFAULT_STATARMOR_TOOLTIP, reduction) end
+ end
+ return frameText, tip, body
+ end },
+ { id = "defense", name = L("DEFENSE", "Defense"), func = function()
+ local base, modifier = 0, 0
+ if UnitDefense then base, modifier = UnitDefense("player") end
+ base = base or 0; modifier = modifier or 0
+ local posBuff = modifier > 0 and modifier or 0
+ local negBuff = modifier < 0 and modifier or 0
+ local frameText, tip = formatStat(L("DEFENSE", "Defense"), base, posBuff, negBuff)
+ return frameText, tip, L["Reduces chance to be hit and crit; raises block/dodge/parry."]
+ end },
+ { id = "dodge", name = L("DODGE", "Dodge"), func = function()
+ local v = (GetDodgeChance and GetDodgeChance()) or 0
+ local str = format(" %.2f%%", v)
+ return str, (L("DODGE_CHANCE", "Dodge Chance") .. str), L["Chance to dodge enemy melee attacks."]
+ end },
+ { id = "parry", name = L("PARRY", "Parry"), func = function()
+ local v = (GetParryChance and GetParryChance()) or 0
+ local str = format(" %.2f%%", v)
+ return str, (L("PARRY_CHANCE", "Parry Chance") .. str), L["Chance to parry. Requires a weapon."]
+ end },
+ { id = "block", name = L("BLOCK", "Block"), func = function()
+ local v = (GetBlockChance and GetBlockChance()) or 0
+ local str = format(" %.2f%%", v)
+ local blockVal = (GetShieldBlock and GetShieldBlock()) or 0
+ return str, (L("BLOCK_CHANCE", "Block Chance") .. str),
+ format(L["Chance to block. Requires a shield.\nBlock Value: %d"], blockVal)
+ end },
+ { id = "resilience", name = L["Resilience"], func = function()
+ local cr = CR_CRIT_TAKEN_MELEE or 15 -- resilience rating (drives all three crit-taken schools)
+ local r = rating(cr)
+ return tostring(r), (L["Resilience"] .. " " .. r),
+ format(L["Reduces the chance you'll be critically hit by %.2f%% and reduces critical damage taken."],
+ ratingBonus(cr))
+ end },
+ },
+ },
+
+ {
+ id = "resistance", title = L("RESISTANCE", "Resistances"),
+ -- Player magic-resist schools (UnitResistance indices): Fire=2, Nature=3, Frost=4, Shadow=5, Arcane=6.
+ stats = (function()
+ local order = { 2, 3, 4, 6, 5 } -- Fire, Nature, Frost, Arcane, Shadow (NewEra Arcane-then-Fire reordered for player set)
+ local set = { 6, 2, 3, 4, 5 } -- Arcane, Fire, Nature, Frost, Shadow (NewEra RESIST_SCHOOLS order)
+ local t = {}
+ for k, i in ipairs(set) do
+ local schoolName = _G["RESISTANCE" .. i .. "_NAME"] or ("Resist" .. i)
+ t[k] = {
+ id = "res" .. i, name = schoolName,
+ func = function()
+ local base, total, positive, negative = UnitResistance("player", i)
+ base = base or 0; total = total or base
+ positive = positive or 0; negative = negative or 0
+ local frameText
+ if abs(negative) > positive then
+ frameText = RED .. total .. CLOSE
+ elseif abs(negative) == positive then
+ frameText = tostring(total)
+ else
+ frameText = GRN .. total .. CLOSE
+ end
+ local tip = schoolName .. " " .. total
+ if positive ~= 0 or negative ~= 0 then
+ tip = tip .. " (" .. base
+ if positive > 0 then tip = tip .. GRN .. " +" .. positive .. CLOSE end
+ if negative < 0 then tip = tip .. RED .. " " .. negative .. CLOSE end
+ tip = tip .. ")"
+ end
+ return frameText, tip, (L("RESISTANCE_TOOLTIP_DESC", "Reduces magic damage taken."))
+ end,
+ }
+ end
+ return t
+ end)(),
+ },
+}
+
+-- ----------------------------------------------------------------------------
+-- DOWNPORT/REPORT: PET stat sections — the sidebar can render PET stats instead of the player's
+-- when pet mode is active (CP.ExpandPetSidebar). Faithful to NewEra/PetPaperDoll.lua's
+-- NE_PetStatsPane: General / Attributes / Attack / Defense / Resistances, read off unit "pet".
+-- Every getter pcall-guarded → "--" on a miss or no-pet (CONTRACT §B). Hunter pets get a
+-- Training-Points row; warlock minions omit it.
+-- ----------------------------------------------------------------------------
+local function petIsHunter()
+ local ok, _, classFile = pcall(UnitClass, "player")
+ return ok and classFile == "HUNTER"
+end
+
+local function petPower()
+ local _, token = UnitPowerType("pet")
+ return (token and _G[token]) or L("MANA", "Mana")
+end
+
+local PET_SECTIONS = {
+ {
+ id = "pet_general", title = L("STAT_CATEGORY_GENERAL", L("GENERAL", "General")),
+ stats = (function()
+ local t = {
+ { id = "pet_health", name = L("HEALTH", "Health"), func = function()
+ local v = UnitHealthMax("pet") or 0
+ return tostring(v), (L("HEALTH", "Health") .. " " .. v), "Your pet's maximum health."
+ end },
+ { id = "pet_power", name = L("MANA", "Mana"),
+ relabel = petPower,
+ func = function()
+ local v = UnitPowerMax("pet") or 0
+ return tostring(v), (petPower() .. " " .. v), nil
+ end },
+ }
+ -- Hunter pets earn training points (warlock minions return nil → "--"). Built once, so we always
+ -- include the row and let the guarded getter degrade rather than gate on class at file-load time
+ -- (UnitClass is nil that early).
+ t[#t + 1] = { id = "pet_training", name = L("PET_TRAIN_BUTTON", "Training Points"),
+ func = function()
+ if not petIsHunter() then return L("NOT_APPLICABLE", "N/A") end
+ local total, spent = GetPetTrainingPoints()
+ total, spent = total or 0, spent or 0
+ local unspent = total - spent
+ return tostring(unspent),
+ (L("PET_TRAIN_BUTTON", "Training Points") .. ": " .. unspent),
+ format("Total: %d\nSpent: %d", total, spent)
+ end }
+ return t
+ end)(),
+ },
+ {
+ id = "pet_attributes", title = L("STAT_CATEGORY_ATTRIBUTES", "Attributes"),
+ stats = (function()
+ local t = {}
+ for i = 1, 5 do
+ local statName = _G["SPELL_STAT" .. i .. "_NAME"] or ("Stat" .. i)
+ t[i] = { id = "pet_stat" .. i, name = statName, func = function()
+ local _, effective = UnitStat("pet", i)
+ effective = effective or 0
+ return tostring(effective), (statName .. " " .. effective), nil
+ end }
+ end
+ return t
+ end)(),
+ },
+ {
+ id = "pet_attack", title = L("MELEE", L("ATTACK", "Attack")),
+ stats = {
+ { id = "pet_damage", name = L("DAMAGE", "Damage"), func = function()
+ local minD, maxD = UnitDamage("pet")
+ local low = max(floor(minD or 0), 1)
+ local high = max(floor(maxD or 0), 1)
+ local v = low .. " - " .. high
+ return v, (L("DAMAGE", "Damage") .. " " .. v), nil
+ end },
+ { id = "pet_ap", name = L("ATTACK_POWER", "Attack Power"), func = function()
+ local base, pos, neg = UnitAttackPower("pet")
+ local eff = (base or 0) + (pos or 0) + (neg or 0)
+ return tostring(eff), (L("ATTACK_POWER", "Attack Power") .. " " .. eff), nil
+ end },
+ { id = "pet_aspeed", name = L("ATTACK_SPEED", "Attack Speed"), func = function()
+ local speed = UnitAttackSpeed("pet") or 0
+ local v = format("%.2f", speed)
+ return v, (L("ATTACK_SPEED", "Attack Speed") .. " " .. v), nil
+ end },
+ },
+ },
+ {
+ id = "pet_defense", title = L("DEFENSE", "Defense"),
+ stats = {
+ { id = "pet_armor", name = L("ARMOR", "Armor"), func = function()
+ local _, eff = UnitArmor("pet")
+ eff = floor(eff or 0)
+ return tostring(eff), (L("ARMOR", "Armor") .. " " .. eff), nil
+ end },
+ },
+ },
+ {
+ id = "pet_resistance", title = L("RESISTANCE", "Resistances"),
+ stats = (function()
+ local set = { 6, 2, 3, 4, 5 } -- Arcane, Fire, Nature, Frost, Shadow
+ local t = {}
+ for k, i in ipairs(set) do
+ local schoolName = _G["RESISTANCE" .. i .. "_NAME"] or ("Resist" .. i)
+ t[k] = { id = "pet_res" .. i, name = schoolName, func = function()
+ local _, total = UnitResistance("pet", i)
+ total = total or 0
+ return tostring(total), (schoolName .. " " .. total),
+ ("Reduces " .. schoolName:lower() .. " damage taken.")
+ end }
+ end
+ return t
+ end)(),
+ },
+}
+
+-- DOWNPORT/REPORT: the active section set — player sections by default, pet sections in pet mode.
+local function activeSections()
+ return CP._sidebarPetMode and PET_SECTIONS or SECTIONS
+end
+
+-- The Item Level block isn't a SECTIONS entry (it's built separately), but it IS a reorderable
+-- block, so it needs an id that lives in the same namespace as the section ids.
+local ITEMLEVEL_ID = "itemlevel"
+
+-- ----------------------------------------------------------------------------
+-- Collapse state (defaults: General collapsed, the rest expanded — NewEra/DUIC default).
+-- ----------------------------------------------------------------------------
+local sectionExpanded = {
+ -- itemlevel isn't a SECTIONS entry (it's built separately), but it lives in this table so it
+ -- collapses like every other header AND rides the same per-character persistence.
+ itemlevel = true,
+ general = false, attributes = true, melee = true, ranged = true,
+ spell = true, defense = true, resistance = true,
+ -- Pet sections default all-expanded.
+ pet_general = true, pet_attributes = true, pet_attack = true,
+ pet_defense = true, pet_resistance = true,
+}
+
+-- ----------------------------------------------------------------------------
+-- Block ORDER (shift+drag reorder). The sidebar lays blocks out in this order instead of the
+-- hardcoded SECTIONS order, so Item Level can be dragged to the bottom, Defense above Melee, etc.
+-- Player mode and pet mode keep separate orders (their block sets don't overlap).
+-- ----------------------------------------------------------------------------
+local blockOrder = { player = nil, pet = nil } -- id arrays; built lazily, overwritten on DB load
+
+local function defaultOrderFor(petMode)
+ local t = {}
+ if petMode then
+ for _, s in ipairs(PET_SECTIONS) do t[#t + 1] = s.id end
+ else
+ t[1] = ITEMLEVEL_ID -- retail pins Item Level at the top; that's only the DEFAULT now
+ for _, s in ipairs(SECTIONS) do t[#t + 1] = s.id end
+ end
+ return t
+end
+
+-- A saved order is untrusted: it can hold ids from an older addon version, duplicates, or be missing
+-- blocks added since. Keep the valid saved ids in their saved order, then splice any missing block
+-- back in at its default index, so a new section appears where it was designed to instead of last.
+local function sanitizeOrder(saved, default)
+ local valid, seen, out = {}, {}, {}
+ for _, id in ipairs(default) do valid[id] = true end
+ if type(saved) == "table" then
+ for _, id in ipairs(saved) do
+ if valid[id] and not seen[id] then seen[id] = true; out[#out + 1] = id end
+ end
+ end
+ for i, id in ipairs(default) do
+ if not seen[id] then
+ seen[id] = true
+ tinsert(out, i <= #out + 1 and i or #out + 1, id)
+ end
+ end
+ return out
+end
+
+-- The order array for the mode that's showing now (lazily seeded with the defaults pre-DB).
+local function currentOrder()
+ local key = CP._sidebarPetMode and "pet" or "player"
+ if not blockOrder[key] then blockOrder[key] = defaultOrderFor(key == "pet") end
+ return blockOrder[key]
+end
+
+-- Persist the section collapse state + block order PER CHARACTER (NE.db.charStats[charKey]). The
+-- defaults above hold until a saved state is loaded (once NE.db is ready); every header toggle and
+-- every completed reorder drag writes it back — so a /reload restores each section's open/closed
+-- state AND the block order for that character.
+local _stateLoaded = false
+local function statsStore(create)
+ if not (NE.db and NE.CharKey) then return nil end
+ if create then
+ NE.db.charStats = NE.db.charStats or {}
+ local key = NE.CharKey()
+ NE.db.charStats[key] = NE.db.charStats[key] or {}
+ return NE.db.charStats[key]
+ end
+ return NE.db.charStats and NE.db.charStats[NE.CharKey()]
+end
+local function loadSectionState()
+ if _stateLoaded then return end
+ -- Need the DB AND a real character name (CharKey is "?-realm" before PLAYER_LOGIN); retry next layout.
+ if not (NE.db and NE.CharKey and UnitName and UnitName("player")) then return end
+ _stateLoaded = true
+ local store = statsStore(false)
+ if store then
+ -- Collapse flags are stored as bare id->bool at the top level, so skip the two ORDER keys
+ -- (they're arrays, and sectionExpanded has no entry for them — the nil check already does it).
+ for id, v in pairs(store) do
+ if sectionExpanded[id] ~= nil then sectionExpanded[id] = v and true or false end
+ end
+ end
+ blockOrder.player = sanitizeOrder(store and store.sectionOrder, defaultOrderFor(false))
+ blockOrder.pet = sanitizeOrder(store and store.petSectionOrder, defaultOrderFor(true))
+end
+local function saveSectionState()
+ local store = statsStore(true)
+ if not store then return end
+ for id, v in pairs(sectionExpanded) do store[id] = v and true or false end
+ store.sectionOrder = blockOrder.player or defaultOrderFor(false)
+ store.petSectionOrder = blockOrder.pet or defaultOrderFor(true)
+end
+
+-- ----------------------------------------------------------------------------
+-- SHIFT + LEFT-DRAG a category header (or the Item Level header) to reorder the blocks.
+--
+-- The dragged block is NOT detached to follow the cursor — the content frame is a ScrollFrame child
+-- and a detached header would be clipped at the pane edge. Instead the list REFLOWS underneath the
+-- cursor: the dragged header dims, and whenever the cursor crosses the midpoint of the neighbouring
+-- block, the two swap and the sidebar re-lays out. Comparing against one neighbour at a time (rather
+-- than computing an absolute insertion index) is what keeps variable-height blocks — a collapsed
+-- 40px header vs an expanded 7-row section — from overshooting past each other.
+--
+-- layoutSidebar publishes pane._blockLayout: the shown blocks in visual order as {id, y, h}, where y
+-- is the offset DOWN from the content frame's top. Cursor Y is converted into that same space.
+-- ----------------------------------------------------------------------------
+local REORDER_EDGE = 16 -- cursor within this many px of the pane edge auto-scrolls
+local REORDER_SCROLL = 8 -- px per tick of auto-scroll
+local REORDER_THROTTLE = 0.03
+
+local dragState = {}
+local dragDriver = CreateFrame("Frame")
+dragDriver:Hide()
+
+local function stopReorder()
+ if dragState.button then dragState.button:SetAlpha(1) end
+ dragState.id, dragState.button = nil, nil
+ dragDriver:Hide()
+ saveSectionState() -- persist the new order per character
+end
+
+-- Swap two ids in the active order array (by id, not by index — pane._blockLayout only holds the
+-- SHOWN blocks, so its indices aren't guaranteed to line up with the order array's).
+local function swapBlocks(idA, idB)
+ local order = currentOrder()
+ local ia, ib
+ for i, id in ipairs(order) do
+ if id == idA then ia = i elseif id == idB then ib = i end
+ end
+ if not (ia and ib) then return false end
+ order[ia], order[ib] = order[ib], order[ia]
+ return true
+end
+
+dragDriver:SetScript("OnUpdate", function(self, elapsed)
+ self._t = (self._t or 0) + (elapsed or 0)
+ if self._t < REORDER_THROTTLE then return end
+ self._t = 0
+
+ local pane = CP._sidebar
+ local content = pane and pane._content
+ local blocks = pane and pane._blockLayout
+ -- The button can be released off-frame (or the panel closed mid-drag) without OnDragStop firing.
+ if not (dragState.id and content and blocks) or not IsMouseButtonDown("LeftButton") then
+ return stopReorder()
+ end
+
+ local _, cursorY = GetCursorPosition()
+ cursorY = cursorY / (content:GetEffectiveScale() or 1)
+ local top = content:GetTop()
+ if not top then return end
+ local offset = top - cursorY -- px down from the content top == the same space as block.y
+
+ -- Auto-scroll when dragging against either edge, so a block can reach an off-screen position.
+ local scroll = pane._scroll
+ if scroll and scroll.GetVerticalScrollRange then
+ local sTop, sBottom = scroll:GetTop(), scroll:GetBottom()
+ local cur = scroll:GetVerticalScroll() or 0
+ if sTop and cursorY > sTop - REORDER_EDGE then
+ scroll:SetVerticalScroll(max(0, cur - REORDER_SCROLL))
+ elseif sBottom and cursorY < sBottom + REORDER_EDGE then
+ scroll:SetVerticalScroll(min(scroll:GetVerticalScrollRange() or 0, cur + REORDER_SCROLL))
+ end
+ end
+
+ local cur
+ for i, b in ipairs(blocks) do if b.id == dragState.id then cur = i break end end
+ if not cur then return end
+
+ local swapWith
+ local above, below = blocks[cur - 1], blocks[cur + 1]
+ if above and offset < above.y + above.h / 2 then
+ swapWith = above.id
+ elseif below and offset > below.y + below.h / 2 then
+ swapWith = below.id
+ end
+
+ if swapWith and swapBlocks(dragState.id, swapWith) then
+ if PlaySound then pcall(PlaySound, "igMainMenuOptionCheckBoxOn") end
+ if CP.LayoutSidebar then CP.LayoutSidebar() end
+ end
+end)
+
+-- Make a header a shift-drag reorder handle. Shared by createCategoryHeader and createItemLevelRow.
+local function enableHeaderReorder(button, blockId)
+ button._blockId = blockId
+ button:RegisterForDrag("LeftButton")
+ button:SetScript("OnDragStart", function(self)
+ -- Plain (unmodified) drag stays inert, as it was before reorder existed — requiring the modifier
+ -- means a user dragging across headers to read them can't scramble their layout by accident.
+ if not IsShiftKeyDown() then return end
+ dragState.id, dragState.button = blockId, self
+ self:SetAlpha(0.55)
+ dragDriver._t = 0
+ dragDriver:Show()
+ end)
+ button:SetScript("OnDragStop", stopReorder)
+end
+
+-- ----------------------------------------------------------------------------
+-- Tooltip rendering for a stat row (pcall the getter — §B).
+-- ----------------------------------------------------------------------------
+local function showRowTooltip(row)
+ local data = row._data
+ if not data or data.isHeader or not data.el then return end
+ local ok, value, tipTitle, tipBody, tipLines = pcall(data.el.func)
+ if not ok then return end
+ GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
+ if type(tipLines) == "table" then
+ if tipTitle then GameTooltip:SetText(tipTitle, 1, 1, 1) else GameTooltip:SetText(data.el.name, 1, 1, 1) end
+ for _, line in ipairs(tipLines) do
+ if line.right then
+ GameTooltip:AddDoubleLine(line.left or "", line.right, 1, 0.82, 0, 1, 1, 1)
+ else
+ GameTooltip:AddLine(line.left or " ", 1, 0.82, 0, not line.white)
+ end
+ end
+ else
+ GameTooltip:SetText(tipTitle or data.el.name, 1, 1, 1)
+ if tipBody and tipBody ~= "" then GameTooltip:AddLine(tipBody, 1, 0.82, 0, true) end
+ end
+ GameTooltip:Show()
+end
+
+-- ----------------------------------------------------------------------------
+-- DOWNPORT/REPORT: faithful NewEra — header + stat rows built ONCE into a tall content frame, laid out
+-- at cumulative Y, scrolled by PIXELS. createCategoryHeader = a 197x40 Button (title CENTER(0,1),
+-- collapse-on-click); createStatRow = a 187x15 Button (faint neutral alternating bg, label/value).
+-- ----------------------------------------------------------------------------
+
+-- A category header: 197x40 Button. Clicking toggles the section's collapse state + re-lays out.
+local function createCategoryHeader(content, section)
+ local row = CreateFrame("Button", nil, content)
+ row:SetSize(HEADER_W, HEADER_H)
+ row._section = section
+
+ -- Header art (197x40). DOWNPORT: the UI-Character-Info-Title atlas isn't shipped on 3.3.5a, so fall
+ -- back to a solid tint (sized to 197x40).
+ local headerBg = row:CreateTexture(nil, "ARTWORK", nil, 0)
+ headerBg:SetAllPoints(row)
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(headerBg, "UI-Character-Info-Title", false)) then
+ headerBg:SetTexture(0.16, 0.13, 0.10, 0.9) -- DOWNPORT: tint fallback for the missing title atlas
+ end
+ row._headerBg = headerBg
+
+ -- Collapse/expand toggle.
+ local toggle = row:CreateTexture(nil, "OVERLAY")
+ toggle:SetSize(14, 14)
+ toggle:SetPoint("LEFT", row, "LEFT", 6, 0)
+ toggle:SetVertexColor(1, 0.82, 0)
+ row._toggle = toggle
+
+ -- Title text, centered on the header (NewEra: CENTER(0,1)).
+ local headerText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ headerText:SetPoint("CENTER", headerBg, "CENTER", 0, 1)
+ headerText:SetText(section.title)
+ row._label = headerText
+
+ row:RegisterForClicks("LeftButtonUp")
+ row:SetScript("OnClick", function()
+ -- Shift is the reorder modifier — a shift-click that never passed the drag threshold must not
+ -- also collapse the section, or every abandoned drag silently toggles it.
+ if IsShiftKeyDown() then return end
+ sectionExpanded[section.id] = not sectionExpanded[section.id]
+ saveSectionState() -- persist per character so /reload keeps this section's open/closed state
+ if PlaySound then pcall(PlaySound, "igCharacterInfoTab") end
+ if CP.LayoutSidebar then CP.LayoutSidebar() end
+ end)
+ enableHeaderReorder(row, section.id)
+ return row
+end
+
+-- Registry of stat elements by id (el.func is looked up live on every hover,
+-- so an external addon can override CP._statElements[id].func in place - e.g.
+-- Enhanced Character Stats replacing "hit"/"s_hit"/"r_hit" with a talent-aware
+-- breakdown - with no need to rebuild or re-anchor any row).
+CP._statElements = CP._statElements or {}
+function CP.GetStatElement(id)
+ return CP._statElements[id]
+end
+
+-- A stat row: 187x15 Button. Faint neutral alternating bg (user preference — NOT the brown atlas).
+local function createStatRow(content, el)
+ local row = CreateFrame("Button", nil, content)
+ row:SetSize(ROW_W, ROW_H)
+ row._el = el
+ if el.id then CP._statElements[el.id] = el end
+
+ -- Alternating-row bg (faint neutral white tint). Shown on even rows by the layout pass.
+ local rowBg = row:CreateTexture(nil, "BACKGROUND")
+ rowBg:SetPoint("CENTER", row, "CENTER", 0, 0)
+ rowBg:SetSize(ROW_W, ROW_H)
+ rowBg:SetTexture(1, 1, 1)
+ rowBg:SetVertexColor(1, 1, 1)
+ rowBg:SetAlpha(0.05)
+ rowBg:Hide()
+ row._bounce = rowBg
+
+ -- Hover highlight.
+ local hl = row:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetAllPoints()
+ hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
+ hl:SetBlendMode("ADD")
+ hl:SetAlpha(0.3)
+
+ local label = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
+ label:SetPoint("LEFT", row, "LEFT", 11, 0)
+ label:SetJustifyH("LEFT")
+ label:SetText(el.name)
+ row._label = label
+
+ local value = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
+ value:SetPoint("RIGHT", row, "RIGHT", -8, 0)
+ value:SetJustifyH("RIGHT")
+ row._value = value
+
+ -- Tooltip uses showRowTooltip, which reads row._data.el.
+ row._data = { isHeader = false, el = el }
+ row:SetScript("OnEnter", showRowTooltip)
+ row:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ return row
+end
+
+-- The retail "Item Level" block: pinned above General. Structured exactly like a category section —
+-- the SAME 197x40 header art carrying the centered "Item Level" title and the same +/- toggle, with
+-- the quality-colored number sitting BELOW the box on the dark pane, the way stat rows sit under
+-- their own headers. Collapsing hides the number and shrinks the block to the bare header, matching
+-- how the stat categories behave. DOWNPORT: _G.ITEM_LEVEL is a format template ("Item Level %d"),
+-- not a plain label — use a literal string here instead of formatting it in.
+--
+-- The container is a Frame with the header as a Button CHILD (not a Button itself), so only the
+-- 197x40 art toggles — clicking the number strip below it does nothing, same as a stat row area.
+local function createItemLevelRow(content)
+ local row = CreateFrame("Frame", nil, content)
+ row:SetSize(HEADER_W, ITEMLEVEL_ROW_H)
+
+ -- Header box: top 197x40 of the block, identical art/'title' treatment to createCategoryHeader.
+ local header = CreateFrame("Button", nil, row)
+ header:SetSize(HEADER_W, HEADER_H)
+ header:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
+ row._header = header
+
+ local bg = header:CreateTexture(nil, "ARTWORK", nil, 0)
+ bg:SetAllPoints(header)
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(bg, "UI-Character-Info-Title", false)) then
+ bg:SetTexture(0.16, 0.13, 0.10, 0.9) -- DOWNPORT: tint fallback for the missing title atlas
+ end
+
+ -- Collapse/expand toggle — same art, placement and tint as createCategoryHeader's.
+ local toggle = header:CreateTexture(nil, "OVERLAY")
+ toggle:SetSize(14, 14)
+ toggle:SetPoint("LEFT", header, "LEFT", 6, 0)
+ toggle:SetVertexColor(1, 0.82, 0)
+ row._toggle = toggle
+
+ local label = header:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ label:SetPoint("CENTER", bg, "CENTER", 0, 1)
+ label:SetText(L("ITEM_LEVEL_LABEL", "Item Level"))
+
+ header:RegisterForClicks("LeftButtonUp")
+ header:SetScript("OnClick", function()
+ if IsShiftKeyDown() then return end -- shift is the reorder modifier (see enableHeaderReorder)
+ sectionExpanded[ITEMLEVEL_ID] = not sectionExpanded[ITEMLEVEL_ID]
+ saveSectionState() -- persist per character, exactly like a category header toggle
+ if PlaySound then pcall(PlaySound, "igCharacterInfoTab") end
+ if CP.LayoutSidebar then CP.LayoutSidebar() end
+ end)
+ enableHeaderReorder(header, ITEMLEVEL_ID)
+
+ -- The number, centered on the strip BELOW the box (not inside the art).
+ local value = row:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
+ value:SetPoint("TOP", bg, "BOTTOM", 0, -3)
+ row._value = value
+
+ return row
+end
+
+-- Lay out the Item Level block at `y` and return its visual height (nil = not shown). Player mode
+-- only — retail doesn't show item level for pets. Collapsed = header art only, so the block shrinks
+-- to HEADER_H, the number hides, and the 17-slot ilvl scan is skipped entirely.
+local function layoutItemLevelBlock(pane, content, y)
+ local ilvlRow = pane._itemLevelRow
+ if not ilvlRow then return nil end
+ if CP._sidebarPetMode then ilvlRow:Hide(); return nil end
+
+ local expanded = sectionExpanded[ITEMLEVEL_ID]
+ ilvlRow._toggle:SetTexture(expanded
+ and "Interface\\Buttons\\UI-MinusButton-Up"
+ or "Interface\\Buttons\\UI-PlusButton-Up")
+
+ local valueFS = ilvlRow._value
+ if expanded then
+ local avg, quality = averageItemLevel()
+ valueFS:SetText(tostring(floor(avg + 0.5)))
+ local qc = quality and _G.ITEM_QUALITY_COLORS and _G.ITEM_QUALITY_COLORS[quality]
+ if qc then valueFS:SetTextColor(qc.r, qc.g, qc.b) else valueFS:SetTextColor(1, 1, 1) end
+ valueFS:Show()
+ else
+ valueFS:Hide()
+ end
+
+ local blockH = expanded and ITEMLEVEL_ROW_H or HEADER_H
+ ilvlRow:SetHeight(blockH)
+ ilvlRow:ClearAllPoints()
+ ilvlRow:SetPoint("TOPRIGHT", content, "TOPRIGHT", 0, -y)
+ ilvlRow:Show()
+ return blockH
+end
+
+-- Lay out one stat section at `y` and return its visual height (nil = no such block in this mode).
+local function layoutSectionBlock(pane, content, sectionId, y)
+ local sr = pane._sectionRows[sectionId]
+ if not sr then return nil end
+
+ -- Header at the fixed content right edge, at the running Y.
+ local header = sr.header
+ header:ClearAllPoints()
+ header:SetPoint("TOPRIGHT", content, "TOPRIGHT", 0, -y)
+ header:Show()
+ header._toggle:SetTexture(sectionExpanded[sectionId]
+ and "Interface\\Buttons\\UI-MinusButton-Up"
+ or "Interface\\Buttons\\UI-PlusButton-Up")
+
+ local blockH = HEADER_H
+ if sectionExpanded[sectionId] then
+ local anchorAbove, anchorIsHeader = header, true
+ local statInCat = 0
+ for _, row in ipairs(sr.rows) do
+ statInCat = statInCat + 1
+ row:ClearAllPoints()
+ row:SetPoint("TOPRIGHT", anchorAbove, "BOTTOMRIGHT", anchorIsHeader and FIRST_ROW_INSET or 0, 0)
+ -- Alternating bg by position within the section.
+ if (statInCat % 2) == 0 then row._bounce:Show() else row._bounce:Hide() end
+ row:Show()
+ anchorAbove, anchorIsHeader = row, false
+ blockH = blockH + ROW_H
+ end
+ else
+ for _, row in ipairs(sr.rows) do row:Hide() end
+ end
+ return blockH
+end
+
+-- ----------------------------------------------------------------------------
+-- Lay out all header+stat rows at cumulative Y honoring collapse state, set content height, resync bar.
+-- Headers anchor to content.TOPRIGHT at the running Y (anti-cascade); first row after a header anchors
+-- TOPRIGHT(-5,0) to the header's BOTTOMRIGHT; subsequent rows chain TOPRIGHT(0,0) row→row.
+--
+-- Block sequence comes from currentOrder() (shift+drag reorderable, saved per character), NOT from
+-- the SECTIONS array — so Item Level is no longer hard-pinned to the top. Each block is followed by
+-- HEADER_GAP of empty space, matching the pre-reorder spacing exactly.
+-- ----------------------------------------------------------------------------
+local function layoutSidebar()
+ loadSectionState() -- restore per-character collapse state + order once NE.db is ready (idempotent)
+ local pane = CP._sidebar
+ if not (pane and pane._content and pane._sectionRows) then return end
+ local content = pane._content
+
+ -- DOWNPORT/REPORT: first hide EVERY built section (player + pet), then lay out only the active
+ -- set, so switching player<->pet mode never leaves the other set's rows visible.
+ for _, sr in pairs(pane._sectionRows) do
+ if sr.header then sr.header:Hide() end
+ for _, row in ipairs(sr.rows) do row:Hide() end
+ end
+
+ local runningY = 0
+ local blocks = {} -- shown blocks in visual order — the reorder drag's hit-test map
+ pane._blockLayout = blocks
+
+ for _, id in ipairs(currentOrder()) do
+ local blockH = (id == ITEMLEVEL_ID)
+ and layoutItemLevelBlock(pane, content, runningY)
+ or layoutSectionBlock(pane, content, id, runningY)
+ if blockH then
+ blocks[#blocks + 1] = { id = id, y = runningY, h = blockH }
+ runningY = runningY + blockH + HEADER_GAP
+ end
+ end
+
+ content:SetHeight(runningY > 0 and runningY or 1)
+
+ -- Clamp the scroll if the content shrank below the current offset. The custom bar resyncs via its
+ -- OnScrollRangeChanged hook + throttled OnUpdate fallback (it polls the ScrollFrame range/value).
+ local scroll = pane._scroll
+ if scroll then
+ local range = (scroll.GetVerticalScrollRange and scroll:GetVerticalScrollRange()) or 0
+ local cur = (scroll.GetVerticalScroll and scroll:GetVerticalScroll()) or 0
+ if cur > range then scroll:SetVerticalScroll(range) end
+ end
+end
+CP.LayoutSidebar = layoutSidebar
+
+-- Recompute every stat row's value (pcall the getter — §B), then re-lay out (collapse may have changed).
+local function refreshSidebar()
+ local pane = CP._sidebar
+ if not pane then return end
+ if pane._sectionRows then
+ for _, section in ipairs(activeSections()) do
+ local sr = pane._sectionRows[section.id]
+ if sr then
+ for _, row in ipairs(sr.rows) do
+ -- DOWNPORT/REPORT: pet rows may carry a relabel() (power token differs per pet) — re-apply.
+ if row._el and row._el.relabel and row._label then
+ local okN, nm = pcall(row._el.relabel)
+ if okN and nm then row._label:SetText(nm) end
+ end
+ local ok, frameText = pcall(row._el.func)
+ row._value:SetText((ok and frameText ~= nil) and tostring(frameText) or "--")
+ -- Remember the movement-speed row so the ticker below can repaint just that one value
+ -- instead of re-running all ~40 getters + a full relayout several times a second.
+ if row._el and row._el.id == SPEED_ID then pane._speedRow = row end
+ end
+ end
+ end
+ end
+ layoutSidebar()
+end
+CP.RefreshSidebar = refreshSidebar
+
+-- ----------------------------------------------------------------------------
+-- Live movement-speed ticker. GetUnitSpeed reports CURRENT speed and fires no event, so the one stat
+-- in this panel that can't be event-driven has to be polled. Bounded hard: the driver only runs
+-- while the stats pane is actually shown (buildSidebar hooks it to the pane's OnShow/OnHide), and it
+-- repaints a single FontString rather than calling refreshSidebar.
+-- ----------------------------------------------------------------------------
+local SPEED_TICK = 0.2
+local speedTicker = CreateFrame("Frame")
+speedTicker:Hide()
+speedTicker:SetScript("OnUpdate", function(self, elapsed)
+ self._t = (self._t or 0) + (elapsed or 0)
+ if self._t < SPEED_TICK then return end
+ self._t = 0
+ local pane = CP._sidebar
+ -- IsVisible, not IsShown: the pane frame's own shown flag stays true while the whole character
+ -- panel is closed, so IsShown would keep repainting a row nobody can see.
+ local row = pane and pane._speedRow
+ if not (row and row._value and row:IsVisible()) then return end
+ local ok, text = pcall(movementSpeedText)
+ row._value:SetText(ok and text or "--")
+end)
+
+-- ----------------------------------------------------------------------------
+-- Build the sidebar: NE_CharacterStatsPane (in InsetRight) → ScrollFrame → tall content → all rows.
+-- DOWNPORT/REPORT: pixel-scroll (plain ScrollFrame), faithful NewEra cumulative-Y layout.
+-- ----------------------------------------------------------------------------
+local function buildSidebar()
+ if CP._sidebar then return CP._sidebar end
+ local f = CP.frame
+ local insetRight = f and (f.InsetRight or CP.InsetRight)
+ if not insetRight then log("buildSidebar: InsetRight not ready"); return nil end
+
+ -- Publish the contract-surface aliases (the brief names NE.charpanel.{Inset, InsetRight}); the
+ -- foundation only set them as frame fields. Non-destructive.
+ CP.InsetRight = CP.InsetRight or insetRight
+ CP.Inset = CP.Inset or f.Inset
+
+ -- Pane fills InsetRight's interior — DOWNPORT/REPORT: NewEra TOPLEFT(3,-3)/BOTTOMRIGHT(-3,2).
+ local pane = CreateFrame("Frame", "NE_CharacterStatsPane", insetRight)
+ pane:SetPoint("TOPLEFT", insetRight, "TOPLEFT", PANE_INSET, -PANE_INSET)
+ pane:SetPoint("BOTTOMRIGHT", insetRight, "BOTTOMRIGHT", -PANE_INSET, 2)
+ CP._sidebar = pane
+
+ -- Gate the movement-speed poll on the pane's visibility (nothing to poll for while it's hidden).
+ -- CreateFrame returns a SHOWN frame, so OnShow won't fire for the state it's already in — sync once.
+ pane:SetScript("OnShow", function() speedTicker._t = 0; speedTicker:Show() end)
+ pane:SetScript("OnHide", function() speedTicker:Hide() end)
+ if pane:IsShown() then speedTicker:Show() end
+
+ -- The pane exists now — let PaperDoll.lua attach the class-themed background.
+ if CP.ApplyClassBackground then pcall(CP.ApplyClassBackground) end
+
+ -- Plain ScrollFrame (pixel-scrolled). Leaves room for the custom bar on the right.
+ local scroll = CreateFrame("ScrollFrame", "NE_StatsScrollFrame", pane)
+ scroll:SetPoint("TOPLEFT", pane, "TOPLEFT", 0, 0)
+ scroll:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", -SCROLLBAR_GAP, 0)
+ scroll:EnableMouseWheel(true)
+ pane._scroll = scroll
+
+ -- Content frame (the scroll child): fixed 197 wide, TALL (height set by layoutSidebar). Anchored
+ -- TOPLEFT so SetVerticalScroll moves it; SetScrollChild ties it to the ScrollFrame.
+ local content = CreateFrame("Frame", "NE_StatsContent", scroll)
+ content:SetWidth(HEADER_W)
+ content:SetHeight(1)
+ content:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
+ scroll:SetScrollChild(content)
+ pane._content = content
+
+ -- The Item Level block (player mode only). Collapsible, and reorderable like any other block —
+ -- layoutSidebar places it wherever currentOrder() says, defaulting to the top.
+ pane._itemLevelRow = createItemLevelRow(content)
+
+ -- Build all header + stat rows ONCE, grouped per section. DOWNPORT/REPORT: build BOTH the player
+ -- sections AND the pet sections (unique ids) into the same content frame; layoutSidebar shows only
+ -- the active set (player vs pet) so CP.ExpandPetSidebar can swap stat owners with no rebuild.
+ pane._sectionRows = {}
+ for _, set in ipairs({ SECTIONS, PET_SECTIONS }) do
+ for _, section in ipairs(set) do
+ local sr = { header = createCategoryHeader(content, section), rows = {} }
+ for _, el in ipairs(section.stats) do
+ sr.rows[#sr.rows + 1] = createStatRow(content, el)
+ end
+ pane._sectionRows[section.id] = sr
+ end
+ end
+
+ -- DOWNPORT/REPORT: drive the ScrollFrame's pixel scroll with the existing custom scrollbar widget,
+ -- adapted to SetVerticalScroll via NE.scrollbar.BuildCustomPixel.
+ if NE.scrollbar and NE.scrollbar.BuildCustomPixel then
+ -- DOWNPORT/REPORT(user): x=-1 → bar sits flush-right in the reserved gap (≈1px from the pane edge)
+ -- instead of 4px left-of-the-scrollframe (which left a gap on the bar's right + clipped values).
+ pcall(NE.scrollbar.BuildCustomPixel, scroll, { x = -1 })
+ end
+
+ refreshSidebar() -- compute values + initial layout
+ return pane
+end
+CP.BuildSidebar = buildSidebar
+
+-- ----------------------------------------------------------------------------
+-- SelectSidebar(i) — pick which pane shows in InsetRight (replaces the foundation stub).
+-- 1 = stats pane (our NE_CharacterStatsPane)
+-- 2 = titles (no system on 3.3.5; never selected — tab is disabled)
+-- 3 = equipment manager (Agent E's NE.charpanel.ShowEquipManager; guard if absent)
+-- Always reflects the choice in the sidebar tab strip (SetSidebarTabSelected, from SidebarTabs.lua).
+-- ----------------------------------------------------------------------------
+local function selectSidebar(i)
+ i = tonumber(i) or 1
+ CP._activeSidebar = i
+
+ local pane = CP._sidebar or buildSidebar()
+ local equipPane = CP._equipPane
+
+ if i == 3 then
+ if pane then pane:Hide() end
+ -- Equipment manager pane is Agent E's surface. Guard: if absent, fall back to stats.
+ if CP.ShowEquipManager then
+ local ok = pcall(CP.ShowEquipManager)
+ if not ok then
+ if pane then pane:Show(); refreshSidebar() end
+ CP._activeSidebar = 1; i = 1
+ end
+ else
+ log("SelectSidebar(3): ShowEquipManager absent; staying on stats")
+ if pane then pane:Show(); refreshSidebar() end
+ CP._activeSidebar = 1; i = 1
+ end
+ else
+ -- Stats (1) — hide any equip pane and show the stats pane.
+ if equipPane then equipPane:Hide() end
+ if CP.HideEquipManager then pcall(CP.HideEquipManager) end
+ if pane then pane:Show(); refreshSidebar() end
+ if i ~= 1 then CP._activeSidebar = 1; i = 1 end
+ end
+
+ if CP.SetSidebarTabSelected then pcall(CP.SetSidebarTabSelected, i) end
+end
+CP.SelectSidebar = selectSidebar
+
+-- ----------------------------------------------------------------------------
+-- ReassertLayout — re-apply the resting sidebar layout (replaces the foundation stub). The foundation's
+-- CharacterPanel.lua positions the slots/model; here we additionally restore the sidebar's expand state
+-- + active pane so a re-show lands consistent. Out-of-combat only (touches our non-protected frame).
+-- DOWNPORT: the original foundation ReassertLayout (slot/model positioning) is preserved by calling
+-- through to it if it stashed the slot logic; we re-implement the sidebar half here.
+-- ----------------------------------------------------------------------------
+local foundationReassert = CP._foundationReassertLayout
+local function reassertLayout()
+ -- Run the foundation's slot/model re-position first (kept under a stashed ref).
+ if foundationReassert then pcall(foundationReassert) end
+ if InCombatLockdown() then return end
+ -- Re-apply sidebar visuals if the sidebar is currently expanded.
+ if CP.IsSidebarExpanded and CP.IsSidebarExpanded() then
+ if not CP._sidebar then buildSidebar() end
+ selectSidebar(CP._activeSidebar or 1)
+ end
+end
+
+-- Stash the foundation ReassertLayout once, then take it over (load order: this file runs after
+-- CharacterPanel.lua, so CP.ReassertLayout already points at the slot/model re-positioner).
+if CP.ReassertLayout and CP.ReassertLayout ~= reassertLayout then
+ CP._foundationReassertLayout = CP.ReassertLayout
+ foundationReassert = CP._foundationReassertLayout
+end
+CP.ReassertLayout = reassertLayout
+
+-- ----------------------------------------------------------------------------
+-- Expand / Collapse convenience wrappers around the foundation's SetSidebarExpanded.
+-- ExpandSidebar grows the frame 338->548, shows InsetRight + the tab strip, builds + shows the stats
+-- pane (default selection 1). CollapseSidebar shrinks back to 338 and hides InsetRight.
+-- ----------------------------------------------------------------------------
+local function sidebarStrip()
+ local f = CP.frame
+ return (f and f._neSidebarTabs) or CP.SidebarTabs
+end
+
+-- DOWNPORT/REPORT: snap the stats sidebar back to the TOP when entering Character/Pet. The sidebar
+-- uses a real pixel ScrollFrame (not the Faux scrolls reset in TabButtons), so it kept its offset
+-- across tab switches. SetVerticalScroll(0) also re-syncs the custom thumb (BuildCustomPixel hooks
+-- OnVerticalScroll). Deferred once so it runs after layoutSidebar has set the scroll range.
+local function resetSidebarScrollTop()
+ local pane = CP._sidebar
+ local scroll = pane and pane._scroll
+ if not (scroll and scroll.SetVerticalScroll) then return end
+ pcall(scroll.SetVerticalScroll, scroll, 0)
+ if C_Timer and C_Timer.After then
+ C_Timer.After(0, function() pcall(scroll.SetVerticalScroll, scroll, 0) end)
+ end
+end
+
+local function expandSidebar()
+ -- DOWNPORT/REPORT: player mode — the stats sidebar reads the PLAYER's stats.
+ CP._sidebarPetMode = false
+ if CP.SetSidebarExpanded then CP.SetSidebarExpanded(true) end
+ -- The stats/titles/equipment tab strip is PLAYER-only — show it in player mode.
+ local strip = sidebarStrip()
+ if strip then strip:Show() end
+ buildSidebar()
+ selectSidebar(CP._activeSidebar or 1)
+ resetSidebarScrollTop()
+end
+CP.ExpandSidebar = expandSidebar
+
+-- DOWNPORT/REPORT: ExpandPetSidebar — the Pet tab is two-pane like Character (model in the Inset,
+-- PET stats in the InsetRight sidebar). selectTab calls this on the Pet tab. It sets PET mode, expands
+-- the sidebar (frame 548 + InsetRight shown), forces the stats pane (never the equipment manager), then
+-- relays out the pet sections. Faithful to NewEra Sidebar.lua:751 / PetPaperDoll.CP.ExpandPetSidebar.
+local function expandPetSidebar()
+ CP._sidebarPetMode = true
+ if CP.SetSidebarExpanded then CP.SetSidebarExpanded(true) end
+ local pane = buildSidebar()
+ -- Always the stats pane in pet mode (no titles/equipment sidebars for a pet) → hide the tab strip.
+ local strip = sidebarStrip()
+ if strip then strip:Hide() end
+ if CP._equipPane then CP._equipPane:Hide() end
+ if CP.HideEquipManager then pcall(CP.HideEquipManager) end
+ CP._activeSidebar = 1
+ if pane then pane:Show() end
+ if CP.SetSidebarTabSelected then pcall(CP.SetSidebarTabSelected, 1) end
+ refreshSidebar()
+ resetSidebarScrollTop()
+end
+CP.ExpandPetSidebar = expandPetSidebar
+
+local function collapseSidebar()
+ if CP.SetSidebarExpanded then CP.SetSidebarExpanded(false) end
+ if CP._sidebar then CP._sidebar:Hide() end
+ if CP._equipPane then CP._equipPane:Hide() end
+end
+CP.CollapseSidebar = collapseSidebar
+
+-- ToggleSidebar — flips expand state (used by SidebarTabs / any caller). Combat-safe (frame width
+-- changes are legal on our non-protected frame).
+local function toggleSidebar()
+ if CP.IsSidebarExpanded and CP.IsSidebarExpanded() then
+ collapseSidebar()
+ else
+ expandSidebar()
+ end
+end
+CP.ToggleSidebar = toggleSidebar
+
+-- ----------------------------------------------------------------------------
+-- Live refresh: rebuild stat values when they change while the panel is open. The stat getters all
+-- read live, so we just repaint the visible rows. Bounded to when the pane is shown.
+-- ----------------------------------------------------------------------------
+local refresher = CreateFrame("Frame")
+local REFRESH_EVENTS = {
+ "UNIT_STATS", "UNIT_AURA", "UNIT_DAMAGE", "UNIT_ATTACK_POWER", "UNIT_RANGED_ATTACK_POWER",
+ "UNIT_ATTACK_SPEED", "UNIT_RANGEDDAMAGE", "UNIT_RESISTANCES", "UNIT_MAXHEALTH", "UNIT_MAXPOWER",
+ "PLAYER_DAMAGE_DONE_MODS", "COMBAT_RATING_UPDATE", "PLAYER_EQUIPMENT_CHANGED",
+ "SPELL_POWER_CHANGED", "PLAYER_LEVEL_UP",
+ "UPDATE_INVENTORY_DURABILITY", -- the Durability row's only trigger (repairs + damage taken)
+}
+for _, ev in ipairs(REFRESH_EVENTS) do pcall(refresher.RegisterEvent, refresher, ev) end
+refresher:SetScript("OnEvent", function(_, event, unit)
+ -- UNIT_* events: only react to the player.
+ if unit and unit ~= "player" then return end
+ local pane = CP._sidebar
+ if pane and pane:IsShown() then refreshSidebar() end
+end)
+
+-- Hook the stock primary-stat updater too (fires whenever the paperdoll recomputes).
+if _G.PaperDollFrame_SetPrimaryStats then
+ pcall(hooksecurefunc, "PaperDollFrame_SetPrimaryStats", function()
+ local pane = CP._sidebar
+ if pane and pane:IsShown() then refreshSidebar() end
+ end)
+end
+
+-- ----------------------------------------------------------------------------
+-- Boot: build the pane at login (gated on the module being enabled). InsetRight exists by then
+-- (InsetFrames.lua builds at PLAYER_LOGIN). Build defensively; the pane stays hidden until expanded.
+-- ----------------------------------------------------------------------------
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ local ok, err = pcall(buildSidebar)
+ if not ok then log("sidebar build failed: " .. tostring(err)) end
+end)
\ No newline at end of file
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SidebarTabs.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SidebarTabs.lua
new file mode 100644
index 0000000..0f61107
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SidebarTabs.lua
@@ -0,0 +1,252 @@
+-- DragonUI_NewEra/modules/character/SidebarTabs.lua — the 3-tab sidebar strip (PaperDollSidebarTabs).
+--
+-- ARCHITECTURE (CONTRACT_S1 §A0 / VISUAL_SPEC): a 168x35 strip above NE.charpanel.InsetRight with 3
+-- tabs (33x35 each). Ported from NewEra/CharacterPanel/SidebarTabs.lua, re-hosted on OUR custom frame's
+-- InsetRight (not Blizzard's CharacterFrame):
+-- Tab1 Character / Stats — player FACE portrait icon → SelectSidebar(1)
+-- Tab2 Titles — permanently DISABLED (no title system on 3.3.5) → tooltip explains
+-- Tab3 Equipment Manager — → SelectSidebar(3) → NE.charpanel.ShowEquipManager (Agent E; guarded)
+--
+-- The strip art is the STOCK 3.3.5a sheet Interface\PaperDollInfoFrame\PaperDollSidebarTabs (canonical
+-- since vanilla, present on this client) — used by raw path + texcoord (no atlas registration needed).
+--
+-- 3.3.5 GOTCHAS (§B): SetPortraitTexture is natively circular; SetMotionScriptsWhileDisabled may be
+-- absent (pcall); no SetShown (Show/Hide); raise nothing (the strip parents to InsetRight). Clicking a
+-- tab plays the stock tab SFX. The strip mirrors InsetRight's visibility via OnShow/OnHide hooks.
+
+local NE = DragonUI_NewEra
+local L = NE:GetLocale()
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+-- DOWNPORT: Interface\PaperDollInfoFrame\PaperDollSidebarTabs is a Cata+ sheet absent on 3.3.5a. We
+-- SHIP the real Blizzard sheet (from ElvUI_Enhanced's copy) so NewEra's texcoords render exactly. The
+-- 64x256 layout matches NewEra's TC rects (decorLeft → 28x11px, etc.).
+local SIDEBAR_BLP = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\CharacterPanel\\PaperDollSidebarTabs"
+
+local function log(msg)
+ if CP._log then CP._log(msg); return end
+ if NE.Log then NE.Log("SIDEBARTABS", msg) end
+end
+
+-- Texcoords transcribed from NewEra/CharacterPanel/SidebarTabs.lua (the stock PaperDollSidebarTabs layout).
+local TC = {
+ decorLeft = { 0.01562500, 0.45312500, 0.00390625, 0.04687500 }, -- 28x11
+ decorRight = { 0.01562500, 0.45312500, 0.05468750, 0.10546875 }, -- 28x13
+ tabBg = { 0.01562500, 0.79687500, 0.61328125, 0.78125000 }, -- inactive 50x43
+ tabBgActive = { 0.01562500, 0.79687500, 0.78906250, 0.95703125 }, -- active row
+ tabHider = { 0.01562500, 0.54687500, 0.11328125, 0.18750000 }, -- 34x19 bottom cap
+ tabHighlight = { 0.01562500, 0.50000000, 0.19531250, 0.31640625 }, -- 31x31 hover
+ titlesIcon = { 0.01562500, 0.53125000, 0.32421875, 0.46093750 }, -- Tab2 icon
+ equipIcon = { 0.01562500, 0.53125000, 0.46875000, 0.60546875 }, -- Tab3 icon
+}
+
+local function getLocaleString(global, fallback)
+ if type(global) == "string" and global ~= "" then
+ local v = _G[global]
+ if type(v) == "string" and v ~= "" then
+ return v
+ end
+ end
+ if type(L) == "table" and type(fallback) == "string" and fallback ~= "" then
+ local rawVal = rawget(L, fallback)
+ if type(rawVal) == "string" and rawVal ~= "" then
+ return rawVal
+ end
+ local ok, res = pcall(function() return L[fallback] end)
+ if ok and type(res) == "string" and res ~= "" then
+ return res
+ end
+ end
+ return fallback
+end
+
+if type(L) == "table" then
+ local mt = getmetatable(L) or {}
+ mt.__call = function(self, global, fallback)
+ return getLocaleString(global, fallback)
+ end
+ setmetatable(L, mt)
+else
+ L = function(global, fallback)
+ return getLocaleString(global, fallback)
+ end
+end
+
+-- Build one tab. iconKind: "face" = player portrait (Tab1); "tex" = sub-rect of the sheet.
+local function buildTab(id, parent, name, iconKind, texCoord, tooltip, enabled, onClick)
+ local tab = CreateFrame("Button", name, parent)
+ tab:SetSize(33, 35)
+ tab._tabId = id
+
+ -- Show tooltips even while disabled (so the Titles tab can explain why it's off). May be absent.
+ if tab.SetMotionScriptsWhileDisabled then pcall(tab.SetMotionScriptsWhileDisabled, tab, true) end
+
+ -- Tab BG (BACKGROUND). styleTab swaps inactive/active rows.
+ local bg = tab:CreateTexture(nil, "BACKGROUND")
+ bg:SetTexture(SIDEBAR_BLP)
+ bg:SetSize(50, 43)
+ bg:SetPoint("BOTTOMLEFT", tab, "BOTTOMLEFT", -9, -2)
+ bg:SetTexCoord(unpack(TC.tabBg))
+ tab.TabBg = bg
+
+ -- Icon (ARTWORK).
+ local icon = tab:CreateTexture(nil, "ARTWORK")
+ tab.Icon = icon
+ if iconKind == "face" then
+ -- Player face portrait, inset to 29x31 so the tab's selected/hover border shows around it.
+ icon:SetSize(29, 31)
+ icon:SetPoint("BOTTOM", tab, "BOTTOM", 1, 0)
+ local function refreshFace()
+ if SetPortraitTexture then pcall(SetPortraitTexture, icon, "player") end
+ icon:SetTexCoord(0.109375, 0.890625, 0.09375, 0.90625)
+ end
+ refreshFace()
+ local ev = CreateFrame("Frame", nil, tab)
+ ev:RegisterEvent("UNIT_PORTRAIT_UPDATE")
+ ev:RegisterEvent("PLAYER_ENTERING_WORLD")
+ ev:SetScript("OnEvent", function(_, evt, unit)
+ if evt == "UNIT_PORTRAIT_UPDATE" and unit and unit ~= "player" then return end
+ refreshFace()
+ end)
+ else
+ icon:SetSize(33, 35)
+ icon:SetPoint("BOTTOM", tab, "BOTTOM", 1, -2)
+ icon:SetTexture(SIDEBAR_BLP)
+ if texCoord then icon:SetTexCoord(unpack(texCoord)) end
+ end
+
+ -- Hider (OVERLAY) — caps the bottom of an UNSELECTED tab; styleTab hides it when selected.
+ local hider = tab:CreateTexture(nil, "OVERLAY")
+ hider:SetTexture(SIDEBAR_BLP)
+ hider:SetSize(34, 19)
+ hider:SetPoint("BOTTOM")
+ hider:SetTexCoord(unpack(TC.tabHider))
+ tab.Hider = hider
+
+ -- Highlight (HIGHLIGHT layer = auto-render on mouseover).
+ local hl = tab:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetTexture(SIDEBAR_BLP)
+ hl:SetSize(31, 31)
+ hl:SetPoint("TOPLEFT", 2, -3)
+ hl:SetTexCoord(unpack(TC.tabHighlight))
+ tab.Highlight = hl
+
+ tab:SetScript("OnEnter", function(self)
+ if not (tooltip and tooltip.title) then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText(tooltip.title, 1, 1, 1)
+ if not self:IsEnabled() and tooltip.disabled then
+ GameTooltip:AddLine(tooltip.disabled, 1, 0.1, 0.1, true)
+ end
+ GameTooltip:Show()
+ end)
+ tab:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ -- Disabled state: alpha 0.5 + desaturated icon.
+ tab:SetScript("OnEnable", function(self)
+ self:SetAlpha(1)
+ if self.Icon and self.Icon.SetDesaturated then self.Icon:SetDesaturated(false) end
+ end)
+ tab:SetScript("OnDisable", function(self)
+ self:SetAlpha(0.5)
+ if self.Icon and self.Icon.SetDesaturated then self.Icon:SetDesaturated(true) end
+ end)
+
+ if onClick then
+ tab:SetScript("OnClick", function(self, ...)
+ if PlaySound then pcall(PlaySound, "igCharacterInfoTab") end
+ onClick(self, ...)
+ end)
+ end
+
+ if enabled then tab:Enable() else tab:Disable() end
+ return tab
+end
+
+-- Selected/unselected styling (NewEra PaperDollFrame_UpdateSidebarTabs).
+local function styleTab(tab, selected)
+ if not tab then return end
+ if selected then
+ if tab.Hider then tab.Hider:Hide() end
+ if tab.Highlight then tab.Highlight:Hide() end
+ if tab.TabBg then tab.TabBg:SetTexCoord(unpack(TC.tabBgActive)) end
+ else
+ if tab.Hider then tab.Hider:Show() end
+ if tab.Highlight then tab.Highlight:Show() end
+ if tab.TabBg then tab.TabBg:SetTexCoord(unpack(TC.tabBg)) end
+ end
+end
+
+local function buildSidebarTabs()
+ local f = CP.frame
+ local insetRight = f and (f.InsetRight or CP.InsetRight)
+ if not insetRight then log("buildSidebarTabs: InsetRight not ready"); return end
+ if f._neSidebarTabs then return f._neSidebarTabs end
+
+ local strip = CreateFrame("Frame", "NE_PaperDollSidebarTabs", insetRight)
+ strip:SetSize(168, 35)
+ -- Anchored ABOVE InsetRight (NewEra offsets verbatim).
+ strip:SetPoint("BOTTOMRIGHT", insetRight, "TOPRIGHT", -6, -1)
+ f._neSidebarTabs = strip
+ CP.SidebarTabs = strip
+
+ -- Decorative caps.
+ local dl = strip:CreateTexture(nil, "ARTWORK")
+ dl:SetTexture(SIDEBAR_BLP); dl:SetSize(28, 11)
+ dl:SetPoint("BOTTOMLEFT"); dl:SetTexCoord(unpack(TC.decorLeft))
+ strip.DecorLeft = dl
+
+ local dr = strip:CreateTexture(nil, "ARTWORK")
+ dr:SetTexture(SIDEBAR_BLP); dr:SetSize(28, 13)
+ dr:SetPoint("BOTTOMRIGHT"); dr:SetTexCoord(unpack(TC.decorRight))
+ strip.DecorRight = dr
+
+ -- Build right-to-left (NewEra chain anchors).
+ -- Tab3: Equipment Manager → SelectSidebar(3) (which routes to ShowEquipManager, guarded).
+ local tab3 = buildTab(3, strip, "NE_PaperDollSidebarTab3", "tex", TC.equipIcon,
+ { title = L("PAPERDOLL_EQUIPMENTMANAGER", "Equipment Manager") }, true,
+ function() if CP.SelectSidebar then CP.SelectSidebar(3) end end)
+ tab3:SetPoint("BOTTOMRIGHT", strip, "BOTTOMRIGHT", -30, 0)
+ strip.Tab3 = tab3
+
+ -- Tab2: Titles — ENABLED on 3.3.5 (WotLK has the title system, unlike Classic Era which NewEra
+ -- targeted). Selects the titles pane (CP.SelectSidebar(2) → TitlesPane.ShowTitles).
+ local tab2 = buildTab(2, strip, "NE_PaperDollSidebarTab2", "tex", TC.titlesIcon,
+ { title = L("PAPERDOLL_SIDEBAR_TITLES", "Titles") }, true,
+ function() if CP.SelectSidebar then CP.SelectSidebar(2) end end)
+ tab2:SetPoint("RIGHT", tab3, "LEFT", -4, 0)
+ strip.Tab2 = tab2
+
+ -- Tab1: Character — player face portrait → SelectSidebar(1) (stats pane).
+ local tab1 = buildTab(1, strip, "NE_PaperDollSidebarTab1", "face", nil,
+ { title = L("PAPERDOLL_SIDEBAR_STATS", "Character") }, true,
+ function() if CP.SelectSidebar then CP.SelectSidebar(1) end end)
+ tab1:SetPoint("RIGHT", tab2, "LEFT", -4, 0)
+ strip.Tab1 = tab1
+
+ -- Selected-state helper consumed by CP.SelectSidebar (Sidebar.lua).
+ CP.SetSidebarTabSelected = function(index)
+ styleTab(strip.Tab1, index == 1)
+ styleTab(strip.Tab2, index == 2)
+ styleTab(strip.Tab3, index == 3)
+ end
+ CP.SetSidebarTabSelected(CP._activeSidebar or 1)
+
+ -- Strip visibility mirrors InsetRight (sidebar expanded). DOWNPORT: no SetShown — Show/Hide only.
+ insetRight:HookScript("OnShow", function() strip:Show() end)
+ insetRight:HookScript("OnHide", function() strip:Hide() end)
+ if not insetRight:IsShown() then strip:Hide() end
+
+ return strip
+end
+CP.BuildSidebarTabs = buildSidebarTabs
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ local ok, err = pcall(buildSidebarTabs)
+ if not ok then log("sidebar tabs build failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Skills.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Skills.lua
new file mode 100644
index 0000000..98fb99b
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/Skills.lua
@@ -0,0 +1,355 @@
+-- DragonUI_NewEra/modules/character/Skills.lua — the Skills secondary tab.
+--
+-- Fills the EMPTY pane DragonUI_NewEra_CharacterPane_Skills (created by TabButtons.lua, parented
+-- to NE.charpanel.frame.Inset). Collapsible skill-type HEADERS + per-skill rank BARS, ported
+-- from NewEra/CharacterPanel/Skills.lua (which itself mirrors its Reputation tab).
+--
+-- DOWNPORT (CONTRACT_S1 §A.2/§B): NewEra used retail WowScrollBox + ScrollUtil over the stock
+-- SkillFrame. 3.3.5a has neither, so we render DIRECTLY into our pane with a NAMED
+-- FauxScrollFrame (unnamed FauxScrollFrameTemplate ERRORS) + a recycled row pool — the proven
+-- DragonflightUICharacter pattern.
+--
+-- DATA: GetNumSkillLines() + GetSkillLineInfo(i). 3.3.5a tuple:
+-- skillName, header, isExpanded, skillRank, numTempPoints, skillModifier, skillMaxRank,
+-- isAbandonable, stepCost, rankCost, minLevel, skillCostType, skillDescription
+-- Headers toggle via ExpandSkillHeader(i)/CollapseSkillHeader(i) (renumbers the list → re-read).
+--
+-- GRACEFUL DEGRADATION (§A.5): every getter is pcall-guarded; if the skill API is absent the
+-- pane shows an "unavailable" line and logs a warning — NEVER errors.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local function log(msg)
+ if CP._log then CP._log("SKILLS: " .. tostring(msg)); return end
+ if NE.Log then NE.Log("SKILLS", msg); return end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffffcc55DragonUI_NewEra|r [skills]: " .. tostring(msg))
+ end
+end
+
+-- Geometry (matches Reputation.lua so the two list tabs share a rhythm).
+local ROW_HEIGHT = 24
+local BAR_W = 132
+local BAR_H = 13
+local LEFT_W = 78
+local RIGHT_W = 54
+
+-- Native 3.3.5a bar art (FDID 136570 does NOT exist — use the native Skills-Bar path).
+local CAP_TEX = "Interface\\PaperDollInfoFrame\\UI-Character-ReputationBar"
+local FILL_TEX = "Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar"
+local function capSource()
+ return (NE.tex and NE.tex.localFiles and NE.tex.localFiles[136567]) or CAP_TEX
+end
+
+local LEFT_TC = { 0.691, 1.0, 0.047, 0.281 }
+local RIGHT_TC = { 0.0, 0.164, 0.3906, 0.625 }
+
+local function setAtlas(tex, name, useAtlasSize)
+ if tex and NE.tex and NE.tex.SetAtlas then return NE.tex.SetAtlas(tex, name, useAtlasSize) end
+ return false
+end
+
+-- Collapsible header (Options_ListExpand 3-piece + chevron; degrades to untextured but clickable).
+local function buildHeader(parent)
+ local h = CreateFrame("Button", nil, parent)
+ h:SetHeight(ROW_HEIGHT)
+
+ h._bgLeft = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgLeft, "options_listexpand_left", true)
+ h._bgLeft:SetPoint("LEFT", h, "LEFT", 0, 0)
+
+ h._bgRight = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgRight, "options_listexpand_right", true)
+ h._bgRight:SetPoint("RIGHT", h, "RIGHT", 0, 0)
+
+ h._bgMiddle = h:CreateTexture(nil, "BACKGROUND")
+ setAtlas(h._bgMiddle, "_options_listexpand_middle", false)
+ h._bgMiddle:SetPoint("TOPLEFT", h._bgLeft, "TOPRIGHT")
+ h._bgMiddle:SetPoint("BOTTOMRIGHT", h._bgRight, "BOTTOMLEFT")
+
+ local hl = h:CreateTexture(nil, "HIGHLIGHT")
+ if hl.SetColorTexture then hl:SetColorTexture(1, 1, 1, 0.10) else hl:SetTexture(1, 1, 1, 0.10) end
+ hl:SetPoint("TOPLEFT", h._bgLeft, "TOPRIGHT")
+ hl:SetPoint("BOTTOMRIGHT", h._bgRight, "BOTTOMLEFT")
+
+ h._name = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h._name:SetPoint("LEFT", h, "LEFT", 12, 0)
+ h._name:SetJustifyH("LEFT")
+
+ h:RegisterForClicks("LeftButtonUp")
+ return h
+end
+
+local function updateHeader(h, info)
+ h._name:SetText(info.name or "")
+ local cap = info.isExpanded and "options_listexpand_right_expanded" or "options_listexpand_right"
+ setAtlas(h._bgRight, cap, true)
+ local idx = info.skillIndex
+ h:SetScript("OnClick", function()
+ if info.isExpanded then
+ if CollapseSkillHeader then pcall(CollapseSkillHeader, idx) end
+ else
+ if ExpandSkillHeader then pcall(ExpandSkillHeader, idx) end
+ end
+ if CP.RefreshSkills then CP.RefreshSkills() end
+ end)
+end
+
+-- Skill rank entry (name + tinted rank bar w/ native cap chrome).
+local function buildEntry(parent)
+ local e = CreateFrame("Button", nil, parent)
+ e:SetHeight(ROW_HEIGHT)
+
+ e._name = e:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ e._name:SetPoint("LEFT", e, "LEFT", 12, 0)
+ e._name:SetJustifyH("LEFT")
+
+ local bar = CreateFrame("StatusBar", nil, e)
+ bar:SetSize(BAR_W, BAR_H)
+ bar:SetPoint("RIGHT", e, "RIGHT", -4, 0)
+ bar:SetStatusBarTexture(FILL_TEX)
+ local fillTex = bar:GetStatusBarTexture()
+ if fillTex then fillTex:SetDrawLayer("BORDER") end
+ bar:SetMinMaxValues(0, 1)
+ bar:SetValue(1)
+
+ local barBg = bar:CreateTexture(nil, "BACKGROUND")
+ barBg:SetAllPoints(bar)
+ if barBg.SetColorTexture then barBg:SetColorTexture(0, 0, 0, 1) else barBg:SetTexture(0, 0, 0, 1) end
+
+ -- DOWNPORT/REPORT: brown ReputationBar end-caps removed (clean bar). A 1px dark OUTLINE frames the
+ -- bar WITHOUT covering the fill — the earlier solid overlay sat on the BORDER-layer fill and hid the
+ -- colored progress ("appearing below the black background"). Outline = 4 edge lines at OVERLAY only.
+ local function barEdge()
+ local t = bar:CreateTexture(nil, "OVERLAY")
+ if t.SetColorTexture then t:SetColorTexture(0, 0, 0, 0.9) else t:SetTexture(0, 0, 0, 0.9) end
+ return t
+ end
+ local eT = barEdge(); eT:SetPoint("TOPLEFT", bar, "TOPLEFT", -1, 1); eT:SetPoint("TOPRIGHT", bar, "TOPRIGHT", 1, 1); eT:SetHeight(1)
+ local eB = barEdge(); eB:SetPoint("BOTTOMLEFT", bar, "BOTTOMLEFT", -1, -1); eB:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 1, -1); eB:SetHeight(1)
+ local eL = barEdge(); eL:SetPoint("TOPLEFT", bar, "TOPLEFT", -1, 1); eL:SetPoint("BOTTOMLEFT", bar, "BOTTOMLEFT", -1, -1); eL:SetWidth(1)
+ local eR = barEdge(); eR:SetPoint("TOPRIGHT", bar, "TOPRIGHT", 1, 1); eR:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", 1, -1); eR:SetWidth(1)
+
+ bar._text = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ bar._text:SetPoint("CENTER", bar, "CENTER", 0, 0)
+
+ e._name:SetPoint("RIGHT", bar, "LEFT", -8, 0)
+ e._bar = bar
+
+ local hl = e:CreateTexture(nil, "HIGHLIGHT")
+ if hl.SetColorTexture then hl:SetColorTexture(1, 1, 1, 0.10) else hl:SetTexture(1, 1, 1, 0.10) end
+ hl:SetAllPoints(e)
+ return e
+end
+
+local function updateEntry(e, info)
+ e._name:SetText(info.name or "")
+
+ local bar = e._bar
+ local rank, maxRank = info.rank or 0, info.maxRank or 0
+ if maxRank <= 0 then maxRank = 1; rank = 0 end
+ bar:SetMinMaxValues(0, maxRank)
+ bar:SetValue(rank)
+
+ -- Color: yellow at cap / green trainable / grey not-learned (stock SkillFrame_Update logic).
+ local r, g, b = 0.5, 0.5, 0.5
+ if rank > 0 and rank >= maxRank then r, g, b = 1.0, 0.95, 0.1
+ elseif rank > 0 then r, g, b = 0.2, 0.85, 0.2 end
+ bar:SetStatusBarColor(r, g, b)
+
+ bar._text:SetText(string.format("%d / %d", rank, maxRank))
+end
+
+-- ---------------------------------------------------------------------------
+-- Pane scaffold (named FauxScrollFrame + pools).
+-- ---------------------------------------------------------------------------
+local pane
+local scroll, content
+local headerPool, entryPool = {}, {}
+local flat = {}
+local NUM_VISIBLE = 0
+local emptyLabel
+
+local function getPane()
+ if pane then return pane end
+ pane = _G.DragonUI_NewEra_CharacterPane_Skills or (CP.EnsurePane and CP.EnsurePane("Skills"))
+ return pane
+end
+
+local function acquireHeader()
+ for _, h in ipairs(headerPool) do
+ if not h._inUse then h._inUse = true; h:Show(); return h end
+ end
+ local h = buildHeader(content)
+ headerPool[#headerPool + 1] = h
+ h._inUse = true
+ return h
+end
+
+local function acquireEntry()
+ for _, en in ipairs(entryPool) do
+ if not en._inUse then en._inUse = true; en:Show(); return en end
+ end
+ local en = buildEntry(content)
+ entryPool[#entryPool + 1] = en
+ en._inUse = true
+ return en
+end
+
+local function releaseAll()
+ for _, h in ipairs(headerPool) do h._inUse = false; h:Hide() end
+ for _, en in ipairs(entryPool) do en._inUse = false; en:Hide() end
+end
+
+local function updateScroll()
+ if not (scroll and content) then return end
+ content:Show()
+ releaseAll()
+
+ local total = #flat
+ if emptyLabel then if total == 0 then emptyLabel:Show() else emptyLabel:Hide() end end
+
+ local nRows = NUM_VISIBLE > 0 and NUM_VISIBLE or 1
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(scroll, total, nRows, ROW_HEIGHT) end
+ if NE.scrollbar and NE.scrollbar.CenterIfNoBar then NE.scrollbar.CenterIfNoBar(scroll, total > nRows) end
+ local offset = (FauxScrollFrame_GetOffset and FauxScrollFrame_GetOffset(scroll)) or 0
+
+ local rowW = (content:GetWidth() or 200)
+ if rowW <= 0 then rowW = 200 end
+
+ for i = 1, nRows do
+ local data = flat[offset + i]
+ if data then
+ local row
+ if data.kind == "header" then
+ row = acquireHeader()
+ updateHeader(row, data.info)
+ else
+ row = acquireEntry()
+ updateEntry(row, data.info)
+ end
+ row:ClearAllPoints()
+ row:SetWidth(rowW)
+ row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(i - 1) * ROW_HEIGHT)
+ row:Show()
+ end
+ end
+end
+
+local function refresh()
+ if not getPane() then return end
+ if not scroll then return end
+ flat = {}
+
+ if not (GetNumSkillLines and GetSkillLineInfo) then
+ log("GetNumSkillLines/GetSkillLineInfo unavailable — Skills pane degraded")
+ updateScroll()
+ return
+ end
+
+ local ok, total = pcall(GetNumSkillLines)
+ if not ok or not total then total = 0 end
+
+ for i = 1, total do
+ -- §B: pcall every getter.
+ local ok2, name, header, isExpanded, rank, _, modifier, maxRank = pcall(GetSkillLineInfo, i)
+ if ok2 and name then
+ local info = {
+ skillIndex = i, name = name, isHeader = header, isExpanded = isExpanded,
+ rank = rank, maxRank = maxRank, modifier = modifier,
+ }
+ flat[#flat + 1] = { kind = header and "header" or "entry", info = info }
+ end
+ end
+
+ updateScroll()
+end
+-- DOWNPORT/REPORT: surface (don't swallow) a refresh error so an empty Skills pane is diagnosable.
+CP.RefreshSkills = function()
+ local ok, err = pcall(refresh)
+ if not ok then log("RefreshSkills error: " .. tostring(err)) end
+end
+
+local function recomputeVisible()
+ if not scroll then return end
+ local h = scroll:GetHeight() or 0
+ NUM_VISIBLE = math.max(1, math.floor(h / ROW_HEIGHT))
+end
+
+local function build()
+ local host = getPane()
+ if not host then log("Skills pane host missing — cannot build"); return false end
+ if scroll then return true end
+
+ local bg = host:CreateTexture(nil, "BACKGROUND")
+ if not setAtlas(bg, "character-panel-background", false) then bg:Hide() end
+ bg:SetPoint("TOPLEFT", host, "TOPLEFT", 2, -2)
+ bg:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -2, 2)
+
+ scroll = CreateFrame("ScrollFrame", "DragonUI_NewEra_SkillsScroll", host, "FauxScrollFrameTemplate")
+ -- DOWNPORT/REPORT: viewport inset to sit INSIDE the engraved inset box (top/left margin), leaving a
+ -- right gutter for the scrollbar (pushed out to the frame border by the negative BuildCustom x below).
+ scroll:SetPoint("TOPLEFT", host, "TOPLEFT", 10, -12)
+ scroll:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -24, 10)
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ FauxScrollFrame_OnVerticalScroll(self, offset, ROW_HEIGHT, updateScroll)
+ end)
+ -- DOWNPORT/REPORT: wheel scrolling via the (hidden) Faux slider value; auto-clamps to its range.
+ scroll:EnableMouseWheel(true)
+ scroll:SetScript("OnMouseWheel", function(self, delta)
+ local sb = _G[(self:GetName() or "") .. "ScrollBar"]
+ if not sb then return end
+ local mn, mx = sb:GetMinMaxValues()
+ local v = sb:GetValue() - delta * ROW_HEIGHT
+ if v < mn then v = mn elseif v > mx then v = mx end
+ sb:SetValue(v)
+ end)
+ scroll:HookScript("OnSizeChanged", function() recomputeVisible(); updateScroll() end)
+ -- DOWNPORT: hand-built minimal scrollbar (Reskin's stock-slider re-skin didn't render).
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, scroll, { x = -8 }) end
+
+ -- DOWNPORT/REPORT (the empty-pane bug): the row container must be a child of the PANE, not of the
+ -- faux ScrollFrame. FauxScrollFrame_Update HIDES the scroll frame whenever the list fits with no
+ -- scrolling needed (numItems <= numToDisplay) — which took content + every row down with it (Honor
+ -- only rendered because its list overflowed). Parent content to host (always shown), anchor it over
+ -- the scroll rect, and raise it above the scroll frame so header buttons stay clickable.
+ content = CreateFrame("Frame", nil, host)
+ content:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
+ content:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
+ content:SetHeight(1)
+ content:SetFrameLevel(scroll:GetFrameLevel() + 5)
+
+ emptyLabel = host:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
+ emptyLabel:SetPoint("TOP", host, "TOP", 0, -40)
+ emptyLabel:SetWidth(200)
+ emptyLabel:SetText(_G.SKILLS and (_G.SKILLS .. " unavailable") or "No skills.")
+ emptyLabel:Hide()
+
+ recomputeVisible()
+ return true
+end
+
+local function init()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ if not build() then return end
+ pcall(refresh)
+ local host = getPane()
+ if host and not host._neSkillsShowHooked then
+ host._neSkillsShowHooked = true
+ host:HookScript("OnShow", function() recomputeVisible(); pcall(refresh) end)
+ end
+end
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:RegisterEvent("SKILL_LINES_CHANGED")
+boot:SetScript("OnEvent", function(_, event)
+ if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" then
+ if C_Timer and C_Timer.After then C_Timer.After(0, init) else init() end
+ else
+ CP.RefreshSkills()
+ end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SlotFrames.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SlotFrames.lua
new file mode 100644
index 0000000..a7708f3
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SlotFrames.lua
@@ -0,0 +1,144 @@
+-- DragonUI_NewEra/modules/character/SlotFrames.lua — decorative metal frame borders around each of
+-- the 19 PaperDoll equipment slot buttons. Ported faithfully from NewEra/CharacterPanel/SlotFrames.lua.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): the slot buttons have ALREADY been reparented into
+-- NE.charpanel.Inset and positioned at the vanilla anchors by CharacterPanel.lua/InsetFrames.lua.
+-- This file ONLY adds the decorative Char-Paperdoll-Parts metal frame texture around each slot — it
+-- does NOT (re)position the slots; that is the foundation's job. (NewEra's applySlotColumnAnchors did
+-- the anchoring too because it owned both; here the foundation owns anchoring, so we drop it.)
+--
+-- Each frame is a BACKGROUND/-1 texture drawn behind the slot's icon, so it borders the icon without
+-- occluding it (the icon + IconBorder live at higher sublevels). Three slices from Char-Paperdoll-
+-- Parts (FDID 410248): LEFT 49x44 (left column), RIGHT 50x44 (right column), BOTTOM 42x53 (weapon
+-- row). Plus the two small gap-fillers that flank MainHand/SecondaryHand.
+--
+-- GRACEFUL DEGRADATION (CONTRACT §B): every step pcall-guarded; if the art FDID isn't shipped the
+-- frame simply renders blank (the slot still works). Never errors out of boot.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local MODULE = "character"
+
+local function log(msg) if CP._log then CP._log(msg) elseif NE.Log then NE.Log("CHARPANEL", msg) end end
+
+-- ----------------------------------------------------------------------------
+-- Art source. Prefer the shipped local BLP (Assets.lua registered FDID 410248); fall back to the
+-- canonical 3.3.5a client path so the metal frame still renders if the local copy is missing.
+-- DOWNPORT: NE.tex.SetAtlas can't help here — these are raw texcoord slices, not registered atlases.
+-- ----------------------------------------------------------------------------
+local CHAR_PARTS_BLP = (NE.tex and NE.tex.localFiles and NE.tex.localFiles[410248])
+ or "Interface\\CharacterFrame\\Char-Paperdoll-Parts"
+
+-- Verbatim slices from retail/NewEra CharacterFrame.xml (see NewEra/CharacterPanel/SlotFrames.lua).
+local LEFT_SLICE = { w = 49, h = 44, left = 0.20703125, right = 0.39843750, top = 0.59375, bottom = 0.93750 }
+local RIGHT_SLICE = { w = 50, h = 44, left = 0.00390625, right = 0.19921875, top = 0.59375, bottom = 0.93750 }
+local BOTTOM_SLICE = { w = 42, h = 53, left = 0.67187500, right = 0.83593750, top = 0.00781, bottom = 0.42188 }
+
+-- Char-Slot-Bottom-Left / Char-Slot-Bottom-Right — decorative gap-fillers flanking the weapon row.
+local BOTTOM_LEFT_GAP = { w = 6, h = 54, left = 0.70703125, right = 0.73046875, top = 0.43750, bottom = 0.85938 }
+local BOTTOM_RIGHT_GAP = { w = 7, h = 54, left = 0.67187500, right = 0.69921875, top = 0.43750, bottom = 0.85938 }
+
+-- Slot -> { slice, anchorPoint, x, y } matching retail's PaperDollItemSlotButton{Left,Right,Bottom}
+-- Template inheritance. Left frame offset TOPLEFT(-4,0); right TOPRIGHT(+4,0); bottom TOPLEFT(-4,+8).
+local SLOT_SIDES = {
+ -- Left column
+ CharacterHeadSlot = { slice = LEFT_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 0 },
+ CharacterNeckSlot = { slice = LEFT_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 0 },
+ CharacterShoulderSlot = { slice = LEFT_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 0 },
+ CharacterBackSlot = { slice = LEFT_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 0 },
+ CharacterChestSlot = { slice = LEFT_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 0 },
+ CharacterShirtSlot = { slice = LEFT_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 0 },
+ CharacterTabardSlot = { slice = LEFT_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 0 },
+ CharacterWristSlot = { slice = LEFT_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 0 },
+ -- Right column
+ CharacterHandsSlot = { slice = RIGHT_SLICE, anchorPoint = "TOPRIGHT", x = 4, y = 0 },
+ CharacterWaistSlot = { slice = RIGHT_SLICE, anchorPoint = "TOPRIGHT", x = 4, y = 0 },
+ CharacterLegsSlot = { slice = RIGHT_SLICE, anchorPoint = "TOPRIGHT", x = 4, y = 0 },
+ CharacterFeetSlot = { slice = RIGHT_SLICE, anchorPoint = "TOPRIGHT", x = 4, y = 0 },
+ CharacterFinger0Slot = { slice = RIGHT_SLICE, anchorPoint = "TOPRIGHT", x = 4, y = 0 },
+ CharacterFinger1Slot = { slice = RIGHT_SLICE, anchorPoint = "TOPRIGHT", x = 4, y = 0 },
+ CharacterTrinket0Slot = { slice = RIGHT_SLICE, anchorPoint = "TOPRIGHT", x = 4, y = 0 },
+ CharacterTrinket1Slot = { slice = RIGHT_SLICE, anchorPoint = "TOPRIGHT", x = 4, y = 0 },
+ -- Bottom weapon row
+ CharacterMainHandSlot = { slice = BOTTOM_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 8 },
+ CharacterSecondaryHandSlot = { slice = BOTTOM_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 8 },
+ CharacterRangedSlot = { slice = BOTTOM_SLICE, anchorPoint = "TOPLEFT", x = -4, y = 8 },
+}
+
+local function applyFrameToSlot(slotName, spec)
+ local slot = _G[slotName]
+ if not slot or slot._neSlotFrameDone then return end
+
+ -- BACKGROUND/-1: behind the slot's icon + IconBorder so the metal frame surrounds the icon but
+ -- never hides it. DOWNPORT: the reparented slot inherits OUR Inset frame level; the texture is a
+ -- CHILD of the slot so it follows the slot's level — no extra level juggling needed.
+ local frame = slot:CreateTexture(nil, "BACKGROUND", nil, -1)
+ frame:SetTexture(CHAR_PARTS_BLP)
+ frame:SetTexCoord(spec.slice.left, spec.slice.right, spec.slice.top, spec.slice.bottom)
+ frame:SetSize(spec.slice.w, spec.slice.h)
+ frame:SetPoint(spec.anchorPoint, slot, spec.anchorPoint, spec.x, spec.y)
+ slot._neSlotFrame = frame
+ slot._neSlotFrameDone = true
+ -- DOWNPORT/REPORT: register into the shared paperdoll-decor list so CP.ShowPaperDoll can hide the
+ -- metal frame on non-Character tabs. (It IS a child of the slot button, so it follows the slot's
+ -- Hide too — registering it here is belt-and-suspenders + keeps the contract explicit.)
+ CP._paperdollDecor = CP._paperdollDecor or {}
+ table.insert(CP._paperdollDecor, frame)
+end
+
+local function applyAll()
+ for name, spec in pairs(SLOT_SIDES) do
+ local ok, err = pcall(applyFrameToSlot, name, spec)
+ if not ok then log("slotframe " .. name .. " failed: " .. tostring(err)) end
+ end
+end
+
+-- The two weapon-row gap-fillers. Anchored to the bottom slots' frame textures (built above), so this
+-- must run AFTER applyAll. Idempotent.
+local function applyGapFillers()
+ local mh = _G.CharacterMainHandSlot
+ if mh and mh._neSlotFrame and not mh._neBottomLeftGap then
+ local gap = mh:CreateTexture(nil, "BACKGROUND", nil, -1)
+ gap:SetTexture(CHAR_PARTS_BLP)
+ gap:SetTexCoord(BOTTOM_LEFT_GAP.left, BOTTOM_LEFT_GAP.right, BOTTOM_LEFT_GAP.top, BOTTOM_LEFT_GAP.bottom)
+ gap:SetSize(BOTTOM_LEFT_GAP.w, BOTTOM_LEFT_GAP.h)
+ gap:SetPoint("TOPRIGHT", mh._neSlotFrame, "TOPLEFT", 0, 0)
+ mh._neBottomLeftGap = gap
+ -- DOWNPORT/REPORT: register the gap-filler into the paperdoll-decor list.
+ CP._paperdollDecor = CP._paperdollDecor or {}
+ table.insert(CP._paperdollDecor, gap)
+ end
+
+ local sh = _G.CharacterSecondaryHandSlot
+ if sh and sh._neSlotFrame and not sh._neBottomRightGap then
+ local gap = sh:CreateTexture(nil, "BACKGROUND", nil, -1)
+ gap:SetTexture(CHAR_PARTS_BLP)
+ gap:SetTexCoord(BOTTOM_RIGHT_GAP.left, BOTTOM_RIGHT_GAP.right, BOTTOM_RIGHT_GAP.top, BOTTOM_RIGHT_GAP.bottom)
+ gap:SetSize(BOTTOM_RIGHT_GAP.w, BOTTOM_RIGHT_GAP.h)
+ gap:SetPoint("TOPLEFT", sh._neSlotFrame, "TOPRIGHT", 0, 0)
+ sh._neBottomRightGap = gap
+ -- DOWNPORT/REPORT: register the gap-filler into the paperdoll-decor list.
+ CP._paperdollDecor = CP._paperdollDecor or {}
+ table.insert(CP._paperdollDecor, gap)
+ end
+end
+
+local function applySlotFrames()
+ applyAll()
+ pcall(applyGapFillers)
+end
+
+CP.ApplySlotFrames = applySlotFrames
+
+-- Boot. DOWNPORT: family gate is NE.modules.IsEnabled("character") (the foundation's module id), not
+-- NewEra's IsBooted("CharacterPanel"). Run on login + entering-world (re-asserts after late loads).
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled(MODULE) then return end
+ local ok, err = pcall(applySlotFrames)
+ if not ok then log("SlotFrames boot failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SlotQuality.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SlotQuality.lua
new file mode 100644
index 0000000..131e2bb
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/SlotQuality.lua
@@ -0,0 +1,93 @@
+-- DragonUI_NewEra/modules/character/SlotQuality.lua — item-quality border coloring on the 19 PaperDoll
+-- equipment slots. Ported from NewEra/CharacterPanel/SlotQuality.lua.
+--
+-- WHY: 3.3.5a's PaperDollFrame never calls SetItemButtonQuality on the equipped slots, so their
+-- IconBorder stays uncolored. We drive the call ourselves on equipment change + login; Core/ItemButton's
+-- hooksecurefunc on SetItemButtonQuality then paints the retail-exact quality border (it resolves each
+-- slot's IconBorder by .IconBorder or $parentIconBorder). PaperDollItemSlotButtonTemplate inherits
+-- ItemButtonTemplate, so each slot carries the IconBorder texture the hook needs.
+--
+-- DOWNPORT vs NewEra:
+-- * Quality read: NewEra used GetItemInfo(itemLink) (pos 3). On 3.3.5a GetInventoryItemQuality(unit,
+-- slotID) returns the quality directly and is cache-independent (no nil while uncached), so we use
+-- it as the primary source per the brief, with GetItemInfo as a fallback for the link.
+-- * Family gate: NE.modules.IsEnabled("character") (foundation module id).
+-- Triggered on PLAYER_EQUIPMENT_CHANGED (per-slot) + PLAYER_ENTERING_WORLD / PLAYER_LOGIN (full pass).
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local MODULE = "character"
+
+local function log(msg) if CP._log then CP._log(msg) elseif NE.Log then NE.Log("CHARPANEL", msg) end end
+
+-- The 19 character equipment slots (Ammo excluded — different button type). Order matches the
+-- foundation's reparent list.
+local SLOTS = {
+ "CharacterHeadSlot", "CharacterNeckSlot", "CharacterShoulderSlot",
+ "CharacterBackSlot", "CharacterChestSlot", "CharacterShirtSlot",
+ "CharacterTabardSlot", "CharacterWristSlot",
+ "CharacterHandsSlot", "CharacterWaistSlot", "CharacterLegsSlot", "CharacterFeetSlot",
+ "CharacterFinger0Slot", "CharacterFinger1Slot",
+ "CharacterTrinket0Slot", "CharacterTrinket1Slot",
+ "CharacterMainHandSlot", "CharacterSecondaryHandSlot", "CharacterRangedSlot",
+}
+
+-- Color one slot's border by its equipped item quality (or hide it if empty).
+local function updateSlotQuality(slot)
+ if not slot or not slot.GetID then return end
+ local id = slot:GetID()
+ if not id or id == 0 then return end
+
+ -- Primary: GetInventoryItemQuality (cache-independent on 3.3.5a). pcall — some private cores return
+ -- nil/error for empty slots.
+ local quality
+ local ok, q = pcall(GetInventoryItemQuality, "player", id)
+ if ok then quality = q end
+
+ local itemLink = GetInventoryItemLink("player", id)
+ -- Fallback: derive quality from the link if the direct getter gave nothing but an item is present.
+ if quality == nil and itemLink then
+ local ok2, _, _, q2 = pcall(GetItemInfo, itemLink)
+ if ok2 then quality = q2 end
+ end
+
+ -- Route through the global so Core/ItemButton's hook paints the retail-exact border. nil quality
+ -- (empty slot) -> SetItemButtonQuality hides the border.
+ if SetItemButtonQuality then
+ pcall(SetItemButtonQuality, slot, quality, itemLink)
+ elseif NE.itembutton and NE.itembutton.ApplyQuality then
+ -- DOWNPORT: belt-and-suspenders if the global is somehow absent on a private core.
+ pcall(NE.itembutton.ApplyQuality, slot, quality)
+ end
+end
+
+local function updateAllSlots()
+ for _, name in ipairs(SLOTS) do
+ updateSlotQuality(_G[name])
+ end
+end
+
+-- CONTRACT surface (brief).
+CP.UpdateAllSlots = updateAllSlots
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+boot:SetScript("OnEvent", function(_, event, slotID)
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled(MODULE) then return end
+ if event == "PLAYER_EQUIPMENT_CHANGED" and slotID then
+ -- Per-slot update is cheaper than a full walk.
+ for _, name in ipairs(SLOTS) do
+ local slot = _G[name]
+ if slot and slot.GetID and slot:GetID() == slotID then
+ updateSlotQuality(slot)
+ return
+ end
+ end
+ return
+ end
+ updateAllSlots()
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/TabButtons.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/TabButtons.lua
new file mode 100644
index 0000000..952c0e7
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/TabButtons.lua
@@ -0,0 +1,361 @@
+-- DragonUI_NewEra/modules/character/TabButtons.lua — OUR OWN DF metal bottom tabs for the custom
+-- character frame, + the 5 empty content panes the Wave-2 content agents fill.
+--
+-- ARCHITECTURE PIVOT (CONTRACT_S1 §A0): we do NOT reskin Blizzard's CharacterFrameTab1..5. We build
+-- our OWN tab buttons hanging off the bottom of NE.charpanel.frame and switch our own content panes.
+--
+-- GEOMETRY (VISUAL_SPEC.md): 6 metal DF tabs — Character / Pet / Skills / Honor / Reputation / Currency.
+-- * 36px tall INACTIVE, 42px tall ACTIVE.
+-- * 1px gap between tabs.
+-- * the ACTIVE tab is raised one frame level (so its taller body draws over its neighbours).
+--
+-- PANES: each tab owns an empty content frame parented to frame.Inset, named
+-- DragonUI_NewEra_CharacterPane_ (Tab in {Character,Pet,Skills,Honor,Reputation,Currency}).
+-- Wave 2 fills them. The Character pane is the SLOTS+MODEL host (those widgets are reparented straight
+-- onto the Inset in CharacterPanel.lua, so the Character pane is a thin always-on container that
+-- never hides the model). Secondary panes cover the Inset and are hidden until selected.
+--
+-- HOW: each tab is a Button inheriting the vanilla "CharacterFrameTabButtonTemplate" (so it carries
+-- the Left/Middle/Right + *Disabled child textures that NE.tabs.ReskinClassicTab swaps to the DF
+-- metal atlases). On click a tab calls NE.charpanel.SelectTab(name).
+--
+-- DOWNPORT/degrade: if the vanilla template is unavailable, fall back to a plain UIPanelButton so the
+-- tab row still works (unstyled) and never errors. No SetShown — Show()/Hide() only.
+
+local NE = DragonUI_NewEra
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+-- Tab heights + spacing (VISUAL_SPEC).
+local TAB_H_INACTIVE = 36
+local TAB_H_ACTIVE = 42
+local TAB_GAP = 1
+local TAB_MIN_W = 70
+
+local function log(msg) if CP._log then CP._log(msg) end end
+
+-- Tab order + display labels (VISUAL_SPEC: Character,Pet,Skills,Honor,Reputation,Currency).
+-- DOWNPORT: use the localized globals where present, else English.
+local TABS = {
+ { key = "Character", label = _G.CHARACTER or "Character" },
+ { key = "Pet", label = _G.PET or "Pet" },
+ { key = "Skills", label = _G.SKILLS or "Skills" },
+ { key = "Honor", label = _G.HONOR or "Honor" },
+ { key = "Reputation", label = _G.REPUTATION or "Reputation" },
+ { key = "Currency", label = _G.CURRENCY or "Currency" },
+}
+
+CP._tabs = CP._tabs or {} -- key -> button
+CP._tabPanes = CP._tabPanes or {} -- key -> content frame (parented to Inset)
+CP._activeTab = CP._activeTab or "Character"
+
+-- Forward declarations (kept as locals, not globals).
+local selectTab, selectInitial
+
+-- Create (or reuse) a content pane for a tab, parented to frame.Inset. Wave 2 fills these.
+-- Named DragonUI_NewEra_CharacterPane_ per the contract.
+local function ensurePane(key)
+ if CP._tabPanes[key] then return CP._tabPanes[key] end
+ local f = CP.frame
+ local host = (f and f.Inset) or f
+ if not host then return nil end
+ local pane = CreateFrame("Frame", "DragonUI_NewEra_CharacterPane_" .. key, host)
+ pane:SetAllPoints(host)
+ pane:SetFrameLevel((host:GetFrameLevel() or 1) + 1)
+ if key ~= "Character" then pane:Hide() end -- DOWNPORT: Hide(), no SetShown
+ CP._tabPanes[key] = pane
+ return pane
+end
+CP.EnsurePane = ensurePane
+
+-- DOWNPORT (faithful to NewEra TabButtons.lua): per-tab width = max(70, textW+24) — NOT equal-fill
+-- (that spread them across the whole bottom). Re-chain only VISIBLE tabs so a hidden Pet tab leaves
+-- no gap.
+local TAB_TEXT_BREATHING = 24
+
+local function resizeTab(tab)
+ if not tab then return end
+ local text = _G[tab:GetName() .. "Text"]
+ local textW = 0
+ if text then text:SetWidth(0); textW = text:GetWidth() or 0 end -- auto-size before measuring
+ tab:SetWidth(math.max(TAB_MIN_W, math.floor(textW + TAB_TEXT_BREATHING)))
+end
+
+-- Pet tab shows only when the player has a controllable pet UI (Blizzard PetTab_Update rule -
+-- Hunter/Warlock/DK/Mage water elemental). Mounts & non-combat companions moved OUT of the Character
+-- panel into the standalone Collections window (modules/collections/*), so the Pet tab no longer
+-- needs to appear for petless classes just to browse them - gating on HasPetUI() alone again avoids
+-- showing an empty "you have no pet" pane to Shamans/Priests/etc.
+local function petTabShown()
+ local ok, v = pcall(function() return HasPetUI and HasPetUI() end)
+ return ok and v and true or false
+end
+
+-- Show the Currency tab only when the player actually has currency. GetCurrencyListSize() counts
+-- currencies AND their category headers; a char with zero currencies has an empty list, and headers
+-- only appear when they have currency children — so size > 0 ⟺ the player has some currency.
+local function currencyTabShown()
+ local ok, n = pcall(function() return GetCurrencyListSize and GetCurrencyListSize() end)
+ return ok and n and n > 0 and true or false
+end
+
+-- Left-pack the VISIBLE tabs: first at frame.BOTTOMLEFT(11,2) by TOPLEFT, rest chain TOPLEFT to the
+-- previous visible tab's TOPRIGHT with a 1px gap (NewEra rechainVisibleTabs). Tabs HANG BELOW the
+-- frame, tops aligned, so the active (42h) tab grows downward.
+local function rechainVisibleTabs(f)
+ local prev
+ for _, def in ipairs(TABS) do
+ local tab = CP._tabs[def.key]
+ if tab and tab:IsShown() then
+ tab:ClearAllPoints()
+ if prev then
+ tab:SetPoint("TOPLEFT", prev, "TOPRIGHT", TAB_GAP, 0)
+ else
+ tab:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 11, 2)
+ end
+ prev = tab
+ end
+ end
+end
+CP._rechainVisibleTabs = rechainVisibleTabs
+CP._resizeTab = resizeTab
+
+-- Build the DF metal tab row across the bottom of our frame.
+local function buildTabs()
+ local f = CP.frame or (CP.BuildFrame and CP.BuildFrame())
+ if not f then log("buildTabs: frame not built yet"); return end
+
+ for i, def in ipairs(TABS) do
+ local name = "DragonUI_NewEra_CharacterTab" .. i
+ local tab = CP._tabs[def.key] or _G[name]
+ if not tab then
+ -- Prefer the vanilla tab template (gives us Left/Middle/Right children to reskin); else plain.
+ local ok, t = pcall(CreateFrame, "Button", name, f, "CharacterFrameTabButtonTemplate")
+ if ok and t then
+ tab = t
+ else
+ tab = CreateFrame("Button", name, f, "UIPanelButtonTemplate")
+ tab._nePlain = true
+ end
+ tab:SetID(i)
+ tab.tabKey = def.key
+ local txt = _G[name .. "Text"]
+ if txt then txt:SetText(def.label) elseif tab.SetText then tab:SetText(def.label) end
+ tab:SetScript("OnClick", function(self)
+ if PlaySound then pcall(PlaySound, "igCharacterInfoTab") end
+ CP.SelectTab(self.tabKey)
+ end)
+ CP._tabs[def.key] = tab
+ end
+
+ -- DF metal skin via the shared Core walker (no-op on plain fallback buttons).
+ if not tab._nePlain and NE.tabs and NE.tabs.ReskinClassicTab then
+ pcall(NE.tabs.ReskinClassicTab, name, {})
+ end
+
+ -- Conditional tabs: Pet (only with a pet UI), Currency (only with currency). Others always shown.
+ if def.key == "Pet" then
+ if petTabShown() then tab:Show() else tab:Hide() end
+ elseif def.key == "Currency" then
+ if currencyTabShown() then tab:Show() else tab:Hide() end
+ end
+
+ resizeTab(tab)
+ tab:SetHeight(TAB_H_INACTIVE)
+
+ ensurePane(def.key)
+ end
+
+ -- Left-pack the visible tabs (after all are sized + visibility set).
+ rechainVisibleTabs(f)
+
+ -- Re-evaluate Pet-tab visibility + re-chain when pets come/go.
+ if not CP._petTabWatcher then
+ local w = CreateFrame("Frame")
+ w:RegisterEvent("UNIT_PET")
+ w:RegisterEvent("PET_UI_UPDATE")
+ w:RegisterEvent("PLAYER_ENTERING_WORLD")
+ w:RegisterEvent("CURRENCY_DISPLAY_UPDATE")
+ w:RegisterEvent("KNOWN_CURRENCY_TYPES_UPDATE")
+ w:RegisterEvent("COMPANION_LEARNED")
+ w:SetScript("OnEvent", function()
+ local petTab = CP._tabs["Pet"]
+ if petTab then if petTabShown() then petTab:Show() else petTab:Hide() end end
+ local curTab = CP._tabs["Currency"]
+ if curTab then if currencyTabShown() then curTab:Show() else curTab:Hide() end end
+ rechainVisibleTabs(CP.frame)
+ end)
+ CP._petTabWatcher = w
+ end
+
+ -- Select the resting tab (Character).
+ selectInitial()
+end
+
+-- DOWNPORT: drive the tab's selected/deselected ART manually (we don't call PanelTemplates_SelectTab,
+-- which would resize the tab). The reskin put the INACTIVE atlas on Left/Middle/Right and the ACTIVE
+-- (gold, taller) atlas on the *Disabled pieces; the vanilla template normally toggles which set is
+-- visible. SELECTED → show the *Disabled (gold/active) pieces, hide the regular ones; DESELECTED →
+-- the reverse. (Without this the unselected tabs wrongly showed the gold art and vice-versa.)
+local function setTabArt(tab, selected)
+ if not tab then return end
+ local n = tab:GetName()
+ local function set(suffix, show)
+ local t = _G[n .. suffix]
+ if t then if show then t:Show() else t:Hide() end end
+ end
+ set("Left", not selected); set("Middle", not selected); set("Right", not selected)
+ set("LeftDisabled", selected); set("MiddleDisabled", selected); set("RightDisabled", selected)
+
+ -- DOWNPORT/REPORT: the SELECTED tab must NOT show the hover highlight (ReskinClassicTab's custom
+ -- HIGHLIGHT-layer 3-slice auto-lit on mouseover even when active — it's shaped/sized for the inactive
+ -- tab and bled over the gold art). Mute it (alpha 0) while selected; restore 0.4 when deselected.
+ local hl = tab._neCustomHL
+ if hl then
+ local a = selected and 0 or 0.4
+ if hl.left and hl.left.SetAlpha then hl.left:SetAlpha(a) end
+ if hl.middle and hl.middle.SetAlpha then hl.middle:SetAlpha(a) end
+ if hl.right and hl.right.SetAlpha then hl.right:SetAlpha(a) end
+ end
+end
+
+-- Switch the active tab: show its pane, hide the rest, swap selection art + height + frame level.
+selectTab = function(key)
+ if not CP._tabs[key] then return end
+ CP._activeTab = key
+ local f = CP.frame
+ local baseLevel = (f and f:GetFrameLevel() or 1) + 2
+ for _, def in ipairs(TABS) do
+ local tab = CP._tabs[def.key]
+ local pane = CP._tabPanes[def.key]
+ local on = (def.key == key)
+ if tab then
+ setTabArt(tab, on) -- selected → gold/active art; else dark/inactive
+ -- 42h active / 36h inactive; active tab raised above siblings. Tabs hang below the frame with
+ -- tops aligned, so the taller active tab grows DOWNWARD.
+ tab:SetHeight(on and TAB_H_ACTIVE or TAB_H_INACTIVE)
+ if CP._resizeTab then CP._resizeTab(tab) end -- keep per-tab text width
+ if tab.SetFrameLevel then tab:SetFrameLevel(baseLevel + (on and 8 or 0)) end
+ -- Label centered; nudge DOWN on the active tab (tabs grow downward from the top-aligned edge,
+ -- so the active tab's extra 6px is below — center the label in the visible body).
+ local txt = _G[tab:GetName() .. "Text"]
+ if txt then txt:ClearAllPoints(); txt:SetPoint("CENTER", tab, "CENTER", 0, on and -3 or 0) end
+ end
+ if pane then
+ if on then
+ -- DOWNPORT/REPORT: raise the ACTIVE secondary pane ABOVE the (now-hidden) paperdoll so its
+ -- content draws cleanly and fills the Inset (panes are SetAllPoints(Inset), so they follow
+ -- the per-tab Inset width automatically). Model sits at host+2, slots at host+5.
+ local host = (f and f.Inset) or f
+ if pane.SetFrameLevel and host and host.GetFrameLevel then
+ pane:SetFrameLevel((host:GetFrameLevel() or 1) + 10)
+ end
+ pane:Show()
+ else
+ pane:Hide() -- DOWNPORT: no SetShown
+ end
+ end
+ end
+
+ -- Keep the visible tab row tight after width/height changes.
+ if CP._rechainVisibleTabs then CP._rechainVisibleTabs(f) end
+
+ -- DOWNPORT/REPORT: hide/show the WHOLE player-paperdoll group (model + 19 slots + decorations) so
+ -- Skills/Reputation/Honor no longer show the paperdoll behind/over their content. Only the
+ -- Character tab shows the player paperdoll; Pet drives its OWN pet model (DragonUI_NewEra_PetModel)
+ -- inside its pane, so the player model/slots hide there too.
+ if CP.ShowPaperDoll then pcall(CP.ShowPaperDoll, key == "Character") end
+
+ -- DOWNPORT/REPORT: per-tab frame + Inset WIDTH (the state-machine fix). ApplyTabState sets the
+ -- wide-tab flag + frame/Inset width and force-collapses the sidebar on the three Era tabs; on
+ -- Character/Pet it restores the narrow fixed-width Inset. Run BEFORE the sidebar expand below so
+ -- the wide-tab flag is set when ExpandSidebar/SetSidebarExpanded run.
+ if CP.ApplyTabState then pcall(CP.ApplyTabState, key) end
+
+ -- DOWNPORT/REPORT (Issue A): ApplyTabState just re-anchored the frame + Inset to the per-tab WIDTH
+ -- (445 for the wide Era tabs). The active pane is SetAllPoints(Inset) once at boot, but re-assert it
+ -- here (in case anything cleared its points) and then trigger the tab's own content RELAYOUT so its
+ -- FauxScrollFrame + content frame re-read the NOW-RESIZED width and re-flow rows (without this the
+ -- rows were laid out at the narrow ~272px width → clipped/empty, and the scrollbar sat at the old x).
+ do
+ local host = (f and f.Inset) or f
+ local pane = CP._tabPanes[key]
+ if pane and host then
+ pane:ClearAllPoints()
+ pane:SetAllPoints(host)
+ end
+ -- DOWNPORT/REPORT: ALWAYS reset the entering tab's scroll to the TOP. Persisting a scroll offset
+ -- across tabs left a new (often shorter) list scrolled partway down — rows missing, layout off.
+ -- Reset the Faux offset + the frame's own vertical scroll BEFORE the refresh re-lays out from row 1.
+ local scrollNames = {
+ Skills = "DragonUI_NewEra_SkillsScroll",
+ Reputation = "DragonUI_NewEra_RepScroll",
+ Honor = "DragonUI_NewEra_HonorScroll",
+ Currency = "DragonUI_NewEra_CurrencyScroll",
+ }
+ local sf = scrollNames[key] and _G[scrollNames[key]]
+ if sf then
+ if FauxScrollFrame_SetOffset then pcall(FauxScrollFrame_SetOffset, sf, 0) end
+ if sf.SetVerticalScroll then pcall(sf.SetVerticalScroll, sf, 0) end
+ end
+ local refreshers = {
+ Skills = CP.RefreshSkills,
+ Reputation = CP.RefreshReputation,
+ Honor = CP.RefreshHonor,
+ Pet = CP.RefreshPet,
+ }
+ local fn = refreshers[key]
+ if fn then
+ pcall(fn)
+ -- DOWNPORT/REPORT: the Inset SetPoint width change resolves on the NEXT frame, so a synchronous
+ -- refresh can still read the old width. Re-run one frame later so rows reflow at the final width.
+ -- (The scroll's OnSizeChanged hook also covers this; the deferred call is belt-and-suspenders.)
+ if C_Timer and C_Timer.After then C_Timer.After(0, function() pcall(fn) end) end
+ end
+ end
+
+ -- Tie the sidebar to the active tab (NewEra Sidebar.lua:743-755): the Character/PaperDoll tab RESTS
+ -- with the right-side stats sidebar EXPANDED (this is the missing right third of the panel); Pet
+ -- expands its pet-stats sidebar; Reputation/Skills/Honor are full-width with no sidebar (already
+ -- force-collapsed by ApplyTabState — SetSidebarExpanded no-ops the expand while wide).
+ if key == "Character" then
+ -- DOWNPORT/REPORT: always return to the STATS view on entering Character, even if the Equipment
+ -- Manager (sidebar 3) was the last sub-tab open. ExpandSidebar restores CP._activeSidebar, so reset
+ -- it to 1 first (selectSidebar(1) inside ExpandSidebar then hides the equip pane + shows stats).
+ CP._activeSidebar = 1
+ if CP.HideEquipManager then pcall(CP.HideEquipManager) end
+ if CP.HideTitles then pcall(CP.HideTitles) end
+ if CP.ExpandSidebar then pcall(CP.ExpandSidebar) end
+ elseif key == "Pet" then
+ -- DOWNPORT/REPORT: if Companions.lua's Mounts & Companions grid is showing, the sidebar space is
+ -- repurposed for its 3D model preview rather than collapsed - SyncPetSidebarForCollection keeps
+ -- the frame expanded and swaps InsetRight's content, so re-entering the Pet tab doesn't yank the
+ -- preview away and put the stale pet stats back up (which unconditional ExpandPetSidebar would do).
+ if CP.IsPetCollectionActive and CP.IsPetCollectionActive() then
+ if CP.SyncPetSidebarForCollection then pcall(CP.SyncPetSidebarForCollection)
+ elseif CP.CollapseSidebar then pcall(CP.CollapseSidebar) end
+ elseif CP.ExpandPetSidebar then pcall(CP.ExpandPetSidebar)
+ elseif CP.ExpandSidebar then pcall(CP.ExpandSidebar) end
+ else
+ if CP.CollapseSidebar then pcall(CP.CollapseSidebar) end
+ end
+ -- Re-assert resting layout (slot/model positions) on tab switch.
+ if CP.ReassertLayout then pcall(CP.ReassertLayout) end
+end
+
+-- Select the resting tab after a (re)build.
+selectInitial = function()
+ selectTab(CP._activeTab or "Character")
+end
+
+-- Publish the surface.
+CP.BuildTabs = buildTabs
+CP.SelectTab = selectTab
+
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:SetScript("OnEvent", function()
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ local ok, err = pcall(buildTabs)
+ if not ok then log("tab build failed: " .. tostring(err)) end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/TitlesPane.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/TitlesPane.lua
new file mode 100644
index 0000000..4bf279f
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/TitlesPane.lua
@@ -0,0 +1,319 @@
+-- DragonUI_NewEra/modules/character/TitlesPane.lua — the Titles sidebar (Tab 2).
+--
+-- DOWNPORT/REPORT: NewEra DISABLED this tab because Classic Era 1.15 has no title system. WotLK 3.3.5
+-- DOES (GetNumTitles / IsTitleKnown / GetTitleName / GetCurrentTitle / SetCurrentTitle), so we implement
+-- a real title picker: a scrollable list of your KNOWN titles plus a "None" entry; click a row to set
+-- that title. Current title is checked + highlighted.
+--
+-- Structure mirrors Skills.lua: a NAMED FauxScrollFrame + a `content` frame parented to the PANE (NOT
+-- the scroll — FauxScrollFrame_Update hides the scroll when the list fits, which would take the rows
+-- with it), windowed to the visible row count, with wheel scrolling and a flush-right custom scrollbar.
+
+local NE = DragonUI_NewEra
+local L = NE:GetLocale()
+NE.charpanel = NE.charpanel or {}
+local CP = NE.charpanel
+
+local ROW_H = 22
+
+local function log(msg) if CP._log then CP._log("TITLES: " .. tostring(msg)) elseif NE.Log then NE.Log("TITLES", msg) end end
+
+local function getLocaleString(global, fallback)
+ if type(global) == "string" and global ~= "" then
+ local v = _G[global]
+ if type(v) == "string" and v ~= "" then
+ return v
+ end
+ end
+ if type(L) == "table" and type(fallback) == "string" and fallback ~= "" then
+ local rawVal = rawget(L, fallback)
+ if type(rawVal) == "string" and rawVal ~= "" then
+ return rawVal
+ end
+ local ok, res = pcall(function() return L[fallback] end)
+ if ok and type(res) == "string" and res ~= "" then
+ return res
+ end
+ end
+ return fallback
+end
+
+if type(L) == "table" then
+ local mt = getmetatable(L) or {}
+ mt.__call = function(self, global, fallback)
+ return getLocaleString(global, fallback)
+ end
+ setmetatable(L, mt)
+else
+ L = function(global, fallback)
+ return getLocaleString(global, fallback)
+ end
+end
+
+-- DOWNPORT/REPORT: match the sidebar's own resolution — InsetRight may be on CP.InsetRight, not
+-- CP.frame.InsetRight (buildSidebar uses `f.InsetRight or CP.InsetRight`).
+local function host() return (CP.frame and CP.frame.InsetRight) or CP.InsetRight end
+local function trim(s) return (tostring(s or "")):gsub("^%s+", ""):gsub("%s+$", "") end
+
+local function setAtlas(tex, name, useAtlasSize)
+ if tex and NE.tex and NE.tex.SetAtlas then return NE.tex.SetAtlas(tex, name, useAtlasSize) end
+ return false
+end
+
+-- 3.3.5 GetTitleName returns the suffix/prefix form (often with a leading/trailing space).
+local function titleText(id)
+ local ok, name = pcall(GetTitleName, id)
+ if ok and name and trim(name) ~= "" then return trim(name) end
+ return nil
+end
+
+-- Build "Name " immediately (optimistic) for the window header, without waiting for UnitPVPName
+-- to catch up. GetTitleName gives a SUFFIX with a leading space (" the Explorer") or a PREFIX with a
+-- trailing space ("Private "); combine accordingly. The event-driven CP.UpdateTitle (UnitPVPName)
+-- corrects any odd format a frame later.
+local function titledName(titleID)
+ local base = (UnitName and UnitName("player")) or ""
+ if not titleID or titleID == 0 then return base end
+ local ok, raw = pcall(GetTitleName, titleID)
+ if not ok or not raw or raw == "" then return base end
+ if raw:sub(1, 1) == " " then return base .. raw
+ elseif raw:sub(-1) == " " then return raw .. base
+ else return base .. " " .. raw end
+end
+
+-- ---------------------------------------------------------------------------
+-- Pane scaffold.
+-- ---------------------------------------------------------------------------
+local pane, scroll, content
+local updateScroll -- forward-declared: buildRow's OnClick (above its definition) calls it
+local rowPool = {}
+local flat = {} -- ordered { { id = titleID (0 = None), text = display } }
+local NUM_VISIBLE = 0
+local emptyLabel
+local _optimisticCurrent -- titleID shown ticked right after a click (GetCurrentTitle lags a frame);
+ -- cleared by refresh() so open/event repaints read the real current title
+
+local function getPane()
+ if pane then return pane end
+ pane = _G.NE_TitlesPane
+ return pane
+end
+
+local function buildRow(parent)
+ local b = CreateFrame("Button", nil, parent)
+ b:SetHeight(ROW_H)
+ b:RegisterForClicks("LeftButtonUp")
+
+ local hl = b:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
+ hl:SetBlendMode("ADD"); hl:SetAlpha(0.4); hl:SetAllPoints(b)
+
+ local sel = b:CreateTexture(nil, "BACKGROUND")
+ sel:SetTexture("Interface\\FriendsFrame\\UI-FriendsFrame-HighlightBar")
+ sel:SetTexCoord(0.2, 0.8, 0, 1); sel:SetBlendMode("ADD"); sel:SetAlpha(0.5)
+ sel:SetAllPoints(b); sel:Hide(); b.Selected = sel
+
+ local check = b:CreateTexture(nil, "OVERLAY")
+ check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
+ check:SetSize(18, 18); check:SetPoint("RIGHT", b, "RIGHT", -4, 0); check:Hide(); b.Check = check
+
+ local txt = b:CreateFontString(nil, "ARTWORK", "GameFontNormalLeft")
+ txt:SetPoint("LEFT", b, "LEFT", 8, 0); txt:SetPoint("RIGHT", b, "RIGHT", -24, 0)
+ txt:SetJustifyH("LEFT"); b.text = txt
+
+ b:SetScript("OnClick", function(self)
+ if self.titleID == nil then return end
+ -- Already the current title? Do nothing — no redundant SetCurrentTitle to the server on re-clicks.
+ local current = (GetCurrentTitle and GetCurrentTitle()) or 0
+ if self.titleID == current then return end
+ if SetCurrentTitle then pcall(SetCurrentTitle, self.titleID) end
+ -- DOWNPORT/REPORT: every listed title is OWNED (IsTitleKnown filter), so the set always succeeds —
+ -- reflect it OPTIMISTICALLY right here (no C_Timer): move the tick + update the window header now.
+ -- GetCurrentTitle/UnitPVPName only catch up a frame later (the UNIT_NAME_UPDATE event repaints from
+ -- the real state then, which matches).
+ _optimisticCurrent = self.titleID
+ updateScroll()
+ if CP.SetWindowTitle then pcall(CP.SetWindowTitle, titledName(self.titleID)) end
+ end)
+ return b
+end
+
+local function acquireRow()
+ for _, r in ipairs(rowPool) do
+ if not r._inUse then r._inUse = true; r:Show(); return r end
+ end
+ local r = buildRow(content)
+ rowPool[#rowPool + 1] = r
+ r._inUse = true
+ return r
+end
+
+local function releaseAll()
+ for _, r in ipairs(rowPool) do r._inUse = false; r:Hide() end
+end
+
+function updateScroll() -- assigns the forward-declared local (see top of file)
+ if not (scroll and content) then return end
+ content:Show()
+ releaseAll()
+
+ local total = #flat
+ if emptyLabel then if total == 0 then emptyLabel:Show() else emptyLabel:Hide() end end
+
+ local nRows = NUM_VISIBLE > 0 and NUM_VISIBLE or 1
+ if FauxScrollFrame_Update then FauxScrollFrame_Update(scroll, total, nRows, ROW_H) end
+ local offset = (FauxScrollFrame_GetOffset and FauxScrollFrame_GetOffset(scroll)) or 0
+
+ local current = _optimisticCurrent
+ if current == nil then current = (GetCurrentTitle and GetCurrentTitle()) or 0 end
+
+ for i = 1, nRows do
+ local data = flat[offset + i]
+ if data then
+ local row = acquireRow()
+ row.titleID = data.id
+ row.text:SetText(data.text)
+ local on = (data.id == current)
+ if on then row.Selected:Show(); row.Check:Show(); row.text:SetTextColor(1, 0.82, 0)
+ else row.Selected:Hide(); row.Check:Hide(); row.text:SetTextColor(1, 1, 1) end
+ row:ClearAllPoints()
+ row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(i - 1) * ROW_H)
+ row:SetPoint("RIGHT", content, "RIGHT", 0, 0)
+ row:Show()
+ end
+ end
+end
+
+-- Rebuild `flat` from the known-title list, then repaint.
+local function refresh()
+ if not getPane() then return end
+ if not scroll then return end
+ _optimisticCurrent = nil -- open/event repaint → trust the real GetCurrentTitle
+ flat = { { id = 0, text = L("PLAYER_TITLE_NONE", "None") } }
+
+ if GetNumTitles and IsTitleKnown then
+ local n = (pcall(GetNumTitles) and GetNumTitles()) or 0
+ for id = 1, n do
+ local okk, known = pcall(IsTitleKnown, id)
+ -- DOWNPORT/REPORT: IsTitleKnown returns 0 for FALSE on this server (not nil), and 0 is TRUTHY in
+ -- Lua — so `if okk and known` treated every title as known. Require it to be truthy AND not 0.
+ if okk and known and known ~= 0 then
+ local t = titleText(id)
+ if t then flat[#flat + 1] = { id = id, text = t } end
+ end
+ end
+ end
+
+ updateScroll()
+end
+CP.RefreshTitles = function()
+ local ok, err = pcall(refresh)
+ if not ok then log("RefreshTitles error: " .. tostring(err)) end
+end
+
+local function recomputeVisible()
+ if not scroll then return end
+ local h = scroll:GetHeight() or 0
+ NUM_VISIBLE = math.max(1, math.floor(h / ROW_H))
+end
+
+local function build()
+ local h = getPane()
+ if not h then
+ -- Pane lives in InsetRight; create it lazily.
+ local ir = host()
+ if not ir then log("build: InsetRight not ready"); return false end
+ pane = CreateFrame("Frame", "NE_TitlesPane", ir)
+ pane:SetPoint("TOPLEFT", ir, "TOPLEFT", 3, -3)
+ pane:SetPoint("BOTTOMRIGHT", ir, "BOTTOMRIGHT", -3, 2)
+ pane:Hide()
+ h = pane
+ end
+ if scroll then return true end
+
+ scroll = CreateFrame("ScrollFrame", "NE_TitlesScroll", h, "FauxScrollFrameTemplate")
+ scroll:SetPoint("TOPLEFT", h, "TOPLEFT", 6, -8)
+ scroll:SetPoint("BOTTOMRIGHT", h, "BOTTOMRIGHT", -10, 6)
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ FauxScrollFrame_OnVerticalScroll(self, offset, ROW_H, updateScroll)
+ end)
+ scroll:EnableMouseWheel(true)
+ scroll:SetScript("OnMouseWheel", function(self, delta)
+ local sb = _G[(self:GetName() or "") .. "ScrollBar"]
+ if not sb then return end
+ local mn, mx = sb:GetMinMaxValues()
+ local v = sb:GetValue() - delta * ROW_H
+ if v < mn then v = mn elseif v > mx then v = mx end
+ sb:SetValue(v)
+ end)
+ scroll:HookScript("OnSizeChanged", function() recomputeVisible(); updateScroll() end)
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, scroll, { x = 0 }) end
+
+ -- Row container: child of the PANE (not the scroll) so it survives the faux scroll hiding when the
+ -- list fits. Anchored over the scroll rect, raised above it for clicks.
+ content = CreateFrame("Frame", nil, h)
+ content:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
+ content:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
+ content:SetHeight(1)
+ content:SetFrameLevel(scroll:GetFrameLevel() + 5)
+
+ emptyLabel = h:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
+ emptyLabel:SetPoint("TOP", h, "TOP", 0, -40)
+ emptyLabel:SetWidth(180)
+ emptyLabel:SetText(L("NO_TITLES_TOOLTIP", "You haven't earned any titles."))
+ emptyLabel:Hide()
+
+ recomputeVisible()
+ return true
+end
+
+-- ---------------------------------------------------------------------------
+-- Show / hide. Mirrors the equipment manager's ShowEquipManager/HideEquipManager.
+-- ---------------------------------------------------------------------------
+function CP.ShowTitles()
+ if not build() then return end
+ if CP._sidebar and CP._sidebar.Hide then CP._sidebar:Hide() end
+ if CP.HideEquipManager then pcall(CP.HideEquipManager) end
+ pane:Show()
+ CP.RefreshTitles()
+ CP._activeSidebar = 2
+end
+
+function CP.HideTitles()
+ if pane then pane:Hide() end
+end
+
+-- ---------------------------------------------------------------------------
+-- Route sidebar index 2 → titles by chaining CP.SelectSidebar (set up by Sidebar.lua + the equipment
+-- pane). index 2 shows titles; any other index hides titles and defers to the previous handler.
+-- ---------------------------------------------------------------------------
+local _prevSelectSidebar = CP.SelectSidebar
+function CP.SelectSidebar(index)
+ if tonumber(index) == 2 then
+ CP._activeSidebar = 2
+ CP.ShowTitles()
+ if CP.SetSidebarTabSelected then pcall(CP.SetSidebarTabSelected, 2) end
+ return
+ end
+ CP.HideTitles()
+ if _prevSelectSidebar then return _prevSelectSidebar(index) end
+end
+
+-- ---------------------------------------------------------------------------
+-- Boot: keep the list live as titles are earned / the current title changes.
+-- ---------------------------------------------------------------------------
+local boot = CreateFrame("Frame")
+boot:RegisterEvent("PLAYER_LOGIN")
+boot:RegisterEvent("PLAYER_ENTERING_WORLD")
+boot:RegisterEvent("KNOWN_TITLES_UPDATE")
+boot:RegisterEvent("PLAYER_TITLE_UPDATE")
+boot:RegisterEvent("UNIT_NAME_UPDATE") -- title display change → tick follows the real current title
+boot:SetScript("OnEvent", function(_, event, unit)
+ if NE.modules and NE.modules.IsEnabled and not NE.modules.IsEnabled("character") then return end
+ if event == "UNIT_NAME_UPDATE" and unit and unit ~= "player" then return end
+ -- Keep the window header's "Name " synced whenever the title changes, even if the pane is shut.
+ if (event == "UNIT_NAME_UPDATE" or event == "PLAYER_TITLE_UPDATE") and CP.UpdateTitle then
+ pcall(CP.UpdateTitle)
+ end
+ if CP._activeSidebar == 2 and pane and pane:IsVisible() then CP.RefreshTitles() end
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/VISUAL_SPEC.md b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/VISUAL_SPEC.md
new file mode 100644
index 0000000..347d08c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/character/VISUAL_SPEC.md
@@ -0,0 +1,67 @@
+# Character Panel — Visual Spec (THE build target)
+
+**Mandate: match `/root/downport/DOWNPORT THIS/NewEra/CharacterPanel/` EXACTLY.** Port NewEra's
+actual geometry/decoration logic faithfully into our custom frame (`DragonUI_NewEra_Character`,
+Blizzard CharacterFrame hidden, slots+model reparented). When in doubt, read the NewEra source value
+and replicate it. The whole window ships as ONE deliverable.
+
+## Container layout (the key DF shift vs vanilla)
+NOT "spread slots to frame edges." It's a CONTAINER design: a FIXED-width left content area (model +
+tight slot columns + gold inner border) and a COLLAPSIBLE right stats sidebar.
+
+```
+┌──────────────────────────────────────────────────────────┐
+ (○ class icon) C H A R A C T E R [✕]
+├──────────────────────────────────────────────────────────┤
+│ ┌─ Inset (FIXED 338w) ────────────┐ │ ┌ InsetRight 209w ┐ │
+│ │ [Head] ⟲ ⟳ controls [Hands] │ │ │ (face)(T)(equip)│ │ sidebar tabs
+│ │ [Neck] ┌──────────────┐ [Waist] │ │ ├─────────────────┤ │
+│ │ [Shldr] │ Model 231×320│ [Legs] │ │ │ General ▾ │ │
+│ │ [Back] │ (race bg, │ [Feet] │ │ │ Attributes ▾ │ │
+│ │ [Chest] │ desaturated)│ [Ring1] │ │ │ Melee ▾ │ │ 7 scrollable
+│ │ [Shirt] │ │ [Ring2] │ │ │ Ranged ▾ │ │ stat sections
+│ │ [Tabrd] └──gold border─┘ [Trk1] │ │ │ Spell ▾ │ │
+│ │ [Wrist] [MH][OH][Rng] [Trk2] │ │ │ Defense ▾ │ │
+│ └──────────(gold inner border)─────┘ │ │ Resistances ▾ │ │
+├──────────────────────────────────────────────────────────┤
+│ [Character] [Pet] [Skills] [Honor] [Reputation] │ metal DF tabs
+└──────────────────────────────────────────────────────────┘
+ 338w collapsed → 548w when sidebar expanded
+```
+
+## Geometry (from NewEra source — replicate)
+| Region | Size | Anchor / offset |
+|---|---|---|
+| Frame | 338×424 collapsed / 548×424 expanded | center; width grows when sidebar shown |
+| Inset (left content) | **FIXED 338×364** | TOPLEFT(4,−60) of Frame |
+| InsetRight (sidebar host) | 209×364 | TOPLEFT(1,0) off Inset.TOPRIGHT; shown only when expanded |
+| Model viewport | 231×320 | (52,−66) within the paperdoll area; race 4-part bg, desaturated + dark overlay |
+| Model controls | 132×32, 5 btns (zoom±, rotate L/R, reset) | TOP(0,−4) of model; hover-reveal, alpha 0.5 |
+| Left slot column (8) | slots ~37px + 49×44 metal frame each | Head at Inset.TOPLEFT(4,−2), chain down: Head,Neck,Shoulder,Back,Chest,Shirt,Tabard,Wrist |
+| Right slot column (8) | slots + 50×44 metal frame each | Hands at Inset.TOPRIGHT(−4,−2), chain down: Hands,Waist,Legs,Feet,Finger0,Finger1,Trinket0,Trinket1 |
+| Bottom weapon row (3) | slots + 42×53 frame + gap fillers | MainHand at Frame.BOTTOMLEFT(84,24): MainHand,SecondaryHand,Ranged |
+| Inner border | 7×7 gold corners + 5px edges + full-width divider 27px above bottom | around the model/slot viewport (Char-Paperdoll-Parts art) |
+| Portrait | 62×62 circular | TOPLEFT(−5,7); CLASS icon (FDID 1662186), never the 3D face |
+| Title | metal strip, text centered | top band |
+| Stats pane (NE_CharacterStatsPane) | 197w, scrollable | InsetRight.TOPLEFT(3,−3); class-themed bg (197×355, ui-character-info--bg) |
+| Stat row | 187×15, alternating bounce bg | chained; sections: General,Attributes,Melee,Ranged,Spell,Defense,Resistances |
+| Sidebar tabs | 168×35 strip, 3 tabs 33×35 | above InsetRight: face / titles(disabled) / equipment |
+| Bottom tabs | metal atlas, 36h inactive / 42h active, 1px gap | below frame: Character,Pet,Skills,Honor,Reputation; active raised frame level |
+
+## Slot/model positioning rule
+NewEra keeps slots at vanilla CharacterFrame positions and DECORATES them. We reparent into our Inset,
+so REPLICATE the 3.3.5 vanilla `PaperDollFrame.xml` slot+model anchors (read
+`/root/wow_interface/Interface/FrameXML/PaperDollFrame.xml`) so they land exactly where NewEra shows
+them, then overlay NewEra's metal slot-frame decorations. The Inset is sized like the vanilla
+paperdoll area so vanilla anchors translate directly.
+
+## 3.3.5 adaptations (already decided)
+- Sidebar/secondary-tab scroll: NewEra uses WowScrollBox (absent on 3.3.5) → **named FauxScrollFrame**
+ + manual row pool. Bounded rows; non-virtualized is fine.
+- Portrait = class icon (no spec system on 3.3.5).
+- Equipment manager: 3.3.5 GLOBAL equip funcs via a `compat/C_EquipmentSet.lua` shim; ItemRack-model
+ fallback.
+- Gotchas: CreateMaskTexture returns nil (guard return), SetNormalTexture takes a path, raise child
+ frame levels, no SetShown, named FauxScrollFrame, pcall stat getters.
+
+Build to THIS. Faithful to NewEra is the acceptance bar.
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Assets.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Assets.lua
new file mode 100644
index 0000000..26237d8
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Assets.lua
@@ -0,0 +1,52 @@
+-- DragonUI_NewEra/modules/collections/Assets.lua — art path constants for the Collections window.
+--
+-- These BLP/TGA files were copied verbatim from the EZCollections addon (the community retail-
+-- Collections port) into Textures/Collections/ so this addon carries its own copy and does NOT
+-- depend on EZCollections being installed. Unlike the shared chrome art (Textures/Assets.lua, which
+-- routes fdid -> path through NE.tex for the nineslice coord tables), these are consumed by DIRECT
+-- SetTexture(path) + SetTexCoord in the journal panes, so a plain path table is all that's needed.
+--
+-- ListButtons.blp is a 256-wide sheet; its three 208x46 row states share left 0.0039..0.8203:
+-- ButtonBackground top 0.00390625 .. 0.18359375
+-- ButtonHighlight top 0.19140625 .. 0.37109375
+-- ButtonSelect top 0.37890625 .. 0.55859375
+-- MountJournal-BG is the right-hand model backdrop; its visible slice is left 0 .. 0.78515625.
+
+local NE = DragonUI_NewEra
+NE.collections = NE.collections or {}
+
+local P = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\Collections\\"
+
+NE.collections.tex = {
+ listButtons = P .. "ListButtons.blp",
+ favoriteIcon = P .. "FavoritesIcon.blp",
+ iconFrame = P .. "WhiteIconFrame.blp",
+ searchIcon = P .. "UI-Searchbox-Icon.blp",
+ mountPortrait = P .. "MountJournalPortrait.blp",
+ petPortrait = P .. "PetJournalPortrait.blp",
+ modelBg = P .. "MountJournal-BG.blp",
+ noMountsBg = P .. "MountJournal-NoMounts.blp",
+ emptyIcon = P .. "MountJournalEmptyIcon.blp",
+ factionIcons = P .. "MountJournalIcons.blp", -- Horde/Alliance corner badges
+ mountUpFavourites = P .. "MountUpFavourites.blp", -- Mounts tab's "Summon Random Favorite" icon (user-supplied art)
+}
+
+-- Sub-rects into ListButtons.blp (left/right/top/bottom), from the EZCollections template.
+NE.collections.listCoords = {
+ background = { 0.00390625, 0.8203125, 0.00390625, 0.18359375 },
+ highlight = { 0.00390625, 0.8203125, 0.19140625, 0.37109375 },
+ select = { 0.00390625, 0.8203125, 0.37890625, 0.55859375 },
+}
+
+-- MountJournalIcons.blp (128x64 DXT5) holds the two faction crests. Bounding boxes below were found
+-- by decoding the alpha channel per-block (4x4px blocks) and measuring each crest's actual extent —
+-- the crests are NOT the same width (~44px vs ~40px), so a naive even 50/50 split stretched both
+-- into the wrong aspect ratio (the "stretched banner" look). The alpha-only decode couldn't tell
+-- which SHAPE was which faction, and the left/right assignment guessed was backwards (confirmed
+-- in-game: Alliance was showing on Horde mounts) — swapped below, same two crops.
+-- Alliance: blocks x1-11, y0-11 -> px x4-48, y0-48
+-- Horde: blocks x14-23, y0-11 -> px x56-96, y0-48
+NE.collections.factionCoords = {
+ [0] = { 56/128, 96/128, 0, 48/64 }, -- Horde
+ [1] = { 4/128, 48/128, 0, 48/64 }, -- Alliance
+}
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Data.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Data.lua
new file mode 100644
index 0000000..598a68e
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Data.lua
@@ -0,0 +1,668 @@
+-- DragonUI_NewEra/modules/collections/Data.lua — mount & companion SOURCE-INFO tables.
+--
+-- The 3.3.5a Companions API (GetCompanionInfo) returns only creatureID/name/spellID/icon — it has
+-- NO description, source ("Vendor: … Zone: … Cost: …") or lore text. Retail's Mount/Pet Journal
+-- shows those from a client-side DB. These two tables are that DB: PORTED VERBATIM from the
+-- EZCollections addon's Data/Mounts.enUS.lua + Data/Pets.enUS.lua (the community-maintained
+-- source strings), re-homed onto the DragonUI_NewEra namespace so this addon carries its own copy
+-- and does NOT depend on EZCollections being installed/enabled at runtime.
+--
+-- ROW FORMAT (both tables), keyed by SPELL ID (the creatureSpellID GetCompanionInfo returns):
+-- [1] creatureDisplayID (unused — EZCollections' own Journal feeds this straight into
+-- ModelFrame:SetCreature for not-yet-learned rows, but SetCreature on
+-- THIS client only accepts a server-resolvable creature/unit reference,
+-- not a raw CreatureDisplayInfo.dbc id; tried and reverted)
+-- [2] type/petTypeIcon (unused)
+-- [3] flags (unused)
+-- [4] name (may be nil → fall back to the live API name)
+-- [5] description / lore (may be nil/"")
+-- [6] sourceType (unused)
+-- [7] sourceText (the coloured "Vendor: … Zone: … Cost: …" block; may be nil)
+--
+-- Consumers use NE.collections.GetInfo(kind, spellID) → name, lore, source.
+
+local NE = DragonUI_NewEra
+NE.collections = NE.collections or {}
+
+
+NE.collections.MountData =
+{
+ [458] = { 284, 2, 0, "Brown Horse", "A favorite among Stormwind's guards thanks to its patience and stamina.", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [459] = { 4268, 2, 64, "Gray Wolf", "This breed of wolf prefers hunting in the fog, relying on its smoky hide to camouflage it from unsuspecting prey.", 2, "|cFFFFD200Vendor: |rOgunaro Wolfrunner|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [468] = { 305, 2, 64, "White Stallion", "The powerful and unyielding white stallion features heavily in the myths of ancient human tribes.", 2, "|cFFFFD200Legacy|r" },
+ [470] = { 308, 2, 0, "Black Stallion", "Rumored to be favored by SI:7 for night missions due to its dark coat.", 2, "|cFFFFD200Vendor: |rUnger Statforth|n|cFFFFD200Zone: |rWetlands|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [471] = { 306, 0, },
+ [472] = { 307, 2, 0, "Pinto", "Its calm temperament makes it ideal to train young children in horseback riding.", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [578] = { 356, 2, 64, "Black Wolf", "Can howl loudly enough to be heard for miles.", 2, "|cFFFFD200Vendor: |rOgunaro Wolfrunner|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [579] = { 4270, 2, 64, "Red Wolf", "Almost none remain in the whole of Azeroth; only the most experienced will have seen one.", 2, "|cFFFFD200Legacy|r" },
+ [580] = { 358, 2, 0, "Timber Wolf", "Can howl loudly enough to be heard for miles.", 2, "|cFFFFD200Vendor: |rOgunaro Wolfrunner|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [581] = { 359, 2, 64, "Winter Wolf", "Ancient furbolg legends claim that wolves born with a white coat are blessed by Azeroth's two moons.", 2, "|cFFFFD200Legacy|r" },
+ [3363] = { 16597, 1, },
+ [5784] = { 304, 2, 0, "Felsteed", "Corrupted by demonic power and once forced to serve the Shadow Council.", 1, "|cFFFFD200Class: |rWarlock" },
+ [6648] = { 4269, 2, 0, "Chestnut Mare", "Sturdy and steady; \"the horse that built Stormwind.\"", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [6653] = { 4271, 2, 0, "Dire Wolf", "Once thought to be extinct except in the far north. Highly sought by those preparing for winter.", 2, "|cFFFFD200Vendor:|r Ogunaro Wolfrunner|n|cFFFFD200Location:|r Orgrimmar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [6654] = { 4272, 2, 0, "Brown Wolf", "Can howl loudly enough to be heard for miles.", 2, "|cFFFFD200Vendor: |rOgunaro Wolfrunner|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [6777] = { 4710, 2, 0, "Gray Ram", "Male rams will often smash their thick skulls against each other for hours to impress a female. In that way, they're very similar to dwarves.", 2, "|cFFFFD200Vendor: |rVeron Amberstill|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [6896] = { 4780, 2, 64, "Black Ram", "I've not seen this breed in in ages, but when they're ram tough they stick around. -Veron Amberstill", 2, "|cFFFFD200Legacy|r" },
+ [6897] = { 4778, 2, },
+ [6898] = { 4777, 2, 0, "White Ram", "Male rams will often smash their thick skulls against each other for hours to impress a female. In that way, they're very similar to dwarves.", 2, "|cFFFFD200Vendor: |rVeron Amberstill|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [6899] = { 4779, 2, 0, "Brown Ram", "Male rams will often smash their thick skulls against each other for hours to impress a female. In that way, they're very similar to dwarves.", 2, "|cFFFFD200Vendor: |rVeron Amberstill|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [8394] = { 6074, 2, 0, "Striped Frostsaber", "This hearty mount's ferocious appetite earned it the nickname \"Dragon Belly\" in the night elf language.", 2, "|cFFFFD200Vendor: |rLelanai|n|cFFFFD200Zone: |rDarnassus|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [8395] = { 6075, 2, 0, "Emerald Raptor", "Bred from the fiercest stock anywhere and guaranteed not to bite (much).", 2, "|cFFFFD200Vendor: |rZjolnir|n|cFFFFD200Zone: |rDurotar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [8396] = { 6076, 2, },
+ [8980] = { 6486, 0, 64, "Skeletal Horse", "Like most undead creatures, it did not choose its fate.", 2, "|cFFFFD200Legacy|r" },
+ [10787] = { 7322, 2, },
+ [10788] = { 7684, 0, },
+ [10789] = { 7687, 2, 0, "Spotted Frostsaber", "This hearty mount's ferocious appetite earned it the nickname \"Dragon Belly\" in the night elf language.", 2, "|cFFFFD200Vendor: |rLelanai|n|cFFFFD200Zone: |rDarnassus|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n" },
+ [10790] = { 7686, 0, 64, "Tiger", "", -1, "|cFFFFD200Legacy|r" },
+ [10792] = { 7689, 0, },
+ [10793] = { 7690, 2, 0, "Striped Nightsaber", "Considered one of Azeroth's perfect predators, these agile beasts can sprint through dense forests without making a sound.", 2, "|cFFFFD200Vendor: |rLelanai|n|cFFFFD200Zone: |rDarnassus|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [10795] = { 7706, 2, 64, "Ivory Raptor", "Bred from the fiercest stock anywhere and guaranteed not to bite (much).", 2, "|cFFFFD200Legacy|r" },
+ [10796] = { 7707, 2, 0, "Turquoise Raptor", "Bred from the fiercest stock anywhere and guaranteed not to bite (much).", 2, "|cFFFFD200Vendor: |rZjolnir|n|cFFFFD200Zone: |rDurotar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [10798] = { 7703, 2, },
+ [10799] = { 7708, 2, 0, "Violet Raptor", "Bred from the fiercest stock anywhere and guaranteed not to bite (much).", 2, "|cFFFFD200Vendor: |rZjolnir|n|cFFFFD200Zone: |rDurotar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [10800] = { 7709, 0, },
+ [10801] = { 7710, 0, },
+ [10802] = { 7711, 2, },
+ [10803] = { 7712, 2, },
+ [10804] = { 7713, 2, },
+ [10873] = { 7739, 2, 0, "Red Mechanostrider", "\"The terradynamic exo-plotters are conjoined by hydraulic imaging to the equilibrium enhancers...\" -Binjy Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [10969] = { 7749, 2, 0, "Blue Mechanostrider", "\"The terradynamic exo-plotters are conjoined by hydraulic imaging to the equilibrium enhancers...\" -Binjy Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [13819] = { 9158, 2, 4, "Warhorse", "Trained to stand firm in righteous combat.", 1, "|cFFFFD200Paladin|r|n" },
+ [15779] = { 10179, 2, 64, "White Mechanostrider Mod B", "Forced to recall the model after numerous complaints of an \"uncontrollable throttle\", Gnomish engineers now refer to a proto-type blunder as a \"model B\".", 2, "|cFFFFD200Legacy|r" },
+ [15780] = { 10178, 2, 64, "Green Mechanostrider", "\"The terradynamic exo-plotters are conjoined by hydraulic imaging to the equilibrium enhancers...\" -Binjy Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [15781] = { 10180, 2, },
+ [16055] = { 7322, 2, 64, "Black Nightsaber", "Considered one of Azeroth's perfect predators, these agile beasts can sprint through dense forests without making a sound.", 2, "|cFFFFD200Legacy|r" },
+ [16056] = { 10322, 2, 64, "Ancient Frostsaber", "The lack of spots or stripes marks this beast as a descendant of the most ancient frostsaber line.", 2, "|cFFFFD200Legacy|r" },
+ [16058] = { 10336, 2, },
+ [16059] = { 10337, 2, },
+ [16060] = { 10338, 2, },
+ [16080] = { 4270, 2, 64, "Red Wolf", "Almost none remain in the whole of Azeroth; only the most experienced will have seen one.", 2, "|cFFFFD200Legacy|r" },
+ [16081] = { 359, 2, 64, "Arctic Wolf", "Once thought to be nearly extinct, this wolf can still occasionally be seen in the company of a few Horde veterans", 0, "|cFFFFD200Legacy|r" },
+ [16082] = { 306, 2, 64, "Palomino", "Human nobles have long favored this majestic breed for its beautiful golden coat and flowing white mane.", 2, "|cFFFFD200Legacy|r" },
+ [16083] = { 305, 2, 64, "White Stallion", "The powerful and unyielding white stallion features heavily in the myths of ancient human tribes.", 2, "|cFFFFD200Legacy|r" },
+ [16084] = { 7704, 2, 64, "Mottled Red Raptor", "Bred from the fiercest stock anywhere and guaranteed not to bite (much).", 2, "|cFFFFD200Legacy|r" },
+ [17229] = { 11021, 2, 4, "Winterspring Frostsaber", "Taming this savage breed requires patience, strength, and a large supply of fake mice and twine.", 1, "|cFFFFD200Quest: |rThey Grow Up So Fast|n|cFFFFD200Zone: |rWinterspring" },
+ [17450] = { 7706, 2, 64, "Ivory Raptor", "Bred from the fiercest stock anywhere and guaranteed not to bite (much).", 2, "|cFFFFD200Legacy|r" },
+ [17453] = { 11147, 2, 0, "Green Mechanostrider", "\"The terradynamic exo-plotters are conjoined by hydraulic imaging to the equilibrium enhancers...\" -Binjy Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [17454] = { 10180, 2, 0, "Unpainted Mechanostrider", "\"The terradynamic exo-plotters are conjoined by hydraulic imaging to the equilibrium enhancers...\" -Binjy Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [17455] = { 11148, 2, },
+ [17456] = { 11149, 2, },
+ [17458] = { 10178, 2, },
+ [17459] = { 11150, 2, 64, "Icy Blue Mechanostrider Mod A", "This historic piece of gnomish engineering was forced into retirement due to its extremely \"touchy\" gyroscometer.", 2, "|cFFFFD200Legacy|r" },
+ [17460] = { 4778, 2, 64, "Frost Ram", "I've not seen this breed in in ages, but when they're ram tough they stick around.\"\" -Veron Amberstill\"", 2, "|cFFFFD200Legacy|r" },
+ [17461] = { 4780, 2, 64, "Black Ram", "I've not seen this breed in in ages, but when they're ram tough they stick around. -Veron Amberstill", 2, "|cFFFFD200Legacy|r" },
+ [17462] = { 11153, 2, 0, "Red Skeletal Horse", "The Forsaken believe that without a purpose, even beasts of burden suffer in undeath.", 2, "|cFFFFD200Vendor: |rZachariah Post|n|cFFFFD200Zone: |rTirisfal Glades|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [17463] = { 11154, 2, 0, "Blue Skeletal Horse", "The Forsaken believe that without a purpose, even beasts of burden suffer in undeath.", 2, "|cFFFFD200Vendor: |rZachariah Post|n|cFFFFD200Zone: |rTirisfal Glades|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [17464] = { 11155, 2, 0, "Brown Skeletal Horse", "The Forsaken believe that without a purpose, even beasts of burden suffer in undeath.", 2, "|cFFFFD200Vendor: |rZachariah Post|n|cFFFFD200Zone: |rTirisfal Glades|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [17465] = { 11156, 2, 0, "Green Skeletal Warhorse", "When fallen heroes are raised into undeath, so too are their horses.", 2, "|cFFFFD200Vendor: |rZachariah Post|n|cFFFFD200Zone: |rTirisfal Glades|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [17481] = { 30542, 2, 0, "Rivendare's Deathcharger", "When Baron Rivendare became a champion of the Scourge, he condemned his favorite horse to join him in undeath.", 0, "|cFFFFD200Drop:|r Baron Rivendare|n|cFFFFD200Location:|r Stratholme" },
+ [18363] = { 11689, 2, 64, "Riding Kodo", "\"The kodo is the backbone of our people. Wherever the Horde goes - in trade, settlement, or war - there you will find the mighty kodo.\" - Harb Clawhoof", 2, "|cFFFFD200Legacy|r" },
+ [18989] = { 12149, 2, 0, "Gray Kodo", "\"The kodo is the backbone of our people. Wherever the Horde goes - in trade, settlement, or war - there you will find the mighty kodo.\" - Harb Clawhoof", 2, "|cFFFFD200Vendor: |rHarb Clawhoof|n|cFFFFD200Zone: |rMulgore|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [18990] = { 11689, 2, 0, "Brown Kodo", "\"The kodo is the backbone of our people. Wherever the Horde goes - in trade, settlement, or war - there you will find the mighty kodo.\" - Harb Clawhoof", 2, "|cFFFFD200Vendor: |rHarb Clawhoof|n|cFFFFD200Zone: |rMulgore|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [18991] = { 12151, 2, 64, "Green Kodo", "An alpha member of the herd, this kodo is a prime example of the mighty creatures which thunder through Desolace.", 2, "|cFFFFD200Legacy|r" },
+ [18992] = { 12148, 2, 64, "Teal Kodo", "\"The kodo is the backbone of our people. Wherever the Horde goes - in trade, settlement, or war - there you will find the mighty kodo.\" - Harb Clawhoof", 2, "|cFFFFD200Legacy|r" },
+ [22717] = { 14332, 2, 0, "Black War Steed", "Trained to withstand brutal hand-to-hand combat in the fiercest battlefields.", 2, "|cFFFFD200Vendor: |rLieutenant Karter|n|cFFFFD200Zone: |rStormwind City|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Alliance:0|t" },
+ [22718] = { 14333, 2, 0, "Black War Kodo", "Bred for their aggressive, unpredictable demeanor by the Kor'kron stablemasters, the Black War Kodos' trumpeting call signifies that blood will soon be shed.", 2, "|cFFFFD200Vendor: |rRaider Bork|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Horde:0|t" },
+ [22719] = { 14334, 2, 0, "Black Battlestrider", "A properly accelerated gnome in battle is a thing of terrible beauty. - High Tinker Mekkatorque", 2, "|cFFFFD200Vendor: |rLieutenant Karter|n|cFFFFD200Zone: |rStormwind City|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Alliance:0|t" },
+ [22720] = { 14335, 2, 0, "Black War Ram", "The dwarves value their ram companions for their fearlessness charging into battle.", 2, "|cFFFFD200Vendor: |rLieutenant Karter|n|cFFFFD200Zone: |rStormwind City|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Alliance:0|t" },
+ [22721] = { 14330, 2, 0, "Black War Raptor", "Bred from the same vicious line of raptors which sired Ohgan and brought victory to the Gurubashi Empire.", 2, "|cFFFFD200Vendor: |rRaider Bork|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Horde:0|t" },
+ [22722] = { 14331, 2, 0, "Red Skeletal Warhorse", "When fallen heroes are raised into undeath, so too are their horses.", 2, "|cFFFFD200Vendor: |rRaider Bork|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Horde:0|t" },
+ [22723] = { 14336, 2, 0, "Black War Tiger", "Like their distant nightsaber cousins, tigers are renowned for their razor-sharp claws, incredible agility, and love of being scratched behind the ears.", 2, "|cFFFFD200Vendor: |rLieutenant Karter|n|cFFFFD200Zone: |rStormwind City|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Alliance:0|t" },
+ [22724] = { 14329, 2, 0, "Black War Wolf", "Prized by Orgrimmar guards for their keen sense of smell.", 2, "|cFFFFD200Vendor: |rRaider Bork|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Horde:0|t" },
+ [23161] = { 14505, 2, 0, "Dreadsteed", "WARNING: It craves only fel energy. Not apples. It hates the seeds.", 1, "|cFFFFD200Class: |rWarlock" },
+ [23214] = { 14565, 2, 4, "Charger", "Redeemed by the Light. Devoted to your cause.", 1, "|cFFFFD200Paladin|r" },
+ [23219] = { 14555, 2, 0, "Swift Mistsaber", "Some historians claim that this breed's misty grey coat is the result of ancient Highborne experiments conducted on nightsabers.", 2, "|cFFFFD200Vendor: |rLelanai|n|cFFFFD200Zone: |rDarnassus|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23220] = { 14557, 2, },
+ [23221] = { 14556, 2, 0, "Swift Frostsaber", "Night elves raise frostsaber cubs from infancy, forging lifelong bonds of trust between rider and beast.", 2, "|cFFFFD200Vendor:|r Lelanai|n|cFFFFD200Location:|r Darnassus|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23222] = { 14551, 2, 0, "Swift Yellow Mechanostrider", "We have made a lot of improvements, but they are still based on the Mekkatorque designs. -Milli Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23223] = { 14552, 2, 0, "Swift White Mechanostrider", "We have made a lot of improvements, but they are still based on the Mekkatorque designs. -Milli Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23225] = { 14553, 2, 0, "Swift Green Mechanostrider", "We have made a lot of improvements, but they are still based on the Mekkatorque designs. -Milli Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23227] = { 14559, 2, 0, "Swift Palomino", "Prized by horse breeders for their discipline and steadfastness.", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23228] = { 14560, 2, 0, "Swift White Steed", "A veteran of some of the toughest battles fought by the Argent Crusade.", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23229] = { 14561, 2, 0, "Swift Brown Steed", "A veteran of some of the toughest battles fought by the Argent Crusade.", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23238] = { 14546, 2, 0, "Swift Brown Ram", "The Barak Tor'ol ram's thick bony skull and massive horns make it the ideal mount for charging through groups of armored enemies.", 2, "|cFFFFD200Vendor: |rVeron Amberstill|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23239] = { 14548, 2, 0, "Swift Gray Ram", "The Barak Tor'ol ram's thick bony skull and massive horns make it the ideal mount for charging through groups of armored enemies.", 2, "|cFFFFD200Vendor: |rVeron Amberstill|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23240] = { 14547, 2, 0, "Swift White Ram", "The Barak Tor'ol ram's thick bony skull and massive horns make it the ideal mount for charging through groups of armored enemies.", 2, "|cFFFFD200Vendor: |rVeron Amberstill|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n" },
+ [23241] = { 14545, 2, 0, "Swift Blue Raptor", "Longtime allies of the trolls, these jungle hunters form a special bond with their masters. They become steed, guardian, and friend.", 2, "|cFFFFD200Vendor: |rZjolnir|n|cFFFFD200Zone: |rDurotar|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23242] = { 14543, 2, 0, "Swift Olive Raptor", "Longtime allies of the trolls, these jungle hunters form a special bond with their masters. They become mount, guardian, and friend.", 2, "|cFFFFD200Vendor: |rZjolnir|n|cFFFFD200Zone: |rDurotar|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23243] = { 14544, 2, 0, "Swift Orange Raptor", "Longtime allies of the trolls, these jungle hunters form a special bond with their masters. They become mount, guardian, and friend.", 2, "|cFFFFD200Vendor: |rZjolnir|n|cFFFFD200Zone: |rDurotar|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23246] = { 14558, 2, 0, "Purple Skeletal Warhorse", "When fallen heroes are raised into undeath, so too are their horses.", 2, "|cFFFFD200Vendor: |rZachariah Post|n|cFFFFD200Zone: |rTirisfal Glades|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23247] = { 14542, 2, 0, "Great White Kodo", "A rare albino of the species first spotted in Desolace. The centaur claim that a white kodo is a harbinger of doom.", 2, "|cFFFFD200Vendor: |rHarb Clawhoof|n|cFFFFD200Zone: |rMulgore|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23248] = { 14550, 2, 0, "Great Gray Kodo", "An alpha member of the herd, this kodo is a prime example of the mighty creatures which thunder through Desolace.", 2, "|cFFFFD200Vendor: |rHarb Clawhoof|n|cFFFFD200Zone: |rMulgore|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23249] = { 14549, 2, 0, "Great Brown Kodo", "An alpha member of the herd, this kodo is a prime example of the mighty creatures which thunder through the Northern Barrens.", 2, "|cFFFFD200Vendor:|r Harb Clawhoof|n|cFFFFD200Location:|r Mulgore|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23250] = { 14540, 2, 0, "Swift Brown Wolf", "The wolves of the Horde are befriended, not domesticated.", 2, "|cFFFFD200Vendor: |rOgunaro Wolfrunner|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23251] = { 14539, 2, 0, "Swift Timber Wolf", "The wolves of the Horde are befriended, not domesticated.", 2, "|cFFFFD200Vendor: |rOgunaro Wolfrunner|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23252] = { 14541, 2, 0, "Swift Gray Wolf", "The wolves of the Horde are befriended, not domesticated.", 2, "|cFFFFD200Vendor: |rOgunaro Wolfrunner|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23338] = { 14602, 2, 0, "Swift Stormsaber", "Rumor has it that this breed was named after the renowned archdruid Malfurion Stormrage.", 2, "|cFFFFD200Vendor: |rLelanai|n|cFFFFD200Zone: |rDarnassus|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [23509] = { 14744, 2, 4, "Frostwolf Howler", "Raised in the Alterac Mountains by the Frostwolf Clan.", 2, "|cFFFFD200Vendor: |rJekyll Flandring|n|cFFFFD200Zone: |rAlterac Mountains|n|cFFFFD200Cost: |r50000|TINTERFACE\\PVPFRAME\\PVP-Currency-Horde:0|t|n|n|cFFFFD200Vendor: |rGrunnda Wolfheart|n|cFFFFD200Zone: |rAlterac Valley|n|cFFFFD200Cost: |r50000|TINTERFACE\\PVPFRAME\\PVP-Currency-Horde:0|t|n|n" },
+ [23510] = { 14745, 2, 4, "Stormpike Battle Charger", "The Stormpike clan's mount trainers have a tradition of etching their names onto the left horn of every battle ram they raise.", 2, "|cFFFFD200Vendor: |rThanthaldis Snowgleam|n|cFFFFD200Zone: |rAlterac Mountains|n|cFFFFD200Cost: |r50000|TINTERFACE\\PVPFRAME\\PVP-Currency-Alliance:0|t|n|n|cFFFFD200Vendor: |rGaelden Hammersmith|n|cFFFFD200Zone: |rAlterac Valley|n|cFFFFD200Cost: |r50000|TINTERFACE\\PVPFRAME\\PVP-Currency-Alliance:0|t|n|n" },
+ [24242] = { 15090, 2, 0, "Swift Razzashi Raptor", "The only known Razzashi Raptors were said to have been in the custody of Bloodlord Mandokir in Zul'Gurub. This species of raptor has not been seen in many years.", 0, "|cFFFFD200Drop: |rBloodlord Mandokir|n|cFFFFD200Location: |rZul'Gurub" },
+ [24252] = { 15104, 2, 0, "Swift Zulian Tiger", "The last known Zulian tigers were kept by High Priest Thekal, deep within Zul'Gurub. They are said to have been hunted into extinction.", 0, "|cFFFFD200Drop: |rHigh Priest Thekal|n|cFFFFD200Location: |rZul'Gurub" },
+ [24576] = { 18768, 0, },
+ [25675] = { 15524, 0, },
+ [25858] = { 15665, 0, },
+ [25859] = { 15665, 2, },
+ [25863] = { 15711, 2, 64, "Black Qiraji Battle Tank", "Although many varieties of Qiraji Battle Tank can still be found in the ruins of Ahn'Qiraj today, the darkest of the species were only seen on the day the Scarab gong was rung.", 0, "|cFFFFD200Legacy|r" },
+ [25953] = { 15666, 2, 0, "Blue Qiraji Battle Tank", "This (relatively) domesticated silithid is at home only in the desert sands of Ahn'Qiraj.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Location: |rTemple of Ahn'Qiraj" },
+ [26054] = { 15716, 2, 0, "Red Qiraji Battle Tank", "This (relatively) domesticated silithid is at home only in the desert sands of Ahn'Qiraj.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Location: |rTemple of Ahn'Qiraj" },
+ [26055] = { 15714, 2, 0, "Yellow Qiraji Battle Tank", "This (relatively) domesticated silithid is at home only in the desert sands of Ahn'Qiraj.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Location: |rTemple of Ahn'Qiraj" },
+ [26056] = { 15715, 2, 0, "Green Qiraji Battle Tank", "This (relatively) domesticated silithid is at home only in the desert sands of Ahn'Qiraj.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Location: |rTemple of Ahn'Qiraj" },
+ [26332] = { 15778, 2, },
+ [26655] = { 15711, 2, 64, "Black Qiraji Battle Tank", "Although many varieties of Qiraji Battle Tank can still be found in the ruins of Ahn'Qiraj today, the darkest of the species were only seen on the day the Scarab gong was rung.", 0, "|cFFFFD200Legacy|r" },
+ [26656] = { 15711, 2, 0, "Black Qiraji Battle Tank", "Although many varieties of Qiraji Battle Tank can still be found in the ruins of Ahn'Qiraj today, the darkest of the species were only seen on the day the Scarab gong was rung.", 1, "|cFFFFD200Quest: |rBang a Gong!|n|cFFFFD200Zone: |rSilithus|n|cFFFFD200Event: |rWar of the Shifting Sands" },
+ [28828] = { 16597, 0, 192, "Nether Drake", "The moods of these tempestuous mounts are known to shift as unpredictably and as violently as their physical forms.", 2, "|cFFFFD200Vendor: |rDrake Dealer Hurlunk|n|cFFFFD200Zone: |rShadowmoon Valley, Outland|n|cFFFFD200Cost: |r200|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [29059] = { 11195, 2, },
+ [30174] = { 17266, 2, 2192, "Riding Turtle", "Slow and steady might not always win you the race but it'll get you there... eventually.", 8, "|cFFFFD200Trading Card Game:|r Saltwater Snapjaw" },
+ [30829] = { 17643, 0, },
+ [30837] = { 14565, 0, },
+ [31700] = { 32568, 0, },
+ [31973] = { 17643, 0, },
+ [32235] = { 18360, 1, 132, "Golden Gryphon", "They love lions in Stormwind, and the gryphon is half lion. Of course they might not like which half. -Gryphon Master Talonaxe", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32239] = { 18357, 1, 132, "Ebon Gryphon", "They love lions in Stormwind, and the gryphon is half lion. Of course they might not like which half. -Gryphon Master Talonaxe", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32240] = { 18359, 1, 132, "Snowy Gryphon", "They love lions in Stormwind, and the gryphon is half lion. Of course they might not like which half. -Gryphon Master Talonaxe", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32242] = { 18406, 1, 132, "Swift Blue Gryphon", "The Horde had dragons, we had the gryphons. We won that war. - Ilsa Blusterbrew", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32243] = { 18363, 1, 132, "Tawny Wind Rider", "After being freed from vicious harpies by Cairne and Thrall, many of these creatures became valuable companions of the Horde.", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32244] = { 18364, 1, 132, "Blue Wind Rider", "After being freed from vicious harpies by Cairne and Thrall, many of these creatures became valuable companions of the Horde.", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32245] = { 18365, 1, 132, "Green Wind Rider", "After being freed from vicious harpies by Cairne and Thrall, many of these creatures became valuable companions of the Horde.", 0, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32246] = { 18377, 1, 132, "Swift Red Wind Rider", "I still don't get why they fly faster when you put armor on them. -Drakma", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32289] = { 18376, 1, 132, "Swift Red Gryphon", "The Horde had dragons, we had the gryphons. We won that war. - Ilsa Blusterbrew", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32290] = { 18375, 1, 132, "Swift Green Gryphon", "The Horde had dragons, we had the gryphons. We won that war. - Ilsa Blusterbrew", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32292] = { 18362, 1, 132, "Swift Purple Gryphon", "The Horde had dragons, we had the gryphons. We won that war. - Ilsa Blusterbrew", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32295] = { 18378, 1, 132, "Swift Green Wind Rider", "I still don't get why they fly faster when you put armor on them. -Drakma", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32296] = { 18380, 1, 132, "Swift Yellow Wind Rider", "I still don't get why they fly faster when you put armor on them. -Drakma", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32297] = { 18379, 1, 132, "Swift Purple Wind Rider", "I still don't get why they fly faster when you put armor on them. -Drakma", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [32345] = { 18545, 1, },
+ [32420] = { 18474, 0, },
+ [33630] = { 7749, 2, 64, "Blue Mechanostrider", "\"The terradynamic exo-plotters are conjoined by hydraulic imaging to the equilibrium enhancers...\" -Binjy Featherwhistle", 2, "|cFFFFD200Vendor: |rMilli Featherwhistle|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [33631] = { 7749, 2, },
+ [33660] = { 19281, 2, 0, "Swift Pink Hawkstrider", "\"Their plumage is more beautiful than any other. Subtlety is for the birds! I mean... other birds.\" -Viera Sunwhisper", 2, "|cFFFFD200Vendor: |rWinaestra|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [34068] = { 2188, 0, },
+ [34406] = { 19658, 2, 0, "Brown Elekk", "Patient and strong, the Elekk exemplify the Draenei attitude towards the hardships of life.", 2, "|cFFFFD200Vendor: |rTorallius the Pack Handler|n|cFFFFD200Zone: |rAzuremyst Isle|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [34407] = { 19659, 2, },
+ [34767] = { 20030, 2, 4, "Thalassian Charger", "Basking in the power of M'uru has strengthened its will.", 1, "|cFFFFD200Paladin|r" },
+ [34769] = { 20029, 2, 4, "Thalassian Warhorse", "A loyal companion to any who would protect Silvermoon.", 1, "|cFFFFD200Paladin|r" },
+ [34790] = { 20149, 2, 0, "Dark War Talbuk", "Despite their docile nature, talbuks have been known to gore seasoned warriors to death when threatened.", 2, "|cFFFFD200Vendor: |rAldraan (Alliance)|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\INV_MISC_RUNE_08.BLP:0|t20|TINTERFACE\\ICONS\\INV_MISC_RUNE_09.BLP:0|t|n|n|cFFFFD200Vendor: |rCoreiel (Horde)|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\INV_MISC_RUNE_08.BLP:0|t20|TINTERFACE\\ICONS\\INV_MISC_RUNE_09.BLP:0|t" },
+ [34795] = { 19280, 2, 0, "Red Hawkstrider", "Some of our brethren prefer horses, but the hawkstrider will always be the true symbol of Silvermoon. -Winaestra", 2, "|cFFFFD200Vendor: |rWinaestra|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [34896] = { 20072, 2, 0, "Cobalt War Talbuk", "Despite their docile nature, talbuks have been known to gore seasoned warriors to death when threatened.", 2, "|cFFFFD200Vendor: |rProvisioner Nasela|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Mag'har - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTrader Narasu|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Kurenai - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [34897] = { 20151, 2, 0, "White War Talbuk", "Despite their docile nature, talbuks have been known to gore seasoned warriors to death when threatened.", 2, "|cFFFFD200Vendor: |rProvisioner Nasela|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Mag'har - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTrader Narasu|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Kurenai - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [34898] = { 20152, 2, 0, "Silver War Talbuk", "Despite their docile nature, talbuks have been known to gore seasoned warriors to death when threatened.", 2, "|cFFFFD200Vendor: |rProvisioner Nasela|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Mag'har - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTrader Narasu|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Kurenai - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [34899] = { 20150, 2, 0, "Tan War Talbuk", "Despite their docile nature, talbuks have been known to gore seasoned warriors to death when threatened.", 2, "|cFFFFD200Vendor: |rProvisioner Nasela|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Mag'har - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTrader Narasu|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Kurenai - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35018] = { 20217, 2, 0, "Purple Hawkstrider", "Some of our brethren prefer horses, but the hawkstrider will always be the true symbol of Silvermoon. -Winaestra", 2, "|cFFFFD200Vendor: |rWinaestra|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35020] = { 20220, 2, 0, "Blue Hawkstrider", "Some of our brethren prefer horses, but the hawkstrider will always be the true symbol of Silvermoon. -Winaestra", 2, "|cFFFFD200Vendor: |rWinaestra|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35022] = { 20222, 2, 0, "Black Hawkstrider", "Some of our brethren prefer horses, but the hawkstrider will always be the true symbol of Silvermoon. -Winaestra", 2, "|cFFFFD200Vendor: |rWinaestra|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35025] = { 20224, 2, 0, "Swift Green Hawkstrider", "Their plumage is more beautiful than any other. Subtlety is for the birds! I mean... other birds. -Viera Sunwhisper", 2, "|cFFFFD200Vendor: |rWinaestra|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35027] = { 20223, 2, 0, "Swift Purple Hawkstrider", "Their plumage is more beautiful than any other. Subtlety is for the birds! I mean... other birds. -Viera Sunwhisper", 2, "|cFFFFD200Vendor: |rWinaestra|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35028] = { 20225, 2, 0, "Swift Warstrider", "You watch what happens when their master is attacked and you will see those birds in a whole new light. - Raider Bork", 2, "|cFFFFD200Vendor: |rRaider Bork|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Horde:0|t" },
+ [35710] = { 20846, 2, 0, "Gray Elekk", "Patient and strong, the Elekk exemplify the Draenei attitude towards the hardships of life.", 2, "|cFFFFD200Vendor: |rTorallius the Pack Handler|n|cFFFFD200Zone: |rAzuremyst Isle|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35711] = { 20847, 2, 0, "Purple Elekk", "Patient and strong, the Elekk exemplify the Draenei attitude towards the hardships of life.", 2, "|cFFFFD200Vendor: |rTorallius the Pack Handler|n|cFFFFD200Zone: |rAzuremyst Isle|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35712] = { 20849, 2, 0, "Great Green Elekk", "An alpha member of the herd, this elekk is a prime example of the mighty creatures which thunder through Northern Nagrand.", 2, "|cFFFFD200Vendor: |rTorallius the Pack Handler|n|cFFFFD200Zone: |rAzuremyst Isle|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35713] = { 20848, 2, 0, "Great Blue Elekk", "An alpha member of the herd, this elekk is a prime example of the mighty creatures which thunder through Northern Nagrand.", 2, "|cFFFFD200Vendor: |rTorallius the Pack Handler|n|cFFFFD200Zone: |rAzuremyst Isle|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35714] = { 20850, 2, 0, "Great Purple Elekk", "An alpha member of the herd, this elekk is a prime example of the mighty creatures which thunder through Northern Nagrand.", 2, "|cFFFFD200Vendor: |rTorallius the Pack Handler|n|cFFFFD200Zone: |rAzuremyst Isle|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [36702] = { 21354, 2, 0, "Fiery Warhorse", "Was one of the most vicious steeds stabled in Karazhan.", 0, "|cFFFFD200Drop: |rAttumen the Huntsman|n|cFFFFD200Location:|r Karazhan" },
+ [37015] = { 21510, 1, 192, "Swift Nether Drake", "The most aggressive nether drakes were awarded those heroes who showed significant prowess and commitment in the gladiatorial arena.", 5, "|cFFFFD200Legacy|r" },
+ [39315] = { 22510, 2, 0, "Cobalt Riding Talbuk", "The orcs once hunted these graceful herd animals as part of the om'riggor rite of passage ceremony.", 2, "|cFFFFD200Vendor: |rProvisioner Nasela|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Mag'har - Exalted|n|cFFFFD200Cost: |r70|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTrader Narasu|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Kurenai - Exalted|n|cFFFFD200Cost: |r70|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39316] = { 22511, 2, 0, "Dark Riding Talbuk", "The orcs once hunted these graceful herd animals as part of the om'riggor rite of passage ceremony.", 2, "|cFFFFD200Vendor: |rAldraan (Alliance)|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Cost: |r70|TINTERFACE\\ICONS\\INV_MISC_RUNE_08.BLP:0|t15|TINTERFACE\\ICONS\\INV_MISC_RUNE_09.BLP:0|t|n|n|cFFFFD200Vendor: |rCoreiel (Horde)|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Cost: |r70|TINTERFACE\\ICONS\\INV_MISC_RUNE_08.BLP:0|t15|TINTERFACE\\ICONS\\INV_MISC_RUNE_09.BLP:0|t" },
+ [39317] = { 22512, 2, 0, "Silver Riding Talbuk", "The orcs once hunted these graceful herd animals as part of the om'riggor rite of passage ceremony.", 2, "|cFFFFD200Vendor: |rProvisioner Nasela|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Mag'har - Exalted|n|cFFFFD200Cost: |r70|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTrader Narasu|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Kurenai - Exalted|n|cFFFFD200Cost: |r70|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39318] = { 22513, 2, 0, "Tan Riding Talbuk", "The orcs once hunted these graceful herd animals as part of the om'riggor rite of passage ceremony.", 2, "|cFFFFD200Vendor: |rProvisioner Nasela|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Mag'har - Exalted|n|cFFFFD200Cost: |r70|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTrader Narasu|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Kurenai - Exalted|n|cFFFFD200Cost: |r70|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39319] = { 22514, 2, 0, "White Riding Talbuk", "The orcs once hunted these graceful herd animals as part of the om'riggor rite of passage ceremony.", 2, "|cFFFFD200Vendor: |rProvisioner Nasela|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Mag'har - Exalted|n|cFFFFD200Cost: |r70|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTrader Narasu|n|cFFFFD200Zone: |rNagrand, Outland|n|cFFFFD200Faction: |rThe Kurenai - Exalted|n|cFFFFD200Cost: |r70|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39450] = { 7712, 0, },
+ [39798] = { 22958, 1, 0, "Green Riding Nether Ray", "Nether Rays are among the most trusted, versatile mounts in all of Outland and beyond.", 2, "|cFFFFD200Vendor: |rGrella|n|cFFFFD200Zone: |rTerokkar Forest, Outland|n|cFFFFD200Faction: |rSha'tari Skyguard - Exalted|n|cFFFFD200Cost: |r200|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39800] = { 22976, 1, 0, "Red Riding Nether Ray", "Nether Rays are among the most trusted, versatile mounts in all of Outland and beyond.", 2, "|cFFFFD200Vendor: |rGrella|n|cFFFFD200Zone: |rTerokkar Forest, Outland|n|cFFFFD200Faction: |rSha'tari Skyguard - Exalted|n|cFFFFD200Cost: |r200|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39801] = { 22975, 1, 0, "Purple Riding Nether Ray", "Nether Rays are among the most trusted, versatile mounts in all of Outland and beyond.", 2, "|cFFFFD200Vendor: |rGrella|n|cFFFFD200Zone: |rTerokkar Forest, Outland|n|cFFFFD200Faction: |rSha'tari Skyguard - Exalted|n|cFFFFD200Cost: |r200|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39802] = { 22977, 1, 0, "Silver Riding Nether Ray", "Nether Rays are among the most trusted, versatile mounts in all of Outland and beyond.", 2, "|cFFFFD200Vendor: |rGrella|n|cFFFFD200Zone: |rTerokkar Forest, Outland|n|cFFFFD200Faction: |rSha'tari Skyguard - Exalted|n|cFFFFD200Cost: |r200|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39803] = { 22978, 1, 128, "Blue Riding Nether Ray", "Nether Rays are among the most trusted, versatile mounts in all of Outland and beyond.", 2, "|cFFFFD200Vendor: |rGrella|n|cFFFFD200Zone: |rTerokkar Forest, Outland|n|cFFFFD200Faction: |rSha'tari Skyguard - Exalted|n|cFFFFD200Cost: |r200|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [39910] = { 22512, 0, },
+ [39949] = { 284, 1, },
+ [40192] = { 18545, 1, 128, "Ashes of Al'ar", "Al'ar was the beloved pet of Kael'thas Sunstrider, who often boasted that death would never claim it. Perhaps he was right.", 0, "|cFFFFD200Drop: |rKael'thas Sunstrider|n|cFFFFD200Location: |rThe Eye, Tempest Keep" },
+ [40212] = { 16597, 1, },
+ [41252] = { 23408, 2, 0, "Raven Lord", "The rest be forgotten to walk upon the ground, clipped wings and shame. -Word of the Raven", 0, "|cFFFFD200Drop:|r Anzu|n|cFFFFD200Zone: |rSethekk Halls (Heroic)" },
+ [41513] = { 23455, 1, 128, "Onyx Netherwing Drake", "After promising to fly you to the ends of Outland, the netherwing drake was eventually drawn to the blue skies of Azeroth as well.", 1, "|cFFFFD200Quest: |rOnyxien the Onyx Netherwing Drake|n|cFFFFD200Zone: |rShattrath City, Outland" },
+ [41514] = { 23456, 1, 128, "Azure Netherwing Drake", "After promising to fly you to the ends of Outland, the netherwing drake was eventually drawn to the blue skies of Azeroth as well.", 1, "|cFFFFD200Quest: |rSuraku the Azure Netherwing Drake|n|cFFFFD200Zone: |rShattrath City, Outland" },
+ [41515] = { 23460, 1, 128, "Cobalt Netherwing Drake", "After promising to fly you to the ends of Outland, the netherwing drake was eventually drawn to the blue skies of Azeroth as well.", 1, "|cFFFFD200Quest: |rJorus the Cobalt Netherwing Drake|n|cFFFFD200Zone: |rShattrath City, Outland" },
+ [41516] = { 23458, 1, 128, "Purple Netherwing Drake", "After promising to fly you to the ends of Outland, the netherwing drake was eventually drawn to the blue skies of Azeroth as well.", 1, "|cFFFFD200Quest: |rMalfas the Purple Netherwing Drake|n|cFFFFD200Zone: |rShattrath City, Outland" },
+ [41517] = { 23457, 1, 128, "Veridian Netherwing Drake", "After promising to fly you to the ends of Outland, the netherwing drake was eventually drawn to the blue skies of Azeroth as well.", 1, "|cFFFFD200Quest: |rZoya the Veridian Netherwing Drake|n|cFFFFD200Zone: |rShattrath City, Outland" },
+ [41518] = { 23459, 1, 128, "Violet Netherwing Drake", "After promising to fly you to the ends of Outland, the netherwing drake was eventually drawn to the blue skies of Azeroth as well.", 1, "|cFFFFD200Quest: |rVoranaku the Violet Netherwing Drake|n|cFFFFD200Zone: |rShattrath City, Outland" },
+ [42363] = { 23756, 2, },
+ [42387] = { 23756, 2, },
+ [42667] = { 23952, 1, },
+ [42668] = { 30305, 1, },
+ [42680] = { 23966, 2, },
+ [42683] = { 23966, 2, },
+ [42692] = { 23966, 2, },
+ [42776] = { 24003, 2, 64, "Spectral Tiger", "In true goblin fashion, Landro Longshot has put a stranglehold on the distribution of these mysterious incorporeal mounts.", 8, "|cFFFFD200Trading Card Game:|r Spectral Tiger" },
+ [42777] = { 24004, 2, 64, "Swift Spectral Tiger", "Historians claim that these incorporeal beasts were created when Draenor exploded, unleashing energies that warped the world's flora and fauna.", 8, "|cFFFFD200Trading Card Game:|r Spectral Tiger" },
+ [42929] = { 21635, 0, },
+ [43688] = { 24379, 2, 64, "Amani War Bear", "Only the swiftest heroes venturing into the troll temple of Zul'Aman were able to subdue these rare battle mounts.", 0, "|cFFFFD200Legacy|r" },
+ [43810] = { 24447, 0, },
+ [43880] = { 24463, 0, },
+ [43883] = { 24462, 0, },
+ [43899] = { 23588, 2, 64, "Brewfest Ram", "Brewers retired this mount from active advertising service after complaints that the beasts were \"too temperamental\" to control in major cities.", 6, "|cFFFFD200Legacy|r" },
+ [43900] = { 24368, 2, 0, "Swift Brewfest Ram", "Dwarves attribute this breed's even temperament to rigorous training, but other races argue that a daily diet of strong ale has something to do with it.", 0, "|cFFFFD200Drop:|r Keg-Shaped Treasure Chest|n|cFFFFD200Holiday:|r Brewfest" },
+ [43927] = { 24488, 1, 128, "Cenarion War Hippogryph", "Members of the Cenarion Circle have shared a long and prosperous partnership with their hippogryph allies. When conflict is unavoidable, the Cenarion War Hippogryph is called into service.", 2, "|cFFFFD200Vendor: |rFedryen Swiftspear|n|cFFFFD200Zone: |rZangarmarsh, Outland|n|cFFFFD200Faction: |rCenarion Expedition - Exalted|n|cFFFFD200Cost: |r2000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [44151] = { 24654, 1, 128, "Turbo-Charged Flying Machine", "Only the most talented engineers have the courage and the mental stamina to create a flying machine that boasts stability and safety. Turbo-charging one is just crazy!", 3, "|cFFFFD200Profession: |rEngineering" },
+ [44153] = { 24653, 1, 128, "Flying Machine", "The backbone of the Alliance air force is comprised predominantly of gnomish flying machines due to their maneuverability and speed.", 3, "|cFFFFD200Profession: |rEngineering" },
+ [44317] = { 21510, 1, 192, "Merciless Nether Drake", "The most aggressive nether drakes were awarded those heroes who showed significant prowess and commitment in the gladiatorial arena.", 5, "|cFFFFD200Legacy|r" },
+ [44655] = { 24906, 1, },
+ [44744] = { 24743, 1, 192, "Merciless Nether Drake", "The most aggressive nether drakes were awarded those heroes who showed significant prowess and commitment in the gladiatorial arena.", 5, "|cFFFFD200Legacy|r" },
+ [44824] = { 24906, 1, },
+ [44825] = { 24906, 1, },
+ [44827] = { 24906, 1, },
+ [45177] = { 17266, 0, },
+ [46197] = { 26192, 1, 64, "X-51 Nether-Rocket", "First designed as a booster for a much larger rocket, the X-51 was determined to give the perfect thrust capacity for a single rider payload.", 8, "|cFFFFD200Trading Card Game:|r X-51 Nether-Rocket" },
+ [46199] = { 26164, 1, 64, "X-51 Nether-Rocket X-TREME", "What it lacks in safety features it more than makes up for in speed and style. Not for the faint of heart.", 8, "|cFFFFD200Trading Card Game:|r X-51 Nether-Rocket" },
+ [46628] = { 26131, 2, 0, "Swift White Hawkstrider", "I may question Prince Kael'thas's loyalties, but never his style. -Elrodan", 0, "|cFFFFD200Drop: |rKael'thas Sunstrider|n|cFFFFD200Zone: |rMagisters' Terrace" },
+ [46980] = { 26616, 0, },
+ [47037] = { 26439, 0, },
+ [47977] = { 23966, 2, },
+ [48023] = { 27153, 1, },
+ [48024] = { 27153, 2, },
+ [48025] = { 27153, 2, 0, "Headless Horseman's Mount", "Be it into the flood, or into the fire, this one will go where you require.", 0, "|cFFFFD200Drop:|r Loot-Filled Pumpkin|n|cFFFFD200Holiday:|r Hallow's End|n" },
+ [48027] = { 26439, 2, 0, "Black War Elekk", "Uniquely aggressive and pugnacious elekk bulls are culled from the herd and sold to Ethereal traders. The traders then train the bulls for combat and sell them to owners unconcerned about safety.", 2, "|cFFFFD200Vendor: |rLieutenant Karter|n|cFFFFD200Zone: |rStormwind City|n|cFFFFD200Cost: |r50000|TInterface\\PVPFrame\\PVP-Currency-Alliance:0|t" },
+ [48778] = { 28302, 2, 0, "Acherus Deathcharger", "Some horses merely adopt the dark. This horse was born in it, molded by it.", 2, "|cFFFFD200Trainer:|r Death Knight|n" },
+ [48954] = { 27541, 2, 64, "Swift Zhevra", "Zhevras are rarely used as mounts in Azeroth due to their stubborn nature and tendency to bite.", 7, "|cFFFFD200Recruit-a-Friend |r" },
+ [49193] = { 27637, 1, 192, "Vengeful Nether Drake", "The most aggressive nether drakes were awarded those heroes who showed significant prowess and commitment in the gladiatorial arena.", 5, "|cFFFFD200Legacy|r" },
+ [49322] = { 27684, 2, 0, "Swift Zhevra", "Zhevras are rarely used as mounts in Azeroth due to their stubborn nature and tendency to bite.", 7, "|cFFFFD200Recruit-a-Friend |r" },
+ [49378] = { 27706, 2, 64, "Brewfest Riding Kodo", "You can't remember exactly how you got this mount. Then again, you can't remember much of anything that happens at Brewfest.", 6, "|cFFFFD200Legacy|r" },
+ [49379] = { 27707, 2, 0, "Great Brewfest Kodo", "Coren Direbrew won this prize after drinking a Tauren druid under the table - and the druid was in bear form. It was one of Direbrew's proudest moments.", 0, "|cFFFFD200Drop:|r Keg-Shaped Treasure Chest|n|cFFFFD200Holiday:|r Brewfest" },
+ [49908] = { 27902, 0, },
+ [50281] = { 27976, 0, },
+ [50869] = { 30507, 2, },
+ [50870] = { 24368, 2, },
+ [51412] = { 28363, 2, 64, "Big Battle Bear", "He's big, he's ready for battle, and he's a bear. What more could you want in a mount?", 8, "|cFFFFD200Trading Card Game:|r The Red Bearon" },
+ [51617] = { 27153, 1, },
+ [51621] = { 27153, 2, },
+ [51960] = { 28531, 1, },
+ [54726] = { 29582, 1, },
+ [54727] = { 29582, 1, },
+ [54729] = { 29582, 1, 128, "Winged Steed of the Ebon Blade", "Dead or alive, you're coming with me.", 2, "|cFFFFD200Vendor: |rDread Commander Thalanor|n|cFFFFD200Zone: |rEastern Plaguelands|n|cFFFFD200Cost: |r1000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [54753] = { 29596, 2, 0, "White Polar Bear", "The frost vrykul believe that wearing a necklace of polar bear claws will grant them otherworldly strength.", 0, "|cFFFFD200Drop:|r Hyldnir Spoils|n|cFFFFD200Location:|r The Storm Peaks" },
+ [55164] = { 29767, 2, 64, "Swift Spectral Gryphon", "Even the dead need a lift every now and then.", 5, "|cFFFFD200Legacy|r" },
+ [55293] = { 24379, 2, },
+ [55531] = { 29929, 2, 4, "Mechano-Hog", "Difficult to build and to maintain, these hogs more than make up for their expense through sheer cool factor.", 3, "|cFFFFD200Profession: |rEngineering" },
+ [58615] = { 31124, 1, 192, "Brutal Nether Drake", "The most aggressive nether drakes were awarded those heroes who showed significant prowess and commitment in the gladiatorial arena.", 2, "|cFFFFD200Legacy|r" },
+ [58819] = { 14561, 2, },
+ [58983] = { 31319, 2, 0, "Big Blizzard Bear", "Whenever you ride this mount, you feel the urge to shout, \"Cry havoc and let slip the murlocs of war!\"", 7, "|cFFFFD200Promotion:|r BlizzCon|n" },
+ [58997] = { 31319, 2, },
+ [58999] = { 31319, 2, },
+ [59567] = { 31694, 1, 128, "Azure Drake", "Always speak politely to an enraged dragon.", 0, "|cFFFFD200Drop:|r Alexstrasza's Gift|n|cFFFFD200Location:|r Eye of Eternity" },
+ [59568] = { 31695, 1, 128, "Blue Drake", "Come not between the dragon and his wrath.", 0, "|cFFFFD200Drop:|r Alexstrasza's Gift|n|cFFFFD200Location:|r Eye of Eternity" },
+ [59569] = { 31717, 1, 128, "Bronze Drake", "Lost time is never found again. Oh, never mind I found it. -Chronormu", 0, "|cFFFFD200Drop: |rInfinite Corruptor|n|cFFFFD200Zone: |rThe Culling of Stratholme" },
+ [59570] = { 31697, 1, 128, "Red Drake", "You should never listen to minstrels' fancies. A dragon would never hurt a soul, unless they tried to hurt him first.", 2, "|cFFFFD200Vendor: |rCielstrasza|n|cFFFFD200Zone: |rDragonblight|n|cFFFFD200Faction: |rThe Wyrmrest Accord - Exalted|n|cFFFFD200Cost: |r2000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [59571] = { 31698, 1, 128, "Twilight Drake", "But the most important rule, the rule you can never forget, no matter how much he cries, no matter how much he begs, never feed him after midnight.", 0, "|cFFFFD200Drop:|r Sartharion|n|cFFFFD200Location:|r The Obsidian Sanctum" },
+ [59572] = { 31699, 2, 64, "Black Polar Bear", "The taunka use black polar bear pelts in their religious rites, usually at the funerals of those who hunt black polar bears.", 2, "|cFFFFD200Legacy|r" },
+ [59573] = { 31700, 2, },
+ [59650] = { 31778, 1, 128, "Black Drake", "It never pays to leave a live dragon out of the equation.", 0, "|cFFFFD200Drop:|r Sartharion" },
+ [59785] = { 31849, 2, 4, "Black War Mammoth", "Normally placid beasts, these intelligent animals form a strong bond with their riders, making them loyal and fearless in battle.", 2, "|cFFFFD200Vendor: |rKnight Dameron|n|cFFFFD200Zone: |rWintergrasp|n|cFFFFD200Cost: |r300|Hitem:43228|h|TInterface\\ICONS\\INV_Misc_Platnumdisks:0|t|h" },
+ [59788] = { 31850, 2, 4, "Black War Mammoth", "Sure-footed and unflappable, these dark-furred mammoths are prized for their steady temperament going into battle.", 2, "|cFFFFD200Vendor: |rStone Guard Mukar|n|cFFFFD200Zone: |rWintergrasp|n|cFFFFD200Cost: |r300|Hitem:43228|h|TInterface\\ICONS\\INV_Misc_Platnumdisks:0|t|h" },
+ [59791] = { 31851, 2, 4, "Wooly Mammoth", "Directly descended from the original progenitor of the mammoth race, wooly mammoths are considered more feral and primal than their shorter-haired cousins.", 2, "|cFFFFD200Vendor: |rMei Francis|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r200|TINTERFACE\\ICONS\\Spell_Holy_ProclaimChampion:0|t" },
+ [59793] = { 31852, 2, 4, "Wooly Mammoth", "This wild variant of the mammoth is not bred for riding. Most wooly mammoths are captured in the wild and tamed by their riders, resulting in a fierce, cunning mount.", 2, "|cFFFFD200Vendor: |rMei Francis|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r200|TINTERFACE\\ICONS\\Spell_Holy_ProclaimChampion:0|t" },
+ [59797] = { 31854, 2, 4, "Ice Mammoth", "The Sons of Hodir believe that riding an ice-white mammoth will protect the rider from frostbite and snow blindness.", 2, "|cFFFFD200Vendor: |rLillehoff|n|cFFFFD200Zone: |rThe Storm Peaks|n|cFFFFD200Faction: |rThe Sons of Hodir - Revered|n|cFFFFD200Cost: |r1000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [59799] = { 31855, 2, 4, "Ice Mammoth", "Many ice giants claim that they have trained their favored white mammoths to trigger avalanches with their trumpeting calls.", 2, "|cFFFFD200Vendor: |rLillehoff|n|cFFFFD200Zone: |rThe Storm Peaks|n|cFFFFD200Faction: |rThe Sons of Hodir - Revered|n|cFFFFD200Cost: |r1000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [59802] = { 31857, 2, },
+ [59804] = { 31858, 2, },
+ [59961] = { 31902, 1, 128, "Red Proto-Drake", "The vrykul were the first to recognize their potential. Or maybe the second, depending on how you look at it.", 5, "|cFFFFD200Achievement: |rGlory of the Hero|n|cFFFFD200Category: |rDungeons & Raids" },
+ [59976] = { 31912, 1, 192, "Black Proto-Drake", "Bred specifically to honor those heroes who showed exceptional resilience and fortitude facing the dangers in Northrend.", 2, "|cFFFFD200Legacy|r" },
+ [59996] = { 32151, 1, 128, "Blue Proto-Drake", "The Storm Peaks! There we will find the beast strong enough to bear me into battle. -Skadi the Ruthless", 0, "|cFFFFD200Drop: |rSkadi the Ruthless|n|cFFFFD200Zone: |rUtgarde Pinnacle" },
+ [60002] = { 32153, 1, 128, "Time-Lost Proto-Drake", "The only reason for time is so that everything doesn't happen at once.", 0, "|cFFFFD200Drop: |rTime-Lost Proto-Drake|n|cFFFFD200Zone: |rThe Storm Peaks" },
+ [60021] = { 32156, 1, 192, "Plagued Proto-Drake", "Bred specifically to honor those heroes who showed significant courage and persistence facing the dangers in Northrend.", 5, "|cFFFFD200Legacy|r" },
+ [60024] = { 32157, 1, 128, "Violet Proto-Drake", "I cannot help but wonder if things would have turned out different if the titans had elevated even one more dragonflight. -Alexstrasza", 5, "|cFFFFD200Achievement: |rWhat a Long, Strange Trip It's Been|n|cFFFFD200Category: |rWorld Events" },
+ [60025] = { 32158, 1, 128, "Albino Drake", "Even the Draconic Native Association tests proved inconclusive in determining if the albino drake is related to a known dragonflight. -Breanni", 5, "|cFFFFD200Achievement: |rLeading the Cavalry|n|cFFFFD200Category: |rGeneral" },
+ [60114] = { 32206, 2, 4, "Armored Brown Bear", "This ursine mount's swiftness is enhanced by its heavy armor, imbued with magic from Kirin Tor magi.", 2, "|cFFFFD200Vendor: |rMei Francis|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r750|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [60116] = { 32207, 2, 4, "Armored Brown Bear", "Your cries of \"For the Horde!\" will be much more menacing from atop a heavily armored bear that's barreling down on Alliance at top speed.", 2, "|cFFFFD200Vendor: |rMei Francis|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r750|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [60118] = { 32203, 2, 4, "Black War Bear", "Killing leaders of the Horde is its own reward, but this intimidating beast is an excellent memento.", 5, "|cFFFFD200Achievement: |rFor The Alliance!|n|cFFFFD200Category: |rPlayer vs. Player" },
+ [60119] = { 32205, 2, 4, "Black War Bear", "This formidable mount is bought with the blood of Alliance leaders, but you would have killed them for free.", 5, "|cFFFFD200Achievement: |rFor The Horde!|n|cFFFFD200Category: |rPlayer vs. Player" },
+ [60120] = { 32208, 1, },
+ [60136] = { 32212, 0, 64, "Grand Caravan Mammoth", "Merchants trust only this thundering, mountainous steed to protect them when venturing down Azeroth's most dangerous trade routes.", 2, "|cFFFFD200Legacy|r" },
+ [60140] = { 32213, 0, 64, "Grand Caravan Mammoth", "A common sight on trade routes all across Azeroth, this calm, stalwart mount is the favorite of merchants traveling to exotic and distant markets.", 2, "|cFFFFD200Legacy|r" },
+ [60424] = { 32286, 2, 4, "Mekgineer's Chopper", "Sporting superb engineering craftsmanship, this chopper is one of the few personal vehicles on Azeroth built for two.", 3, "|cFFFFD200Profession: |rEngineering" },
+ [61229] = { 32335, 1, 132, "Armored Snowy Gryphon", "The nice thing is that when they attack, blood almost never stains their feathers. -Mei Francis", 2, "|cFFFFD200Vendor: |rMei Francis|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r2000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [61230] = { 32336, 1, 132, "Armored Blue Wind Rider", "They are not native to Northrend, but they have adapted extremely well to the cold. -Mei Francis", 2, "|cFFFFD200Vendor: |rMei Francis|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r2000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [61289] = { 23966, 0, },
+ [61294] = { 32562, 1, 128, "Green Proto-Drake", "The vrykul were the first to recognize their potential. Or maybe the second, depending on how you look at it.", 0, "|cFFFFD200Drop:|r Cracked Egg" },
+ [61309] = { 33030, 1, 1024, "Magnificent Flying Carpet", "Standard features include a sturdy imbued frostweave frame and real iceweb spider silk threading. Spare head pillow sold separately.", 3, "|cFFFFD200Profession: |rTailoring" },
+ [61425] = { 32633, 2, 4, "Traveler's Tundra Mammoth", "Ancient manuscripts depict merchants traveling across the frozen tundra with these colossal beasts, leading scholars to believe they are the first domesticated mounts.", 2, "|cFFFFD200Vendor: |rMei Francis|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r20000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [61442] = { 32634, 1, },
+ [61444] = { 32635, 1, },
+ [61446] = { 32636, 1, },
+ [61447] = { 32640, 2, 4, "Traveler's Tundra Mammoth", "Riders and merchants alike trust only this strong and steadfast mammoth to carry them safely across the frigid tundra.", 2, "|cFFFFD200Vendor: |rMei Francis|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r20000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [61451] = { 33029, 1, 1024, "Flying Carpet", "Hop aboard this flying carpet to see the shining, shimmering, and splendid world of Azeroth, wonder by wonder.", 3, "|cFFFFD200Profession: |rTailoring" },
+ [61465] = { 31862, 2, 4, "Grand Black War Mammoth", "Once used by giants to carry heavily armored warriors, these grand war mammoths can easily bear more than one rider of less-than-giant stature.", 0, "|cFFFFD200Drop: |rBoss Drop|n|cFFFFD200Location: |rVault of Archavon" },
+ [61467] = { 31861, 2, 4, "Grand Black War Mammoth", "Rarely, a war mammoth is born stronger and more tractable than its peers. These mammoths are dubbed \"grand\" and carry more than one rider to the front lines.", 0, "|cFFFFD200Drop: |rBoss Drop|n|cFFFFD200Location: |rVault of Archavon" },
+ [61469] = { 31857, 2, 4, "Grand Ice Mammoth", "At home on the narrow, icy trails of the Storm Peaks, these mammoths are the only mounts the ice giants trust to carry large loads and multiple riders.", 2, "|cFFFFD200Vendor: |rLillehoff|n|cFFFFD200Zone: |rThe Storm Peaks|n|cFFFFD200Faction: |rThe Sons of Hodir - Exalted|n|cFFFFD200Cost: |r10000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [61470] = { 31858, 2, 4, "Grand Ice Mammoth", "Due to high winds and avalanches, the Sons of Hodir recommend bringing at least two companions if venturing into the dangerous Storm Peaks.", 2, "|cFFFFD200Vendor: |rLillehoff|n|cFFFFD200Zone: |rThe Storm Peaks|n|cFFFFD200Faction: |rThe Sons of Hodir - Exalted|n|cFFFFD200Cost: |r10000|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [61983] = { 32931, 0, },
+ [61996] = { 31239, 1, 132, "Blue Dragonhawk", "Dragonhawks are among the deadliest known predators. They are able to spot prey from great distances, rush in, and rend with both talon and beak.", 5, "|cFFFFD200Achievement: |rMountain o' Mounts|n|cFFFFD200Category: |rGeneral" },
+ [61997] = { 32944, 1, 132, "Red Dragonhawk", "Dragonhawks are among the deadliest known predators. They are able to spot prey from great distances, rush in, and rend with both talon and beak.", 5, "|cFFFFD200Achievement: |rMountain o' Mounts|n|cFFFFD200Category: |rGeneral" },
+ [62048] = { 25064, 1, 192, "Illidari Doomhawk", "Once a year, a rift in time appears, and strange things occur. The Illidari scouts and Doomwalker were quite surprised to find themselves in Tanaris.", 0, "|cFFFFD200Drop: |rDoomwalker|n|cFFFFD200Zone: |rTanaris" },
+ [63232] = { 33297, 2, 0, "Stormwind Steed", "Although raised in Stormwind, this breed showed unusual aptitude for the cold weather in Icecrown.", 2, "|cFFFFD200Vendor: |rCorporal Arthur Flew|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63635] = { 33299, 2, 0, "Darkspear Raptor", "This raptor breed was originally cultivated by the Drakkari, but has since been adopted by the Darkspear tribe in Northrend.", 2, "|cFFFFD200Vendor: |rSamamba|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63636] = { 33408, 2, 0, "Ironforge Ram", "Wildhammer Fact Checker claims that this specific breed was the first type of ram domesticated by the dwarves when they settled in Dun Morogh.", 2, "|cFFFFD200Vendor: |rDerrick Brindlebeard|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63637] = { 33298, 2, 0, "Darnassian Nightsaber", "When a night elf's mount perishes, it is custom for the rider to keep one of the saber's great fangs as a token of remembrance.", 2, "|cFFFFD200Vendor: |rRook Hawkfist|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63638] = { 33301, 2, 0, "Gnomeregan Mechanostrider", "When you absolutely, positively have to be there right now. -Rillie Spindlenut", 2, "|cFFFFD200Vendor: |rRillie Spindlenut|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63639] = { 33416, 2, 0, "Exodar Elekk", "Exodar Elekk are elekk which have been anointed by O'ros, the Naaru who journeyed from Draenor to Azeroth with Velen and his followers.", 2, "|cFFFFD200Vendor: |rIrisee|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63640] = { 33409, 2, 0, "Orgrimmar Wolf", "For a brief time, many orcs would declare themselves \"hungry like an Orgrimmar wolf.\" Some blood elves still use the phrase ironically.", 2, "|cFFFFD200Vendor: |rFreka Bloodaxe|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63641] = { 33300, 2, 0, "Thunder Bluff Kodo", "An alpha member of the herd, this kodo is a prime example of the mighty creatures which thunder through Mulgore.", 2, "|cFFFFD200Vendor: |rDoru Thunderhorn|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63642] = { 33418, 2, 0, "Silvermoon Hawkstrider", "So I says I feel like chicken tonight. Next thing, I am flat out and that thing is trying to see what my eyes taste like. -Bozzle Blastbolt", 2, "|cFFFFD200Vendor: |rTrellis Morningsun|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63643] = { 33414, 2, 0, "Forsaken Warhorse", "When fallen heroes are raised into undeath, so too are their horses.", 2, "|cFFFFD200Vendor: |rEliza Killian|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [63796] = { 33848, 1, 128, "Mimiron's Head", "A token of titan craftsmanship hoarded by the Old God, Yogg-Saron. Currently the single largest mechanical gnome head in Azeroth.", 0, "|cFFFFD200Drop:|r Yogg-Saron|n|cFFFFD200Location:|r Ulduar" },
+ [63844] = { 33857, 1, 128, "Argent Hippogryph", "Argent Hippogryphs will partner with only the bravest and most battle-tested champions.", 2, "|cFFFFD200Vendor: |rFaction Quartermasters|n|cFFFFD200Location: |rArgent Tournament, Icecrown|n|cFFFFD200Cost: |r150|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t|n" },
+ [63956] = { 33892, 1, 128, "Ironbound Proto-Drake", "His skin more hard than brass was found, that sword nor spear could pierce nor wound.", 5, "|cFFFFD200Achievement: |rGlory of the Ulduar Raider (25 player)|n|cFFFFD200Category: |rDungeons & Raids" },
+ [63963] = { 33904, 1, 128, "Rusted Proto-Drake", "Not as shiny as the other one, but it's still a great find lads. Brann Bronzebeard", 5, "|cFFFFD200Achievement: |rGlory of the Ulduar Raider (10 player)|n|cFFFFD200Category: |rDungeons & Raids" },
+ [64656] = { 34154, 2, 64, "Blue Skeletal Warhorse", "A rare sight. Most disappeared once Baron Rivendare ruled in Stratholme.", 2, "|cFFFFD200Legacy|r" },
+ [64657] = { 34155, 2, 0, "White Kodo", "\"The kodo is the backbone of our people. Wherever the Horde goes - in trade, settlement, or war - there you will find the mighty kodo.\" - Harb Clawhoof", 2, "|cFFFFD200Vendor: |rHarb Clawhoof|n|cFFFFD200Zone: |rMulgore|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [64658] = { 356, 2, 0, "Black Wolf", "Can howl loudly enough to be heard for miles.", 2, "|cFFFFD200Vendor: |rOgunaro Wolfrunner|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [64659] = { 34156, 2, 4, "Venomhide Ravasaur", "You raised this thing from a scaly pup - Mor'vek would be so proud. But he'd still make you clean up the mess it made in the corner.", 1, "|cFFFFD200Quest: |rThey Grow Up So Fast|n|cFFFFD200Zone: |rUn'Goro Crater" },
+ [64681] = { 32198, 1, },
+ [64731] = { 34187, 2, 2192, "Sea Turtle", "Often caught by fishermen at a young age, these mounts are much more comfortable in the water than they are on land.", 3, "|cFFFFD200Profession:|r Fishing|n" },
+ [64761] = { 32208, 1, },
+ [64927] = { 34225, 1, 192, "Deadly Gladiator's Frost Wyrm", "These reanimated vessels of destruction do not fear death; they dispense it.", 2, "|cFFFFD200Legacy|r" },
+ [64977] = { 34238, 2, 0, "Black Skeletal Horse", "The Forsaken believe that without a purpose, even beasts of burden suffer in undeath.", 2, "|cFFFFD200Vendor: |rZachariah Post|n|cFFFFD200Zone: |rTirisfal Glades|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [64992] = { 31319, 2, },
+ [64993] = { 31319, 2, },
+ [65439] = { 34425, 1, 192, "Furious Gladiator's Frost Wyrm", "Some of Azeroth's mightiest dragons have been summoned across the veil between life and death to serve new masters with undying loyalty.", 2, "|cFFFFD200Legacy|r" },
+ [65637] = { 34551, 2, 0, "Great Red Elekk", "Never seen in Nagrand, the first red elekk calves were born in Azeroth after the Exodar's arrival. Some breeders theorize that the unique coloration is somehow caused by the same radiation which ravaged Bloodmyst Isle.", 2, "|cFFFFD200Vendor: |rIrisee|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65638] = { 34550, 2, 0, "Swift Moonsaber", "Many night elves revere these proud mounts because their shimmering coats symbolize the goddess of the moon, Elune.", 2, "|cFFFFD200Vendor: |rRook Hawkfist|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65639] = { 34556, 2, 0, "Swift Red Hawkstrider", "What do you mean is it dyed? Oh, the bird. We did touch up the red just a little. -Trellis Morningsun", 2, "|cFFFFD200Vendor: |rTrellis Morningsun|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65640] = { 34557, 2, 0, "Swift Gray Steed", "A veteran of some of the toughest battles fought by the Argent Crusade.", 2, "|cFFFFD200Vendor: |rCorporal Arthur Flew|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65641] = { 34558, 2, 4, "Great Golden Kodo", "An alpha member of the herd, this kodo is a prime example of the mighty creatures which thunder through the Southern Barrens.", 2, "|cFFFFD200Vendor: |rDoru Thunderhorn|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65642] = { 34553, 2, 0, "Turbostrider", "The mirror's not so you can look back, but so your foes can see something besides your tailpipes. -Rillie Spindlenut", 2, "|cFFFFD200Vendor: |rRillie Spindlenut|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65643] = { 34554, 2, 0, "Swift Violet Ram", "The Barak Tor'ol ram's thick bony skull and massive horns make it the ideal mount for charging through groups of armored enemies.", 2, "|cFFFFD200Vendor: |rDerrick Brindlebeard|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65644] = { 34549, 2, 0, "Swift Purple Raptor", "Longtime allies of the trolls, these jungle hunters form a special bond with their masters. They become mount, guardian, and friend.", 2, "|cFFFFD200Vendor: |rSamamba|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65645] = { 34552, 2, 0, "White Skeletal Warhorse", "When fallen heroes are raised into undeath, so too are their horses.", 2, "|cFFFFD200Vendor: |rEliza Killian|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65646] = { 34555, 2, 0, "Swift Burgundy Wolf", "The wolves of the Horde are befriended, not domesticated.", 2, "|cFFFFD200Vendor: |rFreka Bloodaxe|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t5|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [65917] = { 34655, 2, 64, "Magic Rooster", "Magic in the hands of a genius can do wonderful, miraculous things. Magic in the hands of an idiot can make a giant rooster.", 8, "|cFFFFD200Trading Card Game:|r El Pollo Grande" },
+ [66087] = { 35147, 1, 132, "Silver Covenant Hippogryph", "Silver Covenant Hippogryphs are fiercely loyal, both to their riding partners, and to the Alliance.", 2, "|cFFFFD200Vendor: |rHiren Loresong|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Faction: |rThe Silver Covenant - Exalted|n|cFFFFD200Cost: |r150|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [66088] = { 35148, 1, 132, "Sunreaver Dragonhawk", "The Sunreavers of Dalaran select only the swiftest and most formidable of dragonhawks as their mounts.", 2, "|cFFFFD200Vendor: |rVasarin Redmorn|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Faction: |rThe Sunreavers - Exalted|n|cFFFFD200Cost: |r150|Hcurrency:241|h|TInterface\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t|h" },
+ [66090] = { 33840, 2, 4, "Quel'dorei Steed", "Comes from a lineage of fine horses that once grazed near the shores of the Well of Eternity.", 2, "|cFFFFD200Vendor: |rHiren Loresong|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Faction: |rThe Silver Covenant - Exalted|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [66091] = { 33841, 2, 4, "Sunreaver Hawkstrider", "I demand speed, grace and discipline on the battlefield. This one will serve nicely. -Eressea Dawnsinger", 2, "|cFFFFD200Vendor: |rVasarin Redmorn|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Faction: |rThe Sunreavers - Exalted|n|cFFFFD200Cost: |r100|Hcurrency:241|h|TInterface\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t|h" },
+ [66122] = { 34655, 2, 64, "Magic Rooster", "Magic in the hands of a genius can do wonderful, miraculous things. Magic in the hands of an idiot can make a giant rooster.", 8, "|cFFFFD200Trading Card Game:|r El Pollo Grande" },
+ [66123] = { 34731, 2, 64, "Magic Rooster", "Magic in the hands of a genius can do wonderful, miraculous things. Magic in the hands of an idiot can make a giant rooster.", 8, "|cFFFFD200Trading Card Game:|r El Pollo Grande" },
+ [66124] = { 34732, 2, 64, "Magic Rooster", "Magic in the hands of a genius can do wonderful, miraculous things. Magic in the hands of an idiot can make a giant rooster.", 8, "|cFFFFD200Trading Card Game:|r El Pollo Grande" },
+ [66846] = { 35169, 2, 0, "Ochre Skeletal Warhorse", "When fallen heroes are raised into undeath, so too are their horses.", 2, "|cFFFFD200Vendor: |rZachariah Post|n|cFFFFD200Zone: |rTirisfal Glades|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [66847] = { 35168, 2, 0, "Striped Dawnsaber", "Night elves once believed that seeing this light colored saber breed in the wild was an ill omen.", 2, "|cFFFFD200Vendor: |rLelanai|n|cFFFFD200Zone: |rTeldrassil|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [66906] = { 35179, 2, 0, "Argent Charger", "Trained to accept only champions of the Light as a rider.", 2, "|cFFFFD200Vendor: |rDame Evniki Kapsalis|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t|n" },
+ [66907] = { 35180, 2, },
+ [67336] = { 35362, 1, 192, "Relentless Gladiator's Frost Wyrm", "In life, it could not be tamed, but necromancy and esoteric binding rituals have now combined to bend this indomitable creature to its rider's will.", 5, "|cFFFFD200Legacy|r" },
+ [67466] = { 35445, 2, 0, "Argent Warhorse", "Given only to those who prove themselves a champion in the Argent Tournament.", 2, "|cFFFFD200Vendor: |rDame Evniki Kapsalis|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [68056] = { 35809, 2, 4, "Swift Horde Wolf", "A few of the Argent Crusade's swiftest mounts were awarded to those members of the Horde who performed exceptionally during the tournament in Icecrown.", 0, "|cFFFFD200Drop: |rArgent Crusade Tribute Chest|n|cFFFFD200Location: |rTrial of the Grand Crusader (10 Player)" },
+ [68057] = { 35808, 2, 4, "Swift Alliance Steed", "A few of the Argent Crusade's swiftest mounts were awarded to those members of the Alliance who performed exceptionally during the tournament in Icecrown.", 0, "|cFFFFD200Drop: |rArgent Crusade Tribute Chest|n|cFFFFD200Location: |rTrial of the Grand Crusader (10 Player)" },
+ [68187] = { 35876, 2, 4, "Crusader's White Warhorse", "Awarded to only the most skilled champions from the Trial of the Crusader, these steeds were chosen for Alliance members from Tirion Fordring's own personal stable.", 5, "|cFFFFD200Achievement: |rA Tribute to Immortality|n|cFFFFD200Category: |rFeats of Strength" },
+ [68188] = { 35878, 2, 4, "Crusader's Black Warhorse", "Awarded to only the most skilled champions from the Trial of the Crusader, these steeds were chosen for Horde members from Tirion Fordring's own personal stable.", 5, "|cFFFFD200Achievement: |rA Tribute to Immortality|n|cFFFFD200Category: |rFeats of Strength" },
+ [68768] = { 36483, 2, },
+ [68769] = { 36484, 2, },
+ [69395] = { 36837, 1, 128, "Onyxian Drake", "She has a lot in common with her mother, except for the being killed and reanimated. But she is young still. -Maloriak", 0, "|cFFFFD200Drop: |rOnyxia|n|cFFFFD200Zone: |rOnyxia's Lair" },
+ [71342] = { 38204, 2, 0, "X-45 Heartbreaker", "Apothecary Hummel painted this masterpiece bright pink for an unrequited love.", 0, "|cFFFFD200Drop:|r Heart-Shaped Box" },
+ [71343] = { 38204, 2, },
+ [71344] = { 38204, 2, },
+ [71345] = { 38204, 2, },
+ [71346] = { 38204, 1, },
+ [71347] = { 38204, 1, },
+ [71810] = { 38361, 1, 192, "Wrathful Gladiator's Frost Wyrm", "Many believe that during their early lives, these wyrms were forced to fight their own kind for the amusement of others. Perhaps that is why they possess a particularly combative temperament.", 5, "|cFFFFD200Legacy|r" },
+ [72281] = { 38545, 2, },
+ [72282] = { 38545, 2, },
+ [72283] = { 38545, 1, },
+ [72284] = { 38545, 1, },
+ [72286] = { 38545, 2, 0, "Invincible", "The famous steed of Arthas Menethil, who serves its master in life and in death. Riding him is truly a feat of strength.", 0, "|cFFFFD200Drop: |rThe Lich King|n|cFFFFD200Zone: |rIcecrown Citadel" },
+ [72807] = { 38695, 1, 128, "Icebound Frostbrood Vanquisher", "Raised from the chilling depths of Icecrown, Frostbrood Vanquishers are considered among the fiercest and most relentless of all frost wyrms.", 5, "|cFFFFD200Achievement: |rGlory of the Icecrown Raider (25 player)|n|cFFFFD200Category: |rDungeons & Raids" },
+ [72808] = { 38778, 1, 128, "Bloodbathed Frostbrood Vanquisher", "Bloodbathed Vanquishers are so named when it is determined that a reanimated frost wyrm died a particularly gruesome and bloody death.", 5, "|cFFFFD200Achievement: |rGlory of the Icecrown Raider (10 player)|n|cFFFFD200Category: |rDungeons & Raids" },
+ [73313] = { 39046, 2, 0, "Crimson Deathcharger", "Stabled in the Icecrown Citadel to serve the Lich King's most favored minions.", 1, "|cFFFFD200Quest: |rMograine's Reunion|n|cFFFFD200Zone: |rIcecrown Citadel" },
+ [74854] = { 40165, 1, },
+ [74855] = { 40165, 1, },
+ [74856] = { 40165, 1, 192, "Blazing Hippogryph", "Hippogryphs are highly intelligent, sentient beings; far more than mere \"mounts.\"", 8, "|cFFFFD200Trading Card Game:|r Blazing Hippogryph" },
+ [74918] = { 40191, 2, 64, "Wooly White Rhino", "Captured by Toby \"Mother Goose\" Ironbolt on the Borean Tundra, this shaggy beast carries an enormous grudge after being bridled by a gnome.", 8, "|cFFFFD200Trading Card Game:|r Wooly White Rhino" },
+ [75387] = { 40426, 2, },
+ [75596] = { 40533, 1, 1024, "Frosty Flying Carpet", "\"I remember my first flying carpet... yeti hair and burlap. Not nearly as nice as this one here.\" - Frozo the Renowned", 3, "|cFFFFD200Profession: |rTailoring" },
+ [75614] = { 40625, 2, 0, "Celestial Steed", "A beacon of hope; a guiding light in the darkness.", 9, "|cFFFFD200In-Game Shop|r" },
+ [75617] = { 40625, 1, },
+ [75618] = { 40625, 1, },
+ [75619] = { 40625, 2, },
+ [75620] = { 40625, 2, },
+ [75957] = { 40725, 3, },
+ [75972] = { 40725, 3, },
+ [75973] = { 40725, 3, 0, "X-53 Touring Rocket", "Riding a giant cylinder filled with highly explosive goblin rocket fuel all by yourself is just crazy...so bring a friend.", 7, "|cFFFFD200Recruit-a-Friend |r" },
+ [76153] = { 40625, 1, },
+ [76154] = { 40725, 3, },
+};
+
+NE.collections.PetData =
+{
+ [4055] = { 2671, 9, 2, nil, "A mechanical squirrel's logic center tells it to collect and store both nuts and bolts for the winter.", 3, "|cFFFFD200Profession: |rEngineering" },
+ [10673] = { 7385, 7, 2, nil, "Donni Anthania plans to have a bombay buried with her when she dies. A wise adventurer can put its talents to much better use.", 2, "|cFFFFD200Vendor: |rDonni Anthania|n|cFFFFD200Zone: |rElwynn Forest|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10674] = { 7384, 7, 2, nil, "Donni Anthania invites these cats to her tea parties. But she doesn't serve their favorite drink: the tears of their enemies.", 2, "|cFFFFD200Vendor: |rDonni Anthania|n|cFFFFD200Zone: |rElwynn Forest|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10675] = { 7383, 7, 2, nil, "Old Alterac saying: 'A cat has nine lives, but needs only one.'", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Zone: |rAlterac Mountains" },
+ [10676] = { 7382, 7, 2, nil, "The last person who tried to housebreak this cat quickly learned that a soiled rug is better than a shredded everything-else.", 2, "|cFFFFD200Vendor: |rDonni Anthania|n|cFFFFD200Zone: |rElwynn Forest|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10677] = { 7380, 7, 2, nil, "Known for its blue eyes. Also considered a delicacy by giant murlocs.", 2, "|cFFFFD200Vendor: |rDealer Rashaad|n|cFFFFD200Zone: |rNetherstorm|n|cFFFFD200Cost: |r60|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10678] = { 7381, 7, 2, nil, "Sleeping is this cat's second favorite activity. The first is yawning.", 2, "|cFFFFD200Vendor: |rDonni Anthania|n|cFFFFD200Zone: |rElwynn Forest|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10679] = { 7386, 7, 2, nil, "'The queen had three kittens. The first gave her a juicy rat. The second, a tasty hare. And the white, her favorite, presented the head of her rival.'", 2, "|cFFFFD200Vendor: |rLil Timmy|n|cFFFFD200Zone: |rStormwind City|n|cFFFFD200Cost: |r60|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10680] = { 7390, 2, 2, nil, "Some say this clever bird can be taught to speak--but it's smart enough to keep its beak shut.", 2, "|cFFFFD200Vendor: |rNarkk|n|cFFFFD200Zone: |rStranglethorn Vale|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10682] = { 7391, 2, 2, nil, "The jungle trolls train these birds to mimic calls for help in order to lure unsuspecting travelers into traps.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Zone: |rStranglethorn Vale" },
+ [10683] = { 7387, 2, 2, nil, "Favored pet of the Defias pirates, this colorful bird is handy for remembering passwords, grocery lists, and cracker recipes.", 0, "|cFFFFD200Drop: |rDefias Pirate|n|cFFFFD200Zone: |rThe Deadmines" },
+ [10684] = { 7389, 2, 2, nil, "Favored pet of the goblins of Booty Bay, this colorful bird is renowned for its ability to count coins, tally budgets, and lie about contracts.", 2, "|cFFFFD200Vendor: |rNarkk|n|cFFFFD200Zone: |rStranglethorn Vale|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t|n|n|cFFFFD200Vendor: |rDealer Rashaad|n|cFFFFD200Zone: |rNetherstorm|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10685] = { 7394, 2, 2, nil, "It thinks you taste like chicken too.", 2, "|cFFFFD200Vendor: |rMagus Tirth|n|cFFFFD200Zone: |rThousand Needles|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [10688] = { 7395, 4, 2, nil, "The cockroach is usually an impulse buy. Its owners have a hard time parting with it even after numerous attempts.", 2, "|cFFFFD200Vendor: |rDealer Rashaad|n|cFFFFD200Zone: |rNetherstorm|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t|n|n|cFFFFD200Vendor: |rJeremiah Payson|n|cFFFFD200Zone: |rUndercity|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10695] = { 7543, 1, 2, nil, "Hopes that someday it can grow up to destroy villages, just like mommy.", 0, "|cFFFFD200Drop: |rWhelplings|n|cFFFFD200Zone: |rDustwallow Marsh, Badlands" },
+ [10696] = { 7547, 1, 2, nil, "Like other members of the blue dragonflight, these whimsical little critters display an affinity to arcane magic.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Zone: |rAzshara" },
+ [10697] = { 7544, 1, 2, nil, "Hailing from the Wetlands, this young dragon is just learning how to hunt, breathe fire, and go on cute destructive rampages.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Zone: |rWetlands" },
+ [10698] = { 7545, 1, 2, nil, "Once bred and raised by the green dragon Itharius, this unique breed of dragon has settled in the jungles of Feralas.", 0, "|cFFFFD200Drop: |rAdolescent Whelp, Dreaming Whelp|n|cFFFFD200Zone: |rSwamp of Sorrows" },
+ [10703] = { 7550, 8, 2, nil, "Vendors at the Darkmoon Faire offer strange and exotic wonders. They also sell wood frogs.", 2, "|cFFFFD200Vendor: |rFlik|n|cFFFFD200Event: |rDarkmoon Faire|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [10704] = { 7549, 8, 2, nil, "Known for their powerful legs and keen eyesight.", 2, "|cFFFFD200Vendor: |rFlik|n|cFFFFD200Event: |rDarkmoon Faire|n|cFFFFD200Cost: |r1|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [10706] = { 7555, 2, 2, nil, "This dark-feathered bird of prey is often seen as a harbinger of doom amongst the druids of Teldrassil.", 2, "|cFFFFD200Vendor: |rShylenai|n|cFFFFD200Zone: |rDarnassus|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10707] = { 7553, 2, 2, nil, "Night elf children are given an owl feather on their first birthday as a token of good luck.", 2, "|cFFFFD200Vendor: |rShylenai|n|cFFFFD200Zone: |rDarnassus|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10708] = { 7554, 2, 26, nil, "Winterspring's furbolg prize the brilliant white feathers of these majestic creatures for ritual purposes.", 11, "" },
+ [10709] = { 14421, 4, 2, nil, "A complex system of burrows protects the prairie dog from its neighbors' massive hooves.", 2, "|cFFFFD200Vendor: |rHalpa|n|cFFFFD200Zone: |rThunder Bluff|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10711] = { 7560, 4, 2, nil, "If you go chasing snowshoe rabbits, you know you're going to fall.", 2, "|cFFFFD200Vendor: |rYarlyn Amberstill|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r20|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10713] = { 7561, 7, 2, nil, "A favored companion of Kirin Tor magi, this reptile makes one wonder whether it's truly a snake or something else altogether.", 2, "|cFFFFD200Vendor: |rBreanni|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [10714] = { 7565, 7, 2, nil, "Xan'tish fearlessly tracks, captures, and trains these noble snakes so they will aid their comrades in battle.", 2, "|cFFFFD200Vendor: |rXan'tish|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10716] = { 7562, 7, 2, nil, "The brown snake is native to Horde-controlled territories. It seems unremarkable until its victims are within range.", 2, "|cFFFFD200Vendor: |rXan'tish|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [10717] = { 7567, 7, 2, nil, "The crimson snake is favored among the Darkspear trolls for not only its vicious nature, but also its steadfast loyalty.", 2, "|cFFFFD200Vendor: |rDealer Rashaad|n|cFFFFD200Zone: |rNetherstorm|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t|n|n|cFFFFD200Vendor: |rXan'tish|n|cFFFFD200Zone: |rOrgrimmar|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [12243] = { 8376, 9, 18, nil, "Not to worry; the combat mechanisms and homing logic have been disabled on this unit, I think. --Oglethorpe Obnoticus", 1, "|cFFFFD200Quest:|r An OOX of Your Own|n|cFFFFD200Zone:|r Booty Bay|n" },
+ [13548] = { 30379, 2, 18, nil, "Don't call this bird chicken unless you want egg on your face.", 1, "|cFFFFD200Quest: |rCLUCK!|n|cFFFFD200Zone: |rWestfall" },
+ [15048] = { 9656, 9, 2, nil, "Possibly explosive, definitely adorable. Keep away from open flame.", 3, "|cFFFFD200Profession: |rEngineering" },
+ [15049] = { 9657, 9, 2, nil, "This tiny peacekeeper security bot is often outfitted with the latest arcane nullifiers and crowd pummelers. Not for the environmentally minded.", 3, "|cFFFFD200Profession: |rEngineering" },
+ [15067] = { 9662, 1, 2, nil, "These adorable critters love snuggling with their owners after a long day of brutal, bloody battle.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Zone: |rFeralas" },
+ [15999] = { 10259, 7, 2050, nil, "Worgs are the favored companions of orcs and are fiercely loyal on the battlefield.", 1, "|cFFFFD200Quest: |rKibler's Exotic Pets|n|cFFFFD200Zone: |rLower Blackrock Spire" },
+ [16450] = { 10598, 7, 2, nil, "Although born in the warm heights of Blackrock Spire, the smolderweb hatchling can survive most environments.", 1, "|cFFFFD200Quest: |rEn-Ay-Es-Tee-Why|n|cFFFFD200Zone: |rLower Blackrock Spire" },
+ [17707] = { 11325, 7, 82, nil, "This pint-sized panda cub is highly sought after by adventurers around Azeroth - despite its narcoleptic nature.", 7, "|cFFFFD200Promotion:|r World of Warcraft Collector's Edition" },
+ [17708] = { 11326, 5, 82, nil, "AND THE HEAVENS SHALL TREMBLE, AND MAN SHALL WEEP, AND THE END OF DAYS SHALL--wait, why are you so tall?", 7, "|cFFFFD200Promotion:|r World of Warcraft Collectors Edition" },
+ [17709] = { 11327, 4, 82, nil, "A small, fierce member of the Zerg swarm, the zergling is not to be trifled with. Very dangerous in high numbers.", 7, "|cFFFFD200Promotion:|r World of Warcraft Collectors Edition" },
+ [19772] = { 12419, 9, 2, nil, "Talented engineers have crafted an exact replica of a wood frog, at only ten times the cost of buying the real thing.", 3, "|cFFFFD200Profession: |rEngineering" },
+ [23811] = { 14878, 8, 18, nil, "The legendary ale frog was believed to be extinct until the strange brews at the Darkmoon Faire brought them out of hiding.", 6, "|cFFFFD200World Event:|r Darkmoon Faire" },
+ [24696] = { 15186, 0, 82, nil, "Appearances of this beloved and ultra-reclusive murloc are sure to draw a crowd.", 7, "|cFFFFD200Promotion:|r BlizzCon 2005" },
+ [24988] = { 15358, 0, 16466, nil, "Lurky's charming disposition and lively antics are guaranteed to soothe frustration and lighten even the darkest moods.", 7, "|cFFFFD200Promotion:|r Burning Crusade Collector's Edition (EU only)|n" },
+ [25162] = { 15429, 5, 2, nil, "Warning: Wear gloves and goggles while handling disgusting oozeling. DO NOT WASH. Will stain most armor.", 0, "|cFFFFD200Drop:|r World Drop|n|cFFFFD200Creature:|r Oozes and Slimes" },
+ [26010] = { 15699, 9, 2, nil, "This clever toy is equipped with a secret switch to disable its 'tranquil' mode. Luckily, even the engineers who built it don't know how to find it.", 3, "|cFFFFD200Profession: |rEngineering" },
+ [26045] = { 15710, 6, 2, nil, "He loves to dance and wave and play with you! He melts after Winter Veil, but don't worry; he'll be back again someday!", 6, "|cFFFFD200World Event:|r Feast of Winter Veil|n" },
+ [26529] = { 15706, 4, 2, nil, "Would rather not spend any more time inside a wrapped holiday gift box.", 6, "|cFFFFD200World Event:|r Feast of Winter Veil" },
+ [26533] = { 15698, 0, 2, nil, "These off-season allies work tirelessly to prepare for the Feast of Winter Veil, and would appreciate not being disturbed the rest of the year.", 6, "|cFFFFD200World Event:|r Feast of Winter Veil" },
+ [26541] = { 15705, 0, 2, nil, "During the majestic Winter Veil celebration, these faithful companions spread joy throughout the land. The rest of the year, they are busy.", 6, "|cFFFFD200World Event:|r Feast of Winter Veil" },
+ [27241] = { 16069, 0, 16466, nil, "Gurky is a born performer, though she remains elusive, making public appearances only on the rarest of occasions.", 7, "|cFFFFD200Promotion:|r EU Fansite Promotion|n" },
+ [27570] = { 16085, 0, 2, nil, "The power of love can be a dangerous thing, especially when wielded by an airborne, ill-tempered goblin.", 6, "|cFFFFD200World Event:|r Love is in the Air|n|cFFFFD200Vendor: |rLovely Merchant|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\INV_ValentinesCard01:0|t|n" },
+ [28505] = { 16456, 7, 16466, nil, "This cub shares many traits with its relatives, but it's in a league of its own with its singing ability.", 7, "|cFFFFD200Promotion:|r iCoke" },
+ [28738] = { 16547, 8, 2, nil, "Slow and steady wins the race.", 6, "|cFFFFD200World Event:|r Children's Week" },
+ [28739] = { 16548, 4, 2, nil, "Many assume 'Wiggles' refers to the motion of this pig's posterior. They discover the true meaning when they see their own entrails.", 6, "|cFFFFD200World Event:|r Children's Week" },
+ [28740] = { 16549, 4, 2, nil, "A clever tactician, Whiskers will feign death to fool predators and bill collectors.", 6, "|cFFFFD200World Event:|r Children's Week" },
+ [28871] = { 16701, 6, 2, nil, "This little piece of summer smells of warm evenings and lightning storms. If you look closely, you might be able to make out a face.", 6, "|cFFFFD200World Event:|r Midsummer Fire Festival|n|cFFFFD200Vendor: |rMidsummer Merchant|n|cFFFFD200Cost: |r350|TINTERFACE\\ICONS\\INV_SummerFest_FireFlower:0|t" },
+ [30156] = { 17255, 2, 2, nil, "After a harpy incursion in Feralas, several hippogryph nesting grounds were attacked. Hundreds of hatchlings were orphaned, and they have been entrusted to the care of the druids on Sardor Isle.", 8, "|cFFFFD200Trading Card Game:|r Heroes of Azeroth" },
+ [32298] = { 18381, 1, 82, nil, "Offspring of the nether drakes of Outland, this young dragon is still growing into its role as hunter, protector, and adorable terror of the skies.", 7, "|cFFFFD200Promotion:|r Burning Crusade Collector's Edition" },
+ [33050] = { 18839, 8, 2, nil, "Only magical wishes make magical crawdads. If you wish for a fish, fish for a wish.", 3, "|cFFFFD200Fishing:|r Fishing (430)|n|cFFFFD200Zone:|r Terokkar Forest (Fishing Nodes)" },
+ [35156] = { 20408, 5, 2, nil, "The ethereal Rashaad began breeding these enigmatic creatures after learning of their ability to feed on arcane energies.", 2, "|cFFFFD200Vendor: |rDealer Rashaad|n|cFFFFD200Zone: |rNetherstorm|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35239] = { 20472, 4, 2, nil, "This bunny is the cutest thing its opponent will ever see. It'll also be the last.", 2, "|cFFFFD200Vendor: |rDealer Rashaad|n|cFFFFD200Zone: |rNetherstorm|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35907] = { 21010, 2, 2, nil, "Delicate denizen of Azuremyst Isle, this magical insect has been rumored to possess healing abilities.", 2, "|cFFFFD200Vendor: |rSixx|n|cFFFFD200Zone: |rThe Exodar|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [35909] = { 21009, 2, 2, nil, "A product of exposure to wild magical energies, this insect is the unexpectedly colorful result of Azerothian moth species being released into the eco-domes of the Netherstorm.", 2, "|cFFFFD200Vendor: |rDealer Rashaad|n|cFFFFD200Zone: |rNetherstorm|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [35910] = { 21008, 2, 2, nil, "Prefers flying during thunderstorms, which are common in Azuremyst Isle.", 2, "|cFFFFD200Vendor: |rSixx|n|cFFFFD200Zone: |rThe Exodar|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [35911] = { 21018, 2, 2, nil, "Prefers flying during foggy nights, which are common in Azuremyst Isle.", 2, "|cFFFFD200Vendor: |rSixx|n|cFFFFD200Zone: |rThe Exodar|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [36027] = { 21055, 1, 2, nil, "Once prized by dragonhawk breeders in Silvermoon. Stands out at night to predators, making it necessary to gain awareness at an early age.", 2, "|cFFFFD200Vendor: |rJilanne|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [36028] = { 21064, 1, 2, nil, "Mortal enemies with silver dragonhawks; on pretty good terms with golden dragonhawks.", 2, "|cFFFFD200Vendor: |rJilanne|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [36029] = { 21063, 1, 2, nil, "Ferocious species of dragonhawk, often seen diving down on prey from high in the sky.", 2, "|cFFFFD200Vendor: |rJilanne|n|cFFFFD200Zone: |rEversong Woods|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-SILVERICON.BLP:0|t" },
+ [36031] = { 21056, 1, 2, nil, "Raised by the few dealers still willing to trade in Netherstorm.", 2, "|cFFFFD200Vendor: |rDealer Rashaad|n|cFFFFD200Zone: |rNetherstorm|n|cFFFFD200Cost: |r10|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [36034] = { 21076, 2, 2, nil, "Commonly used by sporelings as a light source as they navigate the waterways of Zangarmarsh, this luminescent insect helps them avoid the dangerous bogflare needlers.", 0, "|cFFFFD200Drop: |rBogflare Needler|n|cFFFFD200Zone: |rZangarmarsh" },
+ [39181] = { 22445, 2, 18, nil, "The magical energies released during Skywing's transformation back into his true arakkoa form liberated this poor creature, a fledgling bird imprisoned by Luanga in order to power his fowl curse.", 1, "|cFFFFD200Quest:|r Skywing|n|cFFFFD200Zone:|r Terokkar Forest|n|n" },
+ [39709] = { 22943, 4, 2, nil, "Many a drunken Brewfest goer has told the tale of this mysterious creature, but few remember that tale in the morning.", 6, "|cFFFFD200World Event:|r Brewfest" },
+ [40405] = { 23198, 4, 16466, nil, "Lucky squeals when he's angry or when he's eating. Either way, you don't want to bother him.", 7, "|cFFFFD200Promotion:|r World Wide Invitational 2007|n" },
+ [40549] = { 23234, 7, 2, nil, "Although loath to part with Bananas, Landro realized it would be safer if the pet was far from Booty Bay when his father, King Mukla, discovered he was missing.", 8, "|cFFFFD200Trading Card Game:|r Through the Dark Portal" },
+ [40613] = { 23231, 5, 2, nil, "He's slightly evil but extremely narcoleptic. Keep him away from critters!", 6, "|cFFFFD200World Event:|r Children's Week" },
+ [40614] = { 23258, 4, 2, nil, "The sin'dorei use hawkstriders as mounts, but this little one was deemed unfit after being diagnosed with numerous anxiety disorders.", 6, "|cFFFFD200World Event:|r Children's Week" },
+ [40634] = { 23266, 4, 2, nil, "Domesticated elekks are trained as mounts by the draenei people. In the wild they are hunted for their ivory tusks.", 6, "|cFFFFD200World Event:|r Children's Week" },
+ [40990] = { 23274, 4, 82, nil, "Stinker uses his aroma to repel predators and attract cats. He's much more successful with the former than the latter.", 5, "|cFFFFD200Achievement: |rShop Smart, Shop Pet...Smart|n|cFFFFD200Category: |rGeneral" },
+ [42609] = { 23909, 6, 2, nil, "This mischievous gourd likes to pop out of the ground and scare you right out of your pants. Hail to the pumpkin song!", 6, "|cFFFFD200World Event:|r Hallow's End" },
+ [43697] = { 24388, 8, 2, nil, "The crocolisk's hide and meat are prized by hunters, but the beast is ferocious and seldom taken by surprise.", 3, "|cFFFFD200Profession: |rFishing|n|cFFFFD200Zone: |rShattrath" },
+ [43698] = { 24389, 8, 2, nil, "Muckbreath is just like other crocolisks, except he has an excellent sense of humor.", 3, "|cFFFFD200Profession:|r Fishing|n|cFFFFD200Zone:|r Shattrath" },
+ [43918] = { 24480, 8, 2, nil, "Amani witch doctors have turned many victims into frogs. Some never recover.", 0, "|cFFFFD200Drop:|r Forest Frog|n|cFFFFD200Zone:|r Zul'Aman" },
+ [44369] = { 24753, 4, 2, nil, "A shy creature, the pink elekk will only make its presence known to those that have befriended it, or to anyone too intoxicated to harm it.", 6, "|cFFFFD200World Event:|r Brewfest|n|cFFFFD200Vendor: |rBelbi Quikswitch|n|cFFFFD200Zone: |rDun Morogh|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\INV_Misc_Coin_01:0|t|n|n|cFFFFD200Vendor: |rBliz Fixwidget|n|cFFFFD200Zone: |rDurotar|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\INV_Misc_Coin_01:0|t" },
+ [45082] = { 25062, 2, 2, nil, "Wild sporebats are vicious creatures, but if tamed at a young age, they can be loyal companions.", 2, "|cFFFFD200Vendor: |rMycah|n|cFFFFD200Zone: |rZangarmarsh|n|cFFFFD200Faction: |rSporeggar - Exalted|n|cFFFFD200Cost: |r30|TINTERFACE\\ICONS\\INV_Mushroom_02:0|t" },
+ [45125] = { 25109, 9, 2, nil, "When the Burning Legion invaded Outland, Landro Longshot contributed this design to the war effort. Both the Alliance and Horde passed on production.", 8, "|cFFFFD200Trading Card Game:|r March of the Legion" },
+ [45127] = { 25110, 2, 2050, nil, "Much quieter than a real dragon.", 8, "|cFFFFD200Trading Card Game:|r March of the Legion" },
+ [45174] = { 25146, 4, 82, nil, "This golden pig honors its ancestors by giving its allies good fortune.", 7, "|cFFFFD200Promotion:|r Recruit-A-Friend|n" },
+ [45175] = { 25147, 4, 82, nil, "Druids think silver pigs have a special bond with golden pigs, as both breeds appeared on Azeroth at the same time.", 7, "|cFFFFD200Promotion:|r Recruit-A-Friend" },
+ [45890] = { 25706, 6, 2, nil, "Elder shaman have warned that wisps of Ragnaros, loyal servants of the Firelord, have taken to the Molten Front. Vigilance is advised.", 11, "" },
+ [46425] = { 26050, 8, 2, nil, "Crocolisks are six-legged amphibian predators found in coastal waters. Archaeologists believe they may be descended from the diemetradons of Un'Goro Crater.", 3, "|cFFFFD200Profession: |rFishing|n|cFFFFD200Zone: |rShattrath" },
+ [46426] = { 26056, 8, 2, nil, "After studying the crocolisk species for several years, the famed angler Nat Pagle determined that baby crocolisks are mostly harmless.", 3, "|cFFFFD200Profession: |rFishing|n|cFFFFD200Zone: |rShattrath" },
+ [46599] = { 26119, 6, 2, nil, "A child of phoenix god Al'ar, the hatchling takes flight immediately after birth.", 0, "|cFFFFD200Drop: |rKael'thas Sunstrider|n|cFFFFD200Zone: |rMagisters' Terrace" },
+ [48406] = { 27217, 1, 16402, nil, "These stunning creatures, awarded to heroes for victory in battle, represent bravery and strength.", 7, "|cFFFFD200Promotion:|r Battleground Event" },
+ [48408] = { 27346, 1, 16402, nil, "Only heroes who have proven their valor through great acts are gifted one of these mystical serpentine creatures.", 7, "|cFFFFD200Promotion:|r China PVP Event" },
+ [49964] = { 27914, 5, 2, nil, "Ethereal soul-traders will travel with those who can free the essences they need from unruly hosts.", 8, "|cFFFFD200Trading Card Game:|r The Hunt for Illidan" },
+ [51716] = { 28470, 2, 2, nil, "Native to the Terokkar Forest, nether rays spawn during the rainy season. Out of the hundreds of offspring resulting from one hatching, very few survive to adulthood.", 2, "|cFFFFD200Vendor: |rGrella|n|cFFFFD200Zone: |rTerokkar Forest|n|cFFFFD200Faction: |rSha'tari Skyguard - Exalted|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [51851] = { 28513, 3, 16402, nil, "Vampire bats have been trained as scouts and mounts by the Forsaken of the Undercity and the Amani trolls of Zul'Aman.", 0, "|cFFFFD200Drop: |rPrince Tenris Mirkblood|n|cFFFFD200Zone: |rKarazhan" },
+ [52615] = { 28883, 3, 82, nil, "Accidentally created when the Lich King's deathly powers raised Rimefang from undeath.", 7, "|cFFFFD200Promotion:|r Wrath of the Lich King Collector's Edition" },
+ [53082] = { 29089, 0, 16466, nil, "'I am Justice itself!'", 7, "|cFFFFD200Promotion:|r World Wide Invitational 2008" },
+ [53316] = { 29147, 3, 2, nil, "When it senses the impending death of a nearby creature, the skull will emit an ear-piercing cackle.", 2, "|cFFFFD200Vendor: |rDarahir|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r40|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [54187] = { 24968, 9, 2, nil, "This mechanical warrior is a favorite gift during Winter Veil. Batteries not included, but rockets definitely are!", 6, "|cFFFFD200World Event:|r Feast of Winter Veil" },
+ [55068] = { 29726, 8, 16466, nil, "Mr. Chilly's appearance evokes a more civilized era, when duelists battled in tuxedos.", 7, "|cFFFFD200Promotion:|r WoW/Blizzard Account Merger" },
+ [59250] = { 31575, 4, 2, nil, "You wouldn't want to meet this rat in a dark alley. Or anywhere else, for that matter.", 3, "|cFFFFD200Profession:|r Fishing|n|cFFFFD200Zone:|r Dalaran" },
+ [61348] = { 32589, 2, 2, nil, "Unlike most of Geen the gorloc's 'mysterious eggs,' this one actually contained a living thing.", 0, "|cFFFFD200Drop: |rMysterious Egg" },
+ [61349] = { 32590, 2, 2, nil, "The rare albino tickbird is revered amongst the Oracles. It is seen as a sign of heavy rainstorms, good grub harvests, and hibernating wolvar.", 0, "|cFFFFD200Drop: |rMysterious Egg" },
+ [61350] = { 32592, 1, 2, nil, "These voracious whelps grow at an incredible rate and can devour nearly twice their weight in raw meat per day.", 0, "|cFFFFD200Drop: |rMysterious Egg" },
+ [61351] = { 32591, 7, 2, nil, "The cobras of Azeroth are renowned for their fast-acting venom and swift strikes.", 0, "|cFFFFD200Drop: |rMysterious Egg" },
+ [61357] = { 32595, 8, 2, nil, "Notable for its wide eyes and sassy slide.", 2, "|cFFFFD200Vendor: |rSairuk|n|cFFFFD200Zone: |rDragonblight|n|cFFFFD200Faction: |rThe Kalu'ak - Exalted|n|cFFFFD200Cost: |r12|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t|n|n|cFFFFD200Vendor: |rTanaika|n|cFFFFD200Zone: |rHowling Fjord|n|cFFFFD200Faction: |rThe Kalu'ak - Exalted|n|cFFFFD200Cost: |r12|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [61472] = { 32643, 6, 82, nil, "To prevent apprentices from secretly using the familiar to complete their chores, the archmagi placed the ritual required to summon the creature across the pages of several spellbooks hidden throughout Dalaran.", 5, "|cFFFFD200Achievement:|r Higher Learning|n|cFFFFD200Category: |rGeneral" },
+ [61725] = { 32791, 4, 2, nil, "Spring rabbits symbolize fertility, and they love to demonstrate why.", 6, "|cFFFFD200World Event:|r Noblegarden|n|cFFFFD200Vendor: |rNoblegarden Merchant|n|cFFFFD200Cost: |r100|TINTERFACE\\ICONS\\Achievement_Noblegarden_Chocolate_Egg:0|t" },
+ [61773] = { 32818, 2, 82, nil, "He's humble, clairvoyant, and delicious with cranberry sauce.", 6, "|cFFFFD200World Event:|r Pilgrim's Bounty" },
+ [61855] = { 32841, 7, 16466, nil, "This rare bear is bred for its small size and perpetually youthful appearance.", 5, "|cFFFFD200Achievement: |rWoW's 4th Anniversary|n|cFFFFD200Category: |rFeats of Strength" },
+ [61991] = { 32939, 4, 82, nil, "She likes the company of friends. Seventy-five of them, to be precise.", 5, "|cFFFFD200Achievement: |rLil' Game Hunter|n|cFFFFD200Category: |rGeneral" },
+ [62491] = { 33188, 6, 2, nil, "It always wakes up dancing. What a happy little tree!", 2, "|cFFFFD200Vendor: |rRook Hawkfist|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [62508] = { 33194, 7, 2, nil, "The bears of Dun Morogh are prized for their hardiness, loyalty, and combat prowess. Their time among the dwarves does little to curb their ferocity.", 2, "|cFFFFD200Vendor: |rDerrick Brindlebeard|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t " },
+ [62510] = { 33197, 2, 2, nil, "Can sense an opponent's heartbeat through echolocation.", 2, "|cFFFFD200Vendor: |rEliza Killian|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t" },
+ [62513] = { 33198, 7, 2, nil, "The volatile Durotar scorpion is a fierce desert predator. It's known for only occasionally stinging its owner.", 2, "|cFFFFD200Vendor: |rFreka Bloodaxe|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t " },
+ [62516] = { 33200, 4, 2, nil, "Unlike the polymorphed variety, trueborn sheep can be quite scary.", 2, "|cFFFFD200Vendor: |rCorporal Arthur Flew|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t " },
+ [62542] = { 33219, 4, 2, nil, "A tallstrider's speed on open ground is unrivaled, so predators rely on stealth and pack tactics when hunting them.", 2, "|cFFFFD200Vendor: |rDoru Thunderhorn|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t " },
+ [62561] = { 33226, 8, 2, nil, "The strand crawler has adapted to the northern cold by hibernating under the island beaches.", 3, "|cFFFFD200Profession: |rFishing|n|cFFFFD200Zone: |rDalaran" },
+ [62562] = { 33205, 6, 2, nil, "The long-term effects of the contamination caused by the Exodar are still under observation.", 2, "|cFFFFD200Vendor: |rIrisee|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t |n" },
+ [62564] = { 33227, 5, 2, nil, "Created by a blood elf apprentice, this broom would be extremely useful if anyone could figure out how to make it stop.", 2, "|cFFFFD200Vendor: |rTrellis Morningsun|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t " },
+ [62609] = { 33238, 0, 659, nil, "Many of Stormwind's youth joined the Argent Crusade as squires, hoping to one day serve the Holy Light in battle.", 1, "|cFFFFD200Quest:|r A Champion Rises|n|cFFFFD200Zone:|r Icecrown" },
+ [62674] = { 33274, 9, 2, nil, "Used as a training mount for young gnomes, the Mechanopeep has none of the speed but all of the charm of its bigger brothers.", 2, "|cFFFFD200Vendor: |rRillie Spindlenut|n|cFFFFD200Zone: |rIcecrown" },
+ [62746] = { 33239, 0, 403, nil, "These young orcs strive to embody the Argent Crusade's virtues: valor, strength, and integrity.", 1, "|cFFFFD200Quest:|r A Champion Rises|n|cFFFFD200Zone:|r Icecrown" },
+ [63318] = { 33578, 0, 16466, nil, "Murkimus is the most fearsome of all murloc arena warriors. And for a time, the only one.", 7, "|cFFFFD200Promotion:|r Arena Tournament" },
+ [63712] = { 33810, 3, 2050, nil, "This is actually three pets in one. You won't know what you'll get until you summon it!", 2, "|cFFFFD200Vendor: |rSamamba|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t|n" },
+ [65358] = { 34364, 7, 2, nil, "Many have mocked this cat before battle. Their last words are usually, 'Here, kitty, kitty.'", 2, "|cFFFFD200Vendor: |rBreanni|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [65381] = { 33530, 0, 18, nil, "The bouncy gurloc is a distant cousin of the murloc.", 6, "|cFFFFD200World Event:|r Children's Week" },
+ [65382] = { 33529, 0, 18, nil, "Despite their smaller stature, the wolvar are some of the most aggressive creatures in Northrend. This often makes their curious nature intimidating.", 6, "|cFFFFD200World Event:|r Children's Week" },
+ [65682] = { 34587, 9, 16466, nil, "A fighting robot fueled by delicious red or blue electrolytes.", 7, "|cFFFFD200Promotion:|r Mountain Dew Promotion" },
+ [66030] = { 34694, 0, 82, nil, "Only the rarest of murlocs is able to successfully cross dimensional barriers. Fewer still return with the kind of advanced armor and weaponry possessed by Grunty.", 7, "|cFFFFD200Promotion:|r BlizzCon 2009" },
+ [66096] = { 34724, 5, 2, nil, "This extremely rare species of mana wyrm is prized for its unique coloration and even temperament.", 2, "|cFFFFD200Vendor: |rHiren Loresong|n|cFFFFD200Zone: |rIcecrown|n|cFFFFD200Faction: |rThe Silver Covenant - Exalted|n|cFFFFD200Cost: |r40|TINTERFACE\\ICONS\\Ability_Paladin_ArtofWar.blp:0|t|n" },
+ [66520] = { 34930, 5, 82, nil, "Considered to be lucky, tigers carved of jade are prized, but taming a jade tiger itself is an even greater prize.", 7, "|cFFFFD200Promotion:|r Recruit-A-Friend" },
+ [67413] = { 35396, 7, 2, nil, "Rarely leaves nest until maturity. Enjoys conducting pranks on the nearby Razormaw nest.", 0, "|cFFFFD200Drop: |rDart's Nest|n|cFFFFD200Zone: |rDustwallow Marsh" },
+ [67414] = { 35395, 7, 2, nil, "The demand for this purple-skinned breed has drastically reduced the raptor population near the Wailing Caverns.", 0, "|cFFFFD200Drop: |rDeviate Guardian, Deviate Ravager|n|cFFFFD200Zone: |rWailing Caverns" },
+ [67415] = { 35400, 7, 2, nil, "This crafty raptor was born in the icy wastes of Northrend. Its harsh upbringing gives it strength when many others would perish.", 0, "|cFFFFD200Drop: |rGundrak Raptor|n|cFFFFD200Zone: |rZul'Drak" },
+ [67416] = { 35387, 7, 2, nil, "This offspring of Takk the Leaper is incredibly agile and can maneuver through difficult environments with ease.", 0, "|cFFFFD200Drop: |rTakk's Nest|n|cFFFFD200Zone: |rThe Barrens" },
+ [67417] = { 35399, 7, 2, nil, "This hatchling is coveted for its unique coloration. Despite Breanni's loving care, the raptor's feral nature cannot be restrained.", 2, "|cFFFFD200Vendor: |rBreanni|n|cFFFFD200Zone: |rDalaran|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [67418] = { 35397, 7, 2, nil, "Larger than other raptor infants, these hatchlings grow into towering reptilian beasts capable of massive devastation.", 0, "|cFFFFD200Drop: |rRavasaur Matriarch's Nest|n|cFFFFD200Zone: |rUn'goro Crater" },
+ [67419] = { 35398, 7, 2, nil, "Razormaw eggs are protected fiercely by their mothers. Their cuteness belies their inherited ferocity.", 0, "|cFFFFD200Drop: |rRazormaw Matriarch's Nest|n|cFFFFD200Zone: |rWetlands" },
+ [67420] = { 35394, 7, 2, nil, "Razzashi raptors are among the cleverest predators in the jungle, making them favored by the Gurubashi trolls.", 0, "|cFFFFD200Drop: |rWorld Drop|n|cFFFFD200Zone: |rStranglethorn Vale" },
+ [67527] = { 35468, 5, 16466, nil, "The onyx panther is invisible at night, like a stalking shadow, and disappears with the dawn.", 7, "|cFFFFD200Promotion:|r Korea World Event" },
+ [68767] = { 36482, 2, 2, nil, "Seeing these kites soaring above Northrend's shores is a clear sign that tuskarr are nearby.", 8, "|cFFFFD200Trading Card Game:|r Scourgewar" },
+ [68810] = { 36511, 5, 2, nil, "Affectionate and curious, these kittens seem to find the most peculiar hiding places.", 8, "|cFFFFD200Trading Card Game:|r Scourgewar" },
+ [69002] = { 36607, 1, 16466, nil, "Spawn of Onyxia, this young dragon has decided that it doesn't want to follow in its grandfathers' cataclysmic footsteps.", 5, "|cFFFFD200Achievement: |rWoW's 5th Anniversary|n|cFFFFD200Category: |rFeats of Strength" },
+ [69452] = { 36871, 6, 82, nil, "Found in the Firelands and the Molten Core, core hounds can trace their lineage back to the alpha male, Magmadar.", 7, "|cFFFFD200Promotion:|r Authenticator Account Link|n" },
+ [69535] = { 36908, 2, 82, nil, "A gift from the Wildhammer dwarves to the heroes of the Alliance, these hatchlings are descendants of the same gryphons ridden by Falstad and his entourage into Grim Batol.", 9, "|cFFFFD200In-Game Shop|r" },
+ [69536] = { 36909, 7, 82, nil, "Thorg No-Legs never paid heed to the old orc saying, 'Don't pull a wyvern's tail.'", 9, "|cFFFFD200In-Game Shop|r" },
+ [69539] = { 36910, 5, 82, nil, "A rarely seen family of tigers, many believe (perhaps incorrectly) that some residual magic or other event is responsible for the unique color.", 7, "|cFFFFD200Promotion:|r Recruit-A-Friend" },
+ [69541] = { 36911, 0, 82, nil, "The pandaren monk is a fearless traveling companion, constantly training in his art... unless you offer him a drink. He considers it rude to refuse.", 9, "|cFFFFD200In-Game Shop|r" },
+ [69677] = { 36979, 3, 82, nil, "In life, Kel'Thuzad betrayed the Kirin Tor and created the Cult of the Damned. In death, he serves the Scourge as the Archlich Lord of Naxxramas.", 9, "|cFFFFD200In-Game Shop|r" },
+ [70613] = { 37865, 4, 82, nil, "Although small, pugs are very dangerous when provoked.", 5, "|cFFFFD200Achievement: |rLooking For Multitudes|n|cFFFFD200Category: |rDungeons & Raids" },
+ [71840] = { 38374, 5, 2, nil, "This, um, 'adorable' little guy likes to play with critters. Just don't let them get too close.", 6, "|cFFFFD200World Event:|r Love is in the Air" },
+ [74932] = { 40198, 6, 2, nil, "While not nearly as destructive as its original master, the Ice Lord Ahune, the frostling has been known to lob chilly snowballs at unsuspecting adventurers.", 6, "|cFFFFD200World Event:|r Midsummer Fire Festival" },
+ [75134] = { 40295, 9, 2, nil, "Jepetto's top-selling toy this season, the bot's new blue chassis has children across Azeroth scrambling for this feisty battle bot.", 2, "|cFFFFD200Vendor: |rWorld Vendors|n|cFFFFD200Cost: |r50|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+ [75613] = { 40624, 1, 82, nil, "Breanni in Dalaran, one of the world's foremost collectors of exotic pets, will only award the mystical Celestial Dragon to fellow collectors whose obsession rivals her own.", 5, "|cFFFFD200Achievement: |rLittlest Pet Shop|n|cFFFFD200Category: |rPet Battles" },
+ [75906] = { 40703, 9, 82, nil, "Modeled after the huge titan prototype in Ulduar, this volatile little construct plays hard with its toys.", 9, "|cFFFFD200In-Game Shop|r" },
+ [75936] = { 40721, 0, 34, nil, "Murkimus is the most fearsome of all murloc arena warriors. And the only one.", 11, "" },
+ [78381] = { 42078, 9, 82, nil, "Powerful artillery of the Terran army. The Thor is always the first one in and the last one out!", 7, "|cFFFFD200Promotion:|r StarCraft II: Wings of Liberty Collector's Edition" },
+ [23531] = { 14755, 1, 16466, nil, "Legends say that these vibrant emerald beings bring good fortune and safe travels to their owners.", 7, "|cFFFFD200Promotion:|r iCoke China" },
+ [23530] = { 14756, 1, 16466, nil, "Said to bring good fortune when spotted in autumn. Known to hide from unfamiliar creatures.", 7, "|cFFFFD200Promotion:|r iCoke China" },
+ [28487] = { 16445, 0, 82, nil, "Terky is as shy as she is adorable, preferring the company of close friends over new ones.", 0, "|cFFFFD200Drop:|r White Murloc Egg" },
+ [25018] = { 15361, 0, 16466, nil, "Murki is one of the world's rarest species of murloc.", 7, "|cFFFFD200Promotion:|r Korean Promotional Event" },
+ [66175] = { 34770, 3, 2, nil, "These possessed puppets are given a semblance of life by whimsical spirits of the departed.", 6, "|cFFFFD200World Event:|r Day of the Dead" },
+ [10699] = { 7546, 1, 2, nil, "The Bronze Whelpling was lost in time until an unusual event allowed her to escape.", 0, "|cFFFFD200Drop: |rIron Horde|n|cFFFFD200Zone: |rBlasted Lands" },
+ [30152] = { 17254, 7, 66, nil, "One of Ash'alah's litter, entrusted to you by the Dreamweavers.", 2, "|cFFFFD200Vendor: |rSylvia Hartshorn|n|cFFFFD200Zone: |rVal'sharah|n|cFFFFD200Faction: |rDreamweavers - Revered|n|cFFFFD200Cost: |r500|TINTERFACE\\MONEYFRAME\\UI-GOLDICON.BLP:0|t" },
+
+ [15648] = { 9936, 7, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+ [23428] = { 14633, 7, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+ [23429] = { 14629, 7, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+ [23430] = { 14631, 7, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+ [23431] = { 14630, 7, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+ [23432] = { 14632, 7, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+ [25849] = { 15661, 8, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+ [62514] = { 33199, 9, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+ [64351] = { 34031, 9, 2, nil, nil, 9, "|cFFFFD200In-Game Shop|r" },
+};
+
+-- ---------------------------------------------------------------------------
+-- Lookup: kind is "MOUNT" or "CRITTER" (the GetNumCompanions/GetCompanionInfo filter names).
+-- Returns name, lore, sourceText — any of which may be nil when the DB has no row for that spell.
+-- ---------------------------------------------------------------------------
+function NE.collections.GetInfo(kind, spellID)
+ if not spellID then return nil end
+ local t = (kind == "CRITTER") and NE.collections.PetData or NE.collections.MountData
+ local row = t and t[spellID]
+ if not row then return nil end
+ return row[4], row[5], row[7]
+end
+
+-- Mount type/flags bitfields (row[2]/row[3]) — EZCollections' non-blizzlike encoding, used for the
+-- type filter (Ground/Flying/Aquatic) and for the location-aware "usable right now" test:
+-- type 0x1 = flying-only 0x2 = can go underwater (aquatic) 0x4 = scripted ground/flying hybrid
+-- flags 0x2 = self-mount (others unused here)
+-- Returns type, flags (both default 0 when the mount has no data row).
+function NE.collections.GetMountType(spellID)
+ if not spellID then return 0, 0 end
+ local row = NE.collections.MountData and NE.collections.MountData[spellID]
+ if not row then return 0, 0 end
+ return row[2] or 0, row[3] or 0
+end
+
+-- ---------------------------------------------------------------------------
+-- Faction-specific mounts (ported from EZCollections Data/Mounts.lua Set(8,...)): the racial mounts
+-- that show a Horde/Alliance crest watermark on their list row. 0 = Horde, 1 = Alliance.
+-- ---------------------------------------------------------------------------
+NE.collections.MountFaction = {}
+do
+ local horde = { 61447, 68188, 16081, 64658, 22722, 22721, 22724, 16084, 18990, 61230, 60119, 64659, 18989, 61467, 65639, 65645, 68056, 17465, 23251, 59788, 59793, 66846, 6654, 8395, 23509, 32243, 32246, 63641, 63643, 6653, 22718, 23243, 32245, 32297, 55531, 63640, 63642, 10796, 32244, 35020, 59797, 60116, 17450, 23250, 32296, 35027, 61469, 61997, 17463, 64977, 65641, 66091, 23241, 63635, 580, 17462, 18991, 23246, 32295, 35028, 65644, 18992, 23242, 23252, 33660, 35018, 35022, 66088, 23248, 23249, 34795, 64657, 65646, 10799, 16080, 17464, 23247, 35025 }
+ local alliance = { 16055, 61425, 16056, 60424, 17229, 16083, 458, 32290, 23225, 61470, 68187, 59799, 61996, 6899, 10789, 32240, 59791, 66090, 6898, 8394, 32292, 61465, 65637, 6648, 63638, 470, 63232, 10793, 10873, 16082, 17459, 17460, 22723, 32235, 32239, 48027, 63637, 17454, 22720, 23222, 23239, 32242, 35711, 60114, 472, 10969, 23221, 23240, 23338, 35710, 35714, 66847, 68057, 6777, 17461, 23219, 23227, 32289, 59785, 15779, 22717, 23510, 61229, 63636, 63639, 65638, 65640, 17453, 22719, 23223, 23229, 23238, 34406, 35712, 35713, 65642, 65643, 66087, 23228, 60118 }
+ for _, id in ipairs(horde) do NE.collections.MountFaction[id] = 0 end
+ for _, id in ipairs(alliance) do NE.collections.MountFaction[id] = 1 end
+end
+function NE.collections.GetFaction(spellID)
+ return spellID and NE.collections.MountFaction[spellID] or nil
+end
+
+-- ---------------------------------------------------------------------------
+-- Source-type labels + accessor. The data row[6] is a 0-based source type; EZCollections' journal
+-- maps it to BATTLE_PET_SOURCE_[type+1] (nil -> 12 "Other"). Those FrameXML strings don't exist on
+-- 3.3.5a, so ship our own copy (used with a _G fallback for localized clients).
+-- ---------------------------------------------------------------------------
+NE.collections.SourceLabels = {
+ [1] = "Drop", [2] = "Quest", [3] = "Vendor", [4] = "Profession", [5] = "Pet Battle",
+ [6] = "Achievement", [7] = "World Event", [8] = "Promotion", [9] = "Trading Card Game",
+ [10] = "In-Game Shop", [11] = "Discovery", [12] = "Other",
+}
+function NE.collections.SourceLabel(idx)
+ return (idx and _G["BATTLE_PET_SOURCE_" .. idx]) or NE.collections.SourceLabels[idx] or "Other"
+end
+-- Returns the 1-based source index for a companion (mounts + pets), matching SourceLabels.
+function NE.collections.GetSourceIndex(kind, spellID)
+ if not spellID then return 12 end
+ local t = (kind == "CRITTER") and NE.collections.PetData or NE.collections.MountData
+ local row = t and t[spellID]
+ local st = row and row[6]
+ return (st ~= nil) and (st + 1) or 12
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Journal.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Journal.lua
new file mode 100644
index 0000000..9acf4c5
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Journal.lua
@@ -0,0 +1,1180 @@
+-- DragonUI_NewEra/modules/collections/Journal.lua — the shared Mounts / Pet-Journal pane.
+--
+-- ONE factory (createJournal) builds a self-contained journal for a given companion kind and is
+-- instantiated twice by Window.lua: a "MOUNT" journal (the Mounts tab) and a "CRITTER" journal (the
+-- Pet Journal tab). Both share the exact retail layout — a scrollable icon+name list on the left, a
+-- rotatable 3D model + info panel on the right, a search box, a filter dropdown, a favourites star, a
+-- "Summon Random Favourite" button, and a Mount/Summon action button — differing only in which
+-- GetCompanionInfo filter they read and which source-info table (Data.lua) they join against.
+--
+-- DATA (3.3.5a pre-Cata Companions API, every call pcall-guarded so a slightly different server
+-- implementation degrades to an empty list rather than erroring):
+-- GetNumCompanions("MOUNT"/"CRITTER")
+-- GetCompanionInfo(kind, index) -> creatureID, creatureName, creatureSpellID, icon, issummoned
+-- CallCompanion(kind, index) / DismissCompanion(kind)
+-- PlayerModel:SetCreature(creatureID)
+-- The description / "Vendor: … Zone: … Cost: …" source text the API does NOT provide comes from
+-- NE.collections.GetInfo(kind, spellID) (Data.lua).
+--
+-- Widgets are parented into per-journal container frames sitting inside the shared insets/bands
+-- (Window.lua), so Journal:Show()/:Hide() toggles the whole group when the tab changes.
+
+local NE = DragonUI_NewEra
+NE.collections = NE.collections or {}
+local C = NE.collections
+
+local ROW_H = 44
+local SEARCH_H = 20
+local ICON_SIZE = 38
+
+local function log(msg) if C._log then C._log(msg) end end
+
+-- ---------------------------------------------------------------------------
+-- Favourites — per-character, persisted in NE.db.companionFavorites (same schema + key format the
+-- retired Character-panel Companions.lua used, so existing favourites carry straight over). Keyed by
+-- kind+creatureID (creatureID is the only stable identity across sessions; slot order is not).
+-- ---------------------------------------------------------------------------
+local function favTable()
+ if not (NE.db and NE.CharKey) then return nil end
+ NE.db.companionFavorites = NE.db.companionFavorites or {}
+ local key = NE.CharKey()
+ NE.db.companionFavorites[key] = NE.db.companionFavorites[key] or {}
+ return NE.db.companionFavorites[key]
+end
+local function favKey(kind, data)
+ if not (data and data.creatureID) then return nil end
+ return kind .. ":" .. tostring(data.creatureID)
+end
+local function isFavorite(kind, data)
+ local t, k = favTable(), favKey(kind, data)
+ return t ~= nil and k ~= nil and t[k] == true
+end
+local function setFavorite(kind, data, fav)
+ local t, k = favTable(), favKey(kind, data)
+ if not (t and k) then return end
+ if fav then t[k] = true else t[k] = nil end
+end
+
+-- ---------------------------------------------------------------------------
+-- Mount category / usability — MODULE-level (kind-parameterized, not closed over a Journal
+-- instance) so the draggable action-bar macro below can call them even in a session where the
+-- Collections window was never opened (no Journal object exists yet).
+-- ---------------------------------------------------------------------------
+
+-- Ground/Flying/Aquatic CATEGORY, ported EXACTLY from EZCollections' own MatchesFilter (the
+-- if/elseif chain below, not the 0x2 "can go underwater" USABILITY bit isUsableNowFor uses below —
+-- those are two different concepts that share the same `type` field, and conflating them was the
+-- earlier bug: type bit 0x2 is set on almost every ordinary ground mount (it just means "doesn't
+-- get dismounted while wading"), so treating it as the Aquatic CATEGORY misclassified nearly
+-- everything as aquatic too, and unchecking Ground alone never hid anything (the Aquatic branch of
+-- the OR still passed). The real Aquatic category comes from FLAGS bit 0x10 instead.
+local function mountCategoryFor(kind, spellID)
+ if kind ~= "MOUNT" then return true, false, false end
+ local ty, flags = 0, 0
+ if NE.collections.GetMountType then ty, flags = NE.collections.GetMountType(spellID) end
+ if bit.band(ty, 0x4) ~= 0 then
+ return true, true, false -- scripted ground/flying hybrid: counts as BOTH
+ elseif bit.band(flags, 0x10) ~= 0 then
+ return false, false, true -- Aquatic
+ elseif bit.band(ty, 0x1) ~= 0 then
+ return false, true, false -- Flying-only
+ else
+ return true, false, false -- Ground (the default — catches type 0, 2, etc.)
+ end
+end
+
+-- Location-aware "can I summon this right now" (ported from EZCollections' IsMountUsable). Pets
+-- have no location restriction. Falls back to true when there's no type data.
+local function isUsableNowFor(kind, data)
+ if kind ~= "MOUNT" then return true end
+ if IsOutdoors and not IsOutdoors() then return false end
+ local ty = NE.collections.GetMountType and (NE.collections.GetMountType(data.spellID)) or 0
+ if bit.band(ty, 0x1) ~= 0 and IsFlyableArea and not IsFlyableArea() then return false end -- flying-only, can't fly here
+ if bit.band(ty, 0x2) == 0 and IsSwimming and IsSwimming() then return false end -- non-aquatic while swimming
+ return true
+end
+
+-- Priority tier for the random-favourite PICK itself (ported from EZCollections' own
+-- SelectRandomFavoriteMount) — separate from isUsableNowFor above, which only answers "can this be
+-- summoned at all right now." Without this, a flying-capable area with BOTH a favourited flying
+-- mount and a favourited ground mount treated them as equally likely picks, so the random summon
+-- button landed on the ground mount roughly half the time even though flying was available (the
+-- reported bug). 1 = most preferred, 3 = least. Ground mounts default to 2; a flying/hybrid mount is
+-- bumped to 1 whenever flight is actually usable right now (not swimming, area is flyable);
+-- swimming favours aquatic-tagged mounts (flags 0x10/0x20) to priority 1 instead.
+local function mountPriorityFor(data, isSwimmingNow, isFlyableNow)
+ local ty, flags = 0, 0
+ if NE.collections.GetMountType then ty, flags = NE.collections.GetMountType(data.spellID) end
+ local priority = 2
+ if bit.band(ty, 0x5) ~= 0 and not isSwimmingNow and isFlyableNow then priority = 1 end
+ if bit.band(flags, 0x10) ~= 0 and isSwimmingNow then priority = 1 end
+ if bit.band(flags, 0x20) ~= 0 and isSwimmingNow then priority = 1 end
+ return priority
+end
+
+-- Splits an already-usable MOUNT pool into 3 priority tiers and returns the highest-priority
+-- non-empty tier. Pets (and any empty/degenerate pool) pass through unchanged.
+local function prioritizedPool(kind, pool)
+ if kind ~= "MOUNT" or #pool <= 1 then return pool end
+ local okSw, swimming = pcall(IsSwimming)
+ swimming = okSw and swimming and true or false
+ local okFly, flyable = pcall(IsFlyableArea)
+ flyable = okFly and flyable and true or false
+ local tiers = { {}, {}, {} }
+ for _, d in ipairs(pool) do
+ local p = mountPriorityFor(d, swimming, flyable)
+ table.insert(tiers[p], d)
+ end
+ return (#tiers[1] > 0 and tiers[1]) or (#tiers[2] > 0 and tiers[2]) or tiers[3]
+end
+
+-- ---------------------------------------------------------------------------
+-- NE.collections.SummonRandomFavorite(kind) — the canonical "summon a random usable favourite"
+-- action. Re-enumerates GetCompanionInfo FRESH each call rather than depending on a Journal's
+-- cached J.all, specifically so this also works when called from the draggable action-bar macro
+-- below (which can fire in a session where the Collections window was never built). Preference
+-- order mirrors retail: usable favourite -> any favourite -> any usable -> anything.
+-- ---------------------------------------------------------------------------
+function NE.collections.SummonRandomFavorite(kind)
+ kind = (kind == "CRITTER") and "CRITTER" or "MOUNT"
+
+ -- Retail's own random-favourite summon toggles OFF first: C_MountJournal.SummonByID(0) checks
+ -- IsMounted() and calls Dismount() instead of picking a new mount; C_PetJournal.SummonRandomPet
+ -- likewise dismisses whatever's already out. Without this, clicking again while already
+ -- mounted/summoned just re-calls CallCompanion on top of an active companion, which the server
+ -- rejects with a "you are already mounted" error instead of swapping or dismounting.
+ if kind == "MOUNT" then
+ if IsMounted and IsMounted() then
+ pcall(Dismount)
+ return
+ end
+ else
+ local okC, count = pcall(GetNumCompanions, "CRITTER")
+ count = (okC and count) or 0
+ for i = 1, count do
+ local okI, _, _, _, _, issummoned = pcall(GetCompanionInfo, "CRITTER", i)
+ if okI and issummoned then
+ pcall(DismissCompanion, "CRITTER")
+ return
+ end
+ end
+ end
+
+ local all = {}
+ local ok, count = pcall(GetNumCompanions, kind)
+ count = (ok and count) or 0
+ for i = 1, count do
+ local okI, creatureID, name, spellID, icon, issummoned = pcall(GetCompanionInfo, kind, i)
+ if okI and name then
+ all[#all + 1] = { index = i, creatureID = creatureID, name = name, spellID = spellID, icon = icon, issummoned = issummoned }
+ end
+ end
+ if #all == 0 then return end
+
+ local t = favTable()
+ local usableFav, fav, usable = {}, {}, {}
+ for _, d in ipairs(all) do
+ local k = favKey(kind, d)
+ local isFav = t and k and t[k] == true
+ local isUse = isUsableNowFor(kind, d)
+ if isUse then usable[#usable + 1] = d end
+ if isFav then
+ fav[#fav + 1] = d
+ if isUse then usableFav[#usableFav + 1] = d end
+ end
+ end
+ local pool = (#usableFav > 0 and usableFav) or (#fav > 0 and fav) or (#usable > 0 and usable) or all
+ -- Favourite status still trumps priority tier (a favourited ground mount beats a non-favourited
+ -- flying one) — priority only breaks ties WITHIN whichever pool got picked above.
+ pool = prioritizedPool(kind, pool)
+ local pick = pool[math.random(#pool)]
+ pcall(CallCompanion, kind, pick.index)
+end
+
+-- ---------------------------------------------------------------------------
+-- "Summon Random Favourite" action-bar entry. 3.3.5a action bar slots only accept SECURE primitives
+-- (spell/item/macro) — "random favourite" isn't a real spell, so (exactly like retail's own
+-- C_MountJournal.Pickup(0), which calls PickupMacro under the hood) we back it with a REAL MACRO.
+-- The macro body just re-enters SummonRandomFavorite via /script, so favourites/usability are
+-- always read live at cast time.
+--
+-- We do NOT try to make the in-window button itself draggable onto a bar: a plain non-secure
+-- Button doesn't hand off a macro pickup on OnDragStart the way retail's real (secure-templated)
+-- MountJournal button does — confirmed this doesn't work here, and EZCollections has the same
+-- limitation. Instead, ensureFavoriteMacro is called lazily on the button's first click (NOT eagerly
+-- at build time — that used to create the macro just from opening the Collections window, before
+-- the player ever asked for it), and the button's tooltip tells the player to drag it off their own
+-- Macros window afterward.
+--
+-- CAVEAT: CreateMacro's icon argument is a NUMERIC INDEX into the macro icon picker's list, NOT a
+-- texture path/name — it CANNOT take our custom MountUpFavourites.blp art. That custom texture is
+-- used on the in-window button (see buildTop below); the macro's own icon falls back to a stock
+-- riding/beast-call icon, resolved to its index below (findMacroIconIndex).
+-- ---------------------------------------------------------------------------
+local MACRO_ICON_PATH = {
+ MOUNT = "Interface\\Icons\\Ability_Mount_RidingHorse",
+ CRITTER = "Interface\\Icons\\Ability_Hunter_BeastCall",
+}
+
+-- Resolve a bare texture path to its index in the macro-icon picker list (CreateMacro/EditMacro
+-- take that INDEX, not a path — passing a path string directly errors, silently swallowed by our
+-- pcall, which is exactly why dragging never picked anything up). Mirrors EZCollections' own
+-- FindMacroIcon helper. Falls back to index 1 (never nil) so macro creation still succeeds.
+local function findMacroIconIndex(path)
+ if not (GetNumMacroIcons and GetMacroIconInfo) then return 1 end
+ local target = path:lower():gsub("/", "\\")
+ local okN, n = pcall(GetNumMacroIcons)
+ if not okN then return 1 end
+ for i = 1, n do
+ local okI, p = pcall(GetMacroIconInfo, i)
+ if okI and p and p:lower():gsub("/", "\\") == target then return i end
+ end
+ return 1
+end
+
+-- Find our macro by its BODY text rather than its name (mirrors EZCollections' own
+-- FindFavoriteMacro). Matching on body — which is a fixed, known string per kind — rather than
+-- name means the macro's Name field is free to be purely cosmetic; nothing here depends on what
+-- it's called, so it's safe to give it a blank display name (see ensureFavoriteMacro below).
+--
+-- Exact match is tried first, but GetMacroBody doesn't always return byte-for-byte what was
+-- passed to CreateMacro (e.g. a trailing newline the client appends) — an exact-only compare
+-- would miss our own previously-created macro every subsequent login, silently creating a NEW
+-- duplicate each time ("the macro keeps getting recreated"). EZCollections' real
+-- FindFavoriteMacro hits this too and falls back to a substring-containment match; do the same
+-- here. Uses the plain-find flag since our body contains literal parentheses/quotes
+-- (`SummonRandomFavorite("MOUNT")`) that would otherwise be parsed as Lua pattern syntax.
+local function findMacroByBody(body)
+ if not GetMacroBody then return nil end
+ local total = (MAX_ACCOUNT_MACROS or 0) + (MAX_CHARACTER_MACROS or 0)
+ local loose
+ for i = 1, total do
+ local okB, b = pcall(GetMacroBody, i)
+ if okB and b then
+ if b == body then return i end
+ if not loose and b:find(body, 1, true) then loose = i end
+ end
+ end
+ return loose
+end
+
+local function ensureFavoriteMacro(kind)
+ if not (CreateMacro and GetMacroBody) then return nil end
+ -- MAX_ACCOUNT_MACROS/MAX_CHARACTER_MACROS and GetNumMacroIcons/GetMacroIconInfo all live in the
+ -- Blizzard_MacroUI addon, which is lazy-loaded (not part of always-on FrameXML) — until it's
+ -- loaded those globals are nil, both capacity checks below silently read as false, and CreateMacro
+ -- never gets called. EZCollections' own macro helpers hit this too, which is why they always call
+ -- LoadAddOn first; do the same here before touching any of them.
+ if LoadAddOn then pcall(LoadAddOn, "Blizzard_MacroUI") end
+
+ local body = string.format('/script DragonUI_NewEra.collections.SummonRandomFavorite("%s")', kind)
+ local existing = findMacroByBody(body)
+ if existing then return existing end
+
+ -- Blank (single-space) display name: the text shown under a macro's icon on the action bar IS
+ -- its Name field — there's no separate label — so this is the standard WoW "icon-only macro"
+ -- trick. Safe specifically because existence is matched by body above, not name; if the user
+ -- (or anything else) happens to have another macro also named " ", it's irrelevant here.
+ local displayName = " "
+ local icon = findMacroIconIndex(MACRO_ICON_PATH[kind] or "Interface\\Icons\\INV_Misc_QuestionMark")
+ local numAccount, numChar = 0, 0
+ if GetNumMacros then
+ local okN, a, c = pcall(GetNumMacros)
+ if okN then numAccount, numChar = a or 0, c or 0 end
+ end
+ -- Prefer an account-wide macro (matches retail's cross-character "favourite" concept); fall back
+ -- to a per-character slot if the account pool is full. Never errors if both pools are full — the
+ -- button just isn't draggable that session (still fully clickable/summonable in the window).
+ if MAX_ACCOUNT_MACROS and numAccount < MAX_ACCOUNT_MACROS then
+ local ok, idx = pcall(CreateMacro, displayName, icon, body, nil)
+ if ok and idx then return idx end
+ end
+ if MAX_CHARACTER_MACROS and numChar < MAX_CHARACTER_MACROS then
+ local ok, idx = pcall(CreateMacro, displayName, icon, body, 1)
+ if ok and idx then return idx end
+ end
+ return nil
+end
+
+-- ===========================================================================
+-- Factory
+-- ===========================================================================
+local function createJournal(kind)
+ local J = { kind = kind }
+ local isMount = (kind == "MOUNT")
+
+ J.all = {} -- every KNOWN companion of this kind (learned + not-yet-learned): {index, creatureID,
+ -- name, spellID, icon, issummoned} — index/creatureID are nil for not-yet-learned
+ -- rows (no 3D model preview for those — see showModel)
+ J.list = {} -- the filtered+ordered view actually shown
+ J.rows = {} -- recycled list-row buttons
+ J.selected = nil -- the currently-selected data row (drives the model + info panel)
+ J.collectedCount = 0 -- how many are actually learned (for the count pill)
+ J.search = ""
+ J.favOnly = false
+ J.showCollected, J.showNotCollected = true, true -- Collected / Not Collected filters
+ J.showUnusable = true -- show mounts not usable here (mounts only)
+ J.showGround, J.showFlying, J.showAquatic = true, true, true -- mount type filters (mounts only)
+ J.sourceHidden = {} -- [sourceIndex] = true -> hide that source
+
+ -- Thin per-instance delegators to the module-level (kind-parameterized) versions above.
+ local function mountCategory(spellID) return mountCategoryFor(kind, spellID) end
+ local function isUsableNow(data) return isUsableNowFor(kind, data) end
+
+ -- --- containers (one per shared region; Show/Hide as a group) --------------
+ local function panel(parent)
+ local p = CreateFrame("Frame", nil, parent)
+ p:SetAllPoints(parent)
+ p:SetFrameLevel((parent:GetFrameLevel() or 1) + 1)
+ return p
+ end
+ local listGroup = panel(C.LeftInset)
+ local displayGroup = panel(C.RightInset)
+ local topGroup = panel(C.TopBand)
+ local bottomGroup = panel(C.BottomBand)
+ J._groups = { listGroup, displayGroup, topGroup, bottomGroup }
+
+ -- ------------------------------------------------------------------------
+ -- Data
+ -- ------------------------------------------------------------------------
+ local function rebuildAll()
+ for i = #J.all, 1, -1 do J.all[i] = nil end
+ local seen = {} -- spellID -> true, for learned companions (so the DB pass below can skip them)
+ local ok, count = pcall(GetNumCompanions, kind)
+ count = (ok and count) or 0
+ local n = 0
+ for i = 1, count do
+ local okI, creatureID, name, spellID, icon, issummoned = pcall(GetCompanionInfo, kind, i)
+ if okI and name then
+ n = n + 1
+ J.all[n] = { index = i, creatureID = creatureID, name = name,
+ spellID = spellID, icon = icon, issummoned = issummoned }
+ if spellID then seen[spellID] = true end
+ end
+ end
+ J.collectedCount = n
+
+ -- Not-yet-learned entries from the bundled source DB (Data.lua), so the list reads like
+ -- retail's full catalog rather than "only what you already have" — greyed out, info-only, no
+ -- summon/favourite/model preview (SetCreature needs a server-resolvable creature/unit
+ -- reference, not just a raw DB display ID — see showModel).
+ -- SKIPPED when the spell doesn't even resolve client-side (GetSpellInfo fails): a hard signal
+ -- that content genuinely isn't on THIS custom server, not just "you haven't gotten it yet" —
+ -- showing a placeholder for something that can't possibly exist here would be pure clutter.
+ -- ALSO skipped when the DB row has no description (row[5]) or no source (row[7]): a catalog
+ -- entry with neither shows a blank info panel and gives the player nothing to act on (no idea
+ -- what it is or where it comes from) — same "pure clutter" reasoning as the GetSpellInfo skip.
+ -- ALSO skipped when it's a faction-specific mount for the OPPOSITE faction and NOT already
+ -- learned: you can't ever legitimately acquire the other side's racial mount, so showing it
+ -- greyed out in the catalog is pure clutter, not an aspirational "not yet collected" entry.
+ -- (Only gates the not-yet-learned pass below — an already-learned opposite-faction mount, e.g.
+ -- from a faction change, still shows normally via the GetCompanionInfo pass above.)
+ local playerFaction
+ if isMount and UnitFactionGroup then
+ local okF, pf = pcall(UnitFactionGroup, "player")
+ if okF then playerFaction = pf end
+ end
+ local db = isMount and NE.collections.MountData or NE.collections.PetData
+ if db then
+ for spellID, row in pairs(db) do
+ if not seen[spellID] then
+ local hasLore = row[5] and row[5] ~= ""
+ local hasSource = row[7] and row[7] ~= ""
+ local factionOK = true
+ if playerFaction then
+ local mf = NE.collections.GetFaction and NE.collections.GetFaction(spellID)
+ if (mf == 0 and playerFaction == "Alliance") or (mf == 1 and playerFaction == "Horde") then
+ factionOK = false
+ end
+ end
+ if hasLore and hasSource and factionOK then
+ local okS, sName, _, sIcon = pcall(GetSpellInfo, spellID)
+ if okS and sName then
+ n = n + 1
+ J.all[n] = {
+ index = nil, creatureID = nil, name = row[4] or sName,
+ spellID = spellID, icon = sIcon or C.tex.emptyIcon, issummoned = false,
+ }
+ end
+ end
+ end
+ end
+ end
+ end
+
+ -- Filtered + ordered view: Favourites, then Collected, then Not Collected — each group
+ -- alphabetical within itself. Data.lua / GetSpellInfo supply the display name where
+ -- GetCompanionInfo's/the DB's is blank.
+ local function rebuildView()
+ for i = #J.list, 1, -1 do J.list[i] = nil end
+ local t = favTable()
+ local favs, collected, uncollected = {}, {}, {}
+ local needle = (J.search or ""):lower()
+ for _, data in ipairs(J.all) do
+ local name = data.name
+ if not name or name == "" then name = C.GetInfo and select(1, C.GetInfo(kind, data.spellID)) end
+ if not name or name == "" then
+ local okS, sn = pcall(GetSpellInfo, data.spellID)
+ if okS then name = sn end
+ end
+ data.displayName = (name and name ~= "" and name) or UNKNOWN or "?"
+
+ local passSearch = (needle == "") or (data.displayName:lower():find(needle, 1, true) ~= nil)
+ local k = favKey(kind, data)
+ local fav = (t and k and t[k]) and true or false
+ data._fav = fav
+
+ -- A not-yet-learned row has no slot index; can't be favourited/summoned/previewed, gated
+ -- purely by the Collected/Not Collected toggle rather than the usability/type-Unusable rule
+ -- (which is meaningless for something you don't have).
+ local passCollected = (data.index and J.showCollected) or ((not data.index) and J.showNotCollected)
+
+ local passType = true
+ if isMount then
+ local ground, flying, aquatic = mountCategory(data.spellID)
+ passType = (ground and J.showGround) or (flying and J.showFlying) or (aquatic and J.showAquatic)
+ if data.index and not J.showUnusable and not isUsableNow(data) then passType = false end
+ end
+ local srcIdx = C.GetSourceIndex and C.GetSourceIndex(kind, data.spellID) or 12
+ local passSource = not (J.sourceHidden and J.sourceHidden[srcIdx])
+ if passSearch and passCollected and passType and passSource and (not J.favOnly or fav) then
+ if fav then table.insert(favs, data)
+ elseif data.index then table.insert(collected, data)
+ else table.insert(uncollected, data) end
+ end
+ end
+ local function byName(a, b) return (a.displayName or "") < (b.displayName or "") end
+ table.sort(favs, byName)
+ table.sort(collected, byName)
+ table.sort(uncollected, byName)
+ local m = 0
+ for i = 1, #favs do m = m + 1; J.list[m] = favs[i] end
+ for i = 1, #collected do m = m + 1; J.list[m] = collected[i] end
+ for i = 1, #uncollected do m = m + 1; J.list[m] = uncollected[i] end
+ end
+
+ -- ------------------------------------------------------------------------
+ -- 3D model + info panel (right inset)
+ -- ------------------------------------------------------------------------
+ local model, infoIcon, infoName, infoSource, infoLore, emptyLabel, notCollectedHint
+
+ local function buildDisplay()
+ -- Generic model backdrop (retail's MountJournal-BG). Visible slice is left 0..0.785.
+ -- Inset 3px off displayGroup's edges (matching RightInset's own dark fill inset in Window.lua's
+ -- buildInset) rather than SetAllPoints: displayGroup is a CHILD frame one level above RightInset,
+ -- so a full-bleed texture here draws OVER RightInset's own InsetFrameTemplate silver border
+ -- (frame-level ordering beats draw-layer ordering across different frames) — hiding it entirely.
+ local bg = displayGroup:CreateTexture(nil, "BACKGROUND")
+ bg:SetPoint("TOPLEFT", displayGroup, "TOPLEFT", 3, -3)
+ bg:SetPoint("BOTTOMRIGHT", displayGroup, "BOTTOMRIGHT", -3, 3)
+ bg:SetTexture(C.tex.modelBg)
+ bg:SetTexCoord(0, 0.78515625, 0, 1)
+
+ -- Header zone (icon/name/source/lore above) grew from 104 -> 170px: longer mount/pet lore
+ -- descriptions (2-4 lines) were overflowing past the old boundary and getting visually covered
+ -- by the model viewport, reading as "cut off". This also gives the modern control bar (below)
+ -- clear room at the model's own TOP edge.
+ model = CreateFrame("PlayerModel", nil, displayGroup)
+ model:SetPoint("TOPLEFT", displayGroup, "TOPLEFT", 4, -170)
+ model:SetPoint("BOTTOMRIGHT", displayGroup, "BOTTOMRIGHT", -4, 6)
+
+ -- Same modern DF model control bar as the Character panel (zoom in/out, rotate L/R, reset,
+ -- hover-reveal, click-drag rotate/pan, wheel-zoom) instead of the old bespoke drag-rotate +
+ -- native rotation-art button pair.
+ if NE.charpanel and NE.charpanel.BuildModelControls then
+ -- rotateButtons MUST be forced to an empty table: the shared helper's own default targets the
+ -- Character panel's GLOBAL rotate-button names by _G lookup — without this override, building
+ -- controls for THIS model would reach into and hide the Character panel's own buttons too.
+ pcall(NE.charpanel.BuildModelControls, model, {
+ rotateButtons = {},
+ panelCheck = function() return C.frame and C.frame:IsShown() and C.activeKind == kind end,
+ })
+ end
+
+ -- Shown over the model area instead of a 3D preview when the selected row is not yet learned
+ -- (no creatureDisplayID is available for those — only the API's real companions can preview).
+ notCollectedHint = displayGroup:CreateFontString(nil, "OVERLAY", "GameFontDisable")
+ notCollectedHint:SetPoint("CENTER", model, "CENTER", 0, 0)
+ notCollectedHint:SetText(NOT_COLLECTED or "Not yet collected")
+ notCollectedHint:Hide()
+
+ -- Info header (icon + name + source + lore), over the top of the backdrop.
+ infoIcon = displayGroup:CreateTexture(nil, "ARTWORK")
+ infoIcon:SetSize(ICON_SIZE, ICON_SIZE)
+ infoIcon:SetPoint("TOPLEFT", displayGroup, "TOPLEFT", 14, -12)
+ infoIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+
+ infoName = displayGroup:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
+ infoName:SetPoint("TOPLEFT", infoIcon, "TOPRIGHT", 10, -2)
+ infoName:SetPoint("RIGHT", displayGroup, "RIGHT", -30, 0)
+ infoName:SetJustifyH("LEFT")
+
+ -- Favourite star (top-right of the display, toggles the selected row's favourite).
+ local fav = CreateFrame("CheckButton", nil, displayGroup)
+ fav:SetSize(24, 24)
+ fav:SetPoint("TOPRIGHT", displayGroup, "TOPRIGHT", -8, -8)
+ local fN = fav:CreateTexture(nil, "ARTWORK"); fN:SetAllPoints(fav); fN:SetTexture(C.tex.favoriteIcon); fN:SetTexCoord(0.03125, 0.8125, 0.03125, 0.8125)
+ local fH = fav:CreateTexture(nil, "HIGHLIGHT"); fH:SetAllPoints(fav); fH:SetTexture("Interface\\Buttons\\ButtonHilight-Square"); fH:SetBlendMode("ADD")
+ fav._star = fN
+ fav:SetScript("OnClick", function(self)
+ if not J.selected then return end
+ local now = not J.selected._fav
+ setFavorite(kind, J.selected, now)
+ J:RefreshView()
+ end)
+ fav:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_LEFT")
+ GameTooltip:SetText(BATTLE_PET_FAVORITE or "Favourite", 1, 0.82, 0)
+ GameTooltip:AddLine("Keeps this at the front of the list.", 1, 1, 1, true)
+ GameTooltip:Show()
+ end)
+ fav:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ J._favStar = fav
+
+ -- TOPLEFT + RIGHT anchors alone were NOT reliably constraining these FontStrings' width for
+ -- wrapping purposes — long lore text rendered as a single line, hard-clipped mid-word right at
+ -- the panel edge instead of wrapping ("Descriptions are still being cut off"). An explicit
+ -- SetWidth + SetWordWrap(true) is deterministic regardless of that anchor ambiguity; height is
+ -- left unconstrained (only a TOPLEFT point) so it still grows downward to fit the text.
+ local INFO_TEXT_W = 316
+
+ infoSource = displayGroup:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ infoSource:SetPoint("TOPLEFT", infoIcon, "BOTTOMLEFT", 0, -6)
+ infoSource:SetWidth(INFO_TEXT_W)
+ infoSource:SetWordWrap(true)
+ infoSource:SetJustifyH("LEFT")
+
+ infoLore = displayGroup:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ infoLore:SetPoint("TOPLEFT", infoSource, "BOTTOMLEFT", 0, -8)
+ infoLore:SetWidth(INFO_TEXT_W)
+ infoLore:SetWordWrap(true)
+ infoLore:SetJustifyH("LEFT")
+ infoLore:SetTextColor(0.8, 0.8, 0.8)
+
+ emptyLabel = displayGroup:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
+ emptyLabel:SetPoint("CENTER", displayGroup, "CENTER", 0, 0)
+ emptyLabel:SetText(isMount and (ERR_NO_RIDING_SKILL or "You have not learned any mounts.")
+ or "You have not learned any companions.")
+ emptyLabel:Hide()
+ end
+
+ local function showModel(data)
+ J.selected = data
+ if not data then
+ if model then model:Hide() end
+ if notCollectedHint then notCollectedHint:Hide() end
+ if infoIcon then infoIcon:SetTexture(nil) end
+ if infoName then infoName:SetText("") end
+ if infoSource then infoSource:SetText("") end
+ if infoLore then infoLore:SetText("") end
+ if J._favStar then J._favStar:Hide() end
+ return
+ end
+ -- Model preview only works for a learned row's real creatureID (from GetCompanionInfo) —
+ -- SetCreature on this client needs a server-resolvable creature/unit reference, not a raw
+ -- CreatureDisplayInfo.dbc id, so a not-yet-learned row (no creatureID) always falls back to
+ -- the "Not yet collected" text hint instead of attempting (and failing) a model preview.
+ local previewID = data.creatureID
+ if model then
+ if previewID then
+ model:Show()
+ pcall(model.SetCreature, model, previewID)
+ -- `.rotation` (not the old `._facing`) is the field the shared modern control bar
+ -- (ModelControls.lua) reads as its drag-rotate baseline — keep it in sync with the actual
+ -- visual reset here, or the next drag would jump from a stale angle.
+ model.rotation = 0
+ pcall(model.SetRotation, model, 0)
+ else
+ model:Hide()
+ end
+ end
+ if notCollectedHint then notCollectedHint:SetShown(not previewID) end
+ if infoIcon then infoIcon:SetTexture(data.icon) end
+ if infoName then infoName:SetText(data.displayName or data.name or "") end
+ local _, lore, source = nil, nil, nil
+ if C.GetInfo then _, lore, source = C.GetInfo(kind, data.spellID) end
+ if infoSource then infoSource:SetText(source or "") end
+ if infoLore then infoLore:SetText(lore or "") end
+ -- Favouriting requires a learned companion (favKey needs a creatureID) — hide the star entirely
+ -- for not-yet-learned rows rather than show a star that would silently no-op if clicked.
+ if J._favStar then
+ if data.index then J._favStar:Show(); J._favStar:SetChecked(data._fav and true or false)
+ else J._favStar:Hide() end
+ end
+ end
+
+ -- ------------------------------------------------------------------------
+ -- Action button (Mount / Summon / Dismiss) — bottom band.
+ -- ------------------------------------------------------------------------
+ local actionBtn
+ local function summonData(data)
+ if not (data and data.index) then return end -- not-yet-learned rows have nothing to summon
+ if data.issummoned then pcall(DismissCompanion, kind)
+ else pcall(CallCompanion, kind, data.index) end
+ if C_Timer and C_Timer.After then C_Timer.After(0.2, function() J:Refresh() end) end
+ end
+ local function summonSelected() summonData(J.selected) end
+ local function buildAction()
+ actionBtn = CreateFrame("Button", nil, bottomGroup, "UIPanelButtonTemplate")
+ actionBtn:SetSize(150, 22)
+ actionBtn:SetPoint("LEFT", bottomGroup, "LEFT", 2, 0)
+ actionBtn:SetText(isMount and (MOUNT or "Mount") or (SUMMON or "Summon"))
+ actionBtn:SetScript("OnClick", summonSelected)
+ end
+ local function updateAction()
+ if not actionBtn then return end
+ local data = J.selected
+ if not (data and data.index) then actionBtn:Disable(); return end
+ actionBtn:Enable()
+ if data.issummoned then
+ actionBtn:SetText(isMount and (BINDING_NAME_DISMOUNT or "Dismiss") or (PET_DISMISS or "Dismiss"))
+ else
+ actionBtn:SetText(isMount and (MOUNT or "Mount") or (SUMMON or "Summon"))
+ end
+ end
+
+ -- ------------------------------------------------------------------------
+ -- Count pill + Summon-Random-Favourite (top band)
+ -- ------------------------------------------------------------------------
+ -- Fallback icon if the custom Mount-tab art isn't shipped; pets have no equivalent custom art yet,
+ -- so they keep showing a live favourite's icon (falls back to the first companion's, then this).
+ local SUMMON_FALLBACK = isMount and "Interface\\Icons\\Ability_Mount_RidingHorse"
+ or "Interface\\Icons\\Ability_Hunter_BeastCall"
+
+ local countLabel, summonBtn
+ local function updateSummonIcon()
+ if not summonBtn then return end
+ if isMount and C.tex.mountUpFavourites then
+ -- User-provided art for this exact button (Mounts tab only).
+ summonBtn._tex:SetTexture(C.tex.mountUpFavourites)
+ return
+ end
+ -- Pets: show a favourite's icon (retail shows the favourite macro's icon); else the first
+ -- companion's; else the static fallback — always a real, relevant icon, never a generic guess.
+ local icon
+ for _, d in ipairs(J.all) do if d._fav then icon = d.icon; break end end
+ icon = icon or (J.all[1] and J.all[1].icon) or SUMMON_FALLBACK
+ summonBtn._tex:SetTexture(icon)
+ end
+
+ local function buildTop()
+ -- Count sits to the RIGHT of the frame portrait (which overlaps the top-left corner), never
+ -- under it — housed in a small recessed pill (dark fill + thin InsetFrameTemplate border), like
+ -- retail's "Total Mounts" box, rather than bare floating text.
+ local countBox = CreateFrame("Frame", nil, topGroup)
+ countBox:SetPoint("LEFT", topGroup, "LEFT", 56, 0)
+ countBox:SetHeight(20)
+ countBox:SetWidth(90)
+ local countBg = countBox:CreateTexture(nil, "BACKGROUND")
+ countBg:SetPoint("TOPLEFT", 3, -3)
+ countBg:SetPoint("BOTTOMRIGHT", -3, 3)
+ if countBg.SetColorTexture then countBg:SetColorTexture(0.05, 0.05, 0.06, 0.92)
+ else countBg:SetTexture(0.05, 0.05, 0.06, 0.92) end
+ if NE.nineslice and NE.nineslice.ApplyLayout then pcall(NE.nineslice.ApplyLayout, countBox, "InsetFrameTemplate") end
+ J._countBox = countBox
+
+ countLabel = countBox:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
+ countLabel:SetPoint("CENTER", countBox, "CENTER", 0, 0)
+
+ summonBtn = CreateFrame("Button", nil, topGroup)
+ summonBtn:SetSize(30, 30)
+ summonBtn:SetPoint("RIGHT", topGroup, "RIGHT", -2, 0)
+ local st = summonBtn:CreateTexture(nil, "ARTWORK"); st:SetAllPoints(summonBtn)
+ st:SetTexture(SUMMON_FALLBACK)
+ st:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ summonBtn._tex = st
+ -- Thin icon border so it reads as a button, not floating art.
+ local br = summonBtn:CreateTexture(nil, "OVERLAY")
+ br:SetTexture(C.tex.iconFrame)
+ br:SetPoint("TOPLEFT", summonBtn, "TOPLEFT", -2, 2)
+ br:SetPoint("BOTTOMRIGHT", summonBtn, "BOTTOMRIGHT", 2, -2)
+ -- Two-line label to the left of the icon (retail "Summon Random Favorite Mount").
+ local slabel = topGroup:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ slabel:SetPoint("RIGHT", summonBtn, "LEFT", -6, 0)
+ slabel:SetWidth(150)
+ slabel:SetJustifyH("RIGHT")
+ slabel:SetText(isMount and "Summon Random\nFavorite Mount" or "Summon Random\nFavorite Companion")
+ summonBtn:SetPushedTexture("Interface\\Buttons\\UI-Quickslot-Depress")
+ summonBtn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
+ -- Dragging this button itself onto an action bar never worked reliably (matches EZCollections'
+ -- own behaviour, per user report) — plain non-secure Buttons don't hand off a macro pickup on
+ -- OnDragStart the way retail's real MountJournal button (a secure template) does. Instead, the
+ -- backing macro is created lazily on the button's FIRST click (not eagerly at build time — the
+ -- player never asked for it just by having the journal open, and eager creation meant a macro
+ -- appeared at login even if the player never touched this button) and the tooltip tells the
+ -- player where to find it afterward: drag it off their own Macros window (ESC > Macros, or
+ -- /macro) onto a bar, same as any other macro.
+ summonBtn:SetScript("OnClick", function()
+ ensureFavoriteMacro(kind)
+ NE.collections.SummonRandomFavorite(kind)
+ if C_Timer and C_Timer.After then C_Timer.After(0.2, function() J:Refresh() end) end
+ end)
+ summonBtn:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_LEFT")
+ GameTooltip:SetText(isMount and "Summon Random Favorite Mount" or "Summon Random Favorite Companion", 1, 1, 1)
+ GameTooltip:AddLine("Picks a random favorite usable here (or any, if none are favorited).", 0.82, 0.82, 0.82, true)
+ GameTooltip:AddLine("To use this on an action bar: open Macros (ESC > Macros), find the unnamed icon-only macro matching this icon, and drag it onto your bar.", 0.6, 0.8, 1, true)
+ GameTooltip:Show()
+ end)
+ summonBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ end
+
+ -- ------------------------------------------------------------------------
+ -- Search box + filter (left inset top) + scrollable list
+ -- ------------------------------------------------------------------------
+ local scroll, content, searchBox, filterBtn
+
+ local function styleRow(row)
+ local co = C.listCoords
+ row.background:SetTexCoord(co.background[1], co.background[2], co.background[3], co.background[4])
+ row.selectedTex:SetTexCoord(co.select[1], co.select[2], co.select[3], co.select[4])
+ end
+
+ local function acquireRow(i)
+ if J.rows[i] then return J.rows[i] end
+ local row = CreateFrame("Button", nil, content)
+ row:SetHeight(ROW_H)
+ row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
+
+ row.background = row:CreateTexture(nil, "BACKGROUND")
+ row.background:SetTexture(C.tex.listButtons)
+ row.background:SetAllPoints(row)
+
+ row.selectedTex = row:CreateTexture(nil, "ARTWORK", nil, 1)
+ row.selectedTex:SetTexture(C.tex.listButtons)
+ row.selectedTex:SetAllPoints(row)
+ row.selectedTex:Hide()
+
+ local hl = row:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetTexture(C.tex.listButtons)
+ hl:SetTexCoord(C.listCoords.highlight[1], C.listCoords.highlight[2], C.listCoords.highlight[3], C.listCoords.highlight[4])
+ hl:SetAllPoints(row)
+
+ row.icon = row:CreateTexture(nil, "BORDER")
+ row.icon:SetSize(ICON_SIZE, ICON_SIZE)
+ row.icon:SetPoint("LEFT", row, "LEFT", 8, 0)
+ row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+
+ -- "Currently summoned" tint over the icon. CheckButtonHilight's actual glow content doesn't
+ -- reach its own texture edges (baked-in padding), so SetAllPoints(icon) rendered visibly
+ -- smaller than the icon — oversize it a few px past the icon bounds instead so the bloom
+ -- reaches/covers the icon edges.
+ row.active = row:CreateTexture(nil, "OVERLAY")
+ row.active:SetPoint("CENTER", row.icon, "CENTER", 0, 0)
+ row.active:SetSize(ICON_SIZE + 10, ICON_SIZE + 10)
+ row.active:SetTexture("Interface\\Buttons\\CheckButtonHilight")
+ row.active:SetBlendMode("ADD")
+ row.active:Hide()
+
+ -- Faction crest watermark on the right (faction-specific mounts only), faded, behind the name.
+ -- Sized to the crest's actual pixel aspect ratio (see Assets.lua factionCoords) — the old flat
+ -- 84x42 box didn't match either crest's real proportions and stretched them.
+ row.faction = row:CreateTexture(nil, "ARTWORK")
+ row.faction:SetSize(32, 36)
+ row.faction:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ row.faction:SetTexture(C.tex.factionIcons)
+ row.faction:SetAlpha(0.65)
+ row.faction:Hide()
+
+ row.name = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ row.name:SetPoint("LEFT", row.icon, "RIGHT", 10, 0)
+ row.name:SetPoint("RIGHT", row, "RIGHT", -26, 0)
+ row.name:SetJustifyH("LEFT")
+
+ row.fav = row:CreateTexture(nil, "OVERLAY")
+ row.fav:SetSize(22, 22)
+ row.fav:SetPoint("TOPLEFT", row.icon, "TOPLEFT", -8, 8)
+ row.fav:SetTexture(C.tex.favoriteIcon)
+ row.fav:SetTexCoord(0.03125, 0.8125, 0.03125, 0.8125)
+ row.fav:Hide()
+
+ styleRow(row)
+
+ row:SetScript("OnClick", function(self, button)
+ local data = self._data
+ if not data then return end
+ if button == "RightButton" then
+ if not data.index then return end -- nothing to Mount/Favourite for a not-yet-learned row
+ J._menuData = data
+ if ToggleDropDownMenu and J.rowMenu then ToggleDropDownMenu(1, nil, J.rowMenu, "cursor", 0, 0) end
+ else
+ -- Left-click always selects (even not-yet-learned rows) — that's how you see its acquisition
+ -- info (source/lore), same as retail's Mount Journal.
+ showModel(data)
+ J:UpdateList() -- refresh selection highlight + action button
+ updateAction()
+ end
+ end)
+ row:SetScript("OnEnter", function(self)
+ local data = self._data
+ if not data then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText(data.displayName or data.name or "", 1, 1, 1)
+ if data.index then
+ GameTooltip:AddLine(data.issummoned and "Right-click to dismiss" or "Right-click to summon", 1, 0.82, 0)
+ else
+ GameTooltip:AddLine(NOT_COLLECTED or "Not yet collected", 0.6, 0.6, 0.6)
+ end
+ GameTooltip:Show()
+ end)
+ row:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ J.rows[i] = row
+ return row
+ end
+
+ -- How many rows fit the scroll height right now (created lazily), and the total pixel height
+ -- available to them.
+ local function visibleRows()
+ local h = scroll and scroll:GetHeight() or 0
+ if h <= 0 then h = 440 end -- best-effort before first layout settles
+ return math.max(1, math.floor(h / ROW_H)), h
+ end
+
+ function J:UpdateList()
+ if not (scroll and content) then return end
+ local shown, availH = visibleRows()
+ -- Stretch rows to consume the FULL available height instead of the fixed ROW_H: ROW_H rarely
+ -- divides the inset's actual height evenly, so flooring alone left a dead gap (often 30-40px)
+ -- of bare background between the last row and the bottom of the inset. content isn't a real
+ -- scroll-child (see the FauxScrollFrame_Update note below) so nothing clips an overhanging
+ -- partial row — stretching each row slightly (a couple px at most) is the only way to reach the
+ -- bottom without one hanging out past the inset's edge. Row art/icon/name are all anchored by
+ -- single-point (LEFT/CENTER-style) anchors that re-center automatically at any row height, so
+ -- this doesn't visibly distort them.
+ local rowH = (shown > 0) and (availH / shown) or ROW_H
+ J._rowH = rowH
+ local offset = FauxScrollFrame_GetOffset(scroll)
+ for i = 1, shown do
+ local row = acquireRow(i)
+ row:ClearAllPoints()
+ row:SetHeight(rowH)
+ row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(i - 1) * rowH)
+ row:SetPoint("RIGHT", content, "RIGHT", -2, 0)
+ local data = J.list[i + offset]
+ if data then
+ row._data = data
+ row.icon:SetTexture(data.icon)
+ row.name:SetText(data.displayName or data.name or "")
+ -- Not-yet-learned rows read grey/desaturated (retail's own "uncollected" treatment) — the
+ -- icon itself is the only visual cue since there's no ring/star for them to omit.
+ if data.index then
+ row.name:SetFontObject(GameFontNormal)
+ row.icon:SetAlpha(1)
+ if row.icon.SetDesaturated then pcall(row.icon.SetDesaturated, row.icon, false) end
+ else
+ row.name:SetFontObject(GameFontDisable)
+ row.icon:SetAlpha(0.45)
+ if row.icon.SetDesaturated then pcall(row.icon.SetDesaturated, row.icon, true) end
+ end
+ if data.issummoned then row.active:Show() else row.active:Hide() end
+ if data._fav then row.fav:Show() else row.fav:Hide() end
+ local fac = isMount and C.GetFaction and C.GetFaction(data.spellID)
+ if fac ~= nil and fac ~= false and C.factionCoords[fac] then
+ local fc = C.factionCoords[fac]
+ row.faction:SetTexCoord(fc[1], fc[2], fc[3], fc[4])
+ row.faction:Show()
+ else
+ row.faction:Hide()
+ end
+ -- Compare by spellID, not slot index: two different not-yet-learned rows both have
+ -- index == nil, so an index-based compare would show EVERY uncollected row as "selected".
+ if J.selected and J.selected.spellID and J.selected.spellID == data.spellID then
+ row.selectedTex:Show()
+ else
+ row.selectedTex:Hide()
+ end
+ row:Show()
+ else
+ row._data = nil
+ row:Hide()
+ end
+ end
+ FauxScrollFrame_Update(scroll, #J.list, shown, rowH)
+ if countLabel then
+ -- The count pill is "how many you actually have", like retail — NOT the full catalog size
+ -- (J.all now also includes not-yet-learned placeholder rows).
+ countLabel:SetText(string.format("%s %d", isMount and (TOTAL_MOUNTS or "Total Mounts") or "Companions", J.collectedCount or 0))
+ if J._countBox then J._countBox:SetWidth(math.max(90, (countLabel:GetStringWidth() or 0) + 24)) end
+ end
+ end
+
+ -- Right-click row context menu: Mount/Summon (or Dismiss), Favourite toggle, Cancel — like retail.
+ local function rowMenuInit(self, level)
+ local data = J._menuData
+ if not data then return end
+ local info = UIDropDownMenu_CreateInfo()
+ info.notCheckable = true
+
+ if data.issummoned then
+ info.text = isMount and (BINDING_NAME_DISMOUNT or "Dismiss") or (PET_DISMISS or "Dismiss")
+ else
+ info.text = isMount and (MOUNT or "Mount") or (SUMMON or "Summon")
+ end
+ info.func = function() summonData(data) end
+ UIDropDownMenu_AddButton(info, level)
+
+ info.text = data._fav and (BATTLE_PET_UNFAVORITE or "Remove Favourite") or (BATTLE_PET_FAVORITE or "Favourite")
+ info.func = function() setFavorite(kind, data, not data._fav); J:RefreshView() end
+ UIDropDownMenu_AddButton(info, level)
+
+ info.text = CANCEL or "Cancel"
+ info.func = nil
+ UIDropDownMenu_AddButton(info, level)
+ end
+
+ -- The distinct source indices present in the current companion set (for the Sources submenu).
+ local function presentSources()
+ local seen, out = {}, {}
+ for _, d in ipairs(J.all) do
+ local idx = C.GetSourceIndex and C.GetSourceIndex(kind, d.spellID) or 12
+ if not seen[idx] then seen[idx] = true; out[#out + 1] = idx end
+ end
+ table.sort(out)
+ return out
+ end
+
+ -- Filter dropdown: Collected / Not Collected, Unusable + Type toggles (mounts), Favourites,
+ -- Sources submenu — matches retail's Mount Journal filter layout. Uses the retail
+ -- func(_, _, _, checked) + checked-as-function pattern so each toggle updates live AND filters.
+ -- Toggling only re-runs rebuildView (not a full companion re-scan), so the menu stays snappy.
+ local function filterMenuInit(self, level)
+ level = level or 1
+ if level == 1 then
+ local info = UIDropDownMenu_CreateInfo()
+ info.isNotRadio = true
+ info.keepShownOnClick = true
+
+ info.text = COLLECTED or "Collected"
+ info.checked = function() return J.showCollected end
+ info.func = function(_, _, _, checked) J.showCollected = checked and true or false; J:RefreshView() end
+ UIDropDownMenu_AddButton(info, level)
+
+ info.text = NOT_COLLECTED or "Not Collected"
+ info.checked = function() return J.showNotCollected end
+ info.func = function(_, _, _, checked) J.showNotCollected = checked and true or false; J:RefreshView() end
+ UIDropDownMenu_AddButton(info, level)
+
+ if isMount then
+ info.text = MOUNT_JOURNAL_FILTER_UNUSABLE or "Unusable"
+ info.checked = function() return J.showUnusable end
+ info.func = function(_, _, _, checked) J.showUnusable = checked and true or false; J:RefreshView() end
+ UIDropDownMenu_AddButton(info, level)
+
+ local title = UIDropDownMenu_CreateInfo()
+ title.text = MOUNT_JOURNAL_FILTER_TYPE or "Type"
+ title.isTitle = true; title.notCheckable = true
+ UIDropDownMenu_AddButton(title, level)
+
+ local types = {
+ { MOUNT_JOURNAL_FILTER_GROUND or "Ground", "showGround" },
+ { MOUNT_JOURNAL_FILTER_FLYING or "Flying", "showFlying" },
+ { MOUNT_JOURNAL_FILTER_AQUATIC or "Aquatic", "showAquatic" },
+ }
+ for _, ty in ipairs(types) do
+ local key = ty[2]
+ local i2 = UIDropDownMenu_CreateInfo()
+ i2.isNotRadio = true; i2.keepShownOnClick = true
+ i2.text = ty[1]
+ i2.checked = function() return J[key] end
+ i2.func = function(_, _, _, checked) J[key] = checked and true or false; J:RefreshView() end
+ UIDropDownMenu_AddButton(i2, level)
+ end
+ end
+
+ if UIDropDownMenu_AddSpace then pcall(UIDropDownMenu_AddSpace, level) end
+
+ info.text = BATTLE_PET_FAVORITE or "Favorites"
+ info.checked = function() return J.favOnly end
+ info.func = function(_, _, _, checked) J.favOnly = checked and true or false; J:RefreshView() end
+ UIDropDownMenu_AddButton(info, level)
+
+ local src = UIDropDownMenu_CreateInfo()
+ src.text = SOURCES or "Sources"
+ src.notCheckable = true; src.hasArrow = true; src.value = "sources"
+ UIDropDownMenu_AddButton(src, level)
+
+ elseif level == 2 and UIDROPDOWNMENU_MENU_VALUE == "sources" then
+ local function refreshSub()
+ if UIDropDownMenu_Refresh then pcall(UIDropDownMenu_Refresh, J.filterMenu, nil, 2) end
+ end
+ local all = UIDropDownMenu_CreateInfo()
+ all.notCheckable = true; all.keepShownOnClick = true
+ all.text = CHECK_ALL or "Check All"
+ all.func = function() J.sourceHidden = {}; J:RefreshView(); refreshSub() end
+ UIDropDownMenu_AddButton(all, level)
+ all.text = UNCHECK_ALL or "Uncheck All"
+ all.func = function()
+ J.sourceHidden = {}
+ for _, idx in ipairs(presentSources()) do J.sourceHidden[idx] = true end
+ J:RefreshView(); refreshSub()
+ end
+ UIDropDownMenu_AddButton(all, level)
+
+ for _, idx in ipairs(presentSources()) do
+ local i2 = UIDropDownMenu_CreateInfo()
+ i2.isNotRadio = true; i2.keepShownOnClick = true
+ i2.text = (C.SourceLabel and C.SourceLabel(idx)) or ("Source " .. idx)
+ i2.checked = function() return not J.sourceHidden[idx] end
+ i2.func = function(_, _, _, checked) J.sourceHidden[idx] = (not checked) and true or nil; J:RefreshView() end
+ UIDropDownMenu_AddButton(i2, level)
+ end
+ end
+ end
+
+ local function buildMenus()
+ J.rowMenu = CreateFrame("Frame", "NE_Collections_" .. kind .. "RowMenu", UIParent, "UIDropDownMenuTemplate")
+ UIDropDownMenu_Initialize(J.rowMenu, rowMenuInit, "MENU")
+ J.filterMenu = CreateFrame("Frame", "NE_Collections_" .. kind .. "FilterMenu", UIParent, "UIDropDownMenuTemplate")
+ UIDropDownMenu_Initialize(J.filterMenu, filterMenuInit, "MENU")
+ end
+
+ local function buildList()
+ searchBox = CreateFrame("EditBox", nil, listGroup, "InputBoxTemplate")
+ searchBox:SetSize(140, SEARCH_H)
+ searchBox:SetPoint("TOPLEFT", listGroup, "TOPLEFT", 12, -8)
+ searchBox:SetAutoFocus(false)
+ searchBox:SetTextInsets(18, 6, 0, 0) -- leave room for the left magnifier
+ -- Search magnifier at the box's left edge (retail SearchBoxTemplate), with a "Search" placeholder.
+ local si = searchBox:CreateTexture(nil, "OVERLAY")
+ si:SetSize(14, 14)
+ si:SetPoint("LEFT", searchBox, "LEFT", 4, 0)
+ si:SetTexture(C.tex.searchIcon)
+ local ph = searchBox:CreateFontString(nil, "ARTWORK", "GameFontDisable")
+ ph:SetPoint("LEFT", searchBox, "LEFT", 20, 0)
+ ph:SetText(SEARCH or "Search")
+ searchBox:SetScript("OnTextChanged", function(self)
+ local txt = self:GetText() or ""
+ if txt == "" then ph:Show() else ph:Hide() end
+ J.search = txt
+ J:RefreshView()
+ end)
+ searchBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
+
+ filterBtn = CreateFrame("Button", "NE_Collections_" .. kind .. "FilterButton", listGroup, "UIPanelButtonTemplate")
+ filterBtn:SetSize(78, SEARCH_H)
+ filterBtn:SetPoint("TOPRIGHT", listGroup, "TOPRIGHT", -6, -8)
+ filterBtn:SetText(FILTER or "Filter")
+ -- Dropdown arrow on the right edge (retail filter button).
+ local far = filterBtn:CreateTexture(nil, "ARTWORK")
+ far:SetSize(8, 8)
+ far:SetPoint("RIGHT", filterBtn, "RIGHT", -6, 0)
+ far:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow")
+ local ftext = filterBtn:GetFontString()
+ if ftext then ftext:ClearAllPoints(); ftext:SetPoint("LEFT", filterBtn, "LEFT", 8, 0) end
+ filterBtn:SetScript("OnClick", function(self)
+ if PlaySound then pcall(PlaySound, "igMainMenuOptionCheckBoxOn") end
+ if ToggleDropDownMenu and J.filterMenu then
+ ToggleDropDownMenu(1, nil, J.filterMenu, self, 0, 0)
+ end
+ end)
+
+ -- Named FauxScrollFrame (an UNNAMED FauxScrollFrameTemplate errors in its OnLoad, which
+ -- concatenates self:GetName()..\"ScrollBar\").
+ scroll = CreateFrame("ScrollFrame", "NE_Collections_" .. kind .. "List", listGroup, "FauxScrollFrameTemplate")
+ scroll:SetPoint("TOPLEFT", listGroup, "TOPLEFT", 6, -(SEARCH_H + 14))
+ scroll:SetPoint("BOTTOMRIGHT", listGroup, "BOTTOMRIGHT", -24, 6)
+ scroll:SetScript("OnVerticalScroll", function(self, offset)
+ FauxScrollFrame_OnVerticalScroll(self, offset, J._rowH or ROW_H, function() J:UpdateList() end)
+ end)
+ scroll:EnableMouseWheel(true)
+ scroll:SetScript("OnMouseWheel", function(self, delta)
+ local sb = _G[(self:GetName() or "") .. "ScrollBar"]
+ if not sb then return end
+ local mn, mx = sb:GetMinMaxValues()
+ local v = sb:GetValue() - delta * (J._rowH or ROW_H)
+ if v < mn then v = mn elseif v > mx then v = mx end
+ sb:SetValue(v)
+ end)
+ scroll:HookScript("OnSizeChanged", function() J:UpdateList() end)
+ -- Hand-built minimal scrollbar (the stock-slider Reskin path doesn't render for FauxScrollFrames).
+ if NE.scrollbar and NE.scrollbar.BuildCustom then pcall(NE.scrollbar.BuildCustom, scroll, { x = -6 }) end
+
+ -- The row container is a child of listGroup (always shown), NOT of the FauxScrollFrame:
+ -- FauxScrollFrame_Update HIDES the scroll frame when the list fits with no scrolling, which would
+ -- take every row down with it (the documented empty-pane bug — see Skills.lua). Anchor content
+ -- over the scroll rect and raise it above the scroll frame.
+ content = CreateFrame("Frame", nil, listGroup)
+ content:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
+ content:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
+ content:SetHeight(1)
+ content:SetFrameLevel(scroll:GetFrameLevel() + 5)
+ end
+
+ -- ------------------------------------------------------------------------
+ -- Public: build once, refresh (rebuild data + relayout), show/hide the group.
+ -- ------------------------------------------------------------------------
+ local built = false
+ local function build()
+ if built then return end
+ built = true
+ buildTop()
+ buildMenus()
+ buildList()
+ buildDisplay()
+ buildAction()
+ end
+
+ -- Re-run the filter/sort + repaint only (search box, filter-menu toggles, favouriting) — NOT a
+ -- full companion re-scan. J.all's table objects are mutated in place by rebuildView (never
+ -- replaced), so J.selected stays a live, valid reference; no re-pointing needed here.
+ function J:RefreshView()
+ rebuildView()
+ if not J.selected and J.list[1] then J.selected = J.list[1] end
+ showModel(J.selected)
+ updateSummonIcon()
+ self:UpdateList()
+ updateAction()
+ if emptyLabel then
+ if #J.list == 0 then
+ emptyLabel:SetText((J.collectedCount or 0) == 0
+ and (isMount and (ERR_NO_RIDING_SKILL or "You have not learned any mounts.")
+ or "You have not learned any companions.")
+ or "No results match your filters.")
+ emptyLabel:Show()
+ else
+ emptyLabel:Hide()
+ end
+ end
+ end
+
+ -- Full refresh: re-scan GetCompanionInfo + the not-yet-learned DB pass (rebuildAll REPLACES
+ -- J.all's table objects), then RefreshView. Used on tab-select and companion-learn/summon events;
+ -- search/filter-only changes use the cheaper RefreshView above instead.
+ function J:Refresh()
+ build()
+ rebuildAll()
+ -- Keep a valid selection across the rebuild: re-point at the same companion by spellID (stable
+ -- identity for both collected AND not-yet-learned rows — unlike creatureID, which is nil for
+ -- every not-yet-learned row and would falsely "match" the first such row it finds via nil==nil).
+ if J.selected and J.selected.spellID then
+ local keep
+ for _, d in ipairs(J.all) do if d.spellID == J.selected.spellID then keep = d; break end end
+ J.selected = keep
+ end
+ self:RefreshView()
+ end
+
+ function J:Show()
+ build()
+ for _, g in ipairs(J._groups) do g:Show() end
+ end
+ function J:Hide()
+ for _, g in ipairs(J._groups) do g:Hide() end
+ end
+
+ return J
+end
+
+-- ---------------------------------------------------------------------------
+-- Window.lua calls this at PLAYER_LOGIN (after the shell exists) to instantiate both journals.
+-- ---------------------------------------------------------------------------
+function C.BuildJournals()
+ if C._journalsBuilt then return end
+ if not (C.LeftInset and C.RightInset and C.TopBand and C.BottomBand) then
+ log("BuildJournals: shell insets not ready"); return
+ end
+ C._journalsBuilt = true
+ C.RegisterJournal("MOUNT", createJournal("MOUNT"))
+ C.RegisterJournal("CRITTER", createJournal("CRITTER"))
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/MicroButton.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/MicroButton.lua
new file mode 100644
index 0000000..bf6e4f2
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/MicroButton.lua
@@ -0,0 +1,224 @@
+-- DragonUI_NewEra/modules/collections/MicroButton.lua
+-- A micromenu-adjacent button that opens the Collections (Mounts & Pet Journal) window.
+--
+-- DragonUI's own micromenu module (DragonUI/modules/micromenu.lua) already RESERVES a slot for
+-- this: its MICRO_BUTTONS array includes `_G.CollectionsMicroButton` between LFDMicroButton and
+-- PVPMicroButton (micromenu.lua:120), and it ships full colored-mode atlas coordinates for it
+-- (MicromenuAtlas["UI-HUD-MicroMenu-Collections-*"], micromenu.lua:192-195) pointing at
+-- Interface\AddOns\DragonUI\Textures\Micromenu\uimicromenu2x.blp -- this client just has no
+-- native CollectionsMicroButton frame for 3.3.5a to have discovered, so that slot has sat
+-- permanently nil. We can't retroactively fill it though: MICRO_BUTTONS is a `local` table built
+-- ONCE at DragonUI's own file-load time (before DragonUI_NewEra ever runs), so creating a real
+-- `_G.CollectionsMicroButton` afterward is invisible to that closure -- same extension-point
+-- problem modules/encounterjournal/MicroButton.lua hit for Adventure Guide, just with the added
+-- irony that here the array slot AND the art already exist. We build a standalone button exactly
+-- like that module does, just reusing DragonUI's own shipped Collections icon (a real in-tree
+-- asset, no need to import another BLP) and inserting at the position DragonUI's own array
+-- already implies is correct: between LFD and PVP.
+--
+-- See modules/encounterjournal/MicroButton.lua for the full rationale on the shift-cluster
+-- technique below -- this is the same mechanism, just targeting a different gap.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+NE.collections = NE.collections or {}
+
+local unpack = unpack
+
+-- DragonUI's own colored micro-icon sheet already contains real Collections art (mirrors
+-- DragonUI/modules/micromenu.lua:192-195 exactly -- verified present in-tree at
+-- DragonUI/Textures/Micromenu/uimicromenu2x.blp).
+local ICON_FILE = [[Interface\AddOns\DragonUI\Textures\Micromenu\uimicromenu2x]]
+local COORDS = {
+ up = { 0.129883, 0.192383, 0.166016, 0.326172 },
+ down = { 0.065430, 0.127930, 0.822266, 0.982422 },
+ mouseover = { 0.129883, 0.192383, 0.001953, 0.162109 },
+ disabled = { 0.065430, 0.127930, 0.658203, 0.818359 },
+}
+-- Shared rounded backplate DragonUI paints behind every native icon -- same rect for every
+-- button (DragonUI/modules/micromenu.lua:2157/2164), not per-icon art.
+local BG = {
+ normal = { 0.065430, 0.127930, 0.330078, 0.490234 },
+ pushed = { 0.065430, 0.127930, 0.494141, 0.654297 },
+}
+
+local btn
+
+local function create()
+ if btn then return btn end
+
+ -- Parent to pUiMicroMenu (not UIParent) to inherit the same effective scale as every native
+ -- button -- see encounterjournal/MicroButton.lua for why a plain-UIParent parent renders
+ -- 1/menuScale times too big on screen.
+ local parent = _G.pUiMicroMenu or UIParent
+ local b = CreateFrame("Button", "NE_CollectionsMicroButton", parent)
+ b:SetSize(32, 40)
+ b:SetFrameStrata("MEDIUM")
+
+ local bg = b:CreateTexture(nil, "BACKGROUND")
+ bg:SetTexture(ICON_FILE)
+ bg:SetTexCoord(unpack(BG.normal))
+ bg:SetPoint("CENTER", b, "CENTER", -1, 1)
+ bg:SetSize(32, 41)
+
+ local bgPushed = b:CreateTexture(nil, "BACKGROUND")
+ bgPushed:SetTexture(ICON_FILE)
+ bgPushed:SetTexCoord(unpack(BG.pushed))
+ bgPushed:SetPoint("CENTER", b, "CENTER", -1, 1)
+ bgPushed:SetSize(32, 41)
+ bgPushed:Hide()
+ b:SetScript("OnMouseDown", function() bg:Hide(); bgPushed:Show() end)
+ b:SetScript("OnMouseUp", function() bg:Show(); bgPushed:Hide() end)
+
+ local n = b:CreateTexture(nil, "ARTWORK")
+ n:SetTexture(ICON_FILE)
+ n:SetTexCoord(unpack(COORDS.up))
+ n:SetAllPoints(b)
+ b:SetNormalTexture(n)
+
+ local p = b:CreateTexture(nil, "ARTWORK")
+ p:SetTexture(ICON_FILE)
+ p:SetTexCoord(unpack(COORDS.down))
+ p:SetAllPoints(b)
+ b:SetPushedTexture(p)
+
+ local h = b:CreateTexture(nil, "HIGHLIGHT")
+ h:SetTexture(ICON_FILE)
+ h:SetTexCoord(unpack(COORDS.mouseover))
+ h:SetAllPoints(b)
+ h:SetBlendMode("ADD")
+ b:SetHighlightTexture(h)
+
+ local d = b:CreateTexture(nil, "ARTWORK")
+ d:SetTexture(ICON_FILE)
+ d:SetTexCoord(unpack(COORDS.disabled))
+ d:SetAllPoints(b)
+ b:SetDisabledTexture(d)
+
+ b:SetScript("OnClick", function()
+ if NE.collections.Toggle then NE.collections.Toggle() end
+ end)
+ b:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_LEFT")
+ GameTooltip:SetText("Collections")
+ GameTooltip:Show()
+ end)
+ b:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ btn = b
+ return b
+end
+
+-- Temporarily restore a DragonUI-noop'd button's real SetPoint, reposition it, then re-install
+-- the no-op -- same pattern DragonUI itself uses when it needs to move one of its own buttons
+-- after initial layout (see MainMenuBarBackpackButton handling in micromenu.lua).
+local function setPointThroughNoop(button, ...)
+ local dragon = NE.dragon
+ local noop = dragon and dragon._noop
+ local wasNooped = (noop ~= nil and button.SetPoint == noop)
+ if wasNooped then button.SetPoint = UIParent.SetPoint end
+ button:ClearAllPoints()
+ button:SetPoint(...)
+ if wasNooped then button.SetPoint = noop end
+end
+
+-- Native buttons from Character through LFD (inclusive), DragonUI's own left-to-right order
+-- (micromenu.lua:112-124), nearest-to-the-gap first. These all shift one slot further left to
+-- open a gap immediately left of PVP, where DragonUI's own MICRO_BUTTONS array already expects
+-- Collections to sit.
+local LEFT_CLUSTER_NAMES = {
+ "LFDMicroButton", "SocialsMicroButton", "QuestLogMicroButton",
+ "AchievementMicroButton", "TalentMicroButton", "SpellbookMicroButton", "CharacterMicroButton",
+}
+
+local function reposition()
+ if not btn then return end
+
+ local pvp = _G.PVPMicroButton
+ local mainMenu = _G.MainMenuMicroButton
+ local help = _G.HelpMicroButton
+ local lfd = _G.LFDMicroButton
+ local menu = _G.pUiMicroMenu
+ if not (pvp and mainMenu and help and lfd and menu and pvp:IsVisible()) then
+ btn:Hide()
+ return
+ end
+
+ -- Live gap measured off Help/MainMenu -- NOT PVP/MainMenu. PVP is exactly the button
+ -- encounterjournal/MicroButton.lua shifts left to open room for its own insertion between PVP
+ -- and MainMenu, so whenever that module is active, PVP no longer sits its native one-gap
+ -- distance from MainMenu -- measuring off it here would read a bloated ~2-gap distance and
+ -- blow up every spacing this module computes (its own button, and the whole LFD..Character
+ -- shift). Help/MainMenu are the two buttons NEITHER module ever moves, so they're a gap
+ -- reference that stays a true single-unit gap regardless of which of these two modules are
+ -- enabled or what order their repositioning runs in.
+ local realGap = help:GetLeft() - mainMenu:GetRight()
+
+ -- Sanity guard: DragonUI's skin may not have applied yet (buttons still at stock Blizzard
+ -- positions). Bail and let the next scheduled retry (or the next real DragonUI refresh) catch
+ -- it once the skin has actually settled.
+ if realGap > 100 or realGap < -100 then
+ btn:Hide()
+ return
+ end
+
+ local menuScale = menu:GetEffectiveScale()
+ local localGap = menuScale ~= 0 and (realGap / menuScale) or realGap
+
+ -- PVP/MainMenu/Help are never touched -- our button sits immediately left of PVP, and the
+ -- whole Character..LFD cluster shifts one slot further left (open screen space there) to make
+ -- room.
+ btn:ClearAllPoints()
+ btn:SetPoint("BOTTOMRIGHT", pvp, "BOTTOMLEFT", -localGap, 0)
+ btn:Show()
+
+ local prev = btn
+ for _, name in ipairs(LEFT_CLUSTER_NAMES) do
+ local b = _G[name]
+ if b then
+ setPointThroughNoop(b, "BOTTOMRIGHT", prev, "BOTTOMLEFT", -localGap, 0)
+ prev = b
+ end
+ end
+end
+NE.collections.RefreshMicroButton = reposition
+
+local function init()
+ create()
+ reposition()
+end
+
+local f = CreateFrame("Frame")
+f:RegisterEvent("PLAYER_LOGIN")
+f:RegisterEvent("UNIT_ENTERING_VEHICLE")
+f:RegisterEvent("UNIT_EXITING_VEHICLE")
+f:SetScript("OnEvent", function(_, event)
+ if event == "PLAYER_LOGIN" then
+ -- DragonUI applies its micromenu skin on the same event, but how long that actually takes to
+ -- settle varies -- retry a few times over the first several seconds; reposition() is
+ -- idempotent (recomputes fresh from current button positions, with its own sanity guard
+ -- against an unsettled layout), so repeated calls are harmless and self-correcting.
+ if C_Timer and C_Timer.After then
+ C_Timer.After(1, init)
+ C_Timer.After(3, reposition)
+ C_Timer.After(6, reposition)
+ C_Timer.After(10, reposition)
+ else
+ init()
+ end
+ f:UnregisterEvent("PLAYER_LOGIN")
+ else
+ reposition()
+ end
+end)
+
+-- Track DragonUI's own relayouts (spacing/scale/grayscale/vehicle changes) instead of its private
+-- layout internals -- these are the module's public refresh entry points.
+do
+ local dragon = NE.dragon
+ if dragon then
+ if dragon.RefreshMicromenu then hooksecurefunc(dragon, "RefreshMicromenu", reposition) end
+ if dragon.RefreshMicromenuSystem then hooksecurefunc(dragon, "RefreshMicromenuSystem", reposition) end
+ if dragon.RefreshMicromenuVehicle then hooksecurefunc(dragon, "RefreshMicromenuVehicle", reposition) end
+ end
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Open.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Open.lua
new file mode 100644
index 0000000..d68fbc5
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Open.lua
@@ -0,0 +1,44 @@
+-- DragonUI_NewEra/modules/collections/Open.lua — open path for the Collections window.
+--
+-- Gives NE_CollectionsFrame a keybind (NEWERA_TOGGLECOLLECTIONS, declared in the addon-root
+-- Bindings.xml) and applies SHIFT-P as its default key on first run — the key that used to open the
+-- old Character-panel "Collection" view, now retired in favour of this standalone window. Mirrors the
+-- guild module's Open.lua (labels + latched default-binding) exactly; the frame is insecure with no
+-- protected children, so the binding body is combat-safe.
+
+local NE = DragonUI_NewEra
+if not NE then return end
+NE.collections = NE.collections or {}
+local C = NE.collections
+
+-- Binding labels for the Key Bindings UI. Bindings.xml declares the action; the settings UI reads
+-- these globals for the header + row label. Never overwrite an existing global.
+BINDING_HEADER_DRAGONUI_NEWERA = BINDING_HEADER_DRAGONUI_NEWERA or "DragonUI New Era"
+BINDING_NAME_NEWERA_TOGGLECOLLECTIONS = BINDING_NAME_NEWERA_TOGGLECOLLECTIONS or "Toggle Collections (Mounts & Pets)"
+
+-- Default keybind: SHIFT-P opens the Collections window. The 3.3.5a Bindings.xml schema has no
+-- `default` attribute, so the default is applied once via SetBinding, latched in the DB so we never
+-- fight a later manual rebind. The player explicitly asked for SHIFT-P (it opened the old companion
+-- view), so we DO take the key over on first run even if it currently maps elsewhere — but only
+-- SHIFT-P, only once, and only if our action isn't already bound to some other key.
+local DEFAULT_KEY = "SHIFT-P"
+
+local function applyDefaultBinding()
+ local db = NE.db
+ if not db or db.collectionsKeybindApplied then return end
+ if InCombatLockdown and InCombatLockdown() then return end -- SetBinding is blocked in combat; retry next login
+ if not (SetBinding and SaveBindings and GetBindingKey) then return end
+ db.collectionsKeybindApplied = true
+
+ if GetBindingKey("NEWERA_TOGGLECOLLECTIONS") then return end -- already bound somewhere — respect it
+ if SetBinding(DEFAULT_KEY, "NEWERA_TOGGLECOLLECTIONS") then
+ local set = (GetCurrentBindingSet and GetCurrentBindingSet()) or 1
+ pcall(SaveBindings, set)
+ end
+end
+
+local ev = CreateFrame("Frame")
+ev:RegisterEvent("PLAYER_LOGIN")
+ev:SetScript("OnEvent", function()
+ applyDefaultBinding()
+end)
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Window.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Window.lua
new file mode 100644
index 0000000..4bd132e
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/collections/Window.lua
@@ -0,0 +1,378 @@
+-- DragonUI_NewEra/modules/collections/Window.lua — the STANDALONE Collections WINDOW HOST.
+--
+-- A dedicated retail-style Collections journal (NE_CollectionsFrame), replacing the old
+-- Mounts & Companions "Collection" view that used to live inside the Character panel's Pet tab
+-- (modules/character/Companions.lua — now retired). Two bottom tabs, Mounts and Pet Journal, each a
+-- self-contained "journal" pane built by Journal.lua over the raw 3.3.5a Companions API
+-- (GetNumCompanions / GetCompanionInfo / CallCompanion / DismissCompanion) and enriched with the
+-- ported source-info database (Data.lua) for the retail-looking right-hand info panel.
+--
+-- SHARED INTERFACE CONTRACT (Journal.lua depends on this — provided EXACTLY):
+-- NE.collections.frame the window Frame (global "NE_CollectionsFrame")
+-- NE.collections.LeftInset recessed panel that hosts the scrollable list
+-- NE.collections.RightInset recessed panel that hosts the model + info display
+-- NE.collections.TopBand thin strip under the title for count/search/filter/summon
+-- NE.collections.BottomBand strip under the insets for the Mount/Summon action button
+-- NE.collections.activeKind "MOUNT" or "CRITTER" (the GetCompanionInfo filter names)
+-- NE.collections.RegisterJournal(kind, obj) obj must expose :Show() :Hide() :Refresh()
+-- NE.collections.Toggle()/.Open()/.SetShown(b)/.SelectTab(kind)
+--
+-- RULES (house style): only // -> -- comments; no SetShown widget method (Show/Hide); guard every
+-- global; this is an INSECURE frame with no protected children, so no combat lockdown dance is
+-- needed (unlike the spellbook's secure spell cards).
+
+local NE = DragonUI_NewEra
+NE.collections = NE.collections or {}
+local C = NE.collections
+
+local FRAME_NAME = "NE_CollectionsFrame"
+local MODULE = "Collections"
+
+-- Geometry (tunable). Portrait frame with a ~24px title band; a top strip for search/count/filter/
+-- summon; twin recessed insets (narrow list on the left, wide model+info on the right); a bottom
+-- strip for the action button. Bottom tabs hang BELOW the frame, character-panel style.
+local FRAME_W = 640
+-- Reduced from 580 (owner feedback: "feels quite stretched height wise" — a screenshot showed a lot
+-- of dead background below the list rows and below the model preview). Width is untouched; TOP_H/
+-- BOTTOM_H/SIDE_PAD stay the same absolute pixel sizes, so the ~100px cut comes entirely out of the
+-- two insets' available height (fewer rows per page, a somewhat smaller model viewport) rather than
+-- out of any of the fixed chrome strips.
+local FRAME_H = 480
+local TITLE_H = 24
+local TOP_H = 34 -- search / count / filter / summon strip height
+local BOTTOM_H = 30 -- action-button strip height
+local SIDE_PAD = 14
+local LEFT_W = 244
+local INSET_GAP = 16
+
+local TAB_H_INACTIVE = 36
+local TAB_H_ACTIVE = 42
+
+-- ---------------------------------------------------------------------------
+-- Logger + guard (NE.Log may be absent on a partial load).
+-- ---------------------------------------------------------------------------
+local function log(msg)
+ if NE.Log then NE.Log("COLLECTIONS", msg); return end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffffcc55DragonUI_NewEra|r [collections]: " .. tostring(msg))
+ end
+end
+C._log = log
+
+local function guard(label, fn)
+ local ok, err = pcall(fn)
+ if not ok then log(label .. " failed: " .. tostring(err)) end
+ return ok
+end
+
+-- Tab order + labels + portrait icon. "MOUNT"/"CRITTER" are the GetCompanionInfo filter names.
+local TABS = {
+ { key = "MOUNT", label = _G.MOUNTS or "Mounts", portrait = C.tex and C.tex.mountPortrait },
+ { key = "CRITTER", label = "Pet Journal", portrait = C.tex and C.tex.petPortrait },
+}
+
+C.journals = C.journals or {} -- kind -> journal object (Show/Hide/Refresh), set by Journal.lua
+C._tabs = C._tabs or {} -- kind -> tab button
+C.activeKind = C.activeKind or "MOUNT"
+
+function C.RegisterJournal(kind, obj)
+ C.journals[kind] = obj
+end
+
+-- ---------------------------------------------------------------------------
+-- Recessed inset (thin gold InsetFrameTemplate border + dark fill) — the same idiom every other
+-- NE window uses (modules/lfg/Window.lua buildRail, modules/character/InsetFrames.lua).
+-- ---------------------------------------------------------------------------
+local function buildInset(name, parent)
+ local inset = CreateFrame("Frame", name, parent)
+ inset:SetFrameLevel((parent:GetFrameLevel() or 1) + 2)
+ local bg = inset:CreateTexture(nil, "BACKGROUND")
+ bg:SetPoint("TOPLEFT", 3, -3)
+ bg:SetPoint("BOTTOMRIGHT", -3, 3)
+ if bg.SetColorTexture then bg:SetColorTexture(0.05, 0.05, 0.06, 0.92)
+ else bg:SetTexture(0.05, 0.05, 0.06, 0.92) end
+ inset._bg = bg
+ if NE.nineslice and NE.nineslice.ApplyLayout then
+ pcall(NE.nineslice.ApplyLayout, inset, "InsetFrameTemplate")
+ end
+ return inset
+end
+
+-- ---------------------------------------------------------------------------
+-- Portrait — the round Mount/Pet journal icon in the metal corner, swapped per active tab. Mirrors
+-- the spellbook's own-portrait recipe (host on f.NineSlice at ARTWORK so it draws over the wood but
+-- under the gold ring; apply the circular cutout mask, then set the flat icon texture).
+-- ---------------------------------------------------------------------------
+local function applyPortrait(f, path)
+ if not f then return end
+ local ringFrame = f.NineSlice or f
+ if not f.portrait then f.portrait = ringFrame:CreateTexture(nil, "ARTWORK") end
+ if NE.portrait and NE.portrait.ApplyCutout then pcall(NE.portrait.ApplyCutout, f.portrait, f) end
+ if path then f.portrait:SetTexture(path) end
+end
+
+-- ---------------------------------------------------------------------------
+-- Bottom tabs (Mounts / Pet Journal) — character-panel style: vanilla CharacterFrameTabButtonTemplate
+-- reskinned to DF metal via NE.tabs.ReskinClassicTab, art driven manually (selected -> *Disabled/gold
+-- pieces). Hang below the frame's BOTTOMLEFT.
+-- ---------------------------------------------------------------------------
+local function setTabArt(tab, selected)
+ if not tab then return end
+ local n = tab:GetName()
+ local function set(suffix, show)
+ local t = _G[n .. suffix]
+ if t then if show then t:Show() else t:Hide() end end
+ end
+ set("Left", not selected); set("Middle", not selected); set("Right", not selected)
+ set("LeftDisabled", selected); set("MiddleDisabled", selected); set("RightDisabled", selected)
+ local hl = tab._neCustomHL
+ if hl then
+ local a = selected and 0 or 0.4
+ if hl.left and hl.left.SetAlpha then hl.left:SetAlpha(a) end
+ if hl.middle and hl.middle.SetAlpha then hl.middle:SetAlpha(a) end
+ if hl.right and hl.right.SetAlpha then hl.right:SetAlpha(a) end
+ end
+end
+
+local function sizeTab(tab)
+ if not tab then return end
+ local text = _G[tab:GetName() .. "Text"]
+ local w = 70
+ if text then text:SetWidth(0); w = math.max(70, math.floor((text:GetWidth() or 0) + 30)) end
+ tab:SetWidth(w)
+end
+
+local function rechainTabs(f)
+ local prev
+ for _, def in ipairs(TABS) do
+ local tab = C._tabs[def.key]
+ if tab then
+ tab:ClearAllPoints()
+ if prev then tab:SetPoint("TOPLEFT", prev, "TOPRIGHT", 1, 0)
+ else tab:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 14, 2) end
+ prev = tab
+ end
+ end
+end
+
+local function buildTabs(f)
+ for i, def in ipairs(TABS) do
+ local name = FRAME_NAME .. "Tab" .. i
+ local tab = C._tabs[def.key] or _G[name]
+ if not tab then
+ local ok, t = pcall(CreateFrame, "Button", name, f, "CharacterFrameTabButtonTemplate")
+ if ok and t then tab = t else
+ tab = CreateFrame("Button", name, f, "UIPanelButtonTemplate"); tab._nePlain = true
+ end
+ tab:SetID(i)
+ tab.tabKey = def.key
+ local txt = _G[name .. "Text"]
+ if txt then txt:SetText(def.label) elseif tab.SetText then tab:SetText(def.label) end
+ tab:SetScript("OnClick", function(self)
+ if PlaySound then pcall(PlaySound, "igCharacterInfoTab") end
+ C.SelectTab(self.tabKey)
+ end)
+ C._tabs[def.key] = tab
+ end
+ if not tab._nePlain and NE.tabs and NE.tabs.ReskinClassicTab then
+ pcall(NE.tabs.ReskinClassicTab, name, {})
+ end
+ sizeTab(tab)
+ tab:SetHeight(TAB_H_INACTIVE)
+ end
+ rechainTabs(f)
+end
+
+-- ---------------------------------------------------------------------------
+-- Tab selection: swap art/height/level, portrait + title, and show the active journal (hide others).
+-- ---------------------------------------------------------------------------
+function C.SelectTab(kind)
+ if not C._tabs[kind] then return end
+ C.activeKind = kind
+ local f = C.frame
+ local baseLevel = (f and f:GetFrameLevel() or 1) + 2
+ local activeDef
+ for _, def in ipairs(TABS) do
+ local tab = C._tabs[def.key]
+ local on = (def.key == kind)
+ if def.key == kind then activeDef = def end
+ if tab then
+ setTabArt(tab, on)
+ tab:SetHeight(on and TAB_H_ACTIVE or TAB_H_INACTIVE)
+ sizeTab(tab)
+ if tab.SetFrameLevel then tab:SetFrameLevel(baseLevel + (on and 8 or 0)) end
+ local txt = _G[tab:GetName() .. "Text"]
+ if txt then txt:ClearAllPoints(); txt:SetPoint("CENTER", tab, "CENTER", 0, on and -3 or 0) end
+ end
+ end
+ rechainTabs(f)
+
+ -- Portrait + title follow the active category (retail shows "Mounts" / "Pet Journal" in the bar).
+ if activeDef then
+ applyPortrait(f, activeDef.portrait)
+ if NE.panelchrome and NE.panelchrome.SetTitle then NE.panelchrome.SetTitle(f, activeDef.label) end
+ end
+
+ -- Show the active journal, hide the rest, then refresh the one now in view.
+ for k, j in pairs(C.journals) do
+ if k == kind then if j.Show then j:Show() end else if j.Hide then j:Hide() end end
+ end
+ local jr = C.journals[kind]
+ if jr and jr.Refresh then guard("journal refresh", function() jr:Refresh() end) end
+end
+
+-- ---------------------------------------------------------------------------
+-- Build the window shell ONCE. Hidden by default.
+-- ---------------------------------------------------------------------------
+local function buildWindow()
+ if C.frame then return C.frame end
+
+ local f = CreateFrame("Frame", FRAME_NAME, UIParent)
+ -- The red 3-slice is the addon's standard button; Watch keeps this window's panel buttons
+ -- skinned as its panes are built (core/ButtonSkin.lua). Opt out per button with _neNoSkin.
+ if NE.buttonskin and NE.buttonskin.Watch then pcall(NE.buttonskin.Watch, f) end
+ f:SetSize(FRAME_W, FRAME_H)
+ f:SetFrameStrata("HIGH")
+ f:SetToplevel(true)
+ f:Hide()
+ C.frame = f
+
+ -- Drag-to-move with saved position (account-wide, persists across /reload + sessions).
+ if NE.FrameUtil and NE.FrameUtil.PersistWindowPosition then
+ NE.FrameUtil.PersistWindowPosition(f, "collections",
+ { point = "CENTER", relPoint = "CENTER", x = 0, y = 40 })
+ else
+ f:SetPoint("CENTER", UIParent, "CENTER", 0, 40)
+ f:EnableMouse(true); f:SetMovable(true); f:SetClampedToScreen(true)
+ f:RegisterForDrag("LeftButton")
+ f:SetScript("OnDragStart", function(self) self:StartMoving() end)
+ f:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
+ end
+
+ -- Open/close sounds via a child watcher (never on the frame itself).
+ guard("sounds", function()
+ if NE.FrameUtil and NE.FrameUtil.WirePanelSounds then
+ NE.FrameUtil.WirePanelSounds(f, "igCharacterInfoOpen", "igCharacterInfoClose")
+ end
+ end)
+
+ -- ESC closes it.
+ guard("escClose", function()
+ if NE.FrameUtil and NE.FrameUtil.EscClose then NE.FrameUtil.EscClose(FRAME_NAME)
+ else tinsert(UISpecialFrames, FRAME_NAME) end
+ end)
+
+ -- Modern DF portrait chrome (nineslice + Rock bg + title). noPortrait: we own the portrait as the
+ -- Mount/Pet journal icon, swapped per tab in SelectTab.
+ guard("chrome.Apply", function()
+ if NE.chrome and NE.chrome.Apply then
+ NE.chrome.Apply(f, { layout = "PortraitFrameTemplate", title = _G.COLLECTIONS or "Collections", noPortrait = true })
+ end
+ end)
+ -- PC.ApplyModernChrome tints its own f.Bg to 0.32 grey (a DOWNPORT fix for a different frame's
+ -- first-paint colour flash), which reads noticeably darker than the plain, full-brightness
+ -- UI-Background-Rock fill our other from-scratch standalone windows use (Guild/LFG/Professions/
+ -- AuctionHouse/Social all build an untinted rock body). Retail's real Mount Journal outer frame
+ -- is that same plain grey stone, so strip the tint back to full colour here to match.
+ guard("bgTint", function()
+ if f.Bg and f.Bg.SetVertexColor then f.Bg:SetVertexColor(1, 1, 1) end
+ end)
+ guard("portrait", function()
+ applyPortrait(f, TABS[1].portrait)
+ f:HookScript("OnShow", function(self) applyPortrait(self, (TABS[1] and TABS[1].portrait)) end)
+ end)
+ guard("closeButton", function()
+ if NE.panelchrome and NE.panelchrome.ModernizeCloseButton and f.CloseButton then
+ NE.panelchrome.ModernizeCloseButton(f.CloseButton, { frameLevelBump = 20 })
+ end
+ end)
+
+ -- Top strip (count / search / filter / summon live here, built by the journals).
+ local top = CreateFrame("Frame", nil, f)
+ top:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD, -(TITLE_H + 6))
+ top:SetPoint("TOPRIGHT", f, "TOPRIGHT", -SIDE_PAD, -(TITLE_H + 6))
+ top:SetHeight(TOP_H)
+ top:SetFrameLevel((f:GetFrameLevel() or 1) + 3)
+ C.TopBand = top
+
+ -- Twin recessed insets.
+ local left = buildInset(FRAME_NAME .. "LeftInset", f)
+ left:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD, -(TITLE_H + TOP_H + 8))
+ left:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", SIDE_PAD, BOTTOM_H + 6)
+ left:SetWidth(LEFT_W)
+ C.LeftInset = left
+
+ local right = buildInset(FRAME_NAME .. "RightInset", f)
+ right:SetPoint("TOPLEFT", left, "TOPRIGHT", INSET_GAP, 0)
+ right:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 6)
+ C.RightInset = right
+
+ -- Bottom strip (the Mount/Summon action button lives here, built by the journals).
+ local bottom = CreateFrame("Frame", nil, f)
+ bottom:SetPoint("BOTTOMLEFT", left, "BOTTOMLEFT", 0, -(BOTTOM_H + 2))
+ bottom:SetPoint("BOTTOMRIGHT", left, "BOTTOMRIGHT", 0, -(BOTTOM_H + 2))
+ bottom:SetHeight(BOTTOM_H)
+ bottom:SetFrameLevel((f:GetFrameLevel() or 1) + 3)
+ C.BottomBand = bottom
+
+ guard("tabs", function() buildTabs(f) end)
+
+ return f
+end
+C.BuildWindow = buildWindow
+
+-- ---------------------------------------------------------------------------
+-- Show / hide / toggle.
+-- ---------------------------------------------------------------------------
+function C.SetShown(shown)
+ local f = C.frame or buildWindow()
+ if not f then return end
+ if shown then
+ f:Show()
+ C.SelectTab(C.activeKind or "MOUNT")
+ else
+ f:Hide()
+ end
+end
+function C.Open() C.SetShown(true) end
+function C.Show() C.SetShown(true) end
+function C.Hide() C.SetShown(false) end
+function C.Toggle()
+ local f = C.frame or buildWindow()
+ if not f then return end
+ if f:IsShown() then C.SetShown(false) else C.SetShown(true) end
+end
+
+-- ---------------------------------------------------------------------------
+-- Boot. PLAYER_LOGIN builds the shell then asks Journal.lua to build the two journals (guarded — it
+-- loads after this file). Companion learn/update refresh the active journal only while shown.
+-- ---------------------------------------------------------------------------
+local function boot(event)
+ if event == "PLAYER_LOGIN" then
+ buildWindow()
+ if C.BuildJournals then guard("buildJournals", C.BuildJournals) end
+ -- Resting selection so the first open shows a populated Mounts tab.
+ guard("selectInitial", function() C.SelectTab(C.activeKind or "MOUNT") end)
+ return
+ end
+ if C.frame and C.frame:IsShown() then
+ local jr = C.journals[C.activeKind]
+ if jr and jr.Refresh then guard("refresh", function() jr:Refresh() end) end
+ end
+end
+C.Boot = boot
+
+if NE.modules and NE.modules.Register then
+ NE.modules.Register(MODULE, {
+ default = true,
+ label = "Collections",
+ category = "Windows",
+ desc = "The modern Dragonflight Collections window (Mounts & Pet Journal). Opens on the companion keybind.",
+ events = {
+ "PLAYER_LOGIN", "COMPANION_LEARNED", "COMPANION_UPDATE", "COMPANION_UNLEARNED",
+ },
+ onBoot = function(event) boot(event) end,
+ })
+else
+ log("NE.modules.Register absent; collections window not booted")
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/AlertData.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/AlertData.lua
new file mode 100644
index 0000000..22f7bb4
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/AlertData.lua
@@ -0,0 +1,97 @@
+-- DragonUI_NewEra/modules/cooldownviewer/AlertData.lua — GENERATED, DO NOT HAND-EDIT.
+--
+-- Curated data for the visual-alert engine's "Usable" event (Alerts.lua). A Usable alert flashes a
+-- spell ONLY if it appears in one of these tables — retail does not flash every ready spell, and
+-- neither do we. Every id below is resolved from this client's own Spell.dbc; only the ability
+-- NAMES and their mechanics are hand-authored. Regenerate with tools/cdm-spellgen/gen_alertdata.py.
+--
+-- Keyed by EVERY rank, because the engine has to recognise the ability at whichever rank the player
+-- actually casts — our curated lists key rank 1, but the item's _rankCDIDs carries the learned ones
+-- and a custom list may hold any of them.
+--
+-- COVERAGE NOTE: an entry here only ever fires if the spell is present in a viewer. Of the abilities
+-- below, Hammer of Wrath, Kill Shot, Overpower and Revenge are in the curated Essential/Utility
+-- lists; Execute, Victory Rush, Riposte and Counterattack are NOT, because those lists are built
+-- from abilities with a real cooldown and these four have none. They stay listed so they work the
+-- moment a custom list adds them.
+
+local NE = DragonUI_NewEra
+NE.cooldownviewer = NE.cooldownviewer or {}
+local M = NE.cooldownviewer
+M.alertdata = M.alertdata or {}
+local A = M.alertdata
+
+-- EXECUTE = { [spellID] = hpFraction } — castable only below a target health fraction.
+A.EXECUTE = {
+ -- Hunter — Kill Shot (target below 20% health)
+ [53351] = 0.20, -- Rank 1
+ [61005] = 0.20, -- Rank 2
+ [61006] = 0.20, -- Rank 3
+ -- Paladin — Hammer of Wrath (target below 20% health)
+ [24275] = 0.20, -- Rank 1
+ [24274] = 0.20, -- Rank 2
+ [24239] = 0.20, -- Rank 3
+ [27180] = 0.20, -- Rank 4
+ [48805] = 0.20, -- Rank 5
+ [48806] = 0.20, -- Rank 6
+ -- Warrior — Execute (target below 20% health)
+ [5308] = 0.20, -- Rank 1
+ [20658] = 0.20, -- Rank 2
+ [20660] = 0.20, -- Rank 3
+ [20661] = 0.20, -- Rank 4
+ [20662] = 0.20, -- Rank 5
+ [25234] = 0.20, -- Rank 6
+ [25236] = 0.20, -- Rank 7
+ [47470] = 0.20, -- Rank 8
+ [47471] = 0.20, -- Rank 9
+}
+
+-- REACTIVE = { [spellID] = true } — usable only after a combat trigger opens a window.
+A.REACTIVE = {
+ -- Hunter — Counterattack (enabled by a parry)
+ [19306] = true, -- Rank 1
+ [20909] = true, -- Rank 2
+ [20910] = true, -- Rank 3
+ [27067] = true, -- Rank 4
+ [48998] = true, -- Rank 5
+ [48999] = true, -- Rank 6
+ -- Rogue — Riposte (enabled by a parry)
+ [14251] = true, -- unranked
+ -- Warrior — Overpower (enabled by a target dodge)
+ [7384] = true, -- unranked
+ -- Warrior — Revenge (enabled by a block, dodge or parry)
+ [6572] = true, -- Rank 1
+ [6574] = true, -- Rank 2
+ [7379] = true, -- Rank 3
+ [11600] = true, -- Rank 4
+ [11601] = true, -- Rank 5
+ [25288] = true, -- Rank 6
+ [25269] = true, -- Rank 7
+ [30357] = true, -- Rank 8
+ [57823] = true, -- Rank 9
+ -- Warrior — Victory Rush (enabled by a killing an XP/honor-granting target)
+ [34428] = true, -- unranked
+}
+
+-- Execute HP fraction for a spell: the id itself, then any known rank (the item caches learned
+-- ranks in _rankCDIDs). nil for non-execute spells, which makes the engine skip the branch.
+function A.ExecuteThreshold(spellID, rankIDs)
+ if spellID and A.EXECUTE[spellID] then return A.EXECUTE[spellID] end
+ if rankIDs then
+ for _, id in ipairs(rankIDs) do
+ if A.EXECUTE[id] then return A.EXECUTE[id] end
+ end
+ end
+ return nil
+end
+
+-- True if the spell, at any rank, is a curated reactive ability.
+function A.IsReactive(spellID, rankIDs)
+ if spellID and A.REACTIVE[spellID] then return true end
+ if rankIDs then
+ for _, id in ipairs(rankIDs) do
+ if A.REACTIVE[id] then return true end
+ end
+ end
+ return false
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Alerts.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Alerts.lua
new file mode 100644
index 0000000..e0c8710
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Alerts.lua
@@ -0,0 +1,662 @@
+-- DragonUI_NewEra/modules/cooldownviewer/Alerts.lua — per-cooldown visual alerts and ready sounds.
+--
+-- Downport of NewEra/CooldownViewer/Alerts.lua. Each tracked cooldown can carry ONE assigned alert
+-- that decorates its icon when the alert's event fires. Retail's event set lives in
+-- Enum.CooldownViewerAlertEventType; upstream ports the three with a faithful pre-Legion meaning,
+-- and so do we:
+--
+-- available — the ability came off cooldown. A one-shot flash on the real-cooldown -> ready
+-- transition. This is also the trigger for the assigned ready SOUND.
+-- refresh — upstream's hydration of retail's PandemicTime event. Retail fires pandemic at
+-- expirationTime - carriedOverToNewCast, gated on that value being > 0; it comes from
+-- C_UnitAuras.GetRefreshExtendedDuration and represents retail's duration ROLLOVER on
+-- refresh. 3.3.5a has neither the API nor the rollover, so the retail trigger could
+-- never fire. Hydrated instead as "the tracked aura is inside the last `window`
+-- fraction of its duration" — i.e. refresh it now.
+-- usable — retail's "conditionally castable" notion. Data-driven from AlertData.lua: a target
+-- below an execute threshold, or a curated reactive ability becoming castable. Only
+-- those spells ever flash; flashing every ready spell is explicitly not the feature.
+--
+-- A FOURTH, which is ours and not retail's:
+--
+-- active — the tracked aura is on the player right now. AURA ROWS ONLY. Retail has no such
+-- event because retail's alerts belong to cooldowns; ours are offered on tracked
+-- buffs too, and all three ported events ask questions about a COOLDOWN. For a proc
+-- with no castable spell of its own name — Surge of Light, a trinket proc, anything
+-- the player does not press — the honest answer to every one of them is "never", and
+-- the only question worth asking is whether the proc is up. See inActiveState.
+--
+-- ── THE PANDEMIC BORDER: WHAT PORTS, AND WHAT DOES NOT ──────────────────────────────────────────
+--
+-- Upstream renders `refresh` with a 1:1 port of retail's CooldownPandemicFXTemplate: a static ring
+-- PLUS three glow textures that cascade outward, every one of them clipped to the ring shape by a
+-- MaskTexture. Those are two separable halves, and only the second half is unportable.
+--
+-- THE RING PORTS AS-IS (§H.2.10). `UI-CooldownManager-PandemicBorder` is already a hollow
+-- rounded-square ring: alpha 0 through the centre, material in two bands at cols 2-8 and 52-58 of
+-- 61, real RGB at full alpha. It needs no mask, no crop and no substitute — it is the retail
+-- pandemic border, and it lives on sheet 6685874, which Phase 8a already ships. (This header used to
+-- say "the art is also absent". That was true before 8a and is not true now.)
+--
+-- THE CASCADE DOES NOT PORT, for two independent reasons:
+--
+-- 1. MaskTexture does not exist on 3.3.5a. It is not merely missing — !!!ClassicAPI defines
+-- CreateMaskTexture and AddMaskTexture as Private.Void ("potentially impossible to implement",
+-- WidgetAPI.lua:279/302/476), and Cell's polyfill returns an inert dummy object. The calls
+-- would succeed silently and clip nothing. That matters here specifically because
+-- PandemicFX-Icon01/02/03 are FILLED 128x128 quads (centre alpha 61/81/24), not rings: without
+-- the clip they scale to 1.5x as square smears across the icon and its neighbours. Worse than
+-- no FX, and the reason those three atlases are deliberately not registered.
+-- 2. `Animation:SetTarget` does not exist on this client either (zero occurrences anywhere in the
+-- AddOns tree). 3.3.5a animations act on the region that owns the AnimationGroup, so the
+-- template's one-group-drives-three-textures structure has no equivalent.
+--
+-- SUBSTITUTION: the ring itself pulses. One texture on one frame, so the animation acts on the
+-- region that owns the group — exactly what this client does support — and no mask is involved at
+-- any point. It carries the same signal as the cascade (this aura is expiring, refresh it) using the
+-- real art, rather than faking a clip we cannot perform.
+--
+-- The three LibCustomGlow renderers remain selectable for `refresh` and stay the only option for the
+-- other two alert types. The ring is simply a fourth FX, and the default for `refresh`.
+--
+-- ── ENGINE ──────────────────────────────────────────────────────────────────────────────────────
+--
+-- `available` is edge-triggered off the item's ConsumeReadyTransition. `refresh` and `usable` need
+-- polling: aura time decay, target health and usability transitions do not all raise events on
+-- 3.3.5a. One shared 0.2s ticker walks EVERY viewer's items, and does nothing at all unless at least
+-- one alert or one ready sound is assigned — the default setup costs a table lookup.
+--
+-- Every live evaluation is pcall-isolated: a bad read must never raise, least of all in combat.
+
+local NE = DragonUI_NewEra
+NE.cooldownviewer = NE.cooldownviewer or {}
+local M = NE.cooldownviewer
+M.alerts = M.alerts or {}
+local AL = M.alerts
+
+local LCG = LibStub and LibStub("LibCustomGlow-1.0", true)
+
+-- ── FX catalogue ────────────────────────────────────────────────────────────────────────────────
+-- Upstream's fx values are indices into NE.groupbuff.VISUAL_ALERT, a Group Buff Filter enum this
+-- addon does not have. We define our own over the LibCustomGlow renderers, keeping 1 = ants as the
+-- default so a stored upstream-shaped value still lands on the intended look.
+AL.FX = {
+ { id = 1, name = "Marching Ants" },
+ { id = 2, name = "Button Glow" },
+ { id = 3, name = "Sparkles" },
+ { id = 4, name = "Pandemic Border" },
+}
+
+local DEFAULT_FX = 1
+-- `refresh` defaults to the real retail ring; the other two alert types have no retail art of their
+-- own, so they keep the ants. Kept as a table rather than an if, so adding art for another type is
+-- one line.
+local DEFAULT_FX_BY_TYPE = { refresh = 4 }
+AL.PANDEMIC_FX = 4
+local DEFAULT_WINDOW = 0.30
+local WINDOW_MIN = 0.10
+local WINDOW_MAX = 0.50
+local AVAILABLE_HOLD = 1.5 -- seconds the one-shot "available" flash stays up
+local TICK = 0.2 -- poll cadence
+
+-- Tint per alert type, for the three LibCustomGlow renderers only — the pandemic ring is untinted
+-- because its art already carries the colour (see startPandemic). `refresh` keeps a pandemic-orange
+-- entry here for the case where the player picks ants or sparkles for it instead of the ring.
+local TINT = {
+ available = { 0.35, 1.00, 0.35, 1 },
+ refresh = { 1.00, 0.50, 0.10, 1 },
+ usable = { 0.95, 0.95, 0.32, 1 },
+ active = { 0.35, 0.75, 1.00, 1 },
+}
+local GLOW_KEY = "NECDMAlert"
+
+-- ── Persistence ─────────────────────────────────────────────────────────────────────────────────
+-- Beside every other Cooldown Manager setting, in DragonUI's profile.
+local function store(create)
+ local cd = M._store and M._store(create)
+ if not cd then return nil end
+ if not cd.alerts then
+ if not create then return nil end
+ cd.alerts = {}
+ end
+ return cd.alerts
+end
+
+function AL.Get(spellID)
+ if not spellID then return nil end
+ local t = store(false)
+ return t and t[spellID] or nil
+end
+
+local function ensureLeaf(spellID)
+ local t = store(true)
+ if not t then return nil end
+ local e = t[spellID]
+ if not e then e = {}; t[spellID] = e end
+ return e
+end
+
+function AL.GetType(spellID)
+ local e = AL.Get(spellID)
+ return e and e.type or nil
+end
+
+-- type = nil disables the alert but KEEPS fx/window, so re-enabling restores the previous choice.
+function AL.SetType(spellID, alertType)
+ if not spellID then return end
+ local e = ensureLeaf(spellID)
+ if not e then return end
+ e.type = alertType
+ if alertType then
+ if e.fx == nil then e.fx = AL.DefaultFX(alertType) end
+ if e.window == nil then e.window = DEFAULT_WINDOW end
+ end
+end
+
+-- The default FX for an alert type. `refresh` gets the real pandemic ring; everything else keeps the
+-- ants. Resolved through one function so the stored value, the settings dropdown and the live
+-- renderer cannot disagree about what "unset" means.
+function AL.DefaultFX(alertType)
+ return DEFAULT_FX_BY_TYPE[alertType or ""] or DEFAULT_FX
+end
+
+function AL.GetFX(spellID)
+ local e = AL.Get(spellID)
+ if e and e.fx then return e.fx end
+ return AL.DefaultFX(e and e.type)
+end
+
+function AL.SetFX(spellID, fx)
+ local e = ensureLeaf(spellID)
+ if e then e.fx = fx or AL.DefaultFX(e.type) end
+end
+
+function AL.GetWindow(spellID)
+ local e = AL.Get(spellID)
+ return (e and e.window) or DEFAULT_WINDOW
+end
+
+function AL.SetWindow(spellID, frac)
+ local e = ensureLeaf(spellID)
+ if not e then return end
+ frac = tonumber(frac) or DEFAULT_WINDOW
+ if frac < WINDOW_MIN then frac = WINDOW_MIN elseif frac > WINDOW_MAX then frac = WINDOW_MAX end
+ e.window = frac
+end
+
+-- Ticker early-out: is any spell carrying an enabled alert?
+function AL.HasAny()
+ local t = store(false)
+ if not t then return false end
+ for _, e in pairs(t) do
+ if e and e.type then return true end
+ end
+ return false
+end
+
+-- ── FX rendering ────────────────────────────────────────────────────────────────────────────────
+-- Idempotent by (fx, alert type): re-applying restarts the animation, and at 5Hz that reads as a
+-- stutter rather than a glow. `_alertFX` records what is currently up.
+-- ── The pandemic ring ───────────────────────────────────────────────────────────────────────────
+--
+-- A CHILD FRAME, not a texture on the item. A child frame draws above every layer of its parent, and
+-- that is the only way to get above the tile's own art here: the gold frame stack occupies OVERLAY
+-- sublevels 0-2 and the cooldown sweep is itself a child frame (§H.2.8). Levelled above the sweep
+-- for the same reason the buff glow is — a ring the swipe eats half of is a ring nobody can read.
+--
+-- OVERSIZED, and the amount comes from the art rather than from taste.
+--
+-- Retail's own anchor is SetAllPoints, and copying it put the ring INSIDE the icon — the owner's
+-- report, and correct. The cell is not a line with glow on both sides. Reading its mid-row alpha:
+--
+-- cols 0-8 5, 15, 30, 50, 75, 106, 141, 181, 255 — a hard edge at col 8, bleeding OUTWARD
+-- cols 9-51 0 — the opening
+-- cols 52-60 255 back down to 5 — the mirror
+--
+-- So the OPENING is what has to land on the icon, and it is 43 of the 61-pixel cell. The remaining
+-- 9 per side are glow that belongs outside the icon entirely. Anchored at SetAllPoints those 9 eat
+-- into the icon instead, which is exactly what "renders in the icon not around it" looks like.
+--
+-- 43 is not a coincidence: it is the size of UI-CooldownManager-OORshadow on the same sheet — the
+-- icon-sized cell in this art family. The overhang is therefore 9/43 of the icon, in the same shape
+-- as ICON_MASK_INSET's 3/64.
+local PANDEMIC_BLEED = 9 / 43
+local PANDEMIC_MIN_OVER = 4 -- a picker tile is small enough to round the bleed away entirely
+local PANDEMIC_PULSE_MIN = 0.45 -- retail cascades; we breathe. See the header.
+local PANDEMIC_PULSE_HALF = 0.75 -- seconds per half-cycle, so a 1.5s loop
+
+-- WHAT THE OPENING HAS TO LAND ON IS THE **VISIBLE** ICON, AND THOSE ARE NOT THE SAME RECT.
+--
+-- A framed viewer tile and a bare picker tile disagree about where the icon ends:
+--
+-- picker tile no frame art, so the visible icon IS the .Icon rect.
+-- viewer tile the gold IconOverlay's opening sits M.IconAperture INSIDE the tile — 6.8px on a
+-- 50px Essential — and covers the icon's outer edge. The .Icon rect is 46px; the
+-- icon you can SEE is ~36px. Anchoring to .Icon therefore parks the ring's bright
+-- edge ~5px out, in the middle of the gold band, with gold showing between icon and
+-- ring. That is the "sits too far away from the icon edge" report.
+--
+-- .Icon is wrong for a framed tile in a second way too: it moves with the icon-inset slider, while
+-- the frame does not — so the visible edge does not move and a ring tied to .Icon would drift off it.
+--
+-- Deriving the aperture instead makes both cases one formula, and it lands almost exactly on the
+-- tile rect for a framed tile (offsets under a pixel on Essential and Utility alike). That is not a
+-- coincidence and it is worth recording: retail anchors this ring with SetAllPoints(item) precisely
+-- because 43/61 = 70.5% of the tile is where a frame of these proportions opens. Retail's anchor was
+-- right for retail's tile; it was wrong here only for the UNFRAMED picker tile, which is exactly
+-- where the first report came from.
+--
+-- ox/oy come from the IconOverlay's own anchor rather than a recorded copy — that is the source of
+-- truth for where the frame sits, and it cannot drift out of sync with it.
+local function pandemicRect(item)
+ local art = item.IconOverlay
+ if art and art.GetPoint and M.IconAperture then
+ local _, rel, _, x, y = art:GetPoint(1) -- TOPLEFT, item, TOPLEFT, -ox, oy
+ local w = (item.GetWidth and item:GetWidth()) or 0
+ local h = (item.GetHeight and item:GetHeight()) or 0
+ if rel and x and y and w > 0 and h > 0 then
+ return item, M.IconAperture(w, -x), M.IconAperture(h, y), w, h
+ end
+ end
+ local host = item.Icon or item
+ local w = (host.GetWidth and host:GetWidth()) or 0
+ local h = (host.GetHeight and host:GetHeight()) or w
+ return host, 0, 0, w, h
+end
+
+-- Re-anchored at every SHOW rather than once at build. The rects come from anchors the client has
+-- not resolved when the tile is built, so a build-time measurement reads 0 and would freeze the ring
+-- at the minimum forever (§H.2.9 learned this on the bar caps). Showing an alert is a state
+-- transition, not a per-frame cost.
+local function anchorPandemicRing(ov)
+ local item = ov._item
+ if not item then return end
+ local host, apX, apY, w, h = pandemicRect(item)
+
+ -- Inset of the ring's own rect from the host, per axis. The opening must equal the visible icon,
+ -- so the frame extends PANDEMIC_BLEED of that beyond it; the aperture then pulls it back in.
+ local function edge(ap, size)
+ local visible = size - 2 * ap
+ if visible <= 0 then return -PANDEMIC_MIN_OVER end
+ local v = ap - visible * PANDEMIC_BLEED
+ if size <= 0 then return -PANDEMIC_MIN_OVER end
+ return math.floor(v + 0.5)
+ end
+ local ex = (w > 0) and edge(apX, w) or -PANDEMIC_MIN_OVER
+ local ey = (h > 0) and edge(apY, h) or -PANDEMIC_MIN_OVER
+
+ if host == ov._hostCache and ex == ov._ex and ey == ov._ey then return end
+ ov._hostCache, ov._ex, ov._ey = host, ex, ey
+ ov:ClearAllPoints()
+ ov:SetPoint("TOPLEFT", host, "TOPLEFT", ex, -ey)
+ ov:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -ex, ey)
+end
+
+local function buildPandemicRing(item)
+ if not (NE.tex and NE.tex.HasAtlas and NE.tex.HasAtlas("UI-CooldownManager-PandemicBorder")) then
+ return nil
+ end
+ local base = (item.Cooldown and item.Cooldown.GetFrameLevel and item.Cooldown:GetFrameLevel())
+ or (item.GetFrameLevel and item:GetFrameLevel())
+ or 1
+ local ov = CreateFrame("Frame", nil, item)
+ ov._item = item
+ ov:SetFrameLevel(base + 4)
+ anchorPandemicRing(ov)
+ ov:Hide()
+
+ ov.Ring = ov:CreateTexture(nil, "OVERLAY")
+ ov.Ring:SetAllPoints(ov)
+ NE.tex.SetAtlas(ov.Ring, "UI-CooldownManager-PandemicBorder", false)
+
+ -- The pulse animates the FRAME, which is what 3.3.5a animations do — they act on the region that
+ -- owns the group. The ring is the frame's only content, so animating the one is animating the
+ -- other, and no SetTarget is needed. Feature-gated: without AnimationGroups the ring still shows,
+ -- just static, which is a complete signal on its own.
+ if ov.CreateAnimationGroup then
+ local ok, ag = pcall(ov.CreateAnimationGroup, ov)
+ if ok and ag then
+ if ag.SetLooping then ag:SetLooping("REPEAT") end
+ local out = ag:CreateAnimation("Alpha")
+ out:SetDuration(PANDEMIC_PULSE_HALF); out:SetOrder(1)
+ local back = ag:CreateAnimation("Alpha")
+ back:SetDuration(PANDEMIC_PULSE_HALF); back:SetOrder(2)
+ -- The native 3.3.5a Alpha animation takes a DELTA via SetChange; SetFromAlpha/SetToAlpha are
+ -- ClassicAPI's polyfill over it (WidgetAPI.lua:414-436), and it only emits the SetChange once
+ -- BOTH halves have been set — hence from-then-to, in that order, on each animation. The raw
+ -- SetChange branch below is the fallback for a client without ClassicAPI loaded.
+ --
+ -- Delta semantics are why startPandemic sets the frame's alpha to 1 before Play: the animation
+ -- moves it by -0.55 from wherever it happens to be, not to an absolute 0.45.
+ if out.SetFromAlpha then
+ out:SetFromAlpha(1); out:SetToAlpha(PANDEMIC_PULSE_MIN)
+ back:SetFromAlpha(PANDEMIC_PULSE_MIN); back:SetToAlpha(1)
+ elseif out.SetChange then
+ out:SetChange(PANDEMIC_PULSE_MIN - 1)
+ back:SetChange(1 - PANDEMIC_PULSE_MIN)
+ end
+ if out.SetSmoothing then out:SetSmoothing("IN_OUT"); back:SetSmoothing("IN_OUT") end
+ ov.anim = ag
+ end
+ end
+ return ov
+end
+
+-- DELIBERATELY UNTINTED, unlike every other FX here. The three LibCustomGlow renderers are
+-- colourless and take their meaning from TINT; this art does not — it ships as the pandemic colour
+-- already (peak 255,48,48). Vertex colour multiplies, so applying TINT.refresh's (1, 0.5, 0.1) to a
+-- ring that is (1, 0.19, 0.19) would land on (1, 0.09, 0.02): a darker, muddier red than the artist
+-- chose, arrived at by "tinting" something that needed no tint.
+local function startPandemic(item)
+ local ov = item._pandemicRing
+ if ov == nil then
+ ov = buildPandemicRing(item) or false
+ item._pandemicRing = ov
+ end
+ if not ov then return false end
+ anchorPandemicRing(ov) -- the icon may have resized (or first resolved) since the last show
+ ov:SetAlpha(1)
+ ov:Show()
+ if ov.anim and ov.anim.Play then ov.anim:Play() end
+ return true
+end
+
+local function stopPandemic(item)
+ local ov = item and item._pandemicRing
+ if not ov then return end
+ if ov.anim and ov.anim.Stop then ov.anim:Stop() end
+ ov:SetAlpha(1) -- the group leaves the frame wherever the pulse stopped
+ ov:Hide()
+end
+
+local function startGlow(item, fx, colour)
+ if fx == 4 then return startPandemic(item) end
+ if not LCG then return false end
+ if fx == 2 and LCG.ButtonGlow_Start then
+ -- ButtonGlow takes no key; it is one-per-frame by construction.
+ LCG.ButtonGlow_Start(item, colour, 0.35)
+ return true
+ elseif fx == 3 and LCG.AutoCastGlow_Start then
+ LCG.AutoCastGlow_Start(item, colour, 4, 0.25, 1, 0, 0, GLOW_KEY)
+ return true
+ elseif LCG.PixelGlow_Start then
+ LCG.PixelGlow_Start(item, colour, 8, 0.25, nil, 2, 0, 0, false, GLOW_KEY)
+ return true
+ end
+ return false
+end
+
+local function stopGlow(item, fx)
+ if fx == 4 then return stopPandemic(item) end
+ if not LCG then return end
+ if fx == 2 then
+ if LCG.ButtonGlow_Stop then LCG.ButtonGlow_Stop(item) end
+ elseif fx == 3 then
+ if LCG.AutoCastGlow_Stop then LCG.AutoCastGlow_Stop(item, GLOW_KEY) end
+ else
+ if LCG.PixelGlow_Stop then LCG.PixelGlow_Stop(item, GLOW_KEY) end
+ end
+end
+
+function AL.ShowFX(item, fx, alertType)
+ if not item then return end
+ fx = fx or AL.DefaultFX(alertType)
+ local sig = tostring(fx) .. ":" .. tostring(alertType)
+ if item._alertFX == sig then return end
+ -- A change of fx family has to stop the OLD renderer, not the new one.
+ if item._alertFX then AL.ClearFX(item) end
+ item._alertFXKind = fx
+ local ok, started = pcall(startGlow, item, fx, TINT[alertType] or TINT.usable)
+ -- Only record a live FX if one actually rendered. With no glow library present startGlow returns
+ -- false without erroring, and claiming success would leave a flag with nothing behind it.
+ if ok and started then item._alertFX = sig else item._alertFXKind = nil end
+end
+
+function AL.ClearFX(item)
+ if not item then return end
+ item._alertFlashUntil = nil
+ if not item._alertFX then return end
+ local fx = item._alertFXKind
+ item._alertFX, item._alertFXKind = nil, nil
+ pcall(stopGlow, item, fx)
+end
+
+-- One-shot flash for the `available` event: show, then auto-clear after a hold.
+function AL.FlashOnce(item, fx, alertType)
+ if not item then return end
+ AL.ShowFX(item, fx, alertType or "available")
+ item._alertFlashUntil = GetTime() + AVAILABLE_HOLD
+ if C_Timer and C_Timer.After then
+ C_Timer.After(AVAILABLE_HOLD, function()
+ -- Only clear if this flash is still the one showing — a later alert may have taken over.
+ if item._alertFlashUntil and GetTime() >= item._alertFlashUntil then
+ item._alertFlashUntil = nil
+ AL.ClearFX(item)
+ end
+ end)
+ end
+end
+
+-- Settings preview: flash any frame so the user can see their choice. Safe on a non-item frame.
+--
+-- The alert type matters here, because it picks the TINT. Previewing everything as "usable" made
+-- the settings tile flash yellow while the live icon then glowed green — the preview was showing a
+-- colour the player had not chosen and would never see. Callers pass the type being configured.
+function AL.Preview(frame, fx, alertType)
+ if not frame then return end
+ local t = TINT[alertType] and alertType or "usable"
+ AL.FlashOnce(frame, fx or AL.DefaultFX(t), t)
+end
+
+-- ── Trigger evaluators ──────────────────────────────────────────────────────────────────────────
+
+-- Is the tracked aura inside the last `window` fraction of its duration?
+--
+-- The aura a cooldown maintains shares the spell's NAME (a DoT, HoT or self-buff), which is the only
+-- handle available: 3.3.5a cannot query an aura by spellID. Player first (HoTs, self-buffs), then
+-- target (DoTs). NE.aura caches one scan per unit per frame, so polling this at 5Hz is cheap.
+local function inRefreshWindow(item, window)
+ local name = item.spellName
+ if not (name and NE.aura) then return false end
+
+ local row = NE.aura.FindByName("player", name)
+ if not (row and row.duration and row.duration > 0) then
+ row = NE.aura.FindByName("target", name)
+ end
+ if not (row and row.duration and row.duration > 0 and row.expiration) then return false end
+
+ local remaining = row.expiration - GetTime()
+ if remaining <= 0 then return false end
+ return (remaining / row.duration) <= (window or DEFAULT_WINDOW)
+end
+
+-- Castable right now: resources available and not on a real cooldown. A GCD-length lockout does not
+-- count as a cooldown — the same heuristic the viewer applies everywhere else.
+local function isSpellUsableNow(spellID, item)
+ if not spellID then return false end
+ -- 3.3.5a's IsUsableSpell takes a NAME (or a spellbook index + bookType), never a spellID. Passing
+ -- an id reads it as an index past the end of the book and returns nil — no error, just a silent
+ -- false. That made this function return false unconditionally, which is why the Usable alert
+ -- never fired for anyone. Same fault, same fix as ItemMixins.lua:545; this site was missed.
+ if not IsUsableSpell then return false end
+ if not IsUsableSpell((item and item.spellName) or spellID) then return false end
+ local start, dur = M.SpellCD(spellID, item and item.spellName, item and item._rankCDIDs)
+ if start and start > 0 and dur and dur > (M.GCD_MAX or 1.51) then return false end
+ return true
+end
+
+-- "Usable" = castable right now.
+--
+-- DIVERGENCE FROM UPSTREAM, and the reason for it: upstream flashes ONLY spells listed in
+-- AlertData's EXECUTE or REACTIVE tables and returns false for everything else. That is defensible
+-- upstream, where the alert can be attached to spells with no cooldown at all. Here it made
+-- "Usable" a dead menu entry for six of the ten classes — AlertData covers eight abilities across
+-- Hunter, Paladin, Warrior and Rogue, so a Priest or a Mage could select it and nothing could ever
+-- happen. The label says what it does: a spell off cooldown and affordable IS usable.
+--
+-- EXECUTE stays as the stricter condition, because target health is something IsUsableSpell knows
+-- nothing about — without it Kill Shot would glow all fight instead of in execute range.
+-- A.IsReactive is deliberately NOT consulted any more: 3.3.5a's IsUsableSpell already reports
+-- Overpower and Revenge as unusable outside their proc window, so the curated check was only
+-- restating what the client says. A.REACTIVE stays in AlertData as data; nothing reads it.
+local function inUsableState(item)
+ local sid = item.spellID
+ if not sid then return false end
+ local A = M.alertdata
+
+ local threshold = A and A.ExecuteThreshold and A.ExecuteThreshold(sid, item._rankCDIDs)
+ if threshold then
+ if not (UnitExists and UnitExists("target")) then return false end
+ if UnitIsDeadOrGhost and UnitIsDeadOrGhost("target") then return false end
+ local hp, hpMax = UnitHealth("target"), UnitHealthMax("target")
+ if not (hp and hpMax and hpMax > 0) then return false end
+ if (hp / hpMax) > threshold then return false end
+ return isSpellUsableNow(sid, item)
+ end
+
+ return isSpellUsableNow(sid, item)
+end
+
+-- "Active" = the tracked aura is on the player right now. AURA ROWS ONLY, and it exists because the
+-- other three all answer questions about a COOLDOWN: has it finished, is it castable, is the buff it
+-- applies running out. None of those is the question a tracked buff asks, which is simply "is this
+-- proc up". A Priest assigning an alert to Surge of Light — a 10s proc with no castable spell of its
+-- own name — had `usable` (never fires: IsUsableSpell does not know the name), `available` (never
+-- fires: no cooldown to finish) and `refresh` (fires for the last 3 seconds of the 10). That is the
+-- report this answers.
+--
+-- The item's own cached flag first: both aura mixins maintain `_auraActive` from the scan that built
+-- them, so this costs nothing on the path that matters. The name lookup is the fallback for an item
+-- that has no such flag, which is any spell tile that somehow carries this type.
+local function inActiveState(item)
+ if item._auraActive ~= nil then return item._auraActive and true or false end
+ local name = item.spellName
+ if not (name and NE.aura and NE.aura.FindByName) then return false end
+ return NE.aura.FindByName("player", name) ~= nil
+end
+
+-- Evaluate one item's assigned alert. `available` is edge-triggered elsewhere, so the ticker leaves
+-- its flash alone rather than clearing it every pass.
+local function evalItem(item)
+ if not item then return end
+
+ -- A one-shot flash owns the icon until its hold expires. Without this the ticker wipes it within
+ -- 200ms — which also silently defeated the settings PREVIEW, whose whole job is to be seen.
+ if item._alertFlashUntil and GetTime() < item._alertFlashUntil then return end
+
+ -- Preferences key off the LISTED id, not the learned-rank one the tile displays.
+ local sid = item.GetSettingsKey and item:GetSettingsKey() or item.spellID
+ if not sid then AL.ClearFX(item); return end
+
+ local cfg = AL.Get(sid)
+ if not (cfg and cfg.type) then AL.ClearFX(item); return end
+
+ if cfg.type == "available" then return end
+
+ if item.IsShown and not item:IsShown() then AL.ClearFX(item); return end
+
+ local on
+ if cfg.type == "refresh" then
+ on = inRefreshWindow(item, cfg.window)
+ elseif cfg.type == "usable" then
+ on = inUsableState(item)
+ elseif cfg.type == "active" then
+ on = inActiveState(item)
+ end
+
+ if on then AL.ShowFX(item, cfg.fx, cfg.type) else AL.ClearFX(item) end
+end
+
+-- ── Ready transition ────────────────────────────────────────────────────────────────────────────
+-- Detected by polling, not by an event: 3.3.5a emits nothing when a cooldown ends, and the viewer's
+-- own OnCooldownDone timer is unreliable because a refresh re-Sets or Clears the swipe and cancels
+-- it. ConsumeReadyTransition is the one-shot edge (ItemMixins.lua); it is shared with nothing else,
+-- so a transition fires exactly once.
+function AL.OnAvailable(item)
+ if not item then return end
+ local cfg = AL.Get(item.GetSettingsKey and item:GetSettingsKey() or item.spellID)
+ if cfg and cfg.type == "available" then
+ AL.FlashOnce(item, cfg.fx, "available")
+ end
+end
+
+local function checkReadyTransition(item)
+ if not (item and item.spellID and item.ConsumeReadyTransition) then return end
+ if item.IsShown and not item:IsShown() then return end
+ if item:ConsumeReadyTransition() and item.FireReadyAlerts then
+ item:FireReadyAlerts()
+ end
+end
+
+-- ── Ticker ──────────────────────────────────────────────────────────────────────────────────────
+-- ALL FOUR viewers, not just the two spell ones.
+--
+-- This used to walk essential and utility only, justified as "the aura viewers have no ready
+-- transition and no curated usable state". Both halves of that are true, and neither was a reason to
+-- skip them. `available` is edge-triggered off ConsumeReadyTransition, which aura items do not
+-- define, so checkReadyTransition returns at its first guard and costs one lookup. `usable` reads
+-- IsUsableSpell, which answers for a self-buff's own spell exactly as it does for a cooldown.
+--
+-- What the omission actually cost was `refresh` — the one alert whose trigger IS an aura decaying,
+-- and therefore the most useful of the three on a TRACKED BUFF. It could be assigned from the menu,
+-- it lit the badge, it previewed on the settings tile, and then it never fired in play.
+--
+-- AL.Stop and M.ResetAlerts have always cleared FX across all four viewers; only the code that SET
+-- it was narrower. That asymmetry is what let this sit unnoticed — the teardown looked complete.
+local function runViewer(viewer)
+ if not (viewer and viewer.IsShown and viewer:IsShown() and viewer.items) then return end
+ for _, item in ipairs(viewer.items) do
+ pcall(evalItem, item)
+ pcall(checkReadyTransition, item)
+ end
+end
+
+local ticker = CreateFrame("Frame")
+ticker.elapsed = 0
+ticker:Hide()
+ticker:SetScript("OnUpdate", function(self, delta)
+ self.elapsed = self.elapsed + delta
+ if self.elapsed < TICK then return end
+ self.elapsed = 0
+ -- Off means off, and a ready SOUND is the reason this cannot be left to "the tiles are hidden
+ -- anyway". A player who assigns sounds, turns the Cooldown Manager off, and then keeps hearing it
+ -- announce cooldowns for viewers that are not on screen has a haunted UI. Cheaper than HasAny too,
+ -- which walks the whole alert table 5 times a second.
+ if not M.IsEnabled() then return end
+ if not (AL.HasAny() or (M.HasAnyReadySound and M.HasAnyReadySound())) then return end
+ -- ForEachViewer rather than a hand-written list, so this cannot go stale against M.viewers again —
+ -- and so it matches AL.Stop and M.ResetAlerts, which have always used it.
+ if not M.ForEachViewer then return end
+ M.ForEachViewer(runViewer)
+end)
+
+-- Started by Register.lua once the viewers exist.
+function AL.Start()
+ ticker:Show()
+end
+
+function AL.Stop()
+ ticker:Hide()
+ M.ForEachViewer(function(viewer)
+ if viewer.items then
+ for _, item in ipairs(viewer.items) do AL.ClearFX(item) end
+ end
+ end)
+end
+
+-- Drop every per-spell alert and ready sound, and take down anything currently glowing. Spell lists
+-- and viewer positions live elsewhere and are deliberately untouched.
+function M.ResetAlerts()
+ local cd = M._store and M._store(true)
+ if cd then
+ cd.alerts = {}
+ cd.sounds = {}
+ end
+ M.ForEachViewer(function(viewer)
+ if viewer.items then
+ for _, item in ipairs(viewer.items) do AL.ClearFX(item) end
+ end
+ end)
+end
+
+AL._ticker = ticker -- test seam
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Assets.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Assets.lua
new file mode 100644
index 0000000..6b1c767
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Assets.lua
@@ -0,0 +1,102 @@
+-- DragonUI_NewEra/modules/cooldownviewer/Assets.lua — art for the cooldown VIEWERS.
+--
+-- (The /cdm settings window's own art is SettingsAssets.lua. This file is the bars and icons.)
+--
+-- PORT_PLAN §H.2 / Phase 8a. Six atlases were being set by name across ItemMixins, AuraItemMixins and
+-- both BuffViewers item shapes, and not one of them was registered — so every `SetAtlas` returned
+-- false and every region rendered as nothing. Nothing errored and nothing was logged, because a
+-- missed atlas is an invisible texture, which is why this survived five phases of in-game testing:
+-- the icons looked like plain spellbook icons and there was nothing to suggest they were meant not to.
+--
+-- WHY IT WAS MISSED. Upstream registers the sheet FDID and stops. On Classic Era the client's own
+-- atlas database answers `C_Texture.GetAtlasInfo` for these names even where the art differs, so a
+-- rect is never needed at the call site. 3.3.5a has no atlas database at all, so we must supply the
+-- rect as well — and only the `SetAtlas` calls were ported, not the data behind them.
+--
+-- WHERE THE RECTS COME FROM, AND WHY THEY ARE NOT 0→1. Transcribed from
+-- ReferenceAddons/NewEra/Generated/AtlasData.lua, the same practice SettingsAssets.lua follows.
+-- §H.2 supposed upstream had no rects for these and that they would have to be derived from the BLP;
+-- it does have them, and they are emphatically NOT full-file rects — 6704514 is a shared 256x128
+-- sheet carrying the icon overlay in its left third and the three bar pieces down its right. Reading
+-- a single-purpose sheet into a 0→1 rect would have stretched the overlay across the bar art.
+--
+-- VERIFIED, not transcribed on faith. Each BLP's real dimensions were read from its header, and the
+-- rect fractions multiplied back out to the pixel:
+--
+-- 6704514 256x128 overlay (0.339844-0.003906)*256 = 86.0 (0.679688-0.007812)*128 = 86.0
+-- bar (0.832031-0.347656)*256 = 124.0 (0.250000-0.171875)*128 = 10.0
+-- bar-bg (0.863281-0.347656)*256 = 132.0 (0.156250-0.007812)*128 = 19.0
+-- bar-pip (0.386719-0.347656)*256 = 10.0 (0.625000-0.265625)*128 = 46.0
+-- 6685874 512x1024 oorshdw (0.343750-0.259766)*512 = 43.0 (0.042969-0.000977)*1024 = 43.0
+-- 5199404 2048x1024 flipbk (0.458496-0.412598)*2048 = 94.0 (0.898438-0.393555)*1024 = 517.0
+--
+-- Every product lands on the declared width/height exactly, which is the check that matters: it
+-- proves these rects belong to the sheets we are shipping. SettingsAssets.lua records the failure
+-- this guards against — retail repacked sheet 7289697, so the Era-generated rects were wrong for the
+-- 12.1.0 BLP and had to be re-derived. The harness now asserts this arithmetic (qa/offline).
+--
+-- ONE BEHAVIOUR CHANGE TO KNOW ABOUT. Registering the GCD flipbook flips `hasFlipbook()` in
+-- ItemMixins from false to true, which retires the fallback ready-flash burst and activates the
+-- 22-frame sprite stepper — a code path that has never once executed, because the atlas it gates on
+-- has never been registered. Its frame math is at least self-consistent with this art (94/2 = 47,
+-- 517/11 = 47, so the sheet is 2x11 frames of 47px, exactly what FLASH_COLS/FLASH_ROWS say). If it
+-- looks wrong in game, deleting the one flipbook line below reverts to the fallback with no other
+-- change — the fallback was written to be the permanent alternative, not a stopgap.
+--
+-- 8b's inputs (6731092 icon-swipe, 5423465 secondarycooldown) are deliberately NOT shipped here.
+-- They are file textures for SetSwipeTexture/SetEdgeTexture rather than atlases, nothing reads them
+-- yet, and shipping art ahead of the code that uses it is how a repo accumulates 8MB of dead weight.
+
+local NE = DragonUI_NewEra
+if not (NE and NE.tex and NE.tex.RegisterLocal) then return end
+
+local PATH = "Interface\\AddOns\\DragonUI_NewEra\\Textures\\CooldownViewer\\"
+
+-- 6704514 — the shared CoolDownManager sheet: icon overlay + the three BuffBar pieces.
+NE.tex.RegisterLocal(6704514, PATH .. "6704514-ui-hud-cooldownmanager-iconoverlay.blp")
+-- 6685874 — the out-of-range shadow.
+NE.tex.RegisterLocal(6685874, PATH .. "6685874-ui-cooldownmanager-oorshadow.blp")
+-- 5199404 — the shared ActionBar sheet; we take only the GCD flipbook strip from it.
+NE.tex.RegisterLocal(5199404, PATH .. "5199404-ui-hud-actionbar-gcd-flipbook.blp")
+
+NE.tex.RegisterAtlases({
+ -- The icon frame. This is the single biggest visual difference between our viewers and retail's:
+ -- without it a tile is a bare icon with no border at all.
+ ["UI-HUD-CoolDownManager-IconOverlay"] = { file = 6704514, left = 0.003906, right = 0.339844, top = 0.007812, bottom = 0.679688, width = 86, height = 86 },
+
+ -- Out-of-range shading, on its own sheet (ItemMixins:39).
+ ["UI-CooldownManager-OORshadow"] = { file = 6685874, left = 0.259766, right = 0.343750, top = 0.000977, bottom = 0.042969, width = 43, height = 43 },
+
+ -- The ready-flash sprite strip: 2 columns x 11 rows of 47px frames (ItemMixins:46). See the
+ -- behaviour-change note in the header before touching this one.
+ ["UI-HUD-ActionBar-GCD-Flipbook"] = { file = 5199404, left = 0.412598, right = 0.458496, top = 0.393555, bottom = 0.898438, width = 94, height = 517 },
+
+ -- BuffBar fill, backing and the pip that rides the fill's right edge (AuraItemMixins:184-189).
+ -- Registration alone; the nine-slice insets AtlasSlice.lua carries for these are §H.2 8d's
+ -- business, and that phase is a visual judgement rather than a data one.
+ ["UI-HUD-CoolDownManager-Bar"] = { file = 6704514, left = 0.347656, right = 0.832031, top = 0.171875, bottom = 0.250000, width = 124, height = 10 },
+ ["UI-HUD-CoolDownManager-Bar-BG"] = { file = 6704514, left = 0.347656, right = 0.863281, top = 0.007812, bottom = 0.156250, width = 132, height = 19 },
+ ["UI-HUD-CoolDownManager-Bar-Pip"] = { file = 6704514, left = 0.347656, right = 0.386719, top = 0.265625, bottom = 0.625000, width = 10, height = 46 },
+
+ -- The pandemic ring (§H.2 8e), on the same sheet as the out-of-range shadow.
+ --
+ -- ONE atlas, not six. The plan expected this phase to need "the unmasked art plus a crop", on the
+ -- assumption that everything in retail's pandemic FX depends on the two clip masks §C2 rules out.
+ -- Decoding the cells says otherwise: PandemicBorder is ALREADY a hollow rounded-square ring — its
+ -- centre is alpha 0, its material sits in two bands at cols 2-8 and 52-58 of 61, and it carries
+ -- real RGB at full alpha (peak 255,48,48). It needs no mask and no crop; it is finished art.
+ --
+ -- What genuinely needed the masks was the CASCADE: PandemicFX-Icon01/02/03 are 128x128 FILLED
+ -- glows (centre alpha 61/81/24), square quads that retail clips to the ring shape before scaling
+ -- them 0.25 -> 1.5. Unmasked they are square smears across the icon and its neighbours — worse
+ -- than nothing, which is why they are NOT registered here. Alerts.lua substitutes an alpha pulse
+ -- on the ring itself, which needs neither a mask nor Animation:SetTarget.
+ --
+ -- Same verification as above, against the real 512x1024 BLP:
+ -- border (0.466797-0.347656)*512 = 61.0 (0.060547-0.000977)*1024 = 61.0
+ -- The bar ring `ui-cooldownmanager-pandemicborderbar` (6685874, 0.470703/0.533203/0.000977/
+ -- 0.032227, 32x32, nine-slice l/t/r/b=15) is deliberately unregistered: the alert ticker walks
+ -- only the icon viewers, so nothing could read it. Upstream's own bar path is dormant for the
+ -- same reason.
+ ["UI-CooldownManager-PandemicBorder"] = { file = 6685874, left = 0.347656, right = 0.466797, top = 0.000977, bottom = 0.060547, width = 61, height = 61 },
+})
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/AuraItemMixins.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/AuraItemMixins.lua
new file mode 100644
index 0000000..29b9c91
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/AuraItemMixins.lua
@@ -0,0 +1,391 @@
+-- DragonUI_NewEra/modules/cooldownviewer/AuraItemMixins.lua — the per-item layer for the two
+-- AURA-driven viewers: NE_CooldownViewerAuraItemMixin (BuffIcon tile) and
+-- NE_CooldownViewerBuffBarItemMixin (BuffBar row).
+--
+-- DOWNPORT of the aura half of NewEra/CooldownViewer/ItemMixins.lua (Phase 3). The cooldown item
+-- lives in ItemMixins.lua; these two track AURAS instead, so they differ in:
+-- * the Cooldown runs in REVERSE (XML reverse="true") — the swipe shows aura time elapsed
+-- * an Applications stack-count label instead of ChargeCount
+-- * no ready flash and no desaturation (the icon stays bright while the aura is up)
+-- * the BuffBar renders a StatusBar that depletes, driven by a per-frame OnUpdate
+--
+-- DEVIATIONS from the 1.15 source, all forced by the 3.3.5a surface (same set as ItemMixins.lua):
+-- MaskTexture removed, SetSwipeTexture/SetSwipeColor dropped or guarded, SetShown replaced with
+-- Show/Hide, GameTooltip:SetSpellByID falls back to the spell hyperlink.
+
+local NE = DragonUI_NewEra
+NE.cooldownviewer = NE.cooldownviewer or {}
+local M = NE.cooldownviewer
+
+-- DOWNPORT: SetShown does not exist on 3.3.5a (CONTRACTS §0).
+local function setShown(region, shown)
+ if not region then return end
+ if shown then region:Show() else region:Hide() end
+end
+
+-- Dispel border for aura items. Retail shows the modern debuff-border glow only when the tracked
+-- aura is HARMFUL (a player debuff), hidden for buffs. Routes through the shared NE.tex helper.
+local function refreshAuraDebuffBorder(item, isHarmful, debuffType)
+ local db = item and item.DebuffBorder
+ if not db then return end
+ if isHarmful and db.Texture and NE.tex and NE.tex.DispelBorder then
+ NE.tex.DispelBorder(db.Texture, debuffType, false)
+ db:Show()
+ else
+ db:Hide()
+ end
+end
+M.RefreshAuraDebuffBorder = refreshAuraDebuffBorder
+
+local QUESTION_MARK = "Interface\\Icons\\INV_Misc_QuestionMark"
+
+-- Per-spell settings (alert, ready sound) are stored under the id the PICKER ROW carries, which for
+-- a ranked buff is not the id the aura scan hands this item. Shared by both mixins, and memoized per
+-- binding: the ticker asks 5x a second, and M.SettingsKeyForAura walks the candidate pool.
+--
+-- The spell items' own GetSettingsKey (ItemMixins.lua) exists for the same reason one rung along —
+-- there it is the trained rank vs the listed rank. Same fault, same shape, different table.
+local function auraSettingsKey(self)
+ local sid = self.spellID
+ if not sid then return nil end
+ if self._settingsKeyFor ~= sid then
+ self._settingsKeyFor = sid
+ self._settingsKey = M.SettingsKeyForAura and M.SettingsKeyForAura(sid, self.spellName) or sid
+ end
+ return self._settingsKey
+end
+
+-- ════════════════════════════════════════════════════════════════════════════════════════════════
+-- BuffIcon tile
+-- ════════════════════════════════════════════════════════════════════════════════════════════════
+
+NE_CooldownViewerAuraItemMixin = {}
+local AuraItem = NE_CooldownViewerAuraItemMixin
+
+function AuraItem:OnLoad()
+ if NE.tex and NE.tex.SetAtlas then
+ if self.IconOverlay then NE.tex.SetAtlas(self.IconOverlay, "UI-HUD-CoolDownManager-IconOverlay", false) end
+ end
+ M.CropIcon(self.Icon)
+
+ NE.cd.ApplyNumbers(self.Cooldown, {
+ font = self.cooldownFont or NE.cd.FONT.viewerAura,
+ })
+ -- REVERSE: the swipe fills as the aura elapses rather than emptying like a cooldown.
+ if self.Cooldown and self.Cooldown.SetReverse then self.Cooldown:SetReverse(true) end
+end
+
+function AuraItem:SetSpell(spellID)
+ self.spellID = spellID
+ local name, _, icon = GetSpellInfo(spellID)
+ self.spellName = name
+ if self.Icon then self.Icon:SetTexture(icon or QUESTION_MARK) end
+ self:RefreshAura()
+end
+
+function AuraItem:SetTimerShown(shown)
+ self.timerShown = shown
+ NE.cd.ApplyNumbers(self.Cooldown, { show = shown })
+end
+
+function AuraItem:SetTooltipsShown(shown)
+ self.tooltipsShown = shown
+end
+
+function AuraItem:SetHideWhenInactive(hide)
+ self.hideWhenInactive = hide
+ self:UpdateShownState()
+end
+
+function AuraItem:UpdateShownState()
+ if not self.spellID then self:Hide(); return end
+ if self._editPreview then self:Show(); return end
+ if not self.hideWhenInactive then self:Show(); return end
+ if self._auraActive then self:Show() else self:Hide() end
+end
+
+-- Refresh from the player's aura matching this item's spell NAME, via the shared one-frame
+-- snapshot. Name matching (not spellID) is what lets a down-ranked buff still resolve.
+function AuraItem:RefreshAura()
+ if not self.spellName then return end
+ local aura = M.findPlayerAuraDataByName(self.spellName)
+ self._auraActive = aura ~= nil
+ refreshAuraDebuffBorder(self, aura and aura.isHarmful, aura and aura.debuffType)
+
+ if aura then
+ if aura.duration and aura.duration > 0 and aura.expirationTime and aura.expirationTime > 0 and self.Cooldown then
+ CooldownFrame_Set(self.Cooldown, aura.expirationTime - aura.duration, aura.duration, 1)
+ elseif self.Cooldown then
+ CooldownFrame_Clear(self.Cooldown)
+ end
+ if self.Applications and self.Applications.Text then
+ if aura.count and aura.count > 1 then
+ self.Applications.Text:SetText(tostring(aura.count))
+ self.Applications.Text:Show()
+ else
+ self.Applications.Text:Hide()
+ end
+ end
+ else
+ if self.Cooldown then CooldownFrame_Clear(self.Cooldown) end
+ if self.Applications and self.Applications.Text then self.Applications.Text:Hide() end
+ end
+
+ if self.hideWhenInactive then self:UpdateShownState() end
+end
+
+function AuraItem:GetSettingsKey() return auraSettingsKey(self) end
+
+-- Forwarding alias so the shared viewer dispatch can call one method name on either item type.
+function AuraItem:RefreshCooldown() self:RefreshAura() end
+
+-- Populate directly from a scanned aura (the auto-track path). No curated spellID needed, so
+-- trinket/potion/proc buffs render without being enumerable in advance.
+function AuraItem:SetAura(name, icon, count, duration, expiration, spellID)
+ self._editPreview = false
+ self.spellID, self.spellName = spellID, name
+ self._auraActive = true
+ refreshAuraDebuffBorder(self, false)
+ if self.Icon then self.Icon:SetTexture(icon or QUESTION_MARK) end
+ if self.Cooldown then
+ if duration and duration > 0 and expiration and expiration > 0 then
+ CooldownFrame_Set(self.Cooldown, expiration - duration, duration, 1)
+ else
+ CooldownFrame_Clear(self.Cooldown)
+ end
+ end
+ if self.Applications and self.Applications.Text then
+ if count and count > 1 then
+ self.Applications.Text:SetText(tostring(count)); self.Applications.Text:Show()
+ else
+ self.Applications.Text:Hide()
+ end
+ end
+end
+
+function AuraItem:SetPreviewAura(name, icon, duration, remaining, spellID)
+ self:SetAura(name, icon, nil, duration, GetTime() + (remaining or duration or 0), spellID)
+ self._editPreview = true
+end
+
+function AuraItem:OnEnter()
+ if not self.spellID then return end
+ if self.tooltipsShown == false then return end
+ GameTooltip:SetOwner(self, "ANCHOR_TOP")
+ -- Named: an aura id this client cannot hyperlink would otherwise show an EMPTY tooltip. See
+ -- M.TooltipSetSpellNamed in ItemMixins.lua.
+ if M.TooltipSetSpellNamed(GameTooltip, self.spellID, self.spellName) then GameTooltip:Show() end
+end
+
+function AuraItem:OnLeave() GameTooltip:Hide() end
+
+-- ════════════════════════════════════════════════════════════════════════════════════════════════
+-- BuffBar row
+-- ════════════════════════════════════════════════════════════════════════════════════════════════
+
+NE_CooldownViewerBuffBarItemMixin = {}
+local BarItem = NE_CooldownViewerBuffBarItemMixin
+
+-- Only animate while an aura (or edit preview) is cached. The inactive state is painted ONCE at
+-- the transition — without this gate the OnUpdate re-cleared the same idle bar 60x/sec.
+local function buffBarOnUpdate(self)
+ if self._auraExpiration then self:RefreshCooldownInfo() end
+end
+
+function BarItem:OnLoad()
+ local set = NE.tex and NE.tex.SetAtlas
+ if set and self.Icon and self.Icon.IconOverlay then
+ set(self.Icon.IconOverlay, "UI-HUD-CoolDownManager-IconOverlay", false)
+ end
+ M.CropIcon(self.Icon and self.Icon.Icon)
+
+ if self.Bar then
+ if NE.tex and NE.tex.SetAtlasOnStatusBar then
+ NE.tex.SetAtlasOnStatusBar(self.Bar, "UI-HUD-CoolDownManager-Bar")
+ end
+ self.Bar:SetStatusBarColor(1.0, 0.5, 0.25) -- retail's BuffBar orange
+ -- BarBG is a 3-slice group, not a texture — it textures its own pieces in BuildBarBG (§H.2.9).
+ if self.Bar.BarBG and M.ApplyBarBGAtlas then M.ApplyBarBGAtlas(self.Bar.BarBG) end
+ if set and self.Bar.Pip then set(self.Bar.Pip, "UI-HUD-CoolDownManager-Bar-Pip", true) end
+ -- Pip rides the fill texture's right edge so it slides left as the bar depletes.
+ if self.Bar.Pip and self.Bar.GetStatusBarTexture then
+ local fillTex = self.Bar:GetStatusBarTexture()
+ if fillTex then
+ self.Bar.Pip:ClearAllPoints()
+ self.Bar.Pip:SetPoint("CENTER", fillTex, "RIGHT", 0, -1)
+ end
+ end
+ end
+
+ self.baseWidth = self:GetWidth()
+ self:SetScript("OnUpdate", buffBarOnUpdate)
+end
+
+function BarItem:SetSpell(spellID)
+ self.spellID = spellID
+ local name, _, icon = GetSpellInfo(spellID)
+ self.spellName = name
+ if self.Icon and self.Icon.Icon then self.Icon.Icon:SetTexture(icon or QUESTION_MARK) end
+ if self.Bar and self.Bar.Name then self.Bar.Name:SetText(name or "?") end
+ self:RefreshAura()
+end
+
+function BarItem:SetAura(name, icon, count, duration, expiration, spellID)
+ self._editPreview = false
+ self.spellID = spellID
+ self.spellName = name
+ self._auraExpiration = expiration
+ self._auraDuration = duration
+ self._auraCount = count
+ self._auraActive = true
+ refreshAuraDebuffBorder(self, false)
+ if self.Icon and self.Icon.Icon then self.Icon.Icon:SetTexture(icon or QUESTION_MARK) end
+ if self.Bar and self.Bar.Name then self.Bar.Name:SetText(name or "?") end
+ if self.Icon and self.Icon.Applications then
+ if count and count > 1 then
+ self.Icon.Applications:SetText(tostring(count)); self.Icon.Applications:Show()
+ else
+ self.Icon.Applications:Hide()
+ end
+ end
+ self:RefreshCooldownInfo()
+end
+
+function BarItem:SetTimerShown(shown)
+ self.timerShown = shown
+ setShown(self.Bar and self.Bar.Duration, shown)
+end
+
+function BarItem:SetTooltipsShown(shown) self.tooltipsShown = shown end
+
+function BarItem:SetHideWhenInactive(hide)
+ self.hideWhenInactive = hide
+ self:UpdateShownState()
+end
+
+function BarItem:UpdateShownState()
+ -- ignoreInLayout must MIRROR visibility here: the grid counts hidden children too
+ -- (includeAsLayoutChildWhenHidden), so a bar hidden without the flag desyncs GetStride from the
+ -- layout-child set and wraps the stack into a phantom extra column.
+ local show
+ if not self.spellID then show = false
+ elseif not self.hideWhenInactive then show = true
+ else show = self._auraActive and true or false end
+ self.ignoreInLayout = not show
+ setShown(self, show)
+end
+
+function BarItem:SetBarContent(content)
+ self.barContent = content
+ setShown(self.Icon, content ~= "nameOnly")
+ setShown(self.Bar and self.Bar.Name, content ~= "iconOnly")
+end
+
+function BarItem:SetBarWidthScale(scale)
+ self.barWidthScale = scale
+ local base = self.baseWidth or 220
+ self:SetWidth(base * (scale or 1.0))
+end
+
+-- Capture the active aura's (expiration, duration) on UNIT_AURA so the OnUpdate can poll cheaply
+-- without re-scanning auras every frame.
+function BarItem:RefreshAura()
+ if self._editPreview then return end
+ if not self.spellName then return end
+ local aura = M.findPlayerAuraDataByName(self.spellName)
+ if aura and aura.expirationTime > GetTime() then
+ self._auraExpiration = aura.expirationTime
+ self._auraDuration = aura.duration
+ self._auraCount = aura.count
+ self._auraActive = true
+ refreshAuraDebuffBorder(self, aura.isHarmful, aura.debuffType)
+ else
+ self._auraExpiration = nil
+ self._auraDuration = nil
+ self._auraCount = nil
+ self._auraActive = false
+ refreshAuraDebuffBorder(self, false)
+ end
+ if self.Icon and self.Icon.Applications then
+ if self._auraActive and self._auraCount and self._auraCount > 1 then
+ self.Icon.Applications:SetText(tostring(self._auraCount))
+ self.Icon.Applications:Show()
+ else
+ self.Icon.Applications:Hide()
+ end
+ end
+ self:RefreshCooldownInfo()
+ if self.hideWhenInactive then self:UpdateShownState() end
+end
+
+function BarItem:SetPreviewAura(name, icon, duration, remaining, spellID)
+ self._editPreview = true
+ self._previewDuration = duration
+ self.spellID = spellID
+ self.spellName = name
+ self._auraDuration = duration
+ self._auraExpiration = GetTime() + (remaining or duration)
+ self._auraActive = true
+ refreshAuraDebuffBorder(self, false)
+ if self.Icon and self.Icon.Icon then self.Icon.Icon:SetTexture(icon or QUESTION_MARK) end
+ if self.Bar and self.Bar.Name then self.Bar.Name:SetText(name or "?") end
+ if self.Icon and self.Icon.Applications then self.Icon.Applications:Hide() end
+ self:RefreshCooldownInfo()
+end
+
+-- Called every frame by OnUpdate AND on UNIT_AURA via RefreshAura.
+function BarItem:RefreshCooldownInfo()
+ if not self.Bar then return end
+ local exp, dur = self._auraExpiration, self._auraDuration
+ if not (exp and dur and dur > 0) then
+ self.Bar:SetMinMaxValues(0, 1)
+ self.Bar:SetValue(0)
+ if self.Bar.Duration then self.Bar.Duration:SetText(""); self._lastDurTenth = nil end
+ setShown(self.Bar.Pip, false)
+ return
+ end
+
+ local currentTime = exp - GetTime()
+ if currentTime <= 0 then
+ if self._editPreview then
+ -- Loop the preview so it stays animated while the editor is open.
+ self._auraExpiration = GetTime() + (self._previewDuration or dur)
+ currentTime = self._auraExpiration - GetTime()
+ else
+ self._auraExpiration = nil
+ self._auraDuration = nil
+ self._auraActive = false
+ self.Bar:SetValue(0)
+ if self.Bar.Duration then self.Bar.Duration:SetText(""); self._lastDurTenth = nil end
+ setShown(self.Bar.Pip, false)
+ if self.hideWhenInactive then self:UpdateShownState() end
+ return
+ end
+ end
+
+ self.Bar:SetMinMaxValues(0, dur)
+ self.Bar:SetValue(currentTime)
+ if self.Bar.Duration and self.Bar.Duration:IsShown() then
+ -- Re-format only when the displayed tenth changes: this runs per frame per active bar, and an
+ -- unconditional format+SetText allocates a fresh string at 60 Hz.
+ local tenth = math.floor(currentTime * 10)
+ if tenth ~= self._lastDurTenth then
+ self._lastDurTenth = tenth
+ self.Bar.Duration:SetText(string.format(COOLDOWN_DURATION_SEC or "%.1f", currentTime))
+ end
+ end
+ setShown(self.Bar.Pip, true)
+end
+
+function BarItem:GetSettingsKey() return auraSettingsKey(self) end
+
+function BarItem:RefreshCooldown() self:RefreshAura() end
+
+function BarItem:OnEnter()
+ if not self.spellID then return end
+ if self.tooltipsShown == false then return end
+ GameTooltip:SetOwner(self, "ANCHOR_TOP")
+ if M.TooltipSetSpellNamed(GameTooltip, self.spellID, self.spellName) then GameTooltip:Show() end
+end
+
+function BarItem:OnLeave() GameTooltip:Hide() end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/BuffViewers.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/BuffViewers.lua
new file mode 100644
index 0000000..3989099
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/BuffViewers.lua
@@ -0,0 +1,411 @@
+-- DragonUI_NewEra/modules/cooldownviewer/BuffViewers.lua — the two AURA-driven viewers.
+--
+-- DOWNPORT of the BuffIcon/BuffBar half of NewEra/CooldownViewer/CooldownViewer.lua plus their XML
+-- templates (Phase 3). As in Phase 1, frames are built in Lua rather than porting the XML:
+-- MaskTexture is a Legion widget absent here, and GridLayoutFrame is supplied by core/GridLayout.lua.
+--
+-- These differ from Essential/Utility in kind, not just degree. Those read SPELL COOLDOWNS from a
+-- curated per-class list. These read LIVE AURAS: any player buff inside the auto-track window
+-- (<= M.BUFF_TRACK_MAX_DURATION), plus explicitly assigned auras from the tracked-aura pool, plus
+-- tracked DoTs on the current target. So they need no class data at all — which is why Phase 3 does
+-- not depend on the Phase 2 WotLK spell tables.
+--
+-- !! THE ARG-SHIFT TRAP: the 1.15 source scans with
+-- local name, icon, count, _, duration, expiration, _, _, _, spellID = UnitBuff("player", i)
+-- which is the MODERN return layout. On 3.3.5a `rank` sits at index 2, shifting everything after
+-- it by one — ported verbatim, `icon` would receive the rank string and `duration` the caster.
+-- All scanning here goes through NE.aura (core/AuraSnapshot.lua), which owns that correction and
+-- also collapses the repeated per-frame walks into one.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+local BaseViewerMixin = M.BaseViewerMixin
+
+local QUESTION_MARK = "Interface\\Icons\\INV_Misc_QuestionMark"
+
+-- ── Item construction ───────────────────────────────────────────────────────────────────────────
+
+-- BuffIcon tile: 40x40 (vs Essential 50, Utility 30), overlay inset -8/+7.
+local function createBuffIconItem(parent)
+ local item = CreateFrame("Frame", nil, parent)
+ item:SetSize(40, 40)
+ item:EnableMouse(true)
+
+ item.Icon = item:CreateTexture(nil, "ARTWORK")
+ -- The mask's inset, which is what fits the icon inside the overlay's border — see
+ -- M.AnchorMaskedIcon in ItemMixins.lua.
+ M.AnchorMaskedIcon(item.Icon, item, 40)
+
+ item.IconOverlay = item:CreateTexture(nil, "OVERLAY")
+ item.IconOverlay:SetPoint("TOPLEFT", item, "TOPLEFT", -8, 7)
+ item.IconOverlay:SetPoint("BOTTOMRIGHT", item, "BOTTOMRIGHT", 8, -7)
+ M.BuildFrameStack(item, 8, 7)
+
+ item.Cooldown = CreateFrame("Cooldown", nil, item)
+ -- Icon rect, not the tile: the sweep would otherwise overhang the icon by the mask inset. See
+ -- M.AnchorMaskedIcon.
+ M.AnchorMaskedIcon(item.Cooldown, item, 40)
+
+ -- Stack count.
+ item.Applications = CreateFrame("Frame", nil, item)
+ item.Applications:SetAllPoints(item)
+ item.Applications.Text = item.Applications:CreateFontString(nil, "OVERLAY", "NumberFontNormal")
+ item.Applications.Text:SetPoint("BOTTOMRIGHT", item, "BOTTOMRIGHT", -2, 2)
+ item.Applications.Text:Hide()
+
+ -- Dispel-coloured border, shown only for tracked DEBUFFS.
+ item.DebuffBorder = CreateFrame("Frame", nil, item)
+ item.DebuffBorder:SetPoint("TOPLEFT", item, "TOPLEFT", -3, 3)
+ item.DebuffBorder:SetPoint("BOTTOMRIGHT", item, "BOTTOMRIGHT", 3, -3)
+ item.DebuffBorder.Texture = item.DebuffBorder:CreateTexture(nil, "OVERLAY")
+ item.DebuffBorder.Texture:SetAllPoints(item.DebuffBorder)
+ item.DebuffBorder:Hide()
+
+ item.cooldownFont = NE.cd.FONT.viewerAura
+ item.includeAsLayoutChildWhenHidden = true
+ item.allowHideWhenInactive = true -- XML KeyValue: BuffIcon honours HideWhenInactive
+
+ for k, v in pairs(NE_CooldownViewerAuraItemMixin) do item[k] = v end
+ item:SetScript("OnEnter", item.OnEnter)
+ item:SetScript("OnLeave", item.OnLeave)
+ item:OnLoad()
+ return item
+end
+
+-- BuffBar row: 220x30 — a 30x30 icon on the left, a 19px-tall StatusBar filling the rest.
+local function createBuffBarItem(parent)
+ local item = CreateFrame("Frame", nil, parent)
+ item:SetSize(220, 30)
+ item:EnableMouse(true)
+
+ local icon = CreateFrame("Frame", nil, item)
+ icon:SetSize(30, 30)
+ icon:SetPoint("LEFT", item, "LEFT", 0, 0)
+ icon.Icon = icon:CreateTexture(nil, "ARTWORK")
+ M.AnchorMaskedIcon(icon.Icon, icon, 30)
+ icon.IconOverlay = icon:CreateTexture(nil, "OVERLAY")
+ icon.IconOverlay:SetPoint("TOPLEFT", icon, "TOPLEFT", -6, 5)
+ icon.IconOverlay:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 6, -5)
+ M.BuildFrameStack(icon, 6, 5)
+ icon.Applications = icon:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
+ icon.Applications:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -5, 5)
+ icon.Applications:SetJustifyH("RIGHT")
+ icon.Applications:Hide()
+ item.Icon = icon
+
+ local bar = CreateFrame("StatusBar", nil, item)
+ bar:SetHeight(19)
+ bar:SetPoint("LEFT", icon, "RIGHT", 2, 0)
+ bar:SetPoint("RIGHT", item, "RIGHT", 0, 0)
+ bar:SetMinMaxValues(0, 1)
+ bar:SetValue(0)
+ -- Retail's -2/+2, +4/-7 anchors live inside BuildBarBG: the group owns its own rect because the
+ -- cap widths are derived from it.
+ bar.BarBG = M.BuildBarBG(bar)
+ bar.Pip = bar:CreateTexture(nil, "OVERLAY")
+ bar.Name = bar:CreateFontString(nil, "OVERLAY", "NumberFontNormal")
+ bar.Name:SetPoint("TOPLEFT", bar, "TOPLEFT", 5, 0)
+ bar.Name:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", -25, 0)
+ bar.Name:SetJustifyH("LEFT")
+ bar.Name:SetJustifyV("MIDDLE")
+ bar.Duration = bar:CreateFontString(nil, "OVERLAY", "NumberFontNormal")
+ bar.Duration:SetPoint("RIGHT", bar, "RIGHT", -8, 0)
+ bar.Duration:SetJustifyH("LEFT")
+ item.Bar = bar
+
+ item.DebuffBorder = CreateFrame("Frame", nil, item)
+ item.DebuffBorder:SetPoint("TOPLEFT", icon, "TOPLEFT", -3, 3)
+ item.DebuffBorder:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 3, -3)
+ item.DebuffBorder.Texture = item.DebuffBorder:CreateTexture(nil, "OVERLAY")
+ item.DebuffBorder.Texture:SetAllPoints(item.DebuffBorder)
+ item.DebuffBorder:Hide()
+
+ item.includeAsLayoutChildWhenHidden = true
+ item.allowHideWhenInactive = true
+
+ for k, v in pairs(NE_CooldownViewerBuffBarItemMixin) do item[k] = v end
+ item:SetScript("OnEnter", item.OnEnter)
+ item:SetScript("OnLeave", item.OnLeave)
+ item:OnLoad()
+ return item
+end
+
+local ITEM_FACTORY = {
+ buffIcon = createBuffIconItem,
+ buffBar = createBuffBarItem,
+}
+
+-- ── Shared aura-scan rebuild ────────────────────────────────────────────────────────────────────
+
+-- Both viewers run the identical scan and differ only in presentation, so it lives here once.
+-- Returns the number of items populated.
+local function rebuildFromAuras(self)
+ local category = self.category
+ local include, exclude = M.GetBuffOverrides(category)
+ local factory = ITEM_FACTORY[category]
+ local n = 0
+ local shownNames = {}
+
+ local function acquire()
+ n = n + 1
+ local item = self.items[n]
+ if not item then
+ item = factory(self)
+ item.viewerFrame = self
+ self.items[n] = item
+ end
+ item.layoutIndex = n
+ item.ignoreInLayout = false
+ return item
+ end
+
+ -- Player self-buffs. DOWNPORT: via NE.aura, not a raw UnitBuff loop — see the arg-shift note in
+ -- the file header.
+ local snap = NE.aura and NE.aura.GetSnapshot("player", "HELPFUL")
+ if snap then
+ for i = 1, snap.n do
+ local row = snap.list[i]
+ -- Record it BEFORE deciding whether to show it. This is the whole of Phase 7a's write path:
+ -- the picker's Tracked Buffs/Bars/Not Displayed sections are fed from here, and recording
+ -- after the decision would mean hiding an aura also removed the row that could unhide it.
+ if M.NoteSeenAura then
+ M.NoteSeenAura(row.spellID, row.name, row.icon, row.duration)
+ end
+ if M.ShouldTrackBuff(row.spellID, row.duration, include, exclude, category, row.name) then
+ local item = acquire()
+ item:SetAura(row.name, row.icon, row.count, row.duration, row.expiration, row.spellID)
+ item:Show()
+ shownNames[row.name] = true
+ end
+ end
+ end
+
+ -- Tracked DoTs on the target (explicit assignments only, never the auto window).
+ for _, a in ipairs(M.ScanTargetTrackedAuras(include, shownNames)) do
+ local item = acquire()
+ item:SetAura(a.name, a.icon, a.count, a.duration, a.expiration, a.spellID)
+ item:Show()
+ end
+
+ -- Retire the extra slots. Clearing _auraActive AND the spell identity is REQUIRED, not just
+ -- Hide(): the RefreshLayout below runs SetHideWhenInactive -> UpdateShownState on every item,
+ -- which re-shows any slot whose cached _auraActive is still true (duplicate bars when a buff
+ -- falls off) or — with HideWhenInactive off — any slot still holding a spellID (a permanent
+ -- empty bar).
+ for i = n + 1, #self.items do
+ local it = self.items[i]
+ it._auraActive = false
+ it._editPreview = false
+ it._auraExpiration = nil
+ it._auraDuration = nil
+ it.spellID = nil
+ it.spellName = nil
+ it.ignoreInLayout = true
+ it:Hide()
+ end
+ return n
+end
+
+-- Re-layout only when the visible COUNT changes. UNIT_AURA fires constantly in combat; when the
+-- set is stable the bars' own OnUpdate already animates them and SetAura refreshed durations, so
+-- skipping Layout avoids a combat hitch. A same-count swap reuses the slot in place.
+local function rebuildAndMaybeLayout(self)
+ local n = rebuildFromAuras(self)
+ if n ~= self._lastShownCount then
+ self._lastShownCount = n
+ self:RefreshLayout()
+ end
+end
+
+-- Contained rebuild: the aura viewers call Rebuild directly from their hottest path (UNIT_AURA), so
+-- an error here would otherwise spam every aura tick in combat.
+local function safeRebuild(self)
+ local ok, err = pcall(self.Rebuild, self)
+ if not ok and NE.Log then
+ NE.Log("CDM", "buff viewer '" .. tostring(self.category) .. "' rebuild error: " .. tostring(err))
+ end
+end
+
+-- Shared event surface for both aura viewers: aura churn on player/target, plus the base's
+-- visibility/level events.
+local function auraViewerOnLoad(self)
+ self.items = {}
+ self:RegisterEvent("PLAYER_ENTERING_WORLD")
+ -- player + target ONLY, engine-side. An unfiltered UNIT_AURA delivers every raid/party/nameplate
+ -- unit's churn to the handler, per plate per tick in big pulls.
+ self:RegisterUnitEvent("UNIT_AURA", "player", "target")
+ self:RegisterEvent("SPELLS_CHANGED")
+ self:RegisterEvent(NE.EV_LEARNED_SPELL)
+ self:RegisterEvent("PLAYER_LEVEL_UP")
+ -- Dual spec: aura assignments live in the per-talent-group layout bucket too.
+ self:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
+ self:RegisterEvent("PLAYER_TALENT_UPDATE")
+ self:RegisterEvent("PLAYER_REGEN_DISABLED")
+ self:RegisterEvent("PLAYER_REGEN_ENABLED")
+ self:RegisterEvent("PLAYER_TARGET_CHANGED")
+end
+
+local function auraViewerOnEvent(self, event, ...)
+ -- The same master-enable gate BaseViewerMixin:OnEvent carries, repeated because the two hottest
+ -- events on this client — UNIT_AURA and PLAYER_TARGET_CHANGED — are answered here and never reach it.
+ -- A gate only in the base would have left an off Cooldown Manager rebuilding both aura viewers on
+ -- every aura tick, which is the one path where that cost is actually measurable.
+ if not M.IsEnabled() then return end
+ if event == "UNIT_AURA" then
+ local unit = ...
+ if unit == "player" or unit == "target" then safeRebuild(self) end
+ return
+ elseif event == "PLAYER_TARGET_CHANGED" then
+ safeRebuild(self)
+ return
+ end
+ BaseViewerMixin.OnEvent(self, event, ...)
+end
+
+-- ── BuffIcon viewer ─────────────────────────────────────────────────────────────────────────────
+
+local BuffIconMixin = {}
+for k, v in pairs(BaseViewerMixin) do BuffIconMixin[k] = v end
+BuffIconMixin.category = "buffIcon"
+
+BuffIconMixin.OnLoad = auraViewerOnLoad
+BuffIconMixin.OnEvent = auraViewerOnEvent
+function BuffIconMixin:OnShow() safeRebuild(self) end
+
+-- All icons share ONE row/column, like the bars. The retail preset keeps IconLimit=1 for this
+-- viewer, so the BASE GetStride (= iconLimit = 1) would wrap every icon onto its own row —
+-- inverting Horizontal into a vertical column. Using the live count lays them out in one row.
+function BuffIconMixin:GetStride()
+ local c = 0
+ for _, it in ipairs(self.items) do
+ if it.layoutIndex and not it.ignoreInLayout then c = c + 1 end
+ end
+ return math.max(c, 1)
+end
+
+local BUFFICON_PREVIEW = {
+ { "Power Infusion", "Interface\\Icons\\Spell_Holy_PowerInfusion", 15, 11, 10060 },
+ { "Berserking", "Interface\\Icons\\Spell_Nature_BloodLust", 20, 14, 20554 },
+ { "Crusader", "Interface\\Icons\\Spell_Holy_HolySmite", 15, 6, 20007 },
+ { "Recklessness", "Interface\\Icons\\Ability_CriticalStrike", 15, 9, 1719 },
+}
+
+local BUFFBAR_PREVIEW = BUFFICON_PREVIEW -- same archetypes; the bar renders them differently
+
+local function rebuildPreview(self, samples)
+ local factory = ITEM_FACTORY[self.category]
+ for i, e in ipairs(samples) do
+ local item = self.items[i]
+ if not item then
+ item = factory(self)
+ item.viewerFrame = self
+ self.items[i] = item
+ end
+ item.layoutIndex = i
+ item.ignoreInLayout = false
+ if item.SetPreviewAura then item:SetPreviewAura(e[1], e[2], e[3], e[4], e[5]) end
+ item:Show()
+ end
+ for i = #samples + 1, #self.items do
+ local it = self.items[i]
+ it._auraActive = false
+ it._editPreview = false
+ it.ignoreInLayout = true
+ it:Hide()
+ end
+ self._lastShownCount = #samples
+ self:RefreshLayout()
+end
+
+function BuffIconMixin:Rebuild()
+ if self._editPreview then return rebuildPreview(self, BUFFICON_PREVIEW) end
+ rebuildAndMaybeLayout(self)
+end
+
+-- ── BuffBar viewer ──────────────────────────────────────────────────────────────────────────────
+
+local BuffBarMixin = {}
+for k, v in pairs(BaseViewerMixin) do BuffBarMixin[k] = v end
+BuffBarMixin.category = "buffBar"
+-- Grow upward from the BOTTOM anchor so existing bars stay put as bars are added or removed; new
+-- bars stack on top.
+BuffBarMixin.growUpward = true
+
+BuffBarMixin.OnLoad = auraViewerOnLoad
+BuffBarMixin.OnEvent = auraViewerOnEvent
+function BuffBarMixin:OnShow() safeRebuild(self) end
+
+-- All bars share ONE column. Count what the GRID will lay out (has layoutIndex, not
+-- ignoreInLayout) rather than only SHOWN bars: hidden-but-not-retired bars still occupy a cell via
+-- includeAsLayoutChildWhenHidden, and counting only shown ones lets stride fall below the
+-- layout-child count, wrapping the stack into a phantom second column.
+function BuffBarMixin:GetStride()
+ local c = 0
+ for _, it in ipairs(self.items) do
+ if it.layoutIndex and not it.ignoreInLayout then c = c + 1 end
+ end
+ return math.max(c, 1)
+end
+
+-- Bars sit closer together than icons.
+function BuffBarMixin:GetAdditionalPaddingOffset() return -2 end
+
+function BuffBarMixin:SetBarContent(content)
+ self.barContent = content
+ for _, item in ipairs(self.items) do
+ if item.SetBarContent then item:SetBarContent(content) end
+ end
+end
+
+function BuffBarMixin:SetBarWidthScale(percent)
+ self.barWidthScale = (percent or 100) / 100
+ for _, item in ipairs(self.items) do
+ if item.SetBarWidthScale then item:SetBarWidthScale(self.barWidthScale) end
+ end
+end
+
+-- Bar content/width must land BEFORE the base call: the base runs Layout(), and item frames are
+-- plain Frames whose width changes don't re-dirty the grid — applying width afterwards leaves the
+-- frame size stale until the next relayout.
+function BuffBarMixin:RefreshLayout()
+ self:SetBarContent(self:GetCategoryOpt("barContent"))
+ self:SetBarWidthScale(self:GetCategoryOpt("barWidthScale"))
+ BaseViewerMixin.RefreshLayout(self)
+end
+
+function BuffBarMixin:Rebuild()
+ if self._editPreview then return rebuildPreview(self, BUFFBAR_PREVIEW) end
+ rebuildAndMaybeLayout(self)
+end
+
+-- ── Frame construction ──────────────────────────────────────────────────────────────────────────
+
+local VIEWER_MIXIN = { buffIcon = BuffIconMixin, buffBar = BuffBarMixin }
+local VIEWER_GLOBAL = {
+ buffIcon = "DragonUI_NewEra_BuffIconCooldownViewer",
+ buffBar = "DragonUI_NewEra_BuffBarCooldownViewer",
+}
+
+function M.CreateBuffViewer(category)
+ if M.viewers[category] then return M.viewers[category] end
+ local mixin = VIEWER_MIXIN[category]
+ if not mixin then return nil end
+
+ local frame = CreateFrame("Frame", VIEWER_GLOBAL[category], UIParent)
+ frame.category = category
+ frame:SetSize(1, 1)
+ frame:SetPoint("BOTTOM", UIParent, "BOTTOM",
+ category == "buffBar" and 420 or 0,
+ M.VIEWER_DEFAULT_Y[category] or 370)
+ frame:Hide()
+
+ NE.gridlayout.Apply(frame)
+ for k, v in pairs(mixin) do frame[k] = v end
+
+ frame:SetScript("OnEvent", frame.OnEvent)
+ frame:SetScript("OnShow", frame.OnShow)
+ frame:OnLoad()
+
+ M.RegisterViewer(category, frame)
+ return frame
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmArsenal.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmArsenal.lua
new file mode 100644
index 0000000..34f2311
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmArsenal.lua
@@ -0,0 +1,342 @@
+-- DragonUI_NewEra/modules/cooldownviewer/CdmArsenal.lua — GENERATED, DO NOT HAND-EDIT.
+--
+-- The full per-class pool of abilities carrying a real cooldown (> 1.5s), keyed rank-1. This is what
+-- the settings panel offers under "Not Displayed": the arsenal a player can ADD to a viewer, as
+-- opposed to the curated defaults that start there.
+--
+-- Upstream ships the equivalent as a generated NE_CDM_HIDDEN global. Ours is a module table so it
+-- cannot collide with anything, and it is regenerated by tools/cdm-spellgen/gen_arsenal.py from this
+-- client's own Spell.dbc + SkillLineAbility.dbc — no id is typed by hand.
+--
+-- Curated ids are deliberately still present here. The adapter subtracts whatever currently sits in
+-- a viewer, so anything already in Essential/Utility never shows up there anyway; baking that
+-- exclusion into the data would rot the moment the curated lists changed.
+--
+-- KNOWN GAP: Data/patch-4.MPQ and patch-S.mpq are encrypted and unreadable, so if this server
+-- overrides spell data there, these ids reflect the stock client.
+
+local NE = DragonUI_NewEra
+NE.cooldownviewer = NE.cooldownviewer or {}
+local M = NE.cooldownviewer
+
+M.ARSENAL_BY_CLASS = {
+
+ DEATHKNIGHT = {
+ 48707, -- Anti-Magic Shell
+ 51052, -- Anti-Magic Zone
+ 42650, -- Army of the Dead
+ 45529, -- Blood Tap
+ 49222, -- Bone Shield
+ 49158, -- Corpse Explosion
+ 49028, -- Dancing Rune Weapon
+ 56222, -- Dark Command
+ 43265, -- Death and Decay
+ 50977, -- Death Gate
+ 49576, -- Death Grip
+ 48743, -- Death Pact
+ 50033, -- Death Rune Mastery
+ 49796, -- Deathchill
+ 47568, -- Empower Rune Weapon
+ 57532, -- Eye of Acherus
+ 63560, -- Ghoul Frenzy
+ 57330, -- Horn of Winter
+ 49184, -- Howling Blast
+ 49203, -- Hungering Cold
+ 49016, -- Hysteria
+ 48792, -- Icebound Fortitude
+ 52372, -- Icy Touch
+ 49039, -- Lichborne
+ 49005, -- Mark of Blood
+ 47528, -- Mind Freeze
+ 61999, -- Raise Ally
+ 46584, -- Raise Dead
+ 48982, -- Rune Tap
+ 47476, -- Strangulate
+ 49206, -- Summon Gargoyle
+ 51271, -- Unbreakable Armor
+ 55233, -- Vampiric Blood
+ },
+ DRUID = {
+ 22812, -- Barkskin
+ 5211, -- Bash
+ 50334, -- Berserk
+ 5209, -- Challenging Roar
+ 8998, -- Cower
+ 1850, -- Dash
+ 5229, -- Enrage
+ 16857, -- Faerie Fire (Feral)
+ 16979, -- Feral Charge - Bear
+ 49376, -- Feral Charge - Cat
+ 33831, -- Force of Nature
+ 22842, -- Frenzied Regeneration
+ 6795, -- Growl
+ 29166, -- Innervate
+ 22570, -- Maim
+ 33878, -- Mangle (Bear)
+ 16689, -- Nature's Grasp
+ 17116, -- Nature's Swiftness
+ 5215, -- Prowl
+ 20484, -- Rebirth
+ 48505, -- Starfall
+ 61336, -- Survival Instincts
+ 18562, -- Swiftmend
+ 5217, -- Tiger's Fury
+ 740, -- Tranquility
+ 50516, -- Typhoon
+ 48438, -- Wild Growth
+ },
+ HUNTER = {
+ 19434, -- Aimed Shot
+ 3044, -- Arcane Shot
+ 19574, -- Bestial Wrath
+ 3674, -- Black Arrow
+ 62757, -- Call Stabled Pet
+ 53209, -- Chimera Shot
+ 5116, -- Concussive Shot
+ 19306, -- Counterattack
+ 19263, -- Deterrence
+ 781, -- Disengage
+ 20736, -- Distracting Shot
+ 53301, -- Explosive Shot
+ 13813, -- Explosive Trap
+ 6991, -- Feed Pet
+ 5384, -- Feign Death
+ 1543, -- Flare
+ 60192, -- Freezing Arrow
+ 1499, -- Freezing Trap
+ 13809, -- Frost Trap
+ 13795, -- Immolation Trap
+ 19577, -- Intimidation
+ 34026, -- Kill Command
+ 53351, -- Kill Shot
+ 56453, -- Lock and Load
+ 53271, -- Master's Call
+ 34477, -- Misdirection
+ 1495, -- Mongoose Bite
+ 2643, -- Multi-Shot
+ 3045, -- Rapid Fire
+ 2973, -- Raptor Strike
+ 23989, -- Readiness
+ 1513, -- Scare Beast
+ 19503, -- Scatter Shot
+ 34490, -- Silencing Shot
+ 34600, -- Snake Trap
+ 19801, -- Tranquilizing Shot
+ 3034, -- Viper Sting
+ 19386, -- Wyvern Sting
+ },
+ MAGE = {
+ 44425, -- Arcane Barrage
+ 12042, -- Arcane Power
+ 11113, -- Blast Wave
+ 1953, -- Blink
+ 11958, -- Cold Snap
+ 11129, -- Combustion
+ 120, -- Cone of Cold
+ 2139, -- Counterspell
+ 44572, -- Deep Freeze
+ 31661, -- Dragon's Breath
+ 12051, -- Evocation
+ 2136, -- Fire Blast
+ 543, -- Fire Ward
+ 122, -- Frost Nova
+ 6143, -- Frost Ward
+ 11426, -- Ice Barrier
+ 45438, -- Ice Block
+ 12472, -- Icy Veins
+ 66, -- Invisibility
+ 55342, -- Mirror Image
+ 53142, -- Portal: Dalaran
+ 11419, -- Portal: Darnassus
+ 32266, -- Portal: Exodar
+ 11416, -- Portal: Ironforge
+ 11417, -- Portal: Orgrimmar
+ 33691, -- Portal: Shattrath
+ 32267, -- Portal: Silvermoon
+ 49361, -- Portal: Stonard
+ 10059, -- Portal: Stormwind
+ 49360, -- Portal: Theramore
+ 11420, -- Portal: Thunder Bluff
+ 11418, -- Portal: Undercity
+ 12043, -- Presence of Mind
+ 43987, -- Ritual of Refreshment
+ 31687, -- Summon Water Elemental
+ 70909, -- Summon Water Elemental (Prototype)
+ },
+ PALADIN = {
+ 31821, -- Aura Mastery
+ 31935, -- Avenger's Shield
+ 31884, -- Avenging Wrath
+ 26573, -- Consecration
+ 35395, -- Crusader Strike
+ 20216, -- Divine Favor
+ 31842, -- Divine Illumination
+ 19752, -- Divine Intervention
+ 54428, -- Divine Plea
+ 498, -- Divine Protection
+ 64205, -- Divine Sacrifice
+ 642, -- Divine Shield
+ 53385, -- Divine Storm
+ 879, -- Exorcism
+ 853, -- Hammer of Justice
+ 53595, -- Hammer of the Righteous
+ 24275, -- Hammer of Wrath
+ 1044, -- Hand of Freedom
+ 1022, -- Hand of Protection
+ 62124, -- Hand of Reckoning
+ 6940, -- Hand of Sacrifice
+ 1038, -- Hand of Salvation
+ 20925, -- Holy Shield
+ 20473, -- Holy Shock
+ 2812, -- Holy Wrath
+ 53407, -- Judgement of Justice
+ 20271, -- Judgement of Light
+ 53408, -- Judgement of Wisdom
+ 633, -- Lay on Hands
+ 20066, -- Repentance
+ 31789, -- Righteous Defense
+ 53600, -- Shield of Righteousness
+ },
+ PRIEST = {
+ 34861, -- Circle of Healing
+ 19236, -- Desperate Prayer
+ 47585, -- Dispersion
+ 64843, -- Divine Hymn
+ 586, -- Fade
+ 6346, -- Fear Ward
+ 47788, -- Guardian Spirit
+ 14914, -- Holy Fire
+ 64901, -- Hymn of Hope
+ 14751, -- Inner Focus
+ 724, -- Lightwell
+ 8092, -- Mind Blast
+ 33206, -- Pain Suppression
+ 47540, -- Penance
+ 10060, -- Power Infusion
+ 17, -- Power Word: Shield
+ 33076, -- Prayer of Mending
+ 64044, -- Psychic Horror
+ 8122, -- Psychic Scream
+ 32379, -- Shadow Word: Death
+ 34433, -- Shadowfiend
+ 15487, -- Silence
+ },
+ ROGUE = {
+ 13750, -- Adrenaline Rush
+ 13877, -- Blade Flurry
+ 2094, -- Blind
+ 31224, -- Cloak of Shadows
+ 14177, -- Cold Blood
+ 51722, -- Dismantle
+ 1725, -- Distract
+ 5277, -- Evasion
+ 1966, -- Feint
+ 14278, -- Ghostly Strike
+ 1776, -- Gouge
+ 1766, -- Kick
+ 408, -- Kidney Shot
+ 51690, -- Killing Spree
+ 14183, -- Premeditation
+ 14185, -- Preparation
+ 14251, -- Riposte
+ 51713, -- Shadow Dance
+ 36554, -- Shadowstep
+ 2983, -- Sprint
+ 1784, -- Stealth
+ 57934, -- Tricks of the Trade
+ 1856, -- Vanish
+ },
+ SHAMAN = {
+ 556, -- Astral Recall
+ 2825, -- Bloodlust
+ 421, -- Chain Lightning
+ 2062, -- Earth Elemental Totem
+ 8042, -- Earth Shock
+ 2484, -- Earthbind Totem
+ 16166, -- Elemental Mastery
+ 51533, -- Feral Spirit
+ 2894, -- Fire Elemental Totem
+ 1535, -- Fire Nova
+ 8050, -- Flame Shock
+ 8056, -- Frost Shock
+ 30874, -- Gift of the Water Spirit
+ 8177, -- Grounding Totem
+ 32182, -- Heroism
+ 51514, -- Hex
+ 51505, -- Lava Burst
+ 60103, -- Lava Lash
+ 16190, -- Mana Tide Totem
+ 16188, -- Nature's Swiftness
+ 20608, -- Reincarnation
+ 61295, -- Riptide
+ 30823, -- Shamanistic Rage
+ 5730, -- Stoneclaw Totem
+ 17364, -- Stormstrike
+ 51490, -- Thunderstorm
+ 55198, -- Tidal Force
+ 57994, -- Wind Shear
+ },
+ WARLOCK = {
+ 59671, -- Challenging Howl
+ 50796, -- Chaos Bolt
+ 17962, -- Conflagrate
+ 603, -- Curse of Doom
+ 6789, -- Death Coil
+ 54785, -- Demon Charge
+ 48020, -- Demonic Circle: Teleport
+ 47193, -- Demonic Empowerment
+ 18708, -- Fel Domination
+ 48181, -- Haunt
+ 5484, -- Howl of Terror
+ 50589, -- Immolation Aura
+ 1122, -- Inferno
+ 47241, -- Metamorphosis
+ 18540, -- Ritual of Doom
+ 29893, -- Ritual of Souls
+ 698, -- Ritual of Summoning
+ 50581, -- Shadow Cleave
+ 6229, -- Shadow Ward
+ 17877, -- Shadowburn
+ 47897, -- Shadowflame
+ 30283, -- Shadowfury
+ 29858, -- Soulshatter
+ },
+ WARRIOR = {
+ 18499, -- Berserker Rage
+ 46924, -- Bladestorm
+ 2687, -- Bloodrage
+ 23881, -- Bloodthirst
+ 1161, -- Challenging Shout
+ 100, -- Charge
+ 12809, -- Concussion Blow
+ 12292, -- Death Wish
+ 676, -- Disarm
+ 55694, -- Enraged Regeneration
+ 60970, -- Heroic Fury
+ 57755, -- Heroic Throw
+ 20252, -- Intercept
+ 3411, -- Intervene
+ 5246, -- Intimidating Shout
+ 12975, -- Last Stand
+ 694, -- Mocking Blow
+ 12294, -- Mortal Strike
+ 7384, -- Overpower
+ 6552, -- Pummel
+ 1719, -- Recklessness
+ 20230, -- Retaliation
+ 6572, -- Revenge
+ 64382, -- Shattering Throw
+ 72, -- Shield Bash
+ 2565, -- Shield Block
+ 23922, -- Shield Slam
+ 871, -- Shield Wall
+ 46968, -- Shockwave
+ 23920, -- Spell Reflection
+ 12328, -- Sweeping Strikes
+ 355, -- Taunt
+ 6343, -- Thunder Clap
+ 1680, -- Whirlwind
+ },
+}
+
+if M.InvalidateCuratedCache then M.InvalidateCuratedCache() end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmAuraCatalog.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmAuraCatalog.lua
new file mode 100644
index 0000000..d776372
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmAuraCatalog.lua
@@ -0,0 +1,215 @@
+-- DragonUI_NewEra/modules/cooldownviewer/CdmAuraCatalog.lua — GENERATED, DO NOT HAND-EDIT.
+--
+-- The per-class pool of trackable SELF-BUFFS: what the Tracked Buffs / Tracked Bars picker offers
+-- you, as opposed to what you happen to have had. Retail reads a Blizzard-curated per-spec aura set
+-- from C_CooldownViewer; 3.3.5a has no such table, so this is derived from the client's own
+-- Spell.dbc + SkillLineAbility.dbc + Talent.dbc + TalentTab.dbc by
+-- tools/cdm-spellgen/gen_auracatalog.py. No id is typed by hand.
+--
+-- An entry is a spell that puts a timed aura on the CASTER, lasting 0 < d <= 120s (the same window
+-- M.BUFF_TRACK_MAX_DURATION gives the runtime auto-tracker), reached either directly or through one
+-- EffectTriggerSpell hop — the hop is where the procs are.
+--
+-- id the rank-1 aura spellID. Matching at runtime is by NAME, not id, so a down-ranked or
+-- higher-ranked cast of the same buff still resolves (see M.GetBuffOverrides).
+-- dur seconds, for the picker's tooltip; the live viewer always reads the real aura.
+-- tree 1-3, the talent tree the granting talent sits in, or nil for a baseline ability.
+-- talent the granting TALENT's name. Present = this row is spec-gated: the runtime asks
+-- GetTalentInfo whether the player actually has it. Absent = always offered.
+--
+-- Racials are deliberately absent — they are race-gated, not class-gated, so they do not fit a
+-- per-class catalog (retail's curated list has none either). The seen-aura registry picks them up
+-- the first time you press them.
+--
+-- KNOWN GAP: Data/patch-4.MPQ and patch-S.mpq are encrypted and unreadable, so if this server
+-- overrides spell data there, these ids reflect the stock client.
+
+local NE = DragonUI_NewEra
+NE.cooldownviewer = NE.cooldownviewer or {}
+local M = NE.cooldownviewer
+
+M.AURA_CATALOG_BY_CLASS = {
+
+ DEATHKNIGHT = {
+ { id = 57330, name = "Horn of Winter", dur = 120 }, -- class
+ { id = 50447, name = "Bloody Vengeance", dur = 30, tree = 1, talent = "Bloody Vengeance" }, -- trigger
+ { id = 49796, name = "Deathchill", dur = 30, tree = 2, talent = "Deathchill" }, -- class
+ { id = 51124, name = "Killing Machine", dur = 30, tree = 2, talent = "Killing Machine" }, -- trigger
+ { id = 61777, name = "Summon Gargoyle", dur = 30, tree = 3, talent = "Summon Gargoyle" }, -- trigger
+ { id = 45529, name = "Blood Tap", dur = 20 }, -- class
+ { id = 63583, name = "Desolation", dur = 20, tree = 3, talent = "Desolation" }, -- trigger
+ { id = 50882, name = "Icy Talons", dur = 20, tree = 2, talent = "Icy Talons" }, -- trigger
+ { id = 50421, name = "Scent of Blood", dur = 20, tree = 1, talent = "Scent of Blood" }, -- trigger
+ { id = 51271, name = "Unbreakable Armor", dur = 20, tree = 2, talent = "Unbreakable Armor" }, -- class
+ { id = 59052, name = "Freezing Fog", dur = 15, tree = 2, talent = "Rime" }, -- trigger
+ { id = 49028, name = "Dancing Rune Weapon", dur = 12, tree = 1, talent = "Dancing Rune Weapon" }, -- class
+ { id = 48792, name = "Icebound Fortitude", dur = 12 }, -- class
+ { id = 51789, name = "Blade Barrier", dur = 10, tree = 1, talent = "Blade Barrier" }, -- trigger
+ { id = 49039, name = "Lichborne", dur = 10, tree = 2, talent = "Lichborne" }, -- class
+ { id = 55233, name = "Vampiric Blood", dur = 10, tree = 1, talent = "Vampiric Blood" }, -- class
+ { id = 48707, name = "Anti-Magic Shell", dur = 5 }, -- class
+ },
+ DRUID = {
+ { id = 16689, name = "Nature's Grasp", dur = 45 }, -- class
+ { id = 61336, name = "Survival Instincts", dur = 20, tree = 2, talent = "Survival Instincts" }, -- class
+ { id = 50334, name = "Berserk", dur = 15, tree = 2, talent = "Berserk" }, -- class
+ { id = 16870, name = "Clearcasting", dur = 15, tree = 3, talent = "Omen of Clarity" }, -- trigger
+ { id = 1850, name = "Dash", dur = 15 }, -- class
+ { id = 22812, name = "Barkskin", dur = 12 }, -- class
+ { id = 5229, name = "Enrage", dur = 10 }, -- class
+ { id = 22842, name = "Frenzied Regeneration", dur = 10 }, -- class
+ { id = 48391, name = "Owlkin Frenzy", dur = 10, tree = 1, talent = "Owlkin Frenzy" }, -- trigger
+ { id = 62606, name = "Savage Defense", dur = 10 }, -- trigger
+ { id = 48505, name = "Starfall", dur = 10, tree = 1, talent = "Starfall" }, -- class
+ { id = 52610, name = "Savage Roar", dur = 9 }, -- class
+ { id = 45281, name = "Natural Perfection", dur = 8, tree = 3, talent = "Natural Perfection" }, -- trigger
+ { id = 69369, name = "Predator's Swiftness", dur = 8, tree = 2, talent = "Predatory Strikes" }, -- trigger
+ { id = 5217, name = "Tiger's Fury", dur = 6 }, -- class
+ { id = 16886, name = "Nature's Grace", dur = 3, tree = 1, talent = "Nature's Grace" }, -- trigger
+ },
+ HUNTER = {
+ { id = 62757, name = "Call Stabled Pet", dur = 120 }, -- class
+ { id = 34026, name = "Kill Command", dur = 30 }, -- class
+ { id = 34477, name = "Misdirection", dur = 30 }, -- class
+ { id = 35098, name = "Rapid Killing", dur = 20, tree = 2, talent = "Rapid Killing" }, -- trigger
+ { id = 3045, name = "Rapid Fire", dur = 15 }, -- class
+ { id = 53230, name = "Rapid Recuperation Effect", dur = 15, tree = 2, talent = "Rapid Recuperation" }, -- trigger
+ { id = 53220, name = "Improved Steady Shot", dur = 12, tree = 2, talent = "Improved Steady Shot" }, -- trigger
+ { id = 56453, name = "Lock and Load", dur = 12, tree = 3, talent = "Lock and Load" }, -- trigger
+ { id = 6150, name = "Quick Shots", dur = 12 }, -- trigger
+ { id = 53257, name = "Cobra Strikes", dur = 10, tree = 1, talent = "Cobra Strikes" }, -- trigger
+ { id = 34471, name = "The Beast Within", dur = 10 }, -- class
+ { id = 34833, name = "Master Tactician", dur = 8, tree = 3, talent = "Master Tactician" }, -- trigger
+ { id = 34501, name = "Expose Weakness", dur = 7, tree = 3, talent = "Expose Weakness" }, -- trigger
+ { id = 60798, name = "Monkey Speed", dur = 6 }, -- trigger
+ { id = 19263, name = "Deterrence", dur = 5 }, -- class
+ { id = 15571, name = "Dazed", dur = 4 }, -- trigger
+ { id = 982, name = "Revive Pet", dur = 3 }, -- class
+ },
+ MAGE = {
+ { id = 11426, name = "Ice Barrier", dur = 60, tree = 3, talent = "Ice Barrier" }, -- class
+ { id = 1463, name = "Mana Shield", dur = 60 }, -- class
+ { id = 543, name = "Fire Ward", dur = 30 }, -- class
+ { id = 6143, name = "Frost Ward", dur = 30 }, -- class
+ { id = 55342, name = "Mirror Image", dur = 30 }, -- class
+ { id = 54748, name = "Burning Determination", dur = 20, tree = 2, talent = "Burning Determination" }, -- trigger
+ { id = 12472, name = "Icy Veins", dur = 20, tree = 3, talent = "Icy Veins" }, -- class
+ { id = 12042, name = "Arcane Power", dur = 15, tree = 1, talent = "Arcane Power" }, -- class
+ { id = 12536, name = "Clearcasting", dur = 15, tree = 1, talent = "Arcane Concentration" }, -- trigger
+ { id = 44544, name = "Fingers of Frost", dur = 15, tree = 3, talent = "Fingers of Frost" }, -- trigger
+ { id = 57761, name = "Fireball!", dur = 15, tree = 3, talent = "Brain Freeze" }, -- trigger
+ { id = 44401, name = "Missile Barrage", dur = 15, tree = 1, talent = "Missile Barrage" }, -- trigger
+ { id = 54741, name = "Firestarter", dur = 10, tree = 2, talent = "Firestarter" }, -- trigger
+ { id = 45438, name = "Ice Block", dur = 10 }, -- class
+ { id = 64343, name = "Impact", dur = 10, tree = 2, talent = "Impact" }, -- trigger
+ { id = 44413, name = "Incanter's Absorption", dur = 10, tree = 1, talent = "Incanter's Absorption" }, -- trigger
+ { id = 31643, name = "Blazing Speed", dur = 8 }, -- class
+ { id = 36032, name = "Arcane Blast", dur = 6 }, -- trigger
+ { id = 46989, name = "Improved Blink", dur = 4, tree = 1, talent = "Improved Blink" }, -- trigger
+ { id = 66, name = "Invisibility", dur = 3 }, -- class
+ { id = 1953, name = "Blink", dur = 1 }, -- class
+ },
+ PALADIN = {
+ { id = 53655, name = "Judgements of the Pure", dur = 60, tree = 1, talent = "Judgements of the Pure" }, -- trigger
+ { id = 20050, name = "Vengeance", dur = 30, tree = 3, talent = "Vengeance" }, -- trigger
+ { id = 31884, name = "Avenging Wrath", dur = 20 }, -- class
+ { id = 31842, name = "Divine Illumination", dur = 15, tree = 1, talent = "Divine Illumination" }, -- class
+ { id = 54428, name = "Divine Plea", dur = 15 }, -- class
+ { id = 53672, name = "Infusion of Light", dur = 15, tree = 1, talent = "Infusion of Light" }, -- trigger
+ { id = 31834, name = "Light's Grace", dur = 15, tree = 1, talent = "Light's Grace" }, -- trigger
+ { id = 53489, name = "The Art of War", dur = 15, tree = 3, talent = "The Art of War" }, -- trigger
+ { id = 498, name = "Divine Protection", dur = 12 }, -- class
+ { id = 642, name = "Divine Shield", dur = 12 }, -- class
+ { id = 20925, name = "Holy Shield", dur = 10, tree = 2, talent = "Holy Shield" }, -- class
+ { id = 20128, name = "Redoubt", dur = 10, tree = 2, talent = "Redoubt" }, -- trigger
+ { id = 20178, name = "Reckoning", dur = 8, tree = 2, talent = "Reckoning" }, -- trigger
+ { id = 31821, name = "Aura Mastery", dur = 6, tree = 1, talent = "Aura Mastery" }, -- class
+ },
+ PRIEST = {
+ { id = 63731, name = "Serendipity", dur = 20, tree = 2, talent = "Serendipity" }, -- trigger
+ { id = 15258, name = "Shadow Weaving", dur = 15, tree = 3, talent = "Shadow Weaving" }, -- trigger
+ { id = 15271, name = "Spirit Tap", dur = 15, tree = 3, talent = "Spirit Tap" }, -- trigger
+ { id = 586, name = "Fade", dur = 10 }, -- class
+ { id = 33151, name = "Surge of Light", dur = 10, tree = 2, talent = "Surge of Light" }, -- trigger
+ { id = 45237, name = "Focused Will", dur = 8, tree = 1, talent = "Focused Will" }, -- trigger
+ { id = 34754, name = "Holy Concentration", dur = 8, tree = 2, talent = "Holy Concentration" }, -- trigger
+ { id = 49694, name = "Improved Spirit Tap", dur = 8, tree = 3, talent = "Improved Spirit Tap" }, -- trigger
+ { id = 27813, name = "Blessed Recovery", dur = 6 }, -- class
+ { id = 33143, name = "Blessed Resilience", dur = 6, tree = 2, talent = "Blessed Resilience" }, -- trigger
+ { id = 59887, name = "Borrowed Time", dur = 6, tree = 1, talent = "Borrowed Time" }, -- trigger
+ { id = 47585, name = "Dispersion", dur = 6, tree = 3, talent = "Dispersion" }, -- class
+ { id = 14743, name = "Focused Casting", dur = 6, tree = 1, talent = "Martyrdom" }, -- trigger
+ },
+ ROGUE = {
+ { id = 57934, name = "Tricks of the Trade", dur = 30 }, -- class
+ { id = 14183, name = "Premeditation", dur = 20, tree = 3, talent = "Premeditation" }, -- class
+ { id = 14143, name = "Remorseless", dur = 20, tree = 1, talent = "Remorseless Attacks" }, -- trigger
+ { id = 13750, name = "Adrenaline Rush", dur = 15, tree = 2, talent = "Adrenaline Rush" }, -- class
+ { id = 13877, name = "Blade Flurry", dur = 15, tree = 2, talent = "Blade Flurry" }, -- class
+ { id = 5277, name = "Evasion", dur = 15 }, -- class
+ { id = 2983, name = "Sprint", dur = 15 }, -- class
+ { id = 11327, name = "Vanish", dur = 10 }, -- trigger
+ { id = 14278, name = "Ghostly Strike", dur = 7, tree = 3, talent = "Ghostly Strike" }, -- class
+ { id = 48659, name = "Feint", dur = 6 }, -- class
+ { id = 51713, name = "Shadow Dance", dur = 6, tree = 3, talent = "Shadow Dance" }, -- class
+ { id = 5171, name = "Slice and Dice", dur = 6 }, -- class
+ { id = 31224, name = "Cloak of Shadows", dur = 5 }, -- class
+ { id = 36554, name = "Shadowstep", dur = 3, tree = 3, talent = "Shadowstep" }, -- class
+ { id = 51690, name = "Killing Spree", dur = 2, tree = 2, talent = "Killing Spree" }, -- class
+ { id = 32645, name = "Envenom", dur = 1 }, -- class
+ { id = 51699, name = "Honor Among Thieves", dur = 1 }, -- class
+ },
+ SHAMAN = {
+ { id = 16166, name = "Elemental Mastery", dur = 30, tree = 1, talent = "Elemental Mastery" }, -- class
+ { id = 53817, name = "Maelstrom Weapon", dur = 30, tree = 2, talent = "Maelstrom Weapon" }, -- trigger
+ { id = 16246, name = "Clearcasting", dur = 15, tree = 1, talent = "Elemental Focus" }, -- trigger
+ { id = 16257, name = "Flurry", dur = 15, tree = 2, talent = "Flurry" }, -- trigger
+ { id = 43339, name = "Focused", dur = 15 }, -- class
+ { id = 30823, name = "Shamanistic Rage", dur = 15, tree = 2, talent = "Shamanistic Rage" }, -- class
+ { id = 53390, name = "Tidal Waves", dur = 15, tree = 3, talent = "Tidal Waves" }, -- trigger
+ { id = 30165, name = "Elemental Devastation", dur = 10, tree = 1, talent = "Elemental Devastation" }, -- trigger
+ { id = 55198, name = "Tidal Force", dur = 2, tree = 3, talent = "Tidal Force" }, -- class
+ },
+ WARLOCK = {
+ { id = 47241, name = "Metamorphosis", dur = 30, tree = 2, talent = "Metamorphosis" }, -- trigger
+ { id = 6229, name = "Shadow Ward", dur = 30 }, -- class
+ { id = 54274, name = "Backdraft", dur = 15, tree = 3, talent = "Backdraft" }, -- trigger
+ { id = 18708, name = "Fel Domination", dur = 15, tree = 2, talent = "Fel Domination" }, -- class
+ { id = 50589, name = "Immolation Aura", dur = 15 }, -- trigger
+ { id = 47383, name = "Molten Core", dur = 15, tree = 2, talent = "Molten Core" }, -- trigger
+ { id = 63165, name = "Decimation", dur = 10, tree = 2, talent = "Decimation" }, -- trigger
+ { id = 64368, name = "Eradication", dur = 10, tree = 1, talent = "Eradication" }, -- trigger
+ { id = 47426, name = "Kindling Soul", dur = 10 }, -- trigger
+ { id = 18093, name = "Pyroclasm", dur = 10, tree = 3, talent = "Pyroclasm" }, -- trigger
+ { id = 53756, name = "Sudden Fear", dur = 10 }, -- class
+ { id = 33151, name = "Surge of Light", dur = 10 }, -- trigger
+ { id = 34936, name = "Backlash", dur = 8, tree = 3, talent = "Backlash" }, -- trigger
+ { id = 48020, name = "Demonic Circle: Teleport", dur = 1 }, -- class
+ },
+ WARRIOR = {
+ { id = 12292, name = "Death Wish", dur = 30, tree = 2, talent = "Death Wish" }, -- class
+ { id = 12328, name = "Sweeping Strikes", dur = 30, tree = 1, talent = "Sweeping Strikes" }, -- class
+ { id = 12976, name = "Last Stand", dur = 20 }, -- class
+ { id = 32216, name = "Victorious", dur = 20 }, -- trigger
+ { id = 12966, name = "Flurry", dur = 15, tree = 2, talent = "Flurry" }, -- trigger
+ { id = 12880, name = "Enrage", dur = 12, tree = 2, talent = "Enrage" }, -- trigger
+ { id = 1719, name = "Recklessness", dur = 12 }, -- class
+ { id = 20230, name = "Retaliation", dur = 12 }, -- class
+ { id = 871, name = "Shield Wall", dur = 12 }, -- class
+ { id = 18499, name = "Berserker Rage", dur = 10 }, -- class
+ { id = 29131, name = "Bloodrage", dur = 10 }, -- trigger
+ { id = 55694, name = "Enraged Regeneration", dur = 10 }, -- class
+ { id = 65156, name = "Juggernaut", dur = 10, tree = 1, talent = "Juggernaut" }, -- trigger
+ { id = 29841, name = "Second Wind", dur = 10 }, -- class
+ { id = 2565, name = "Shield Block", dur = 10 }, -- class
+ { id = 52437, name = "Sudden Death", dur = 10, tree = 1, talent = "Sudden Death" }, -- trigger
+ { id = 60503, name = "Taste for Blood", dur = 9, tree = 1, talent = "Taste for Blood" }, -- trigger
+ { id = 23885, name = "Bloodthirst", dur = 8 }, -- class
+ { id = 46924, name = "Bladestorm", dur = 6, tree = 1, talent = "Bladestorm" }, -- class
+ { id = 16488, name = "Blood Craze", dur = 6, tree = 2, talent = "Blood Craze" }, -- trigger
+ { id = 46916, name = "Slam!", dur = 5, tree = 2, talent = "Bloodsurge" }, -- trigger
+ { id = 23920, name = "Spell Reflection", dur = 5 }, -- class
+ { id = 50227, name = "Sword and Board", dur = 5, tree = 3, talent = "Sword and Board" }, -- trigger
+ },
+}
+
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmSeedWotLK.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmSeedWotLK.lua
new file mode 100644
index 0000000..9d5e7cb
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CdmSeedWotLK.lua
@@ -0,0 +1,773 @@
+-- DragonUI_NewEra/modules/cooldownviewer/CdmSeedWotLK.lua — WotLK (3.3.5a) data seed.
+--
+-- The 3.3.5a counterpart of NewEra's CdmSeedTBC.lua, and structured the same way: ADDITIVE appends
+-- onto the vanilla curated lists in ClassData.lua, which carry the 1.12 staples whose rank-1 ids
+-- still resolve on this client. The two COOLDOWN tables list only genuinely new-to-WotLK abilities —
+-- plus the whole of DEATHKNIGHT, which has no vanilla base at all.
+--
+-- ROTATION_ADD is not bound by that, and is mostly vanilla spells. It exists because the generator
+-- could not previously emit a spell with no cooldown AT ALL — the resolver's castability test is
+-- `cooldown > 1.5s`, so Wrath, Frostbolt, Shred and every other filler resolved to nothing and were
+-- rejected as unresolvable names. Nothing had ever curated them, in either file.
+--
+-- FACTS, NOT GUESSES. Every spell ID below was resolved from THIS CLIENT'S OWN DATA, not typed
+-- from memory:
+-- * Spell.dbc (DBFilesClient\\Spell.dbc, from Data/enUS/patch-enUS-3.MPQ — the
+-- highest-priority readable archive) for name, rank and cooldown
+-- * SkillLineAbility.dbc for class attribution, via each skill line's ClassMask
+-- The generator authors the curation BY NAME and resolves each name to the rank-1 CASTABLE id —
+-- defined as the lowest-rank, lowest-id entry that actually carries a cooldown > 1.5s. That last
+-- filter is what separates a real ability from its triggered sub-spells (e.g. Penance resolves to
+-- 47540, not its 47666/47750 heal/damage triggers; Death Grip to 49576, not 49560/49575).
+--
+-- CAVEAT: two archives in this install (patch-4.MPQ, patch-S.mpq) are encrypted and unreadable, so
+-- if the server overrides a spell's data there, these ids reflect the stock client rather than the
+-- server's edit. Nothing observed suggests it does, but that is the one gap in the sourcing.
+--
+-- Bucketing follows retail's split: Essential = offensive burst / damage / throughput cooldowns,
+-- Utility = defensives, interrupts, CC, escapes and raid cooldowns.
+--
+-- The BuffIcon / BuffBar viewers are deliberately NOT seeded: they auto-track any player buff
+-- <= 120s (see CooldownViewer.lua), which already covers WotLK procs and trinkets, and upstream
+-- removed its long-maintenance-buff seed as clutter.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+if not M then return end
+
+local ESSENTIAL_ADD = {
+ DEATHKNIGHT = {
+ 42650, -- Army of the Dead
+ 49206, -- Summon Gargoyle
+ 49028, -- Dancing Rune Weapon
+ 47568, -- Empower Rune Weapon
+ 49184, -- Howling Blast
+ 43265, -- Death and Decay
+ 45529, -- Blood Tap
+ 46584, -- Raise Dead
+ 57330, -- Horn of Winter
+ },
+ DRUID = {
+ 48505, -- Starfall
+ 50334, -- Berserk
+ 50516, -- Typhoon
+ 48438, -- Wild Growth
+ 18562, -- Swiftmend
+ },
+ HUNTER = {
+ 53301, -- Explosive Shot
+ 53209, -- Chimera Shot
+ 53351, -- Kill Shot
+ 3674, -- Black Arrow
+ 34490, -- Silencing Shot
+ },
+ MAGE = {
+ 44572, -- Deep Freeze
+ 55342, -- Mirror Image
+ 12472, -- Icy Veins
+ 44425, -- Arcane Barrage
+ 11113, -- Blast Wave
+ 31687, -- Summon Water Elemental
+ },
+ PALADIN = {
+ 53385, -- Divine Storm
+ 53595, -- Hammer of the Righteous
+ 53600, -- Shield of Righteousness
+ 31935, -- Avenger's Shield
+ 2812, -- Holy Wrath
+ 20216, -- Divine Favor
+ 31842, -- Divine Illumination
+ 35395, -- Crusader Strike
+ },
+ PRIEST = {
+ 47540, -- Penance
+ 14914, -- Holy Fire
+ 34861, -- Circle of Healing
+ 33076, -- Prayer of Mending
+ 64843, -- Divine Hymn
+ 64901, -- Hymn of Hope
+ },
+ ROGUE = {
+ 51690, -- Killing Spree
+ 51713, -- Shadow Dance
+ 14278, -- Ghostly Strike
+ 57934, -- Tricks of the Trade
+ },
+ SHAMAN = {
+ 51533, -- Feral Spirit
+ 51490, -- Thunderstorm
+ 51505, -- Lava Burst
+ 60103, -- Lava Lash
+ 61295, -- Riptide
+ 32182, -- Heroism
+ 8050, -- Flame Shock
+ },
+ WARLOCK = {
+ 47241, -- Metamorphosis
+ 48181, -- Haunt
+ 50796, -- Chaos Bolt
+ 47897, -- Shadowflame
+ 47193, -- Demonic Empowerment
+ },
+ WARRIOR = {
+ 46924, -- Bladestorm
+ 64382, -- Shattering Throw
+ 12328, -- Sweeping Strikes
+ 57755, -- Heroic Throw
+ 23922, -- Shield Slam
+ 7384, -- Overpower
+ 6572, -- Revenge
+ },
+}
+
+local UTILITY_ADD = {
+ DEATHKNIGHT = {
+ 48792, -- Icebound Fortitude
+ 48707, -- Anti-Magic Shell
+ 51052, -- Anti-Magic Zone
+ 49576, -- Death Grip
+ 47528, -- Mind Freeze
+ 47476, -- Strangulate
+ 48743, -- Death Pact
+ 48982, -- Rune Tap
+ 55233, -- Vampiric Blood
+ 49222, -- Bone Shield
+ 51271, -- Unbreakable Armor
+ 49039, -- Lichborne
+ 49203, -- Hungering Cold
+ 56222, -- Dark Command
+ 61999, -- Raise Ally
+ },
+ DRUID = {
+ 61336, -- Survival Instincts
+ 22812, -- Barkskin
+ 22842, -- Frenzied Regeneration
+ 16689, -- Nature's Grasp
+ 16979, -- Feral Charge - Bear
+ 49376, -- Feral Charge - Cat
+ 22570, -- Maim
+ 5211, -- Bash
+ 20484, -- Rebirth
+ 29166, -- Innervate
+ 1850, -- Dash
+ },
+ HUNTER = {
+ 53271, -- Master's Call
+ 19386, -- Wyvern Sting
+ 60192, -- Freezing Arrow
+ 13813, -- Explosive Trap
+ 13809, -- Frost Trap
+ 1543, -- Flare
+ 19577, -- Intimidation
+ },
+ MAGE = {
+ 6143, -- Frost Ward
+ 543, -- Fire Ward
+ 11958, -- Cold Snap
+ 31661, -- Dragon's Breath
+ 45438, -- Ice Block
+ },
+ PALADIN = {
+ 64205, -- Divine Sacrifice
+ 6940, -- Hand of Sacrifice
+ 1038, -- Hand of Salvation
+ 31821, -- Aura Mastery
+ 54428, -- Divine Plea
+ 20066, -- Repentance
+ 20925, -- Holy Shield
+ 31789, -- Righteous Defense
+ },
+ PRIEST = {
+ 47585, -- Dispersion
+ 33206, -- Pain Suppression
+ 47788, -- Guardian Spirit
+ 64044, -- Psychic Horror
+ },
+ ROGUE = {
+ 51722, -- Dismantle
+ 1966, -- Feint
+ 1725, -- Distract
+ 36554, -- Shadowstep
+ 31224, -- Cloak of Shadows
+ },
+ SHAMAN = {
+ 51514, -- Hex
+ 57994, -- Wind Shear
+ 8177, -- Grounding Totem
+ 2484, -- Earthbind Totem
+ 5730, -- Stoneclaw Totem
+ 16188, -- Nature's Swiftness
+ },
+ WARLOCK = {
+ 48020, -- Demonic Circle: Teleport
+ 29858, -- Soulshatter
+ 18708, -- Fel Domination
+ 6229, -- Shadow Ward
+ },
+ WARRIOR = {
+ 46968, -- Shockwave
+ 55694, -- Enraged Regeneration
+ 60970, -- Heroic Fury
+ 676, -- Disarm
+ 12809, -- Concussion Blow
+ 1161, -- Challenging Shout
+ 20230, -- Retaliation
+ },
+}
+
+-- ROTATION — the spells a spec presses constantly, none of which has a cooldown.
+--
+-- Its own table rather than more rows in ESSENTIAL_ADD, for two reasons. It documents the difference:
+-- everything above is something you WAIT for, and everything here is something that is simply always
+-- there. And it is what lets verify.py keep asserting "every id carries a real cooldown" of the two
+-- lists where that is true, instead of dropping the check for all three.
+--
+-- Appended into ESSENTIAL_BY_CLASS all the same, which is where retail would show them.
+--
+-- PER CLASS, not per spec: the runtime gate is "has the player learned it", and every druid has
+-- learned Wrath. So this is the union across a class's specs and a Feral druid will find Balance's
+-- nukes in the picker — prune per spec in /cdm, which is stored per talent group.
+local ROTATION_ADD = {
+ DEATHKNIGHT = {
+ 45477, -- Icy Touch
+ 45462, -- Plague Strike
+ 45902, -- Blood Strike
+ 55050, -- Heart Strike
+ 49998, -- Death Strike
+ 49020, -- Obliterate
+ 55090, -- Scourge Strike
+ 49143, -- Frost Strike
+ 66217, -- Rune Strike
+ 47541, -- Death Coil
+ 48721, -- Blood Boil
+ 50842, -- Pestilence
+ },
+ DRUID = {
+ 5176, -- Wrath
+ 2912, -- Starfire
+ 8921, -- Moonfire
+ 5570, -- Insect Swarm
+ 5221, -- Shred
+ 33876, -- Mangle (Cat)
+ 1822, -- Rake
+ 1079, -- Rip
+ 22568, -- Ferocious Bite
+ 52610, -- Savage Roar
+ 6807, -- Maul
+ 33745, -- Lacerate
+ 779, -- Swipe (Bear)
+ 5185, -- Healing Touch
+ 50464, -- Nourish
+ 774, -- Rejuvenation
+ 8936, -- Regrowth
+ 33763, -- Lifebloom
+ },
+ HUNTER = {
+ 56641, -- Steady Shot
+ 1978, -- Serpent Sting
+ 1510, -- Volley
+ 1130, -- Hunter's Mark
+ },
+ MAGE = {
+ 133, -- Fireball
+ 116, -- Frostbolt
+ 30451, -- Arcane Blast
+ 5143, -- Arcane Missiles
+ 2948, -- Scorch
+ 44457, -- Living Bomb
+ 30455, -- Ice Lance
+ 44614, -- Frostfire Bolt
+ 11366, -- Pyroblast
+ 1449, -- Arcane Explosion
+ 10, -- Blizzard
+ 2120, -- Flamestrike
+ },
+ PALADIN = {
+ 635, -- Holy Light
+ 19750, -- Flash of Light
+ 20186, -- Judgement of Wisdom
+ 31801, -- Seal of Vengeance
+ 20154, -- Seal of Righteousness
+ 20375, -- Seal of Command
+ },
+ PRIEST = {
+ 585, -- Smite
+ 589, -- Shadow Word: Pain
+ 15407, -- Mind Flay
+ 34914, -- Vampiric Touch
+ 48045, -- Mind Sear
+ 2061, -- Flash Heal
+ 2060, -- Greater Heal
+ 139, -- Renew
+ 596, -- Prayer of Healing
+ },
+ ROGUE = {
+ 1752, -- Sinister Strike
+ 53, -- Backstab
+ 1329, -- Mutilate
+ 16511, -- Hemorrhage
+ 2098, -- Eviscerate
+ 32645, -- Envenom
+ 1943, -- Rupture
+ 5171, -- Slice and Dice
+ 8647, -- Expose Armor
+ 51723, -- Fan of Knives
+ },
+ SHAMAN = {
+ 403, -- Lightning Bolt
+ 331, -- Healing Wave
+ 8004, -- Lesser Healing Wave
+ 1064, -- Chain Heal
+ 974, -- Earth Shield
+ 3599, -- Searing Totem
+ 8190, -- Magma Totem
+ },
+ WARLOCK = {
+ 686, -- Shadow Bolt
+ 29722, -- Incinerate
+ 348, -- Immolate
+ 172, -- Corruption
+ 980, -- Curse of Agony
+ 30108, -- Unstable Affliction
+ 27243, -- Seed of Corruption
+ 1120, -- Drain Soul
+ 689, -- Drain Life
+ 1454, -- Life Tap
+ 5740, -- Rain of Fire
+ },
+ WARRIOR = {
+ 78, -- Heroic Strike
+ 845, -- Cleave
+ 1464, -- Slam
+ 5308, -- Execute
+ 772, -- Rend
+ 7386, -- Sunder Armor
+ 20243, -- Devastate
+ 34428, -- Victory Rush
+ 1715, -- Hamstring
+ },
+}
+
+local STARTER_BY_CLASS = {
+ DEATHKNIGHT = {
+ [1] = {
+ 45477, -- Icy Touch
+ 45462, -- Plague Strike
+ 55050, -- Heart Strike
+ 49998, -- Death Strike
+ 66217, -- Rune Strike
+ 47541, -- Death Coil
+ 48721, -- Blood Boil
+ 50842, -- Pestilence
+ 49028, -- Dancing Rune Weapon
+ 57330, -- Horn of Winter
+ },
+ [2] = {
+ 45477, -- Icy Touch
+ 45462, -- Plague Strike
+ 49020, -- Obliterate
+ 49143, -- Frost Strike
+ 49184, -- Howling Blast
+ 45902, -- Blood Strike
+ 50842, -- Pestilence
+ 47568, -- Empower Rune Weapon
+ 57330, -- Horn of Winter
+ },
+ [3] = {
+ 45477, -- Icy Touch
+ 45462, -- Plague Strike
+ 55090, -- Scourge Strike
+ 45902, -- Blood Strike
+ 47541, -- Death Coil
+ 43265, -- Death and Decay
+ 50842, -- Pestilence
+ 49206, -- Summon Gargoyle
+ 46584, -- Raise Dead
+ 57330, -- Horn of Winter
+ },
+ },
+ DRUID = {
+ [1] = {
+ 5176, -- Wrath
+ 2912, -- Starfire
+ 8921, -- Moonfire
+ 5570, -- Insect Swarm
+ 48505, -- Starfall
+ 50516, -- Typhoon
+ 16914, -- Hurricane
+ 770, -- Faerie Fire (Balance)
+ },
+ [2] = {
+ 5221, -- Shred
+ 33876, -- Mangle (Cat)
+ 1822, -- Rake
+ 1079, -- Rip
+ 22568, -- Ferocious Bite
+ 52610, -- Savage Roar
+ 5217, -- Tiger's Fury
+ 6807, -- Maul
+ 33745, -- Lacerate
+ 779, -- Swipe (Bear)
+ 50334, -- Berserk
+ 16857, -- Faerie Fire (Feral)
+ },
+ [3] = {
+ 5185, -- Healing Touch
+ 50464, -- Nourish
+ 774, -- Rejuvenation
+ 8936, -- Regrowth
+ 33763, -- Lifebloom
+ 48438, -- Wild Growth
+ 18562, -- Swiftmend
+ 17116, -- Nature's Swiftness
+ },
+ },
+ HUNTER = {
+ [1] = {
+ 56641, -- Steady Shot
+ 3044, -- Arcane Shot
+ 1978, -- Serpent Sting
+ 14288, -- Multi-Shot
+ 53351, -- Kill Shot
+ 19574, -- Bestial Wrath
+ 3045, -- Rapid Fire
+ 1130, -- Hunter's Mark
+ },
+ [2] = {
+ 56641, -- Steady Shot
+ 3044, -- Arcane Shot
+ 1978, -- Serpent Sting
+ 53209, -- Chimera Shot
+ 19434, -- Aimed Shot
+ 14288, -- Multi-Shot
+ 53351, -- Kill Shot
+ 34490, -- Silencing Shot
+ 3045, -- Rapid Fire
+ 1130, -- Hunter's Mark
+ },
+ [3] = {
+ 56641, -- Steady Shot
+ 53301, -- Explosive Shot
+ 1978, -- Serpent Sting
+ 3674, -- Black Arrow
+ 3044, -- Arcane Shot
+ 14288, -- Multi-Shot
+ 53351, -- Kill Shot
+ 3045, -- Rapid Fire
+ 1130, -- Hunter's Mark
+ },
+ },
+ MAGE = {
+ [1] = {
+ 30451, -- Arcane Blast
+ 5143, -- Arcane Missiles
+ 44425, -- Arcane Barrage
+ 1449, -- Arcane Explosion
+ 12043, -- Presence of Mind
+ 12042, -- Arcane Power
+ 55342, -- Mirror Image
+ 2136, -- Fire Blast
+ },
+ [2] = {
+ 133, -- Fireball
+ 2948, -- Scorch
+ 11366, -- Pyroblast
+ 44457, -- Living Bomb
+ 2120, -- Flamestrike
+ 11113, -- Blast Wave
+ 11129, -- Combustion
+ 2136, -- Fire Blast
+ 55342, -- Mirror Image
+ 44614, -- Frostfire Bolt
+ },
+ [3] = {
+ 116, -- Frostbolt
+ 30455, -- Ice Lance
+ 44572, -- Deep Freeze
+ 10, -- Blizzard
+ 44614, -- Frostfire Bolt
+ 12472, -- Icy Veins
+ 31687, -- Summon Water Elemental
+ 2136, -- Fire Blast
+ },
+ },
+ PALADIN = {
+ [1] = {
+ 635, -- Holy Light
+ 19750, -- Flash of Light
+ 20473, -- Holy Shock
+ 20216, -- Divine Favor
+ 31842, -- Divine Illumination
+ 20271, -- Judgement of Light
+ 20186, -- Judgement of Wisdom
+ 26573, -- Consecration
+ 879, -- Exorcism
+ },
+ [2] = {
+ 31935, -- Avenger's Shield
+ 53595, -- Hammer of the Righteous
+ 53600, -- Shield of Righteousness
+ 26573, -- Consecration
+ 2812, -- Holy Wrath
+ 20271, -- Judgement of Light
+ 20186, -- Judgement of Wisdom
+ 879, -- Exorcism
+ 24275, -- Hammer of Wrath
+ },
+ [3] = {
+ 35395, -- Crusader Strike
+ 53385, -- Divine Storm
+ 20186, -- Judgement of Wisdom
+ 20271, -- Judgement of Light
+ 26573, -- Consecration
+ 879, -- Exorcism
+ 24275, -- Hammer of Wrath
+ 20375, -- Seal of Command
+ 31801, -- Seal of Vengeance
+ 20154, -- Seal of Righteousness
+ },
+ },
+ PRIEST = {
+ [1] = {
+ 47540, -- Penance
+ 2061, -- Flash Heal
+ 2060, -- Greater Heal
+ 139, -- Renew
+ 596, -- Prayer of Healing
+ 33076, -- Prayer of Mending
+ 10060, -- Power Infusion
+ 14751, -- Inner Focus
+ 585, -- Smite
+ 14914, -- Holy Fire
+ },
+ [2] = {
+ 2061, -- Flash Heal
+ 2060, -- Greater Heal
+ 139, -- Renew
+ 596, -- Prayer of Healing
+ 33076, -- Prayer of Mending
+ 34861, -- Circle of Healing
+ 64843, -- Divine Hymn
+ 64901, -- Hymn of Hope
+ 14914, -- Holy Fire
+ 585, -- Smite
+ },
+ [3] = {
+ 8092, -- Mind Blast
+ 15407, -- Mind Flay
+ 589, -- Shadow Word: Pain
+ 34914, -- Vampiric Touch
+ 2944, -- Devouring Plague
+ 48045, -- Mind Sear
+ },
+ },
+ ROGUE = {
+ [1] = {
+ 1329, -- Mutilate
+ 32645, -- Envenom
+ 1943, -- Rupture
+ 5171, -- Slice and Dice
+ 2098, -- Eviscerate
+ 14177, -- Cold Blood
+ 8647, -- Expose Armor
+ 51723, -- Fan of Knives
+ 57934, -- Tricks of the Trade
+ },
+ [2] = {
+ 1752, -- Sinister Strike
+ 2098, -- Eviscerate
+ 5171, -- Slice and Dice
+ 1943, -- Rupture
+ 13750, -- Adrenaline Rush
+ 13877, -- Blade Flurry
+ 51690, -- Killing Spree
+ 51723, -- Fan of Knives
+ 11305, -- Sprint
+ 57934, -- Tricks of the Trade
+ },
+ [3] = {
+ 53, -- Backstab
+ 16511, -- Hemorrhage
+ 2098, -- Eviscerate
+ 5171, -- Slice and Dice
+ 1943, -- Rupture
+ 51713, -- Shadow Dance
+ 14183, -- Premeditation
+ 14278, -- Ghostly Strike
+ 51723, -- Fan of Knives
+ 57934, -- Tricks of the Trade
+ },
+ },
+ SHAMAN = {
+ [1] = {
+ 403, -- Lightning Bolt
+ 421, -- Chain Lightning
+ 51505, -- Lava Burst
+ 8050, -- Flame Shock
+ 8042, -- Earth Shock
+ 16166, -- Elemental Mastery
+ 51490, -- Thunderstorm
+ 8190, -- Magma Totem
+ 3599, -- Searing Totem
+ 32182, -- Heroism
+ 2825, -- Bloodlust
+ },
+ [2] = {
+ 17364, -- Stormstrike
+ 60103, -- Lava Lash
+ 403, -- Lightning Bolt
+ 8050, -- Flame Shock
+ 8042, -- Earth Shock
+ 51533, -- Feral Spirit
+ 8190, -- Magma Totem
+ 3599, -- Searing Totem
+ 32182, -- Heroism
+ 2825, -- Bloodlust
+ },
+ [3] = {
+ 331, -- Healing Wave
+ 8004, -- Lesser Healing Wave
+ 1064, -- Chain Heal
+ 61295, -- Riptide
+ 974, -- Earth Shield
+ 403, -- Lightning Bolt
+ 8050, -- Flame Shock
+ 32182, -- Heroism
+ 2825, -- Bloodlust
+ },
+ },
+ WARLOCK = {
+ [1] = {
+ 172, -- Corruption
+ 980, -- Curse of Agony
+ 30108, -- Unstable Affliction
+ 48181, -- Haunt
+ 686, -- Shadow Bolt
+ 1120, -- Drain Soul
+ 689, -- Drain Life
+ 1454, -- Life Tap
+ 27243, -- Seed of Corruption
+ 603, -- Curse of Doom
+ },
+ [2] = {
+ 686, -- Shadow Bolt
+ 348, -- Immolate
+ 172, -- Corruption
+ 980, -- Curse of Agony
+ 47241, -- Metamorphosis
+ 47193, -- Demonic Empowerment
+ 6353, -- Soul Fire
+ 1454, -- Life Tap
+ 47897, -- Shadowflame
+ },
+ [3] = {
+ 29722, -- Incinerate
+ 348, -- Immolate
+ 17962, -- Conflagrate
+ 50796, -- Chaos Bolt
+ 686, -- Shadow Bolt
+ 17877, -- Shadowburn
+ 603, -- Curse of Doom
+ 5740, -- Rain of Fire
+ 1454, -- Life Tap
+ 47897, -- Shadowflame
+ },
+ },
+ WARRIOR = {
+ [1] = {
+ 12294, -- Mortal Strike
+ 7384, -- Overpower
+ 1464, -- Slam
+ 5308, -- Execute
+ 772, -- Rend
+ 46924, -- Bladestorm
+ 12328, -- Sweeping Strikes
+ 78, -- Heroic Strike
+ 64382, -- Shattering Throw
+ 2687, -- Bloodrage
+ },
+ [2] = {
+ 23881, -- Bloodthirst
+ 78, -- Heroic Strike
+ 845, -- Cleave
+ 5308, -- Execute
+ 1464, -- Slam
+ 12292, -- Death Wish
+ 1719, -- Recklessness
+ 34428, -- Victory Rush
+ 2687, -- Bloodrage
+ },
+ [3] = {
+ 23922, -- Shield Slam
+ 6572, -- Revenge
+ 20243, -- Devastate
+ 78, -- Heroic Strike
+ 845, -- Cleave
+ 7386, -- Sunder Armor
+ 57755, -- Heroic Throw
+ 34428, -- Victory Rush
+ 2687, -- Bloodrage
+ },
+ },
+}
+
+-- Append (deduped) into the existing per-class arrays. A class with no vanilla entry — i.e.
+-- DEATHKNIGHT — gets a fresh list rather than being skipped.
+local function appendAll(target, byClass)
+ if not target then return end
+ for class, adds in pairs(byClass) do
+ local list = target[class]
+ if not list then list = {}; target[class] = list end
+ local seen = {}
+ for _, id in ipairs(list) do seen[id] = true end
+ for _, id in ipairs(adds) do
+ if not seen[id] then seen[id] = true; list[#list + 1] = id end
+ end
+ end
+end
+
+appendAll(M.ESSENTIAL_BY_CLASS, ESSENTIAL_ADD)
+appendAll(M.UTILITY_BY_CLASS, UTILITY_ADD)
+appendAll(M.ESSENTIAL_BY_CLASS, ROTATION_ADD)
+
+-- The same ids again, flattened into a set, because ONE consumer needs to ask the question the other
+-- direction: given a spell, does it have a real cooldown at all?
+--
+-- ItemMixin:ConsumeReadyTransition — the edge the `available` alert and the ready sound hang off — is
+-- defined as "was on a REAL cooldown, now is not", and a real cooldown explicitly excludes the GCD
+-- and cast lockouts. For everything in the two COOLDOWN tables that is exactly right: a GCD flash on
+-- Starfall would be noise, because the event worth hearing about is the 90 seconds ending.
+--
+-- For a ROTATION spell it makes the edge unreachable. Wrath has no cooldown, so IsOnRealCooldown is
+-- false forever, `_wasOnRealCD` is never armed, and the transition never fires — the "available alert
+-- does nothing on the new filler spells" report. `usable` was unaffected because it polls
+-- IsUsableSpell rather than waiting on an edge, which is why one worked and the other did not.
+--
+-- Flat rather than per-class: an id is unique to one class already, and the lookup happens on a tile
+-- that has long since stopped caring which class list it came from.
+--
+-- Membership is a statement about the SPELL, and every entry here was verified cooldown-less against
+-- Spell.dbc by the same generator that authored the lists. It is not a runtime guess: "have we seen
+-- a real cooldown on this yet" would misfire for every long cooldown the player has not pressed this
+-- session, flashing a 2-minute ability at the end of an unrelated global.
+M.GCD_ONLY_SPELLS = M.GCD_ONLY_SPELLS or {}
+for _, ids in pairs(ROTATION_ADD) do
+ for _, id in ipairs(ids) do M.GCD_ONLY_SPELLS[id] = true end
+end
+
+-- STARTER LAYOUTS, per class and talent tab. Not appended anywhere: this is a lookup the runtime
+-- reads when a starter is applied (M.ApplySpecStarter), listing which of the class's Essential spells
+-- stay ON. Everything else in the list goes to Not Displayed rather than being deleted, so it is one
+-- drag away in the picker.
+--
+-- Tab INDEX is the key because that is what GetTalentTabInfo answers. Order is from TalentTab.dbc:
+-- 1/2/3 = Arms/Fury/Protection, Holy/Protection/Retribution, Beast Mastery/Marksmanship/Survival,
+-- Assassination/Combat/Subtlety, Discipline/Holy/Shadow, Blood/Frost/Unholy,
+-- Elemental/Enhancement/Restoration, Arcane/Fire/Frost, Affliction/Demonology/Destruction,
+-- Balance/Feral Combat/Restoration.
+M.STARTER_BY_CLASS = STARTER_BY_CLASS
+
+-- SPELL_DATA_BY_CATEGORY holds references to the same tables, so the appends above are already
+-- visible through it. The learn-gate memoises the curated set on first use, though, so drop that
+-- cache now that the tables have grown.
+if M.InvalidateCuratedCache then M.InvalidateCuratedCache() end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/ClassData.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/ClassData.lua
new file mode 100644
index 0000000..9b97f6d
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/ClassData.lua
@@ -0,0 +1,419 @@
+-- DragonUI_NewEra/modules/cooldownviewer/ClassData.lua — curated per-class cooldown data.
+--
+-- DOWNPORT of NewEra/CooldownViewer/ClassData.lua. The BODY of this file is copied byte-for-byte
+-- from the 1.15 source (mechanically, not retyped — a transcription slip here is a silently wrong
+-- spell). Only the namespace preamble is rewritten for the DragonUI_NewEra addon table.
+--
+-- FLAVOUR NOTE: these are the VANILLA 1.15 curated lists. Rank-1 spell IDs are stable across
+-- expansions, so they resolve correctly on 3.3.5a — but they are not COMPLETE for WotLK: Death
+-- Knight is absent entirely, and every other class has 3.3.5a abilities missing (Bladestorm,
+-- Penance, Deep Freeze, Killing Spree, ...). CdmSeedWotLK.lua (Phase 2) appends those additively,
+-- exactly as CdmSeedTBC.lua does for TBC upstream. Until it lands, expect a sparse but correct set.
+--
+-- Tables provided: M.ESSENTIAL_BY_CLASS / UTILITY_BY_CLASS / BUFFICON_BY_CLASS / BUFFBAR_BY_CLASS
+-- / SPELL_DATA_BY_CATEGORY (the category->table map) / RACIAL_BY_RACE.
+
+local NE = DragonUI_NewEra
+NE.cooldownviewer = NE.cooldownviewer or {}
+local M = NE.cooldownviewer
+-- Vanilla 1.15.x per-class Essential cooldown data.
+--
+-- Retail's Essential bucket = OFFENSIVE moves: offensive buffs, damage burst
+-- cooldowns, offensive procs/passives, on-CD damage abilities. Anything that
+-- contributes to the player's burst window or sustained damage output.
+-- Confirmed by user observation 2026-05-28: large icons = offensive, small
+-- icons = defensive/CC/utility.
+M.ESSENTIAL_BY_CLASS = {
+ WARRIOR = {
+ 1719, -- Recklessness (offensive burst)
+ 12292, -- Death Wish (Fury 31pt burst)
+ 23881, -- Bloodthirst (Fury 31pt)
+ 12294, -- Mortal Strike (Arms 31pt)
+ 2687, -- Bloodrage (rage gen, 1min)
+ },
+ PALADIN = {
+ 24275, -- Hammer of Wrath (Ret execute)
+ 20473, -- Holy Shock (Holy 31pt)
+ 20271, -- Judgement (main Ret damage on CD)
+ 879, -- Exorcism (holy damage, 15s)
+ 26573, -- Consecration (AoE, 8s)
+ },
+ HUNTER = {
+ 19574, -- Bestial Wrath (BM 31pt burst)
+ 3045, -- Rapid Fire (offensive haste)
+ 19434, -- Aimed Shot (channeled damage CD)
+ 14288, -- Multi-Shot (AoE CD)
+ 3044, -- Arcane Shot (instant, 6s)
+ },
+ ROGUE = {
+ 13750, -- Adrenaline Rush (Combat 31pt)
+ 13877, -- Blade Flurry (Combat cleave)
+ 14177, -- Cold Blood (Assa next-crit)
+ 14183, -- Premeditation (Sub opener)
+ 11305, -- Sprint (positioning)
+ },
+ PRIEST = {
+ 8092, -- Mind Blast (8s)
+ 10060, -- Power Infusion (Disc 31pt, 3min)
+ 14751, -- Inner Focus (Disc 15pt, 3min)
+ 2944, -- Devouring Plague (Undead, 3min)
+ },
+ MAGE = {
+ 11129, -- Combustion (Fire 31pt)
+ -- 12472 was here labelled "Cold Snap" and is Icy Veins; 11958 sits in Utility below labelled
+ -- "Ice Block" and is Cold Snap. Two wrong labels that between them left mages with no Ice Block
+ -- at all. Both abilities are curated by name in tools/cdm-spellgen/gen_wotlk.py, which resolves
+ -- them from Spell.dbc, so they are dropped here rather than relabelled — one home per ability.
+ 12043, -- Presence of Mind (Arcane 30pt)
+ 12042, -- Arcane Power (Arcane 31pt)
+ 2136, -- Fire Blast (instant, 8s)
+ },
+ WARLOCK = {
+ 6789, -- Death Coil (fear + drain)
+ 17962, -- Conflagrate (Destro 31pt)
+ 6353, -- Soul Fire (big nuke, 1min)
+ 17877, -- Shadowburn (execute, 15s)
+ 603, -- Curse of Doom (60s DoT)
+ },
+ DRUID = {
+ 770, -- Faerie Fire (Balance)
+ 16857, -- Faerie Fire (Feral) armor debuff
+ 17116, -- Nature's Swiftness (Resto 30pt)
+ 16914, -- Hurricane (Balance 30pt AoE)
+ 5217, -- Tiger's Fury (Cat damage buff)
+ },
+ SHAMAN = {
+ 2825, -- Bloodlust (Enhance 31pt haste)
+ 16166, -- Elemental Mastery (Ele 31pt)
+ 17364, -- Stormstrike (Enhance 31pt)
+ 8042, -- Earth Shock (interrupt/damage)
+ 421, -- Chain Lightning (6s)
+ },
+}
+
+-- Vanilla 1.15.x per-class Utility cooldown data.
+--
+-- Retail's Utility bucket = DEFENSIVE buffs, CC, interrupts, escapes, dispels,
+-- and minor utility. Anything that doesn't directly contribute to damage but
+-- mitigates damage, controls enemies, or repositions. Smaller 30×30 icons.
+M.UTILITY_BY_CLASS = {
+ WARRIOR = {
+ 871, -- Shield Wall (defensive)
+ 12975, -- Last Stand (defensive)
+ 23920, -- Spell Reflection
+ 6552, -- Pummel (interrupt)
+ 72, -- Shield Bash (interrupt)
+ 5246, -- Intimidating Shout (fear CC)
+ 18499, -- Berserker Rage (CC immune)
+ 100, -- Charge (mobility, 15s)
+ },
+ PALADIN = {
+ 633, -- Lay on Hands (panic)
+ 642, -- Divine Shield (immunity)
+ 498, -- Divine Protection
+ 1022, -- Blessing of Protection
+ 10308, -- Hammer of Justice (stun)
+ 1044, -- Blessing of Freedom
+ 4987, -- Cleanse (dispel)
+ 19752, -- Divine Intervention
+ },
+ HUNTER = {
+ 5384, -- Feign Death
+ 19263, -- Deterrence (Survival defensive)
+ 1499, -- Freezing Trap (CC)
+ 19503, -- Scatter Shot (CC)
+ 5116, -- Concussive Shot (slow)
+ 19801, -- Tranquilizing Shot (dispel)
+ 781, -- Disengage (threat drop)
+ 1513, -- Scare Beast (fear beast)
+ },
+ ROGUE = {
+ 5277, -- Evasion (defensive)
+ 2094, -- Blind (CC)
+ 408, -- Kidney Shot (stun)
+ 1776, -- Gouge (CC)
+ 1766, -- Kick (interrupt)
+ 6770, -- Sap (CC)
+ 14185, -- Preparation (Sub 31pt reset)
+ 1856, -- Vanish (escape)
+ },
+ PRIEST = {
+ 17, -- Power Word: Shield (Weakened Soul 15s)
+ 15487, -- Silence (Shadow 31pt)
+ 586, -- Fade (30s)
+ 8122, -- Psychic Scream (fear)
+ 6346, -- Fear Ward (Dwarf priest racial)
+ 19236, -- Desperate Prayer (Human/Dwarf priest racial)
+ 15286, -- Vampiric Embrace (sustain)
+ 724, -- Lightwell (Holy 31pt)
+ },
+ MAGE = {
+ -- 11958 was here labelled "Ice Block" and is Cold Snap — see the note in Essential above. Both,
+ -- plus the Ice Block that was missing entirely, are curated in gen_wotlk.py.
+ 11426, -- Ice Barrier (shield)
+ 1953, -- Blink (escape)
+ 122, -- Frost Nova (root)
+ 2139, -- Counterspell (interrupt)
+ 12051, -- Evocation (mana)
+ 118, -- Polymorph (CC)
+ 120, -- Cone of Cold (slow)
+ },
+ WARLOCK = {
+ 5782, -- Fear (CC)
+ 5484, -- Howl of Terror (AoE fear)
+ 710, -- Banish (CC)
+ 19647, -- Spell Lock (interrupt, Felhunter)
+ 693, -- Soulstone (battle rez)
+ 6229, -- Shadow Ward (absorb)
+ 1714, -- Curse of Tongues (cast slow)
+ 18788, -- Demonic Sacrifice
+ },
+ DRUID = {
+ 22812, -- Barkskin (defensive)
+ 5211, -- Bash (Bear stun)
+ 339, -- Entangling Roots (root)
+ 29166, -- Innervate (mana)
+ 740, -- Tranquility (AoE heal)
+ 20484, -- Rebirth (combat rez)
+ 1850, -- Dash (movement)
+ 22842, -- Frenzied Regeneration (Bear defensive)
+ },
+ SHAMAN = {
+ 16188, -- Nature's Swiftness (Resto 30pt)
+ 20608, -- Reincarnation (ankh)
+ 8177, -- Grounding Totem
+ 8143, -- Tremor Totem (fear/sleep)
+ 370, -- Purge (dispel)
+ 2484, -- Earthbind Totem (slow)
+ 16190, -- Mana Tide Totem (Resto mana)
+ 5730, -- Stoneclaw Totem (taunt)
+ },
+}
+
+-- Vanilla 1.15.x per-class BuffIcon data.
+--
+-- Self-buffs / debuffs the player wants visible. Tracked via UNIT_AURA on the
+-- player unit; the item shows icon + remaining time (REVERSE Cooldown swipe)
+-- + stack count. Spellbook IDs match the buff aura's spell ID (which is
+-- usually the same as the cast ID for most vanilla spells).
+M.BUFFICON_BY_CLASS = {
+ WARRIOR = {
+ 25289, -- Battle Shout rank 8 (placeholder — vanilla has rank 7 = 11551)
+ 1719, -- Recklessness (also tracked as a buff while active)
+ 12292, -- Death Wish (active buff)
+ 18499, -- Berserker Rage (active buff)
+ 871, -- Shield Wall (active buff)
+ 20230, -- Retaliation
+ },
+ PALADIN = {
+ 25291, -- Blessing of Might rank 8 (placeholder)
+ 25290, -- Blessing of Wisdom rank 6
+ 25895, -- Greater Blessing of Wisdom rank 1 (raid 40min)
+ 642, -- Divine Shield (active buff)
+ 498, -- Divine Protection (active buff)
+ 20925, -- Holy Shield
+ },
+ HUNTER = {
+ 13165, -- Aspect of the Hawk rank 7
+ 5118, -- Aspect of the Cheetah
+ 20043, -- Aspect of the Wild rank 3
+ 19574, -- Bestial Wrath (active buff)
+ 19506, -- Trueshot Aura (Survival 31pt? — actually Marksmanship)
+ 3045, -- Rapid Fire (active buff while casting)
+ },
+ ROGUE = {
+ 6774, -- Slice and Dice rank 2
+ 5277, -- Evasion (active buff)
+ 13877, -- Blade Flurry (active buff)
+ 13750, -- Adrenaline Rush (active buff)
+ 14177, -- Cold Blood (next ability crit)
+ 1856, -- Vanish (active buff during invis)
+ },
+ PRIEST = {
+ -- BuffIcon = TrackedBuff in retail: player self-auras worth visualizing.
+ -- Removed: 13128 Renew (cast on others — won't appear on player), 25217 PW:Shield
+ -- (tracked via Weakened Soul in Utility — duplicate), 25431 Prayer of Healing (cast on others).
+ -- All entries here MUST be auras that appear on the PLAYER (UnitBuff/UnitDebuff).
+ 15473, -- Shadowform (Shadow toggle — visible as long as in Shadowform)
+ 14751, -- Inner Focus (15s buff after activation, Disc 15pt)
+ 10060, -- Power Infusion (15s damage/haste buff, Disc 31pt)
+ },
+ MAGE = {
+ 23028, -- Arcane Intellect (Greater) rank 5
+ 25304, -- Frostbolt rank 11 — not a buff! Need to revisit
+ 11129, -- Combustion (active buff)
+ 11958, -- Ice Block (active buff)
+ 6117, -- Mage Armor / Frost Armor rank 1
+ 7302, -- Frost Armor rank 1
+ },
+ WARLOCK = {
+ 25311, -- Soul Link aura ID (placeholder)
+ 687, -- Demon Skin
+ 696, -- Demon Armor rank 5 placeholder
+ 755, -- Health Funnel
+ 18708, -- Fel Domination
+ 1454, -- Life Tap
+ },
+ DRUID = {
+ 21849, -- Gift of the Wild rank 1
+ 9846, -- Mark of the Wild rank 7
+ 9492, -- Cat Form
+ 9634, -- Dire Bear Form
+ 22812, -- Barkskin (active buff)
+ 1066, -- Aquatic Form
+ },
+ SHAMAN = {
+ 25361, -- Strength of Earth Totem rank 4 buff
+ 25420, -- Grace of Air Totem rank 3 buff
+ 324, -- Lightning Shield rank 1
+ 2825, -- Bloodlust active buff
+ 8178, -- Grounding Totem effect
+ 20608, -- Reincarnation passive
+ },
+}
+
+-- Vanilla 1.15.x per-class BuffBar data.
+--
+-- Long-duration tracked auras displayed as horizontal status bars (icon +
+-- name + remaining time). Typical entries are 10+ minute consumables /
+-- buffs / resurrection prompts that the user wants persistent visibility on.
+-- Vanilla examples: Soulstone, Slow Fall, Bandage, Mage food, etc.
+M.BUFFBAR_BY_CLASS = {
+ WARRIOR = {
+ 23397, -- Berserker Stance buff (passive — placeholder)
+ 7384, -- Overpower buffed
+ 12328, -- Sweeping Strikes
+ },
+ PALADIN = {
+ 25292, -- Holy Light rank 11 (placeholder)
+ 20473, -- Holy Shock (Holy talent)
+ 25895, -- Greater Blessing of Wisdom (40min)
+ 25890, -- Greater Blessing of Sanctuary
+ },
+ HUNTER = {
+ 20043, -- Aspect of the Wild (long buff)
+ 13165, -- Aspect of the Hawk
+ 13161, -- Aspect of the Beast
+ },
+ ROGUE = {
+ 6774, -- Slice and Dice (combo finisher duration tracking)
+ },
+ PRIEST = {
+ -- BuffBar = TrackedBar in retail: long-duration auras visualized as bars.
+ -- Removed: 21849 Gift of the Wild (druid spell, not priest's buff).
+ -- All entries here MUST be auras that appear on the PLAYER.
+ 21562, -- Prayer of Fortitude (1hr raid stam buff)
+ 27681, -- Prayer of Spirit (1hr raid spirit buff)
+ 27683, -- Prayer of Shadow Protection (1hr raid shadow res buff)
+ 1243, -- Power Word: Fortitude rank 1 (30min self-cast stam buff)
+ },
+ MAGE = {
+ 23028, -- Arcane Brilliance (1hr)
+ 1463, -- Mana Shield
+ 11129, -- Combustion
+ 7301, -- Frost Armor — long
+ },
+ WARLOCK = {
+ 20707, -- Soulstone resurrection (30min)
+ 693, -- Soulstone cooldown
+ 25311, -- Soul Link aura
+ },
+ DRUID = {
+ 21849, -- Gift of the Wild (1hr raid buff)
+ 9885, -- Regrowth rank 8
+ 17329, -- Thorns rank 6
+ 8936, -- Regrowth rank 1 — HoT
+ },
+ SHAMAN = {
+ 16177, -- Ancestral Healing
+ 10623, -- Strength of Earth Totem buff
+ 20577, -- Cannibalize
+ 16191, -- Mana Tide Totem effect
+ },
+}
+
+M.SPELL_DATA_BY_CATEGORY = {
+ essential = M.ESSENTIAL_BY_CLASS,
+ utility = M.UTILITY_BY_CLASS,
+ buffIcon = M.BUFFICON_BY_CLASS,
+ buffBar = M.BUFFBAR_BY_CLASS,
+}
+
+-- Vanilla 1.15.x racial cooldowns. Merged into the player's category list at
+-- runtime in GetActiveSpellList based on race. Vanilla racials are class-
+-- agnostic so they live in their own table keyed by race ("Dwarf", "Orc", ...
+-- as returned by UnitRace).
+--
+-- Classifications mirror the per-class rules:
+-- essential = offensive (Blood Fury, Berserking)
+-- utility = defensive / CC / escapes (Stoneform, War Stomp, Will of the Forsaken)
+-- buffIcon = self-aura toggles (Shadowmeld stealth state, Berserking active buff)
+-- NOTE: keys are the canonical race FILE name returned by UnitRace() second
+-- return value, NOT the localized display name:
+-- Night Elf → "NightElf" (no space)
+-- Undead → "Scourge" (in-game faction name returned by UnitRace)
+M.RACIAL_BY_RACE = {
+ Human = {
+ -- Perception (long CD, situational stealth-detect) — omit as not worth a slot.
+ essential = {},
+ utility = {},
+ buffIcon = {},
+ buffBar = {},
+ },
+ Dwarf = {
+ essential = {},
+ utility = { 20594 }, -- Stoneform (3min CD, immune bleed/poison/disease, +armor)
+ buffIcon = { 20594 }, -- Stoneform active buff
+ buffBar = {},
+ },
+ NightElf = {
+ essential = {},
+ utility = { 20580 }, -- Shadowmeld (10s CD, conditional stealth)
+ buffIcon = {},
+ buffBar = {},
+ },
+ Gnome = {
+ essential = {},
+ utility = { 20589 }, -- Escape Artist (1.5min CD, root/snare break)
+ buffIcon = {},
+ buffBar = {},
+ },
+ Draenei = {
+ essential = {},
+ utility = { 28880 }, -- Gift of the Naaru (1.5min CD, heal)
+ buffIcon = {},
+ buffBar = {},
+ },
+ Orc = {
+ essential = { 20572 }, -- Blood Fury (2min CD, +AP/SP burst)
+ utility = {},
+ buffIcon = { 20572 }, -- Blood Fury active buff (15s)
+ buffBar = {},
+ },
+ Tauren = {
+ essential = {},
+ utility = { 20549 }, -- War Stomp (2min CD, 2s AoE stun)
+ buffIcon = {},
+ buffBar = {},
+ },
+ Troll = {
+ essential = { 26297 }, -- Berserking (3min CD, haste based on missing HP)
+ utility = {},
+ buffIcon = { 26297 }, -- Berserking active buff (10s)
+ buffBar = {},
+ },
+ Scourge = {
+ -- UnitRace returns "Scourge" for Undead — the in-game faction name.
+ essential = {},
+ utility = { 7744, 20577 }, -- Will of the Forsaken (5min CD), Cannibalize (2min CD)
+ buffIcon = {},
+ buffBar = {},
+ },
+ BloodElf = {
+ -- UnitRace returns "BloodElf" for Blood Elves — the canonical race file name.
+ essential = {},
+ utility = { 28730 }, -- Arcane Torrent (2min CD, silence + mana restore)
+ buffIcon = {},
+ buffBar = {},
+ },
+}
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CooldownViewer.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CooldownViewer.lua
new file mode 100644
index 0000000..1094435
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/CooldownViewer.lua
@@ -0,0 +1,1396 @@
+-- DragonUI_NewEra/modules/cooldownviewer/CooldownViewer.lua — Cooldown Manager core.
+--
+-- DOWNPORT of NewEra/CooldownViewer/CooldownViewer.lua. Retail's Cooldown Manager reads its
+-- per-spec cooldown sets from C_CooldownViewer, which does not exist on 3.3.5a (nor on Classic Era
+-- or TBC Classic — NewEra hit the same wall). So this is retail's VISUAL model driven by curated
+-- per-class data plus live GetSpellCooldown / UNIT_AURA reads, exactly as upstream built it.
+--
+-- PHASE 1 SCOPE: Essential + Utility viewers. BuffIcon / BuffBar (aura-driven) are Phase 3; the
+-- alert engine and the standalone settings panel are Phase 4. The category plumbing below keeps all
+-- four names so those phases are additive.
+--
+-- THE ONE STRUCTURAL DEVIATION — settings storage:
+--
+-- Upstream reads every setting through `NE.editmode` (EM.Register / EM.GetFrameSettingStored /
+-- EM.const.StoredToDisplay), a 6,441-line reimplementation of retail Edit Mode. This addon does not
+-- have it, DragonUI's movers are position-only, and DragonUI is read-only (CONTRACTS §0). Per
+-- PORT_PLAN §B1 we drop Edit Mode entirely: `getOpt` is the single chokepoint every setting reads
+-- through, so it is retargeted at our own profile table and the retail settings-int codec
+-- (M.CDV_CODEC, sliderToStored, StoredToDisplay) is gone. Positions come from DragonUI's
+-- MoversSystem; the ten settings are rendered in the New Era options tab.
+
+local NE = DragonUI_NewEra
+NE.cooldownviewer = NE.cooldownviewer or {}
+local M = NE.cooldownviewer
+
+-- Curated per-class/per-race data lives in ClassData.lua (loaded before this file):
+-- M.ESSENTIAL_BY_CLASS / UTILITY_BY_CLASS / BUFFICON_BY_CLASS / BUFFBAR_BY_CLASS /
+-- SPELL_DATA_BY_CATEGORY / RACIAL_BY_RACE.
+
+-- Upstream defines this in the buff-viewer section (Phase 3). Declared here so ShouldTrackBuff and
+-- the curated-set walk can reference it unconditionally.
+M.BUFFBAR_EXCLUDE = M.BUFFBAR_EXCLUDE or {}
+
+-- Defaults, from retail's HUDLayout preset (EditModePresetLayouts.lua).
+M.DEFAULTS = {
+ orientation = "horizontal",
+ iconLimit = 12,
+ iconDirection = "right",
+ iconSize = 100,
+ iconPadding = 2,
+ opacity = 100,
+ visibleSetting = "always",
+ hideWhenInactive = true,
+ showTimer = true,
+ showTooltips = true,
+ -- BuffBar-only (Phase 3). Retail's Modern preset stores BarWidthScale such that it displays as
+ -- 150%; upstream's owner reduced that to 100% once the pixel pin was removed, which is what we
+ -- take here.
+ barContent = "iconAndName",
+ barWidthScale = 100,
+}
+
+M.FRAME_ID = {
+ essential = "CooldownViewerEssential",
+ utility = "CooldownViewerUtility",
+ buffIcon = "CooldownViewerBuffIcon",
+ buffBar = "CooldownViewerBuffBar",
+}
+
+-- Per-frame overrides (retail preset values).
+--
+-- Essential/Utility templates do NOT set `allowHideWhenInactive` in retail XML, so retail's
+-- ShouldBeShown returns true unconditionally — they always show known cooldowns regardless of the
+-- HideWhenInactive setting. Only BuffIcon and BuffBar honour it. UpdateShownState reads this.
+local PER_FRAME_DEFAULT_OVERRIDES = {
+ CooldownViewerEssential = { iconLimit = 12, allowHideWhenInactive = false },
+ CooldownViewerUtility = { iconLimit = 7, allowHideWhenInactive = false },
+ CooldownViewerBuffIcon = { iconLimit = 1, iconPadding = 5, allowHideWhenInactive = true },
+ CooldownViewerBuffBar = { iconLimit = 1, iconPadding = 5, allowHideWhenInactive = true,
+ orientation = "vertical", iconDirection = "left" },
+}
+M.PER_FRAME_DEFAULT_OVERRIDES = PER_FRAME_DEFAULT_OVERRIDES
+
+-- Default anchors (retail Mainline preset), consumed by Register.lua as the movers' defaultPoint.
+-- BuffBar is offset to x=420 in retail's preset (see BuffViewers.lua CreateBuffViewer), the others
+-- are horizontally centred.
+M.VIEWER_DEFAULT_Y = { utility = 240, essential = 310, buffIcon = 370, buffBar = 430 }
+
+-- ── Settings store ──────────────────────────────────────────────────────────────────────────────
+-- Lives in DragonUI's profile alongside every other NewEra setting (integration/Register.lua
+-- ensureProfile owns the `newera` sub-table), so it follows DragonUI profiles and the options tab
+-- can bind to it directly.
+local function store(create)
+ local cfg = NE.Config and NE.Config()
+ if not cfg then return nil end
+ if not cfg.cooldownviewer then
+ if not create then return nil end
+ cfg.cooldownviewer = {}
+ end
+ local cd = cfg.cooldownviewer
+ if not cd.frames then
+ if not create then return cd end
+ cd.frames = {}
+ end
+ return cd
+end
+M._store = store
+
+-- ── Per-spec layout ─────────────────────────────────────────────────────────────────────────────
+--
+-- What sits in which category is a property of how you PLAY, and on this client that changes with a
+-- click: dual talent specialisation (3.1) gives every character two talent groups. Discipline wants
+-- Mind Blast on Essential and Holy does not, and before this the two shared one list.
+--
+-- So the three tables that describe a layout — the per-category spell lists, the aura assignments and
+-- trinket placement — live in a per-group bucket rather than at the top of the store. Deliberately
+-- NOT in a bucket:
+--
+-- * seenAura, the registry of auras the scan has met. That is knowledge, not layout: an aura you
+-- met in Holy is still an aura this character can get, and hiding it from the Discipline picker
+-- would be a bug wearing a feature's clothes.
+-- * everything under `frames` (position, size, orientation, opacity...). Retail's Edit Mode is not
+-- spec-aware either, and a viewer that jumped across the screen on a respec would read as broken.
+--
+-- Stored under `specLayouts`. NOT `layouts` — that key is already the saved-preset table
+-- (SettingsPresets.lua:64), and the first draft of this both shadowed it and, in ResetTracking,
+-- silently deleted every saved layout the player had. The harness caught it on the presets tests.
+--
+-- Keyed by talent GROUP, not by "spec". A group's identity is what the client actually gives us;
+-- inferring "Discipline" from the heaviest talent tree would be a guess that changes under the player
+-- mid-levelling, and would collide the moment two groups share a tree (two Fire specs, say).
+local LAYOUT_KEYS = { "customLists", "trackedAura", "equipAssign" }
+M.LAYOUT_KEYS = LAYOUT_KEYS
+
+function M.IsPerSpecLayout()
+ local cd = store(false)
+ if cd and cd.perSpecLayout ~= nil then return cd.perSpecLayout and true or false end
+ return true
+end
+
+function M.ActiveTalentGroup()
+ local g = GetActiveTalentGroup and GetActiveTalentGroup()
+ g = tonumber(g) or 1
+ if g < 1 then g = 1 end
+ return g
+end
+
+-- ── Which spec is this? ─────────────────────────────────────────────────────────────────────────
+--
+-- Points spent per tree, which is the only signal 3.3.5a offers and, for this purpose, a good one:
+-- the FIRST point is already a declaration of intent, and a starter layout wants intent rather than
+-- power. Someone who puts point one in Balance is levelling Balance.
+--
+-- IT RETURNS NIL RATHER THAN GUESSING, in the two cases where there is nothing to read:
+--
+-- * NO POINTS AT ALL. Below level 10 there is no signal whatsoever and a pick would be one in three.
+-- * AN EXACT TIE. 5/5/0 has no right answer either.
+--
+-- Both are handled by the caller offering the choice, never by picking the first tab — the owner's
+-- call, and the right one: an arbitrary index presented as a detected spec is worse than a question.
+--
+-- GROUP-AWARE, because GetTalentTabInfo takes one. That is what makes this usable for the INACTIVE
+-- spec's layout bucket as well as the active one; without it, dual spec would detect the same tree
+-- twice. Signature is (index, isInspect, isPet, group) on this client — see modules/talents/Behavior.
+--
+-- Deliberately NOT cached. It is read when a starter is applied and when a fresh bucket is seeded,
+-- both of which are one-off user-facing moments, and a stale answer there is worse than a DBC read.
+function M.DetectSpec(group)
+ if not (GetNumTalentTabs and GetTalentTabInfo) then return nil end
+ group = tonumber(group) or M.ActiveTalentGroup()
+ local best, bestPts, tied, total = nil, 0, false, 0
+ for tab = 1, (GetNumTalentTabs() or 0) do
+ local name, _, spent = GetTalentTabInfo(tab, false, false, group)
+ spent = tonumber(spent) or 0
+ total = total + spent
+ if spent > bestPts then
+ best, bestPts, tied = tab, spent, false
+ elseif spent == bestPts and spent > 0 then
+ tied = true
+ end
+ M._specNames = M._specNames or {}
+ M._specNames[tab] = name or M._specNames[tab]
+ end
+ if total == 0 or tied or not best then return nil end
+ return best, M._specNames and M._specNames[best], bestPts
+end
+
+-- The three tree names for the menu, in tab order. Read from the client rather than from the seed:
+-- the seed's tab->spec mapping is a comment for whoever edits it, and this is what a player reads.
+function M.SpecNames()
+ local out = {}
+ if not (GetNumTalentTabs and GetTalentTabInfo) then return out end
+ for tab = 1, (GetNumTalentTabs() or 0) do
+ out[tab] = (GetTalentTabInfo(tab, false, false, M.ActiveTalentGroup())) or ("Tree " .. tab)
+ end
+ return out
+end
+
+-- The bucket name. "shared" when the feature is off, which is also the bucket a new per-spec bucket
+-- inherits from — so turning it ON carries the layout you already had into the group you are in,
+-- rather than dropping you onto curated defaults with no warning.
+function M.LayoutKey()
+ if not M.IsPerSpecLayout() then return "shared" end
+ return "spec" .. M.ActiveTalentGroup()
+end
+
+local function deepCopy(v)
+ if type(v) ~= "table" then return v end
+ local out = {}
+ for k, vv in pairs(v) do out[k] = deepCopy(vv) end
+ return out
+end
+M.DeepCopyLayout = deepCopy
+
+-- One-time migration off the flat store. Every existing bucket gets a COPY of what the character had
+-- before, so a respec on an upgraded character lands on the layout they already curated rather than
+-- on curated defaults — the upgrade itself must not look like data loss. The legacy tables are then
+-- cleared, so there is exactly one home for a layout and no dead copy to drift.
+local function migrateLayout(cd)
+ if cd.specLayouts then return end
+ cd.specLayouts = {}
+ local had = false
+ for _, k in ipairs(LAYOUT_KEYS) do
+ if type(cd[k]) == "table" and next(cd[k]) ~= nil then had = true; break end
+ end
+ if not had then return end
+ for _, key in ipairs({ "shared", "spec1", "spec2" }) do
+ local bucket = {}
+ for _, k in ipairs(LAYOUT_KEYS) do bucket[k] = deepCopy(cd[k]) end
+ cd.specLayouts[key] = bucket
+ end
+ for _, k in ipairs(LAYOUT_KEYS) do cd[k] = nil end
+ if NE.Log then NE.Log("CDM", "layout copied into per-spec buckets") end
+end
+
+-- A bucket that does not exist yet inherits from "shared" — so turning the feature ON carries the
+-- layout in front of you into the group you are in, instead of blanking it.
+local function layoutBucket(create)
+ local cd = store(create)
+ if not cd then return nil end
+ if create then migrateLayout(cd) end
+ local key = M.LayoutKey()
+ if not cd.specLayouts then
+ if not create then return nil end
+ cd.specLayouts = {}
+ end
+ if not cd.specLayouts[key] then
+ if not create then return nil end
+ local bucket = {}
+ local shared = key ~= "shared" and cd.specLayouts.shared
+ if shared then
+ for _, k in ipairs(LAYOUT_KEYS) do bucket[k] = deepCopy(shared[k]) end
+ end
+ cd.specLayouts[key] = bucket
+ end
+ return cd.specLayouts[key]
+end
+M._layoutBucket = layoutBucket
+
+function M.SetPerSpecLayout(v)
+ local cd = store(true)
+ if cd then
+ migrateLayout(cd)
+ cd.perSpecLayout = v and true or false
+ end
+ M.InvalidateCuratedCache()
+ M.RefreshActiveViewer()
+end
+
+-- ── Per-character appearance ────────────────────────────────────────────────────────────────────
+--
+-- OPT-IN, and OFF by default. DragonUI's profile is shared across the account, so out of the box
+-- orientation, icons per row, icon size, padding, opacity and the rest are one setup for every
+-- character — which is usually what you want, and is what shipped for phases.
+--
+-- Turned on, a character's changes go to its own bucket and are read back as OVERRIDES on the shared
+-- table, key by key. That structure is doing real work:
+--
+-- * Nothing jumps when the option is toggled. A character that has changed nothing reads exactly
+-- what the account reads, because the fall-through IS the seed. No copy, no migration, no
+-- "enable it and watch four viewers resize".
+-- * Turning it back OFF simply stops consulting the bucket. The overrides survive, so toggling is
+-- reversible rather than destructive.
+-- * A setting the player never touched on this character keeps tracking the shared one. Change
+-- icon size account-wide and every character that has not overridden it follows.
+--
+-- Frame POSITION is not governed by this and is always per character (integration/Register.lua's
+-- NE.FramePositionKey). That was a separate, explicit decision by the owner; the switch here is
+-- about appearance, which is the part worth sharing by default.
+local function charKey()
+ local name = UnitName and UnitName("player")
+ if not name or name == "" then return nil end
+ local realm = GetRealmName and GetRealmName()
+ if realm and realm ~= "" then return name .. "-" .. realm end
+ return name
+end
+M.CharacterKey = charKey
+
+function M.IsPerCharacterFrames()
+ local cd = store(false)
+ return (cd and cd.perCharacterFrames) and true or false
+end
+
+-- The per-character bucket, or nil when the option is off, the client cannot name the player yet, or
+-- nothing has been stored for this character. Every caller must handle nil by falling back to the
+-- shared table — that fall-back is the whole design, not an error path.
+local function charFrames(create)
+ local cd = store(create)
+ if not cd then return nil end
+ if not (cd.perCharacterFrames and charKey()) then return nil end
+ if not cd.charFrames then
+ if not create then return nil end
+ cd.charFrames = {}
+ end
+ local key = charKey()
+ if not cd.charFrames[key] then
+ if not create then return nil end
+ cd.charFrames[key] = {}
+ end
+ return cd.charFrames[key]
+end
+M._charFrames = charFrames
+
+-- Do saved layouts carry APPEARANCE as well as lists? Off by default, and separate from everything
+-- else on purpose.
+--
+-- A layout has always meant "what I track": spell lists, tracked auras, trinket placement, alerts and
+-- sounds. Appearance is a different kind of thing, and an imported layout that silently resizes and
+-- reorients four viewers is how "load a layout" stops being an action people trust. Capture is
+-- unconditional — a share string always carries appearance, costing a few hundred bytes — so the
+-- decision belongs to whoever APPLIES it rather than to whoever exported it.
+function M.LayoutsIncludeAppearance()
+ local cd = store(false)
+ return (cd and cd.layoutsIncludeAppearance) and true or false
+end
+
+function M.SetLayoutsIncludeAppearance(v)
+ local cd = store(true)
+ if cd then cd.layoutsIncludeAppearance = v and true or false end
+end
+
+function M.SetPerCharacterFrames(v)
+ local cd = store(true)
+ if cd then cd.perCharacterFrames = v and true or false end
+ -- Re-apply immediately: the values a viewer should be reading have just changed underneath it, and
+ -- waiting for the next refresh would leave the window disagreeing with the frames.
+ M.ForEachViewer(function(viewer)
+ if viewer.RefreshLayout then viewer:RefreshLayout() end
+ end)
+ -- …and the page itself, whose sliders are now reading a different table.
+ local CDS = NE.cooldownviewersettings
+ if CDS and CDS.RefreshSettingsPage then CDS.RefreshSettingsPage() end
+end
+
+-- THE settings chokepoint. Upstream read the retail Edit Mode stored-value table here and converted
+-- through the settings codec; we read our own table and store display values directly.
+local function getOpt(frameID, key)
+ local cd = store(false)
+
+ -- This character's override first, then the account-wide value. Both are consulted, in that order,
+ -- and only a key this character has actually set diverges.
+ local mine = charFrames(false)
+ local mineFrame = mine and mine[frameID]
+ if mineFrame and mineFrame[key] ~= nil then return mineFrame[key] end
+
+ local frame = cd and cd.frames and cd.frames[frameID]
+ if frame and frame[key] ~= nil then return frame[key] end
+
+ local overrides = PER_FRAME_DEFAULT_OVERRIDES[frameID]
+ if overrides and overrides[key] ~= nil then return overrides[key] end
+ return M.DEFAULTS[key]
+end
+M.GetOpt = getOpt
+
+-- Write one setting and re-apply it live. Goes to this character's bucket when the option is on, and
+-- to the shared table otherwise — so with it off nothing about the old behaviour changes.
+function M.SetOpt(frameID, key, value)
+ local cd = store(true)
+ if not cd then return end
+ local dest = charFrames(true) or cd.frames
+ if not dest then return end
+ dest[frameID] = dest[frameID] or {}
+ dest[frameID][key] = value
+ local viewer = M.GetViewerByFrameID and M.GetViewerByFrameID(frameID)
+ if viewer and viewer.RefreshLayout then viewer:RefreshLayout() end
+end
+
+-- Restore one frame's settings to defaults.
+--
+-- With per-character appearance ON this clears BOTH this character's overrides and the shared entry,
+-- because the button says "defaults" and landing on the account-wide setup instead would be a
+-- different promise. Other characters' overrides are untouched — they are separate buckets.
+function M.ResetOpts(frameID)
+ local cd = store(true)
+ if not cd then return end
+ local mine = charFrames(false)
+ if mine then mine[frameID] = nil end
+ if cd.frames then cd.frames[frameID] = nil end
+ local viewer = M.GetViewerByFrameID and M.GetViewerByFrameID(frameID)
+ if viewer and viewer.RefreshLayout then viewer:RefreshLayout() end
+end
+
+function M.GetEssentialOpt(key) return getOpt(M.FRAME_ID.essential, key) end
+function M.GetUtilityOpt(key) return getOpt(M.FRAME_ID.utility, key) end
+function M.GetBuffIconOpt(key) return getOpt(M.FRAME_ID.buffIcon, key) end
+function M.GetBuffBarOpt(key) return getOpt(M.FRAME_ID.buffBar, key) end
+
+-- ── Enable toggles ──────────────────────────────────────────────────────────────────────────────
+--
+-- OFF by default, by the owner's decision, and it is the only module here that ships that way.
+--
+-- Everything else NewEra adds REPLACES something the player already had — a nicer character sheet, a
+-- nicer guild window — so shipping it on is shipping an improvement. Four viewers that appear in the
+-- middle of the screen are not a replacement for anything: they are new HUD furniture, sized and
+-- placed by us, over whatever the player has already arranged there. Deciding that for them on first
+-- login is the one thing a HUD addon should not do.
+--
+-- The flag is the whole of it. Nothing else keys off `enabled`, so turning it on later reads exactly
+-- the same curated lists, layouts and alerts a profile that had it on all along would read — there is
+-- no seed to miss and no first-run path that only happens while it is on. See M.SetEnabled.
+--
+-- NOTE for existing profiles: a profile that never touched this toggle stored nothing, so it lands on
+-- the new default and the viewers are hidden after this change. One tick in DragonUI's options brings
+-- back the exact setup that was there, because disabling has never deleted anything.
+function M.IsEnabled()
+ local cd = store(false)
+ if cd and cd.enabled ~= nil then return cd.enabled end
+ return false
+end
+
+-- Live both ways, and UpdateVisibility is genuinely all it takes. A viewer that was hidden all session
+-- has stale (or no) items, but the Show inside UpdateVisibility fires OnShow, and OnShow is Rebuild —
+-- so switching on mid-session populates the viewers on the way up. That is what lets this be the only
+-- gate: no boot-time work is skipped while it is off, so nothing has to be caught up afterwards.
+function M.SetEnabled(v)
+ local cd = store(true)
+ if cd then cd.enabled = v and true or false end
+ M.ForEachViewer(function(viewer) viewer:UpdateVisibility() end)
+ -- The window goes with the viewers. SettingsPanel refuses to OPEN while the module is off; this is
+ -- the other half of that — a window already up would otherwise sit there configuring four frames it
+ -- can no longer show, with a Position button that leads nowhere.
+ if not v then
+ local CDS = NE.cooldownviewersettings
+ if CDS and CDS.HidePanel then CDS.HidePanel() end
+ end
+end
+
+-- Per-category enable. Retail's HUDLayout preset has all four viewers on.
+local CATEGORY_DEFAULT_ENABLED = {
+ essential = true, utility = true, buffIcon = true, buffBar = true,
+}
+
+function M.IsCategoryEnabled(category)
+ local cd = store(false)
+ if cd and cd.categories then
+ local v = cd.categories[category]
+ if v ~= nil then return v end
+ end
+ local d = CATEGORY_DEFAULT_ENABLED[category]
+ if d == nil then return true end
+ return d
+end
+
+function M.SetCategoryEnabled(category, v)
+ local cd = store(true)
+ if cd then
+ cd.categories = cd.categories or {}
+ cd.categories[category] = v and true or false
+ end
+ local viewer = M.viewers and M.viewers[category]
+ if viewer then viewer:UpdateVisibility() end
+end
+
+-- ── Viewer registry ─────────────────────────────────────────────────────────────────────────────
+-- Upstream addressed viewers through XML-created globals (NE_EssentialCooldownViewer, ...). We
+-- build frames in Lua (Viewers.lua), so they register here instead.
+M.viewers = M.viewers or {}
+
+function M.RegisterViewer(category, frame)
+ M.viewers[category] = frame
+end
+
+function M.GetViewerByFrameID(frameID)
+ for category, frame in pairs(M.viewers) do
+ if M.FRAME_ID[category] == frameID then return frame end
+ end
+ return nil
+end
+
+function M.ForEachViewer(fn)
+ for _, frame in pairs(M.viewers) do
+ if frame then fn(frame) end
+ end
+end
+
+-- ── Item helpers ────────────────────────────────────────────────────────────────────────────────
+
+-- On-use item cooldown by itemID. C_Container.GetItemCooldown takes an itemID despite the
+-- namespace; the global GetItemCooldown is the 3.3.5a original.
+function M.ItemCooldown(itemID)
+ if itemID and C_Container and C_Container.GetItemCooldown then return C_Container.GetItemCooldown(itemID) end
+ if itemID and GetItemCooldown then return GetItemCooldown(itemID) end
+ return 0, 0, 0
+end
+
+-- Resolve an item's inventory icon. Returns nil until the server caches the item; the viewer's
+-- GET_ITEM_INFO_RECEIVED handler re-resolves when it lands.
+function M.ResolveItemIcon(itemID)
+ if not itemID then return nil end
+ local icon = (C_Item and C_Item.GetItemIconByID and C_Item.GetItemIconByID(itemID))
+ or (GetItemIcon and GetItemIcon(itemID))
+ if not icon and C_Item and C_Item.RequestLoadItemDataByID then
+ C_Item.RequestLoadItemDataByID(itemID)
+ end
+ return icon
+end
+
+function M.GetItemUseSpell(item)
+ if not item then return nil end
+ if C_Item and C_Item.GetItemSpell then return C_Item.GetItemSpell(item) end
+ if GetItemSpell then return GetItemSpell(item) end
+ return nil
+end
+
+-- On-use item entries live in the custom lists keyed by their USE-SPELL id, carrying itemID +
+-- track ("item" = GetItemCooldown / "spell" = GetSpellCooldown on the use-spell).
+--
+-- MUST use GetCustomList, never GetEditableList. This is a read-only QUERY, and it runs from
+-- ItemMixins:SetSpell for every icon on every rebuild. GetEditableList SEEDS AND PERSISTS the
+-- custom list from the curated defaults as a side effect, so calling it here silently froze the
+-- then-current spell list into SavedVariables the very first time a viewer built. After that
+-- GetActiveSpellList took the `custom` branch forever and ignored the curated tables — which is
+-- why every ability added by CdmSeedWotLK.lua stayed invisible on a character that had run an
+-- earlier build. A query must not pin persistent state.
+function M.GetItemMeta(spellID, class)
+ if not class then local _; _, class = UnitClass("player") end
+ if not (spellID and class) then return nil end
+ for _, key in ipairs({ "essential", "utility" }) do
+ local list = M.GetCustomList(key, class)
+ if list then
+ for _, e in ipairs(list) do
+ if e.itemID and e.spellID == spellID then return e.itemID, e.track or "spell" end
+ end
+ end
+ end
+ return nil
+end
+
+-- Equipped-trinket auto-discovery and the per-token assignment registry live in Equip.lua, which
+-- loads after this file and defines M.GetEquipActiveItems / M.GetEquipAssignment /
+-- M.GetEquipItemsForCategory. This stub is the fallback for the case Equip.lua is absent (a partial
+-- install, or the offline harness loading a subset): an empty list makes the Rebuild branch that
+-- consumes it a clean no-op rather than an error.
+function M.GetEquipItemsForCategory(_)
+ return {}
+end
+
+-- ── Spell list resolution ───────────────────────────────────────────────────────────────────────
+
+-- Per-character custom list, per category:
+-- store().customLists[][] = { { spellID=, enabled= }, ... }
+-- nil means "use the curated default for this class".
+local function getCustomTable(category)
+ local cd = layoutBucket(true) -- per talent group; see the Per-spec layout section
+ if not (cd and category) then return nil end
+ cd.customLists = cd.customLists or {}
+ cd.customLists[category] = cd.customLists[category] or {}
+ return cd.customLists[category]
+end
+
+function M.GetCustomList(category, class)
+ if not class then local _; _, class = UnitClass("player") end
+ local t = getCustomTable(category)
+ return t and t[class] or nil
+end
+
+function M.SetCustomList(category, class, list)
+ if not class then local _; _, class = UnitClass("player") end
+ local t = getCustomTable(category)
+ if not t then return end
+ t[class] = list
+end
+
+function M.ResetCustomList(category, class)
+ if not class then local _; _, class = UnitClass("player") end
+ local t = getCustomTable(category)
+ if not t then return end
+ t[class] = nil
+end
+
+-- One-time migration: drop custom lists that were never deliberately created.
+--
+-- Until the spell picker lands (Phase 4) there is NO user-facing way to author a custom list, so
+-- any list already in SavedVariables is an artifact of the GetItemMeta side effect described above
+-- — a frozen snapshot of whatever the curated defaults happened to be at the time, which then
+-- masks every later addition (the entire WotLK seed, for a character that had run an earlier
+-- build). Clearing them restores the curated path and loses nothing the user chose.
+--
+-- Versioned so it runs exactly once and can never wipe a genuinely authored list later. Upstream
+-- carries the same guarded one-time reset for the same reason ("hiding the new defaults").
+local CUSTOM_LIST_RESET_VERSION = "customListsV2"
+
+function M.MigrateStaleCustomLists()
+ local cd = store(true)
+ if not cd then return false end
+ if cd[CUSTOM_LIST_RESET_VERSION] then return false end
+ cd[CUSTOM_LIST_RESET_VERSION] = true
+ -- Both homes: the flat key on a store that predates the per-spec change, and every bucket on one
+ -- that has already been migrated. Clearing only the flat key would have quietly stopped working
+ -- for exactly the characters this exists to repair.
+ local had = cd.customLists and next(cd.customLists) ~= nil
+ cd.customLists = {}
+ for _, bucket in pairs(cd.specLayouts or {}) do
+ if bucket.customLists and next(bucket.customLists) ~= nil then had = true end
+ bucket.customLists = {}
+ end
+ M.InvalidateCuratedCache()
+ if had and NE.Log then
+ NE.Log("CDM", "cleared stale auto-seeded spell lists; curated defaults restored")
+ end
+ return had
+end
+
+function M.GetEditableList(category, class)
+ if not class then local _; _, class = UnitClass("player") end
+ local t = getCustomTable(category)
+ if not (t and class) then return nil end
+ if not t[class] then
+ local source = M.SPELL_DATA_BY_CATEGORY and M.SPELL_DATA_BY_CATEGORY[category]
+ local default = source and source[class] or {}
+ local seeded = {}
+ for _, spellID in ipairs(default) do
+ table.insert(seeded, { spellID = spellID, enabled = true })
+ end
+ t[class] = seeded
+ end
+ return t[class]
+end
+
+-- Show or hide one spell in a category, as a DELIBERATE user action.
+--
+-- This is the only caller of GetEditableList, and that is the whole point: seeding a custom list is
+-- a side effect with teeth (PORT_PLAN §E5 — a read-only query doing it froze the curated defaults
+-- into SavedVariables and hid every later addition). Seeding is correct HERE, because the user has
+-- just expressed an explicit preference about this list, so a list is what should exist afterwards.
+--
+-- Returns true if anything changed.
+function M.SetSpellEnabled(category, spellID, enabled)
+ if not (category and spellID) then return false end
+ local _, class = UnitClass("player")
+ local list = M.GetEditableList(category, class)
+ if not list then return false end
+
+ enabled = enabled and true or false
+ for _, entry in ipairs(list) do
+ if entry.spellID == spellID then
+ if entry.enabled == enabled then return false end
+ entry.enabled = enabled
+ M.RefreshActiveViewer(category)
+ return true
+ end
+ end
+
+ -- Not in the seeded list: a racial, or a spell added after the list was authored. Append it so
+ -- the preference sticks rather than being silently dropped.
+ list[#list + 1] = { spellID = spellID, enabled = enabled }
+ M.RefreshActiveViewer(category)
+ return true
+end
+
+-- Should the settings panel list curated spells the player has not learned?
+--
+-- Off by default, and it now governs EVERY spell section including Not Displayed (§H.3.21). It used
+-- to mean "tint the ones you can't cast", which left an untalented druid's Mangle sitting in the
+-- catalog looking like something they had merely chosen not to display. It now means what it says:
+-- off, the picker is the character's own arsenal; on, it is the class's.
+function M.GetShowUnlearned()
+ local cd = store(false)
+ return (cd and cd.showUnlearned) and true or false
+end
+
+function M.SetShowUnlearned(v)
+ local cd = store(true)
+ if cd then cd.showUnlearned = v and true or false end
+ M.RefreshActiveViewer()
+end
+
+-- Light the tile's frame gold while the spell's own buff is on the player (§H.2 8c, the substitute
+-- for retail's gold swipe). ON by default: it is a retail feature, not an addition of ours.
+function M.IsBuffGlowEnabled()
+ local cd = store(false)
+ if cd and cd.buffGlow ~= nil then return cd.buffGlow and true or false end
+ return true
+end
+
+function M.SetBuffGlowEnabled(v)
+ local cd = store(true)
+ if cd then cd.buffGlow = v and true or false end
+ -- Turning it OFF has to reach tiles that are glowing right now, and RefreshCooldown only revisits
+ -- the glow when something about the spell changes — which for a buff already up may be a minute away.
+ M.RefreshActiveViewer()
+end
+
+-- Is this spell currently enabled in the category? Reads the custom list WITHOUT seeding one.
+function M.IsSpellEnabled(category, spellID)
+ if not (category and spellID) then return true end
+ local _, class = UnitClass("player")
+ local list = M.GetCustomList(category, class)
+ if not list then return true end -- no custom list: the curated default is shown
+ for _, entry in ipairs(list) do
+ if entry.spellID == spellID then return entry.enabled and true or false end
+ end
+ return true
+end
+
+-- Merge per-race racials into a category's list. UnitRace's 2nd return is the canonical key
+-- ("Human", "Scourge", ...). Deduped, and never overrides a deliberate user disable.
+local function appendRacials(out, category, exclude)
+ local _, raceFile = UnitRace("player")
+ if not raceFile then return end
+ local bucket = M.RACIAL_BY_RACE and M.RACIAL_BY_RACE[raceFile]
+ local list = bucket and bucket[category]
+ if not list then return end
+ local seen = {}
+ for _, id in ipairs(out) do seen[id] = true end
+ for _, spellID in ipairs(list) do
+ if not seen[spellID] and not (exclude and exclude[spellID]) then
+ out[#out + 1] = spellID
+ seen[spellID] = true
+ end
+ end
+end
+
+-- Learn-gate scope: the set of ids we are willing to call a CLASS ABILITY. Anything outside it is a
+-- user-added item/external whose use-spell is never "known", so it is always trackable. Memoised.
+--
+-- The ARSENAL belongs in here (§H.3.21) even though it is not curation. Before, the set was the
+-- curated lists alone — so the moment the player dragged an arsenal-only ability out of the picker
+-- and into a viewer, it fell through the escape hatch and the viewer showed it whether or not they
+-- could cast it. Mangle (Bear) is exactly that: arsenal-only, since the seed curates the Cat form.
+-- The escape hatch is for ids that are not class abilities at all, and the arsenal is nothing but.
+local curatedSetCache = {}
+local function curatedSet(class)
+ if curatedSetCache[class] then return curatedSetCache[class] end
+ local set = {}
+ if M.SPELL_DATA_BY_CATEGORY then
+ for _, byClass in pairs(M.SPELL_DATA_BY_CATEGORY) do
+ local list = byClass[class]
+ if list then for _, id in ipairs(list) do set[id] = true end end
+ end
+ end
+ if M.ARSENAL_BY_CLASS and M.ARSENAL_BY_CLASS[class] then
+ for _, id in ipairs(M.ARSENAL_BY_CLASS[class]) do set[id] = true end
+ end
+ if M.RACIAL_BY_RACE then
+ for _, bucket in pairs(M.RACIAL_BY_RACE) do
+ for _, catName in ipairs({ "essential", "utility", "buffIcon", "buffBar" }) do
+ local l = bucket[catName]
+ if l then for _, id in ipairs(l) do set[id] = true end end
+ end
+ end
+ end
+ curatedSetCache[class] = set
+ return set
+end
+
+-- Invalidated when the curated tables are appended to (the Phase 2 WotLK seed does exactly that).
+function M.InvalidateCuratedCache()
+ curatedSetCache = {}
+end
+
+-- Does the player know this ability, at ANY rank?
+--
+-- DOWNPORT / BUGFIX: the source tries IsSpellKnown(spellID) first, then GetSpellInfo(name). Both
+-- are unreliable here, and between them they hid every talent-granted and higher-rank ability:
+--
+-- * IsSpellKnown(id) tests ONE EXACT RANK. Our curated lists key each ability by its rank-1 id,
+-- so the moment the player trains rank 2 the check reports false — Penance (47540) on a Disc
+-- priest who has trained past rank 1, for instance.
+-- * GetSpellInfo(name) is a spell-database lookup on this client, not a reliable membership test.
+--
+-- Neither is level-aware either, which matters on a level-squished server where a character can
+-- legitimately know an ability far below its stock level (Divine Hymn at 60).
+--
+-- So we ask the SPELLBOOK, by name (core/SpellRanks.lua). That is authoritative, rank-agnostic and
+-- level-agnostic: if it is in the book, the player has it.
+--
+-- The old checks are kept, but ONLY for the case the book scan is unavailable — they are a fallback,
+-- not a widening. §E4 originally chained all three unconditionally so the fix "could only widen what
+-- shows", and that was the wrong shape: `GetSpellInfo(name)` answers from the spell DATABASE, so the
+-- last link says yes to abilities the character cannot have (an untalented druid's Mangle). Chained
+-- unconditionally it made the gate a near-no-op, which is invisible while every consumer merely
+-- TINTS an unlearned row and becomes the whole behaviour the moment one HIDES it (§H.3.21).
+--
+-- The book being built is the condition, not the book being non-empty: an empty book is a real
+-- answer for a character with no spells, whereas "not built yet" is an absence of one.
+local function isLearned(spellID)
+ local name = GetSpellInfo(spellID)
+ local SB = NE.spellbook
+ if SB and SB.EnsureBuilt and SB.EnsureBuilt() then
+ if name and SB.IsSpellNameKnown and SB.IsSpellNameKnown(name) then return true end
+ -- Still worth asking: IsSpellKnown is a STRICT exact-rank test, so it can only ever confirm, and
+ -- it covers the odd grant that never surfaces in the book under a name we can match.
+ return (IsSpellKnown and IsSpellKnown(spellID)) and true or false
+ end
+ -- No book to ask. Fail OPEN — showing an ability the player lacks is a smaller fault than hiding
+ -- one they have because a scan had not run yet.
+ if IsSpellKnown and IsSpellKnown(spellID) then return true end
+ return (name and GetSpellInfo(name)) and true or false
+end
+
+-- The raw learn test, exported. IsTrackable below is this PLUS a curated-set escape hatch, and the
+-- two are not interchangeable: the escape hatch exists for user-added on-use items, which are never
+-- "known" spells, and it waves through everything outside the curated tables — including the whole
+-- generated ARSENAL. Ask IsTrackable about a viewer row; ask this about a picker row, where every
+-- entry is a class ability by construction and "not in the curated set" means nothing at all.
+function M.IsSpellLearned(spellID)
+ if not spellID then return true end
+ return isLearned(spellID)
+end
+
+-- Race gate. Upstream reads the generated NE_SPELL_RACEMASK table (SkillLineAbility RaceMask) to
+-- hide never-learnable-by-race spells. We do not ship that data, and the upstream design already
+-- FAILS OPEN on a missing mask — so this is currently a permissive pass-through. Kept as the seam.
+function M.SpellAllowedForRace(spellID)
+ local mask = spellID and _G.NE_SPELL_RACEMASK and _G.NE_SPELL_RACEMASK[spellID]
+ if not mask then return true end
+ return true
+end
+
+function M.IsTrackable(spellID, class)
+ if not class then local _; _, class = UnitClass("player") end
+ if not (spellID and class) then return true end
+ if not curatedSet(class)[spellID] then return true end
+ return isLearned(spellID)
+end
+
+function M.IsCuratedSpell(spellID, class)
+ if not class then local _; _, class = UnitClass("player") end
+ return (spellID and class and curatedSet(class)[spellID]) and true or false
+end
+
+-- The live spell list for a category. `includeUnlearned` skips the learn-gate (used by the
+-- options-tab preview so an untrained character still sees the full curated set).
+--
+-- `class` is an override for the callers that already hold one — the settings adapter takes a class
+-- argument all the way down, and it has to be able to ask THIS function what is on screen rather
+-- than re-deriving it (§H.3.22). Defaults to the player's, which is what every viewer wants.
+function M.GetActiveSpellList(category, includeUnlearned, class)
+ if not class then local _; _, class = UnitClass("player") end
+ if not class then return {} end
+
+ local raw = {}
+ local present -- spellIDs the user explicitly listed (enabled OR disabled)
+ local custom = M.GetCustomList(category, class)
+ if custom then
+ present = {}
+ for _, entry in ipairs(custom) do
+ present[entry.spellID] = true
+ if entry.enabled then raw[#raw + 1] = entry.spellID end
+ end
+ else
+ local source = M.SPELL_DATA_BY_CATEGORY and M.SPELL_DATA_BY_CATEGORY[category]
+ local classList = (source and source[class]) or {}
+ for _, spellID in ipairs(classList) do raw[#raw + 1] = spellID end
+ end
+
+ -- Racials are class-agnostic and were never in the editable seed, so append in BOTH paths
+ -- (deduped) — otherwise the moment a custom list is seeded, every racial silently vanishes.
+ -- `present` keeps an explicit user disable authoritative.
+ appendRacials(raw, category, present)
+
+ -- NAME-level dedupe (§H.3.23). An ABILITY is not an ID: Spell.dbc holds a separate row per rank,
+ -- plus variants that never reach a spellbook, so one ability can enter this list under two ids and
+ -- render as two identical tiles. Reported as "two versions of Icy Touch" — the DK's is curated as
+ -- 45477 (the trained rank 1) and generated into the arsenal as 52372.
+ --
+ -- Deduping by NAME rather than by id is the honest test, because the name is exactly what makes the
+ -- two indistinguishable: the tile draws GetSpellInfo's name and icon, so a list that holds both has
+ -- no way to tell the player which is which, and no reason to.
+ --
+ -- Which id survives does not matter, and that is why this is safe. Every runtime read resolves
+ -- through highestKnownRankID (ItemMixins), which maps a spellID to the player's highest KNOWN rank
+ -- BY NAME — so any id for the ability lands on the same cooldown, icon and tooltip. First occurrence
+ -- wins, which keeps a deliberate user ordering and keeps curated ids ahead of appended racials.
+ --
+ -- An id the client cannot name is never collapsed: GetSpellInfo returns nil for it, there is nothing
+ -- to compare, and dropping it would be guessing.
+ local allowed, byName = {}, {}
+ for _, id in ipairs(raw) do
+ if M.SpellAllowedForRace(id) then
+ local name = GetSpellInfo(id)
+ if not (name and byName[name]) then
+ if name then byName[name] = true end
+ allowed[#allowed + 1] = id
+ end
+ end
+ end
+ if includeUnlearned then return allowed end
+
+ local out = {}
+ for _, id in ipairs(allowed) do
+ if M.IsTrackable(id, class) then out[#out + 1] = id end
+ end
+ return out
+end
+
+-- ── Tracked auras (BuffIcon / BuffBar) ──────────────────────────────────────────────────────────
+--
+-- Retail's TrackedBuff/TrackedBar read a Blizzard-curated per-spec aura set from C_CooldownViewer.
+-- With no such data, upstream drives both buff viewers from an AUTO-TRACK window — any live player
+-- buff with 0 < duration <= BUFF_TRACK_MAX_DURATION — plus a manual override pool. That is what
+-- ports; the curated BUFFICON_BY_CLASS / BUFFBAR_BY_CLASS tables in ClassData.lua are unused here
+-- (they were full of duration-0 permanent toggles, which is the wrong thing for these viewers).
+--
+-- ONE pool, like retail: each aura is in exactly one state — "icon" (TrackedBuff), "bar"
+-- (TrackedBar) or "hidden" (force-excluded from both).
+-- store().trackedAura[] = { { spellID = 10060, assignment = "icon" }, ... }
+-- It starts EMPTY by design: the auto window provides the defaults, this is the override registry.
+
+local BUFF_TRACK_MAX_DURATION = 120
+
+local VALID_ASSIGNMENT = { icon = true, bar = true, hidden = true }
+
+local function getTrackedAuraTable()
+ local cd = layoutBucket(true) -- per talent group; the seen registry below is NOT
+ if not cd then return nil end
+ cd.trackedAura = cd.trackedAura or {}
+ return cd.trackedAura
+end
+
+function M.GetTrackedAuraList(class)
+ if not class then local _; _, class = UnitClass("player") end
+ local t = getTrackedAuraTable()
+ if not (t and class) then return nil end
+ t[class] = t[class] or {}
+ return t[class]
+end
+
+-- `name` is stored alongside the id because matching is by name (see GetBuffOverrides): a catalog
+-- row is rank 1, the aura the player actually gets may be any rank, and GetSpellInfo cannot always
+-- resolve an id this client only knows as an aura.
+-- The best name available for an aura id, in falling order of certainty: the client, what the scan
+-- recorded, the catalog. Resolved HERE rather than threaded through the picker's drag and menu
+-- paths, so every writer stores a name whether or not its caller happened to know one.
+function M.ResolveAuraName(spellID)
+ if not spellID then return nil end
+ local nm = GetSpellInfo(spellID)
+ if nm then return nm end
+ local _, class = UnitClass("player")
+ if not class then return nil end
+ local cd = store(false)
+ local bag = cd and cd.seenAura and cd.seenAura[class]
+ if bag and bag[spellID] and bag[spellID].name then return bag[spellID].name end
+ for _, c in ipairs((M.AURA_CATALOG_BY_CLASS or {})[class] or {}) do
+ if c.id == spellID then return c.name end
+ end
+ return nil
+end
+
+function M.SetAuraAssignment(class, spellID, assignment, name)
+ if not VALID_ASSIGNMENT[assignment] then return end
+ local list = M.GetTrackedAuraList(class)
+ if not (list and spellID) then return end
+ name = name or M.ResolveAuraName(spellID)
+ for _, e in ipairs(list) do
+ if e.spellID == spellID then
+ e.assignment = assignment
+ if name and not e.name then e.name = name end -- late-arriving name, never overwritten
+ M.RefreshActiveViewer(); return
+ end
+ end
+ list[#list + 1] = { spellID = spellID, assignment = assignment, name = name }
+ M.RefreshActiveViewer()
+end
+
+function M.RemoveTrackedAura(class, spellID)
+ local list = M.GetTrackedAuraList(class)
+ if not list then return end
+ for i = #list, 1, -1 do
+ if list[i].spellID == spellID then table.remove(list, i) end
+ end
+ M.RefreshActiveViewer()
+end
+
+function M.ResetTrackedAura(class)
+ if not class then local _; _, class = UnitClass("player") end
+ local t = getTrackedAuraTable()
+ if t and class then t[class] = {} end
+ M.RefreshActiveViewer()
+end
+
+-- ── What the picker can OFFER (Phase 7) ─────────────────────────────────────────────────────────
+--
+-- The pool above is an OVERRIDE registry, and its only writer is the picker. On its own that makes
+-- the Tracked Buffs / Tracked Bars tab a store whose sole editor is a view of itself: nothing can
+-- ever enter it, so the tab is empty on every character forever, and ScanTargetTrackedAuras — whose
+-- input is that registry — never has anything to look for either. Two sources fix that:
+--
+-- * the CATALOG (CdmAuraCatalog.lua, generated): what you CAN have. This is retail's shape, where
+-- the Buffs menu lists a Blizzard-curated per-spec aura set. Spec-gating is per TALENT rather
+-- than per spec, which on this client is strictly better — it follows respecs and dual spec.
+-- * the SEEN registry: what you HAVE had. The catalog cannot cover racials, trinket and set
+-- procs, or anything this client grants outside a class skill line, so the live scan records
+-- what it meets. This is the safety net that lets the catalog be imperfect.
+
+local SEEN_CAP = 60 -- per class
+local SEEN_RENOTE = 5 -- seconds; a re-note inside this window is skipped
+
+local function getSeenAuraTable()
+ local cd = store(true)
+ if not cd then return nil end
+ cd.seenAura = cd.seenAura or {}
+ return cd.seenAura
+end
+
+-- Called from the aura scan for every player buff it walks, BEFORE the include/exclude decision.
+-- The ordering is load-bearing: recorded after, an aura the player hides would drop out of the
+-- registry, its row would vanish from Not Displayed, and nothing would be left to unhide it with.
+-- Returns true only when a NEW aura was added.
+function M.NoteSeenAura(spellID, name, icon, duration)
+ if not (spellID and name) then return false end
+ -- Exactly the window the viewers use, so the tab describes what the bars are doing rather than
+ -- some wider idea of it. Also what keeps food buffs, flasks and permanent toggles out.
+ if not (duration and duration > 0 and duration <= BUFF_TRACK_MAX_DURATION) then return false end
+ local _, class = UnitClass("player")
+ local t = class and getSeenAuraTable()
+ if not t then return false end
+ t[class] = t[class] or {}
+ local bag = t[class]
+ local now = (GetTime and GetTime()) or 0
+
+ local e = bag[spellID]
+ if e then
+ if (now - (e.last or 0)) < SEEN_RENOTE then return false end
+ e.name, e.icon, e.dur, e.last = name, icon, duration, now
+ return false
+ end
+
+ -- Capped, evicting the least recently seen. Unbounded this follows the character forever and is
+ -- copied into every layout snapshot (§G.11), so a raid night of procs would bloat both.
+ local n, oldest, oldestAt = 0, nil, nil
+ for k, v in pairs(bag) do
+ n = n + 1
+ local last = v.last or 0
+ if not oldestAt or last < oldestAt then oldest, oldestAt = k, last end
+ end
+ if n >= SEEN_CAP and oldest then bag[oldest] = nil end
+
+ bag[spellID] = { name = name, icon = icon, dur = duration, last = now }
+ return true
+end
+
+function M.GetSeenAuraList(class)
+ if not class then local _; _, class = UnitClass("player") end
+ local t = store(false)
+ local bag = t and t.seenAura and class and t.seenAura[class]
+ local out = {}
+ if not bag then return out end
+ for spellID, e in pairs(bag) do
+ out[#out + 1] = { spellID = spellID, name = e.name, icon = e.icon, dur = e.dur, last = e.last }
+ end
+ -- Duration descending, matching the catalog's order and the bars', so the grid reads the same way
+ -- however a row got there.
+ table.sort(out, function(a, b)
+ if (a.dur or 0) ~= (b.dur or 0) then return (a.dur or 0) > (b.dur or 0) end
+ return (a.name or "") < (b.name or "")
+ end)
+ return out
+end
+
+function M.ResetSeenAura(class)
+ if not class then local _; _, class = UnitClass("player") end
+ local t = getSeenAuraTable()
+ if t and class then t[class] = {} end
+end
+
+-- ── The talent gate ─────────────────────────────────────────────────────────────────────────────
+-- A catalog row carrying `talent` is only offered when the player has that talent. Cached with a
+-- short TTL rather than event-invalidated: the only caller is the picker, so this is never in the
+-- aura hot path, and a TTL self-heals after a respec without needing to be told about one.
+local TALENT_TTL = 2
+local talentRanks, talentStamp
+
+local function talentTable()
+ local now = (GetTime and GetTime()) or 0
+ if talentRanks and (now - (talentStamp or 0)) < TALENT_TTL then return talentRanks end
+ local t, n = {}, 0
+ if GetNumTalentTabs and GetNumTalents and GetTalentInfo then
+ for tab = 1, (GetNumTalentTabs() or 0) do
+ for i = 1, (GetNumTalents(tab) or 0) do
+ -- 3.3.5a flat 10-tuple; rank is the 5th return (see modules/talents/Behavior.lua).
+ local name, _, _, _, rank = GetTalentInfo(tab, i)
+ if name then t[name:lower()] = rank or 0; n = n + 1 end
+ end
+ end
+ end
+ t._count = n
+ talentRanks, talentStamp = t, now
+ return t
+end
+
+-- Fails OPEN. No talent rows at all means the API has not answered yet — early login, or a build
+-- without the talent globals — NOT that the player has spent nothing. Hiding every spec-gated row
+-- on that reading would reproduce the exact bug this phase exists to fix.
+function M.HasTalent(name)
+ if not name then return true end
+ local t = talentTable()
+ if t._count == 0 then return true end
+ return (t[name:lower()] or 0) > 0
+end
+
+function M.InvalidateTalentCache() talentRanks, talentStamp = nil, nil end
+
+-- The catalog for a class, spec-filtered. `includeUntalented` is the picker's Show Unlearned
+-- escape hatch: the gate is derived data and can be wrong, so there is a way to see past it.
+function M.GetAuraCatalog(class, includeUntalented)
+ if not class then local _; _, class = UnitClass("player") end
+ local all = class and (M.AURA_CATALOG_BY_CLASS or {})[class]
+ if not all then return {} end
+ local out = {}
+ for _, e in ipairs(all) do
+ if (not e.talent) or includeUntalented or M.HasTalent(e.talent) then out[#out + 1] = e end
+ end
+ return out
+end
+
+-- Catalog ∪ seen, minus anything the player has already assigned by hand — the rows the picker can
+-- offer as AUTO. Deduped by lowercased name, not by id: the same buff arrives from the catalog at
+-- rank 1 and from the scan at whatever rank was cast, and those are two ids for one buff.
+function M.GetAuraCandidates(class, includeUntalented)
+ if not class then local _; _, class = UnitClass("player") end
+ local out, byName, explicit = {}, {}, {}
+
+ for _, e in ipairs(M.GetTrackedAuraList(class) or {}) do
+ if e.spellID then explicit[e.spellID] = true end
+ local nm = e.name or (e.spellID and GetSpellInfo(e.spellID))
+ if nm then explicit[nm:lower()] = true end
+ end
+
+ local function add(entry)
+ local key = entry.name and entry.name:lower()
+ if not key then return end
+ if byName[key] or explicit[key] or (entry.spellID and explicit[entry.spellID]) then return end
+ byName[key] = true
+ out[#out + 1] = entry
+ end
+
+ for _, c in ipairs(M.GetAuraCatalog(class, includeUntalented)) do
+ local nm, _, icon = GetSpellInfo(c.id)
+ add({ spellID = c.id, name = c.name or nm, icon = icon, dur = c.dur,
+ talent = c.talent, tree = c.tree, catalog = true,
+ -- Only ever true when Show Unlearned opened the gate; the tile tints on it.
+ untalented = (c.talent and not M.HasTalent(c.talent)) or nil })
+ end
+ for _, s in ipairs(M.GetSeenAuraList(class)) do
+ add({ spellID = s.spellID, name = s.name, icon = s.icon, dur = s.dur, seen = true })
+ end
+ return out
+end
+
+-- The key an aura's per-spell SETTINGS (alert, ready sound) are stored under.
+--
+-- The picker writes them under the row's own spellID: a catalog row is rank 1, a seen row is
+-- whatever rank was first met. A live viewer item carries the id the aura scan actually returned.
+-- Those are the same id for an unranked proc and different ids the moment a buff has ranks, and an
+-- id-only lookup then finds nothing at all — the alert is stored, the badge is lit, and the viewer
+-- never sees it. GetBuffOverrides already solved exactly this for ASSIGNMENTS by keying on name as
+-- well as id; this is the same rule applied to the settings stores.
+--
+-- Resolution order matches the picker's own: an explicit tracked entry wins, then the candidate pool
+-- (catalog ∪ seen), then the id we were handed. Callers memoize per binding — see the aura mixins'
+-- GetSettingsKey — because this allocates and the ticker runs at 5Hz.
+function M.SettingsKeyForAura(spellID, name)
+ if not name then return spellID end
+ local lower = name:lower()
+ local _, class = UnitClass("player")
+ if not class then return spellID end
+
+ for _, e in ipairs(M.GetTrackedAuraList(class) or {}) do
+ local nm = e.name or (e.spellID and GetSpellInfo(e.spellID))
+ if e.spellID and nm and nm:lower() == lower then return e.spellID end
+ end
+ for _, c in ipairs(M.GetAuraCandidates(class, true) or {}) do
+ if c.spellID and c.name and c.name:lower() == lower then return c.spellID end
+ end
+ return spellID
+end
+
+-- Auras never to auto-track, by spellID. Starts empty; populate as real noise turns up.
+M.BUFFBAR_EXCLUDE = M.BUFFBAR_EXCLUDE or {}
+
+-- Derive the include/exclude sets for one buff viewer from the single pool. An aura assigned to
+-- THIS viewer is a force-include; assigned to the other viewer, or hidden, is a force-exclude — so
+-- a bar-assigned aura never also shows as an icon.
+--
+-- Each set is keyed BY BOTH id and lowercased name, in the one table — spellIDs are numbers and
+-- names are strings, so they cannot collide. The name key is what makes an assignment survive rank:
+-- the player assigns Renew from a catalog row holding rank 1, then casts rank 14, and an id-only
+-- lookup would quietly not match. ScanTargetTrackedAuras already had to match DoTs by name for the
+-- same reason; this makes that the rule rather than the exception.
+function M.GetBuffOverrides(category)
+ local include, exclude = {}, {}
+ local _, class = UnitClass("player")
+ local tracked = class and M.GetTrackedAuraList(class)
+ if tracked then
+ for _, e in ipairs(tracked) do
+ if e.spellID then
+ local here = (category == "buffIcon" and e.assignment == "icon")
+ or (category == "buffBar" and e.assignment == "bar")
+ local set = here and include or exclude
+ set[e.spellID] = true
+ local nm = e.name or GetSpellInfo(e.spellID)
+ if nm then set[nm:lower()] = true end
+ end
+ end
+ end
+ return include, exclude
+end
+
+-- Auto-track toggle. OFF by default: a newly met aura is RECORDED (the seen registry, §H.1) and
+-- listed under Not Displayed on the Tracked Buffs tab, but nothing appears on screen until it is
+-- assigned.
+--
+-- This used to default ON, so any buff under two minutes showed itself the first time it landed. That
+-- makes trinket procs and potions work with no setup, and it also means a raid's worth of incidental
+-- buffs — food, flasks, other people's cooldowns, every proc on every trinket — arrive uninvited on a
+-- viewer the player has carefully curated. Discovery is worth keeping; automatic display is not.
+-- Turning it back on restores the old behaviour exactly, and the picker is now populated either way.
+function M.IsAutoTrackBuffs()
+ local cd = store(false)
+ if cd and cd.autoTrackBuffs ~= nil then return cd.autoTrackBuffs end
+ return false
+end
+
+-- While a spell's own buff is on the player, show the AURA's remaining time (retail's behaviour) or
+-- the spell's COOLDOWN (the default here). See the note at RefreshCooldown's aura branch: retail needs
+-- the aura on the swipe because a gold tint is its only "buffed" signal, and since §H.2 8c ours is a
+-- separate glow, so the number is free to be the more useful one.
+function M.BuffShowsAuraTime()
+ local cd = store(false)
+ if cd and cd.buffShowsAuraTime ~= nil then return cd.buffShowsAuraTime and true or false end
+ return false
+end
+
+function M.SetBuffShowsAuraTime(v)
+ local cd = store(true)
+ if cd then cd.buffShowsAuraTime = v and true or false end
+ M.RefreshActiveViewer()
+end
+
+function M.SetAutoTrackBuffs(v)
+ local cd = store(true)
+ if cd then cd.autoTrackBuffs = v and true or false end
+ M.RefreshActiveViewer("buffIcon")
+ M.RefreshActiveViewer("buffBar")
+end
+
+-- Where auto-tracked short buffs render: "icon" | "bar" | "both".
+function M.AutoTrackDest()
+ local cd = store(false)
+ if cd and cd.autoTrackDest then return cd.autoTrackDest end
+ return "both"
+end
+
+function M.SetAutoTrackDest(v)
+ local cd = store(true)
+ if cd then cd.autoTrackDest = v end
+ M.RefreshActiveViewer("buffIcon")
+ M.RefreshActiveViewer("buffBar")
+end
+
+M.BUFF_TRACK_MAX_DURATION = BUFF_TRACK_MAX_DURATION
+
+-- The per-aura decision. Explicit assignments win; otherwise the auto window applies, honouring
+-- the icon/bar/both destination. `name` is optional and trailing so existing callers still work,
+-- but passing it is what makes an assignment rank-proof — see GetBuffOverrides.
+function M.ShouldTrackBuff(spellID, duration, include, exclude, category, name)
+ local key = name and name:lower()
+ if spellID and include[spellID] then return true end
+ if key and include[key] then return true end
+ if spellID and (exclude[spellID] or M.BUFFBAR_EXCLUDE[spellID]) then return false end
+ if key and exclude[key] then return false end
+ if not M.IsAutoTrackBuffs() then return false end
+ if category then
+ local dest = M.AutoTrackDest()
+ if dest == "icon" and category ~= "buffIcon" then return false end
+ if dest == "bar" and category ~= "buffBar" then return false end
+ end
+ return (duration and duration > 0 and duration <= BUFF_TRACK_MAX_DURATION) and true or false
+end
+
+-- Tracked auras on the TARGET (DoTs). Retail registers UNIT_AURA for player and target, so a
+-- tracked debuff on the target shows in the buff viewers. This adds the target as a SECONDARY
+-- source for EXPLICITLY tracked auras only — never the auto window, which would flood the viewer
+-- with every enemy debuff. Matched by NAME so a down-ranked DoT still resolves.
+function M.ScanTargetTrackedAuras(include, shownNames)
+ local out = {}
+ if not include then return out end
+ if not (UnitExists and UnitExists("target")) then return out end
+
+ -- `include` is keyed by id AND by lowercased name (GetBuffOverrides), so both key types resolve
+ -- here: a string key IS the name, and is the only thing that works for an aura id this client
+ -- cannot hand back to GetSpellInfo.
+ local wantName = {}
+ for key in pairs(include) do
+ if type(key) == "string" then
+ wantName[key] = true
+ else
+ local nm = GetSpellInfo(key)
+ if nm then wantName[nm:lower()] = true end
+ end
+ end
+ if not next(wantName) then return out end
+
+ -- DOWNPORT: the source iterates UnitDebuff/UnitBuff directly with MODERN return positions. On
+ -- 3.3.5a every field after the first is shifted by one (`rank` at index 2). Going through
+ -- NE.aura keeps that correction in exactly one place.
+ local function scan(filter)
+ local snap = NE.aura and NE.aura.GetSnapshot("target", filter)
+ if not snap then return end
+ for i = 1, snap.n do
+ local row = snap.list[i]
+ if row.name and wantName[row.name:lower()] and not (shownNames and shownNames[row.name]) then
+ out[#out + 1] = {
+ name = row.name, icon = row.icon, count = row.count,
+ duration = row.duration, expiration = row.expiration, spellID = row.spellID,
+ }
+ if shownNames then shownNames[row.name] = true end
+ end
+ end
+ end
+ scan("HARMFUL") -- DoTs are debuffs on the target (primary case)
+ scan("HELPFUL") -- (rare) a tracked buff the player placed on the target
+ return out
+end
+
+function M.RefreshActiveViewer(category)
+ if category == nil then
+ M.ForEachViewer(function(v) if v:IsShown() then v:Rebuild() end end)
+ return
+ end
+ local viewer = M.viewers[category]
+ if viewer and viewer:IsShown() then viewer:Rebuild() end
+end
+
+-- Reset the per-character tracking data (rank-specific list overrides). Storing a SPECIFIC rank is
+-- pointless — every rank shares one cooldown and the viewer resolves the displayed rank live — and
+-- a stale snapshot also masks newly-curated defaults. Positions/scale live in DragonUI's mover
+-- store, NOT here, so this never touches layout.
+-- Reset THIS CHARACTER's lists, and nobody else's.
+--
+-- It used to blank `cd.specLayouts` and `cd.seenAura` whole. Every table under them is keyed by
+-- CLASS — customLists[category][CLASS], trackedAura[CLASS], seenAura[CLASS] — because the store is
+-- one shared DragonUI profile, not one per character. So a wipe took every class with it: a Priest
+-- pressing "reset" cleared the Warlock, the Druid and the Mage as well. Reported from the game as
+-- "the reset character button actually resets all characters", and that is exactly what it was.
+--
+-- STILL every layout BUCKET, not just the active one. That half was right and stays: the player is
+-- asking for a clean slate, not a clean slate in one talent group. What changes is that each bucket
+-- now loses only this class's slice of itself.
+function M.ResetTracking()
+ local cd = store(true)
+ if not cd then return end
+ local _, class = UnitClass("player")
+ if not class then return end
+
+ -- Force the one-time migration first. It nils the legacy top-level keys itself, so afterwards
+ -- there is exactly one home for a layout and this function has one shape to handle rather than
+ -- two. (The old code deleted those keys outright for the same reason, unscoped.)
+ if M._layoutBucket then M._layoutBucket(true) end
+
+ -- Trinket placement is keyed by "item:" with NO class dimension, so it cannot be sliced
+ -- the way the others can. Scoped instead to the tokens this character can actually see — the
+ -- trinkets it has equipped — which leaves another character's placements alone. A token for an
+ -- item nobody has equipped is unreachable in the picker anyway.
+ local myTokens = {}
+ for _, e in ipairs((M.GetEquipActiveItems and M.GetEquipActiveItems()) or {}) do
+ if e.token then myTokens[e.token] = true end
+ end
+
+ local function scrub(bucket)
+ if type(bucket) ~= "table" then return end
+ -- customLists is keyed CATEGORY first, class second, so this walks the categories and takes one
+ -- class out of each rather than dropping the category tables themselves.
+ for _, byClass in pairs(bucket.customLists or {}) do
+ if type(byClass) == "table" then byClass[class] = nil end
+ end
+ if type(bucket.trackedAura) == "table" then bucket.trackedAura[class] = nil end
+ if type(bucket.equipAssign) == "table" then
+ for token in pairs(myTokens) do bucket.equipAssign[token] = nil end
+ end
+ end
+
+ for _, bucket in pairs(cd.specLayouts or {}) do scrub(bucket) end
+ scrub(cd) -- belt and braces: a store that somehow still carries the flat legacy tables
+
+ -- Observed history goes too, for this class only. It is not a choice the player made, but leaving
+ -- it would mean "reset tracking" left rows behind that the player could not account for — and the
+ -- catalog keeps the picker populated either way, so nothing is lost but the record of one
+ -- character's procs.
+ if type(cd.seenAura) == "table" then cd.seenAura[class] = nil end
+
+ M.InvalidateCuratedCache()
+ M.ForEachViewer(function(v) if v.Rebuild and v:IsShown() then v:Rebuild() end end)
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/EditorPanel.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/EditorPanel.lua
new file mode 100644
index 0000000..d900837
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/EditorPanel.lua
@@ -0,0 +1,555 @@
+-- DragonUI_NewEra/modules/cooldownviewer/EditorPanel.lua — each viewer's settings as a dialog beside
+-- the frame, in edit mode.
+--
+-- WHAT THIS REPLACES. The first pass at retail's on-the-frame settings (§H.3.10) was a right-click
+-- CONTEXT MENU: every setting as a submenu of radios. It worked, and it was the wrong shape. Retail —
+-- and NewEra's own 1.15 edit mode, which is what the owner is comparing against — opens a small DIALOG
+-- next to the selected frame, with the sliders and dropdowns visible at once. The difference is not
+-- cosmetic: a menu shows you one setting at a time and hides the value you are trying to match, and a
+-- numeric setting inside it has to become a list of discrete rows (§H.3.10 shipped opacity in steps of
+-- 5 for exactly that reason, and had to explain in a tooltip why a value could tick nothing at all).
+-- A dialog shows every value at once and takes a real slider, so both compromises simply go away.
+--
+-- SEAM: NE.RegisterHUDFrame's `editorSettings` field — a callback, not a menu generator, so the host
+-- glue in integration/Register.lua does not care what a module chooses to open (CONTRACTS §4). Every
+-- DragonUI touch lives there: the mouse handler, the edit-mode gate, SelectEditorFrame, EditorMode.
+--
+-- WIDGETS ARE THE /cdm SETTINGS KIT (SettingsControls.lua), not a second set. One new control was
+-- needed — AddCompactSlider, a one-line slider with nudge arrows — because the tab's tall two-line
+-- slider is right for a page you scroll and wrong for a dialog that has to sit on the screen next to
+-- the thing it edits without burying it.
+--
+-- THIS IS NOW THE ONLY EDITOR FOR THESE VALUES. For a while it was the second one, with the /cdm
+-- Settings tab rendering the same thirteen controls and a refresh contract holding the two together —
+-- which SettingsOptions.lua's header forbids for good reason. The owner's call was to delete the
+-- duplicate rather than keep syncing it, so those sections left the tab and what stands in their place
+-- is four buttons that open this. The dialog still re-reads every control through col:Refresh() on
+-- open, because a layout apply or a reset can still move a value underneath it.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+-- Geometry transcribed from retail's EditModeSystemSettingsDialog, via NewEra's own transcription of
+-- it (ReferenceAddons/NewEra/EditMode/SettingsPopup.lua): 343×32 rows spaced 2, a 100px label column,
+-- 20px content inset each side, and the buttons region at the bottom.
+local ROW_W, ROW_H, ROW_GAP = 343, 32, 2
+local LABEL_W = 100
+local CONTENT_X = 20
+local PANEL_W = ROW_W + (CONTENT_X * 2)
+local BODY_TOP = 43 -- title at TOP -15, options at its BOTTOM -12
+local BTN_H = 28
+-- Revert and Reset sit SIDE BY SIDE on one row (owner steer). Stacked, they cost 46px of a dialog
+-- that has to sit on screen next to the frame it edits, and they read as a list of two things to
+-- work down rather than the pair of alternatives they are.
+local BTN_GAP = 3
+local BTN_W = math.floor((ROW_W - BTN_GAP) / 2)
+local FOOTER_H = 15 + BTN_H + 10 -- button row, divider above it
+local SIDE_GAP = 12 -- clearance between the dialog and the frame it edits
+
+-- Same value/label pairs, in the same order, as the /cdm Settings tab.
+local ORIENTATION = { { "horizontal", "Horizontal" }, { "vertical", "Vertical" } }
+local DIRECTION = { { "right", "Right" }, { "left", "Left" } }
+local VISIBILITY = { { "always", "Always" }, { "incombat", "In Combat" }, { "hidden", "Hidden" } }
+local BAR_CONTENT = { { "iconAndName", "Icon and Name" }, { "iconOnly", "Icon Only" },
+ { "nameOnly", "Name Only" } }
+
+local function pct(v) return tostring(v) .. "%" end
+local function px(v) return tostring(v) .. "px" end
+
+local panel -- the one dialog
+local pages = {} -- category -> { body, col, dirtyCheck }
+local current -- category currently shown
+local snapshots = {} -- category -> the values Revert goes back to, per editor session
+
+-- THE TAB IS NO LONGER A SECOND VIEW ONTO THESE VALUES. It was, and every write here called
+-- CDS.RefreshSettingsPage to stop the two drifting; the owner's call was to remove the duplicate
+-- rather than keep syncing it, so the tab now carries only settings this dialog does not. That makes
+-- the notify dead weight on a path that runs on every tick of a slider drag, so it is gone. If a
+-- per-viewer control is ever added back to the tab, this is the contract it has to restore —
+-- SettingsOptions.lua's header is where that rule is written down.
+
+local function specFor(category)
+ for _, s in ipairs(M.VIEWER_SPECS or {}) do
+ if s.category == category then return s end
+ end
+ return nil
+end
+
+-- ── Revert ──────────────────────────────────────────────────────────────────────────────────────
+--
+-- "Revert Changes" goes back to how this viewer was when the editor was opened, NOT to defaults —
+-- that is what the button below it is for, and conflating the two is how someone loses a setup they
+-- spent ten minutes on. So the snapshot is taken the first time the dialog is opened for a viewer in
+-- an editor session, and dropped when the editor closes.
+
+local function snapshotOf(category)
+ local frameID = M.FRAME_ID[category]
+ local t = { _enabled = M.IsCategoryEnabled(category) }
+ for key in pairs(M.DEFAULTS or {}) do t[key] = M.GetOpt(frameID, key) end
+ return t
+end
+
+local function ensureSnapshot(category)
+ if not snapshots[category] then snapshots[category] = snapshotOf(category) end
+ return snapshots[category]
+end
+
+local function isDirty(category)
+ local snap = snapshots[category]
+ if not snap then return false end
+ local now = snapshotOf(category)
+ for key, v in pairs(snap) do
+ if now[key] ~= v then return true end
+ end
+ return false
+end
+
+local function revert(category)
+ local snap = snapshots[category]
+ if not snap then return end
+ local frameID = M.FRAME_ID[category]
+ for key, v in pairs(snap) do
+ if key ~= "_enabled" then M.SetOpt(frameID, key, v) end
+ end
+ M.SetCategoryEnabled(category, snap._enabled)
+end
+
+-- ── The Reset confirm ───────────────────────────────────────────────────────────────────────────
+--
+-- OUR OWN FRAME, INSIDE THE DIALOG. It was a StaticPopup, and it kept opening underneath DragonUI's
+-- Exit Edit Mode / Reset All Positions buttons — which sit on UIParent at TOOLTIP frame level 1000
+-- (DragonUI/modules/editor_mode.lua:190, 210) and park at screen centre, exactly where a StaticPopup
+-- lands. Two attempts at out-stacking them failed in game (first aiming at the wrong frame, then
+-- reading the level at show time), so the fight is not worth having: a confirm parented to THIS DIALOG
+-- draws inside the dialog's own stacking, above it by construction, and appears beside the viewer
+-- being edited rather than in the middle of the screen where those buttons live. There is nothing left
+-- for it to lose to.
+--
+-- It is also MODAL to the dialog — a blocker fills the panel behind it — because the question is about
+-- the very settings underneath, and letting someone go on nudging sliders behind an unanswered "are
+-- you sure?" invites answering it about a different state than the one they read.
+--
+-- The text is rewritten per viewer at ask time. A generic "are you sure?" over four
+-- differently-configured viewers is the kind of prompt people learn to click through.
+
+local CONFIRM_W = PANEL_W - 60
+local CONFIRM_BTN_W = 110
+
+local function ensureConfirm(f)
+ if f.confirm then return f.confirm end
+
+ -- Fills the dialog, eats every click meant for the controls behind. Below the confirm, above
+ -- everything else the panel holds.
+ local blocker = CreateFrame("Frame", nil, f)
+ blocker:SetAllPoints(f)
+ blocker:EnableMouse(true)
+ blocker:SetFrameLevel(f:GetFrameLevel() + 10)
+ blocker:Hide()
+ local dim = blocker:CreateTexture(nil, "BACKGROUND")
+ dim:SetAllPoints()
+ dim:SetTexture(0, 0, 0, 0.55)
+
+ local c = CreateFrame("Frame", "NE_CDMEditorConfirm", f)
+ c:SetWidth(CONFIRM_W)
+ c:SetPoint("CENTER", f, "CENTER", 0, 0)
+ c:SetFrameLevel(blocker:GetFrameLevel() + 10)
+ c:EnableMouse(true)
+ c:Hide()
+
+ -- Same chrome as the dialog it sits in: black under the DiamondMetal "Dialog" nineslice. Opaque
+ -- rather than the dialog's 0.8, so the controls it is asking about do not read through it.
+ local bg = c:CreateTexture(nil, "BACKGROUND", nil, -5)
+ bg:SetTexture(0, 0, 0, 0.95)
+ bg:SetPoint("TOPLEFT", 7, -7)
+ bg:SetPoint("BOTTOMRIGHT", -7, 7)
+ c.Bg = bg
+ if NE.nineslice and NE.nineslice.ApplyLayout then
+ pcall(NE.nineslice.ApplyLayout, c, "Dialog")
+ end
+
+ -- WIDTH, not two anchors. Anchoring TOPLEFT and TOPRIGHT does give the FontString a width, but on
+ -- 3.3.5a that width TRUNCATES with an ellipsis instead of wrapping — the second paragraph came out as
+ -- "This viewer's position, size, orientation and vis...". An explicit SetWidth wraps, which is the
+ -- same reason SettingsControls' AddText sets one.
+ c.Text = c:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
+ c.Text:SetWidth(CONFIRM_W - 36)
+ c.Text:SetPoint("TOP", c, "TOP", 0, -20)
+ c.Text:SetJustifyH("CENTER")
+
+ local function confirmButton(label, w, onClick)
+ local b = CreateFrame("Button", nil, c, "UIPanelButtonTemplate")
+ b:SetSize(w, BTN_H)
+ b:SetText(label)
+ b:SetScript("OnClick", onClick)
+ if NE.button and NE.button.Skin then pcall(NE.button.Skin, b) end
+ return b
+ end
+
+ c.YesButton = confirmButton(YES or "Yes", CONFIRM_BTN_W, function()
+ local cat = c._category
+ M.HideConfirm()
+ if not cat then return end
+ M.ResetOpts(M.FRAME_ID[cat])
+ M.RefreshEditorPanel()
+ end)
+ c.YesButton:SetPoint("BOTTOMRIGHT", c, "BOTTOM", -4, 16)
+
+ c.NoButton = confirmButton(NO or "No", CONFIRM_BTN_W, function() M.HideConfirm() end)
+ c.NoButton:SetPoint("BOTTOMLEFT", c, "BOTTOM", 4, 16)
+
+ c.Blocker = blocker
+ f.confirm = c
+ return c
+end
+
+-- Ask about `category`. Public so the test can drive the same path the button does.
+function M.ShowConfirmReset(category)
+ if not (panel and category) then return false end
+ local c = ensureConfirm(panel)
+ local spec = specFor(category)
+ c._category = category
+ c.Text:SetText("Reset " .. ((spec and spec.label) or category) ..
+ " to its default layout?\n\nThis viewer's position, size, orientation and visibility all go back " ..
+ "to stock. Nothing else is affected, and it cannot be undone.")
+ -- Sized to the wrapped text. The floor covers the offline harness, whose FontString cannot measure.
+ local textH = math.max(60, (c.Text.GetStringHeight and c.Text:GetStringHeight() or 0) + 4)
+ c:SetHeight(20 + textH + 18 + BTN_H + 16)
+ c.Blocker:Show()
+ c:Show()
+ return true
+end
+
+function M.HideConfirm()
+ local c = panel and panel.confirm
+ if not c then return end
+ c._category = nil
+ c:Hide()
+ c.Blocker:Hide()
+end
+
+M.IsConfirmShown = function()
+ local c = panel and panel.confirm
+ return (c and c:IsShown()) and true or false
+end
+
+-- ── One viewer's page ───────────────────────────────────────────────────────────────────────────
+
+local function buildPage(category)
+ local Kit = NE.cooldownviewersettings and NE.cooldownviewersettings.controls
+ if not (Kit and Kit.New) then return nil end
+
+ local frameID = M.FRAME_ID[category]
+ local spec = specFor(category)
+
+ local body = CreateFrame("Frame", nil, panel)
+ body:SetPoint("TOPLEFT", panel, "TOPLEFT", CONTENT_X, -BODY_TOP)
+ body:SetWidth(ROW_W)
+ body:Hide()
+
+ -- The dialog's metrics, not the Settings tab's. Same widgets either way — see the theme note in
+ -- SettingsControls.lua.
+ local col = Kit.New(body, ROW_W, {
+ labelFont = "GameFontHighlightMedium",
+ checkSize = 32,
+ checkH = ROW_H,
+ sliderH = ROW_H,
+ dropH = ROW_H,
+ labelW = LABEL_W,
+ -- controlW deliberately unset: the dropdowns FILL the control column, so their left edge lands on
+ -- the compact slider's nudge arrow and their right edge on its value text. A fixed 200 lined up
+ -- with neither.
+ dropdownArt = true,
+ buttonH = BTN_H + 4,
+ buttonArtH = BTN_H,
+ })
+
+ local function get(key) return M.GetOpt(frameID, key) end
+ -- Every write goes through here: store, then the OTHER view, then this dialog's own Revert state.
+ local function set(key)
+ return function(v)
+ M.SetOpt(frameID, key, v)
+ if panel and panel.UpdateRevert then panel.UpdateRevert() end
+ end
+ end
+ local function getter(key) return function() return get(key) end end
+
+ col:AddCheckbox({
+ label = "Enabled",
+ desc = "Show this viewer at all. The editor handle stays either way, so this is reversible from "
+ .. "right here.",
+ get = function() return M.IsCategoryEnabled(category) end,
+ set = function(v)
+ M.SetCategoryEnabled(category, v)
+ if panel and panel.UpdateRevert then panel.UpdateRevert() end
+ end,
+ })
+
+ col:AddDropdown({
+ label = "Orientation", values = ORIENTATION,
+ get = getter("orientation"), set = set("orientation"),
+ })
+ col:AddCompactSlider({
+ label = "Icon Limit", min = 1, max = 20, step = 1,
+ desc = "How many icons before the layout wraps. Vertical orientation reads this as icons per "
+ .. "column.",
+ get = getter("iconLimit"), set = set("iconLimit"),
+ })
+ col:AddDropdown({
+ label = "Icon Direction", values = DIRECTION,
+ get = getter("iconDirection"), set = set("iconDirection"),
+ })
+ col:AddCompactSlider({
+ label = "Icon Size", min = 50, max = 200, step = 10, format = pct,
+ get = getter("iconSize"), set = set("iconSize"),
+ })
+ col:AddCompactSlider({
+ label = "Icon Padding", min = 0, max = 14, step = 1, format = px,
+ desc = "Gap between icons. Retail offsets this by -4, so the low end overlaps slightly — that is "
+ .. "the stock look, not a bug.",
+ get = getter("iconPadding"), set = set("iconPadding"),
+ })
+ col:AddCompactSlider({
+ label = "Opacity", min = 50, max = 100, step = 1, format = pct,
+ get = getter("opacity"), set = set("opacity"),
+ })
+ col:AddDropdown({
+ label = "Visibility", values = VISIBILITY,
+ desc = "When this viewer is on screen at all. Hidden still leaves the editor handle here.",
+ get = getter("visibleSetting"), set = set("visibleSetting"),
+ })
+
+ -- Only where it does something: retail's Essential/Utility templates do not set
+ -- allowHideWhenInactive, so UpdateShownState ignores the setting there. The /cdm tab drops the
+ -- control for the same reason, and a row that silently does nothing is worse than no row.
+ if get("allowHideWhenInactive") then
+ col:AddCheckbox({
+ label = "Hide When Inactive",
+ desc = "Show a slot only while its aura is active.",
+ get = function() return get("hideWhenInactive") and true or false end,
+ set = set("hideWhenInactive"),
+ })
+ end
+
+ col:AddCheckbox({
+ label = "Show Timer",
+ desc = "Draw the countdown number on each icon.",
+ get = function() return get("showTimer") and true or false end,
+ set = set("showTimer"),
+ })
+ col:AddCheckbox({
+ label = "Show Tooltips",
+ desc = "Show a tooltip when hovering an icon.",
+ get = function() return get("showTooltips") and true or false end,
+ set = set("showTooltips"),
+ })
+
+ -- Bar-only, exactly as retail exposes them (the BuffBar system alone).
+ if spec and spec.bar then
+ col:AddDropdown({
+ label = "Bar Content", values = BAR_CONTENT,
+ get = getter("barContent"), set = set("barContent"),
+ })
+ col:AddCompactSlider({
+ label = "Bar Width", min = 50, max = 200, step = 5, format = pct,
+ get = getter("barWidthScale"), set = set("barWidthScale"),
+ })
+ end
+
+ -- Retail's AddExtraButtons puts a system's own extra actions at the END of the options stack,
+ -- above the Revert/Reset region — which is where NewEra's dialog carries this one too.
+ col:AddButton({
+ label = "Cooldown Manager Settings",
+ width = ROW_W,
+ desc = "Closes edit mode and opens the Cooldown Manager window, which carries the settings that "
+ .. "are not per-viewer: alerts, ready sounds, buff tracking, icon fit and the resets.",
+ onClick = function()
+ -- Leave the editor first: it covers the screen, and DragonUI saves every frame's position on
+ -- the way out, so nothing is lost by going this way.
+ if NE.CloseFrameEditor then NE.CloseFrameEditor() end
+ if M.OpenSettingsPanel then M.OpenSettingsPanel("settings") end
+ end,
+ })
+
+ col:Relayout()
+ local page = { body = body, col = col, category = category }
+ pages[category] = page
+ return page
+end
+
+-- ── The dialog ──────────────────────────────────────────────────────────────────────────────────
+
+local function ensurePanel()
+ if panel then return panel end
+
+ local f = CreateFrame("Frame", "NE_CDMEditorPanel", UIParent)
+ f:SetSize(PANEL_W, 200)
+ -- Above the editor's own handles, which CreateUIFrame puts at FULLSCREEN. A settings dialog that
+ -- renders behind the frame it configures is not a settings dialog.
+ f:SetFrameStrata("FULLSCREEN_DIALOG")
+ f:SetClampedToScreen(true)
+ f:EnableMouse(true)
+ f:SetMovable(true)
+ f:RegisterForDrag("LeftButton")
+ f:SetScript("OnDragStart", function(self) self:StartMoving() end)
+ f:SetScript("OnDragStop", function(self)
+ self:StopMovingOrSizing()
+ self._moved = true -- once you place it yourself, it stops jumping to each frame you select
+ end)
+ f:Hide()
+
+ -- CHROME: retail's DialogBorderTranslucentTemplate — black at 0.8 inset 7, under the DiamondMetal
+ -- "Dialog" nineslice. NOT PanelChrome's portrait frame, which is window chrome: applied to a small
+ -- floating dialog it brings the rock fill and the portrait ring, and reads as a window that lost
+ -- its contents.
+ local bg = f:CreateTexture(nil, "BACKGROUND", nil, -5)
+ bg:SetTexture(0, 0, 0, 0.8)
+ bg:SetPoint("TOPLEFT", 7, -7)
+ bg:SetPoint("BOTTOMRIGHT", -7, 7)
+ f.Bg = bg
+ if NE.nineslice and NE.nineslice.ApplyLayout then
+ pcall(NE.nineslice.ApplyLayout, f, "Dialog")
+ end
+
+ f.TitleText = f:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
+ f.TitleText:SetPoint("TOP", 0, -15)
+
+ local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
+ close:SetPoint("TOPRIGHT")
+ local PC = NE.chrome
+ if PC and PC.ModernizeCloseButton then
+ pcall(PC.ModernizeCloseButton, close, { anchor = false })
+ end
+ close:SetScript("OnClick", function() M.HideEditorPanel() end)
+ f.CloseButton = close
+
+ -- Buttons region: Revert and Reset as a pair on one row, under a divider. Built once and pointed at
+ -- whichever viewer is showing — two buttons differing only in which category they act on would be
+ -- two copies of this block. "Cooldown Manager Settings" is NOT here; retail puts a system's extra
+ -- actions at the end of the options stack, and so does buildPage.
+ local function footerButton(label, w, onClick)
+ local b = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
+ b:SetSize(w, BTN_H)
+ b:SetText(label)
+ b:SetScript("OnClick", function() if current then onClick(current) end end)
+ if NE.button and NE.button.Skin then pcall(NE.button.Skin, b) end
+ return b
+ end
+
+ f.buttonsDivider = f:CreateTexture(nil, "ARTWORK")
+ f.buttonsDivider:SetTexture("Interface\\FriendsFrame\\UI-FriendsFrame-OnlineDivider")
+ f.buttonsDivider:SetSize(330, 16)
+ f.buttonsDivider:SetPoint("BOTTOM", 0, 15 + BTN_H + 1)
+
+ f.revertButton = footerButton("Revert Changes", BTN_W, function(category)
+ revert(category)
+ M.RefreshEditorPanel()
+ end)
+ f.revertButton:SetPoint("BOTTOMLEFT", CONTENT_X, 15)
+
+ -- Reset CONFIRMS. Revert is bounded — it undoes this editor session and the button greys itself out
+ -- when there is nothing to undo — but Reset throws away every layout choice ever made for this
+ -- viewer, and nothing in the addon can put it back. The two now sit a few pixels apart, so the one
+ -- that is not reversible asks.
+ f.resetButton = footerButton("Reset to Default", BTN_W, function(category)
+ M.ShowConfirmReset(category)
+ end)
+ f.resetButton:SetPoint("BOTTOMRIGHT", -CONTENT_X, 15)
+
+ -- Revert is disabled until there IS something to revert. A button that is always live and usually
+ -- does nothing teaches you to ignore it, and this one is the undo.
+ function f.UpdateRevert()
+ local on = current and isDirty(current)
+ if on then f.revertButton:Enable() else f.revertButton:Disable() end
+ end
+
+ panel = f
+ return f
+end
+
+-- Place beside the frame being edited, on whichever side has room. Skipped once the player has
+-- dragged the dialog somewhere themselves.
+--
+-- ANCHORED TO UIParent, NOT TO THE FRAME, and that is the whole point. A relative point kept the
+-- dialog glued to the viewer's edge — so dragging Icon Size or Icon Limit resized the viewer and the
+-- dialog SLID SIDEWAYS under the cursor, mid-drag, which is unusable. The position is resolved once,
+-- here, into screen coordinates; the dialog then stays where it was put until another frame is
+-- selected. The trade is that a viewer which grows a lot can end up overlapping its own dialog, and
+-- a dialog that holds still is worth more than one that never touches the frame.
+local function place(f, anchor)
+ if f._moved then return end
+ f:ClearAllPoints()
+ local sw = (UIParent and UIParent:GetWidth()) or 1024
+ local cx = anchor and anchor.GetCenter and select(1, anchor:GetCenter())
+ local left = anchor and anchor.GetLeft and anchor:GetLeft()
+ local right = anchor and anchor.GetRight and anchor:GetRight()
+ local top = anchor and anchor.GetTop and anchor:GetTop()
+ if not (cx and left and right and top) then
+ f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
+ elseif cx > sw / 2 then
+ f:SetPoint("TOPRIGHT", UIParent, "BOTTOMLEFT", left - SIDE_GAP, top)
+ else
+ f:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", right + SIDE_GAP, top)
+ end
+end
+
+-- ── API ─────────────────────────────────────────────────────────────────────────────────────────
+
+function M.ShowEditorPanel(category, anchor)
+ if not (category and M.FRAME_ID and M.FRAME_ID[category]) then return false end
+ local f = ensurePanel()
+ if not f then return false end
+
+ local page = pages[category] or buildPage(category)
+ if not page then return false end
+
+ for cat, p in pairs(pages) do
+ if cat ~= category then p.body:Hide() end
+ end
+ page.body:Show()
+ -- An unanswered confirm names ONE viewer and acts on `current`; selecting another frame would leave
+ -- it asking about the old one and resetting the new. It goes with the page it belongs to.
+ if current ~= category then M.HideConfirm() end
+ current = category
+
+ -- Re-read every control before showing. Something else may have moved these underneath us — the
+ -- /cdm tab, a layout apply, a reset — and a dialog that opens on stale values is indistinguishable
+ -- from one whose settings did not take.
+ page.col:Refresh()
+ local h = page.col:Relayout()
+
+ local spec = specFor(category)
+ f.TitleText:SetText((spec and spec.label) or category)
+
+ -- Size to the page. Height varies by viewer — Buff Bars carries two rows nothing else does — so
+ -- this is computed rather than a constant that would clip one of them. The buttons region is
+ -- anchored to the bottom edge, so it follows for free.
+ f:SetHeight(BODY_TOP + h + 10 + FOOTER_H)
+
+ ensureSnapshot(category)
+ f.UpdateRevert()
+ place(f, anchor or (M.viewers and M.viewers[category] and M.viewers[category].editorAnchor))
+ f:Show()
+ return true
+end
+
+-- Re-read the open page. Used by the footer buttons, which change values without going through a
+-- control's own setter.
+function M.RefreshEditorPanel()
+ local page = current and pages[current]
+ if not page then return end
+ page.col:Refresh()
+ if panel and panel.UpdateRevert then panel.UpdateRevert() end
+end
+
+-- Closing the editor drops the Revert snapshots: "revert" means "back to how this was when I started
+-- editing", and once you have left, that session is over. Keeping them would silently arm the button
+-- with a state from an hour ago.
+function M.HideEditorPanel()
+ M.HideConfirm()
+ snapshots = {}
+ current = nil
+ if panel then panel:Hide() end
+end
+
+M.IsEditorPanelShown = function() return (panel and panel:IsShown()) and true or false end
+M._editorPanel = function() return panel, pages end -- test seam
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Equip.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Equip.lua
new file mode 100644
index 0000000..9c7495c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Equip.lua
@@ -0,0 +1,131 @@
+-- DragonUI_NewEra/modules/cooldownviewer/Equip.lua — on-use trinket discovery and the per-item
+-- assignment registry. Downport of NewEra/CooldownViewer/CooldownViewerEquip.lua (182 lines).
+--
+-- THE MODEL (retail PTR 12.1.0 EquipSlotEssential/EquipSlotTracked, which upstream reproduces).
+-- An item cooldown does not belong to a viewer by default. It is DISCOVERED, keyed by a stable
+-- token, and parked in a source pool — "Equip Active" in the /cdm panel — with no on-screen
+-- presence at all. The player drags it into Essential or Utility to make it show. Assignment is
+-- persisted per token, so re-equipping the same trinket restores the choice:
+--
+-- nil unassigned — in the source pool, in no viewer (the default)
+-- "essential" / "utility" shown in that cooldown viewer
+-- "hidden" explicitly removed by the player (NOT the same as unassigned:
+-- an absent key falls back to the class default, a present one never
+-- does, so "hidden" has to be storable)
+--
+-- WHAT IS PORTED, AND WHAT IS NOT
+--
+-- Trinkets port exactly, and they cost nothing in curation: GetInventoryItemID gives the item in
+-- each trinket slot and GetItemSpell gives its on-use spell. Every id comes from the client at
+-- runtime; nothing is typed. That is the whole active-trinket feature.
+--
+-- CONSUMABLES ARE DEFERRED, and the reason is data, not effort. Upstream's M.POTION_CATEGORIES is
+-- a hand-curated table of ~45 VANILLA item ids bucketed into Healing/Mana/Healthstone/Soulstone/
+-- Combat, best-rank-first, so the runtime can show ONE slot per bucket for the best item held.
+-- WotLK adds a whole tier to every one of those buckets (Runic potions, the Fel/Master healthstone
+-- line, Endless-series flasks), and this project's prime directive is that ids come from client
+-- data, never from memory. The 3.3.5a client cannot supply them: Item.dbc carries no item names and
+-- no item→spell link — both live server-side in item_template — so there is nothing to generate a
+-- bucketed list FROM. PORT_PLAN §G.9 scopes the way through (classify at runtime by the use-spell's
+-- Spell.dbc effect, which IS client data), and that is a generator pass rather than a paste.
+--
+-- THE PASSIVE POOL IS CUT, and this is not a scheduling decision. Upstream's GetEquipPassiveItems
+-- walks the same two trinket slots and surfaces each one's USE-spell as a trackable aura. So a proc
+-- trinket — the only kind for which a passive aura row would mean anything — returns nil from
+-- GetItemSpell and never appears; the pool can only ever contain on-use trinkets that are already
+-- rows in the active pool. Upstream flags this itself ("Era has no on-equip-aura data layer",
+-- TODO(hydrate)). Shipping it here would add a second source section whose contents duplicate the
+-- first. The equipPassive category and its half of the legal-target matrix go with it.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+-- Slot ids, from the client's own globals where it defines them. 13/14 is the 3.3.5a literal.
+M.TRINKET_SLOTS = { INVSLOT_TRINKET1 or 13, INVSLOT_TRINKET2 or 14 }
+
+-- ── Assignment registry ─────────────────────────────────────────────────────────────────────────
+
+-- Same accessor shape as CooldownViewer.lua's file-local `store`, which is not exported. Two lines
+-- is cheaper than widening that file's surface for one consumer.
+local function store(create)
+ local cfg = NE.Config and NE.Config()
+ if not cfg then return nil end
+ if not cfg.cooldownviewer then
+ if not create then return nil end
+ cfg.cooldownviewer = {}
+ end
+ return cfg.cooldownviewer
+end
+
+-- Per-class defaults ("the Cooldown Manager must include X for this class"). Upstream's only
+-- entries are the Warlock's Healthstone and Soulstone, which are potion-category tokens — so this
+-- is empty until consumables land, and it stays here so §G.9 is a data change, not a code change.
+M.DEFAULT_EQUIP_BY_CLASS = {}
+
+local function defaultEquipAssignment(token)
+ if not token then return nil end
+ local _, class = UnitClass("player")
+ local byClass = class and M.DEFAULT_EQUIP_BY_CLASS[class]
+ return byClass and byClass[token] or nil
+end
+
+function M.GetEquipAssignment(token)
+ if not token then return nil end
+ -- Trinket placement is layout, so it follows the talent group like the spell lists do.
+ local cd = M._layoutBucket and M._layoutBucket(false)
+ local reg = cd and cd.equipAssign
+ -- A PRESENT key — including "hidden" — is an explicit choice and wins. Only a genuinely absent
+ -- token falls through to the class default, which is what makes "hidden" distinct from unassigned.
+ if reg and reg[token] ~= nil then return reg[token] end
+ return defaultEquipAssignment(token)
+end
+
+function M.SetEquipAssignment(token, cat)
+ if not token then return end
+ local cd = M._layoutBucket and M._layoutBucket(true)
+ if not cd then return end
+ cd.equipAssign = cd.equipAssign or {}
+ cd.equipAssign[token] = cat -- nil clears back to the source pool
+end
+
+function M.ResetEquipAssignments()
+ local cd = M._layoutBucket and M._layoutBucket(true)
+ if cd then cd.equipAssign = {} end
+end
+
+-- ── Discovery ───────────────────────────────────────────────────────────────────────────────────
+
+-- Every equipped trinket that has an on-use spell. Entries are what the viewer's Rebuild loop and
+-- the settings panel both consume:
+-- { kind = "active", source = "trinket", token = "item:", itemID, spellID, slot, label }
+--
+-- `label` is the USE-SPELL's name, not the item's — that is what GetItemSpell returns, and it is
+-- the more useful of the two on a 38px tile ("Speed" beats "Mirror of Truth"). It can be nil until
+-- the server caches the item; GET_ITEM_INFO_RECEIVED re-renders when it lands.
+function M.GetEquipActiveItems()
+ local out = {}
+ if not GetInventoryItemID then return out end
+ for _, slot in ipairs(M.TRINKET_SLOTS) do
+ local itemID = GetInventoryItemID("player", slot)
+ if itemID then
+ local name, spellID = M.GetItemUseSpell(itemID)
+ if spellID then
+ out[#out + 1] = { kind = "active", source = "trinket", token = "item:" .. itemID,
+ itemID = itemID, spellID = spellID, slot = slot, label = name }
+ end
+ end
+ end
+ return out
+end
+
+-- The discovered items assigned to one VIEWER category. Note the argument is a viewer category
+-- ("essential" / "utility"), not a settings category id — assignment values are stored in viewer
+-- terms so this comparison is direct, and SettingsAdapter maps its own ids onto them.
+function M.GetEquipItemsForCategory(cat)
+ local out = {}
+ if not cat then return out end
+ for _, e in ipairs(M.GetEquipActiveItems()) do
+ if M.GetEquipAssignment(e.token) == cat then out[#out + 1] = e end
+ end
+ return out
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/ItemMixins.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/ItemMixins.lua
new file mode 100644
index 0000000..6495a10
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/ItemMixins.lua
@@ -0,0 +1,1235 @@
+-- DragonUI_NewEra/modules/cooldownviewer/ItemMixins.lua — the per-item layer of the Cooldown
+-- Manager: one tile that binds a spell (or an on-use item) and renders its cooldown.
+--
+-- DOWNPORT of NewEra/CooldownViewer/ItemMixins.lua, Phase 1 scope: the COOLDOWN item mixin only
+-- (Essential + Utility). The aura mixins (BuffIcon / BuffBar) are Phase 3 and are not ported here.
+--
+-- DEVIATIONS from the 1.15 source, all forced by the 3.3.5a widget/API surface:
+--
+-- * MaskTexture — removed. Legion widget; unavailable, unpolyfillable, and an unknown XML
+-- node breaks the whole file's parse. CONTRACTS §0 lists SetMask as a hard
+-- rule. The rounded look now comes from CropIcon + the IconOverlay art.
+-- * SetSwipeTexture — removed (WoD+). We take the engine's built-in sweep.
+-- * SetSwipeColor — guarded (WoD+). The gold-aura vs black-cooldown distinction is therefore
+-- not colour-coded on this client; the aura path still drives the swipe.
+-- * SetShown — replaced with Show/Hide (CONTRACTS §0 hard rule).
+-- * C_Spell.IsSpellUsable / IsSpellInRange — neither !!!ClassicAPI nor compat/C_Spell.lua provides
+-- these, so we go straight to the 3.3.5a globals IsUsableSpell / IsSpellInRange.
+-- The source already carried those as fallbacks.
+-- * highestKnownRankID — REWRITTEN. The source uses `select(7, GetSpellInfo(name))`, which is the
+-- spellID on Era/TBC's modern engine but is **castTime** on 3.3.5a (the WotLK
+-- signature is name, rank, icon, cost, isFunnel, powerType, castTime, minRange,
+-- maxRange — see !!!ClassicAPI/Util/C_Spell.lua:39). Ported verbatim it would
+-- return e.g. 1500 for a 1.5s cast and use it as a spell ID. We resolve
+-- through NE.spellbook (core/SpellRanks.lua) instead.
+
+local NE = DragonUI_NewEra
+NE.cooldownviewer = NE.cooldownviewer or {}
+local M = NE.cooldownviewer
+
+local SB = NE.spellbook
+
+-- Atlas wiring for one item tile. Era's engine atlas DB doesn't know retail nicknames; resolve via
+-- NE.tex.SetAtlas. DOWNPORT: the IconMask branch is gone (see header). Every call is guarded so a
+-- not-yet-shipped atlas degrades to a plain icon rather than erroring.
+local function applyItemAtlases(item)
+ local set = NE.tex and NE.tex.SetAtlas
+ if not set then return end
+ if item.IconOverlay then set(item.IconOverlay, "UI-HUD-CoolDownManager-IconOverlay", false) end
+ -- The stacked copies that deepen the frame. Same atlas, same rect — see Frame strength.
+ for _, t in ipairs(item.IconOverlayStack or {}) do
+ set(t, "UI-HUD-CoolDownManager-IconOverlay", false)
+ end
+ if item.OutOfRange then set(item.OutOfRange, "UI-CooldownManager-OORshadow", false) end
+
+ -- The buff glow textures itself in M.BuildBuffGlow — it is a frame with a texture inside, not a
+ -- region on the tile, so it has nothing to collect here.
+
+ -- The ready-flash sprite. The retail GCD flipbook atlas is not registered on this client, so give
+ -- the texture the fallback highlight the stepper pulses instead (see the Ready flash section).
+ local flash = item.CooldownFlash
+ if flash and flash.Flipbook then
+ if NE.tex.HasAtlas and NE.tex.HasAtlas("UI-HUD-ActionBar-GCD-Flipbook") then
+ set(flash.Flipbook, "UI-HUD-ActionBar-GCD-Flipbook", false)
+ else
+ flash.Flipbook:SetTexture(M.FLASH_FALLBACK_TEXTURE)
+ flash.Flipbook:SetBlendMode("ADD")
+ -- Warm gold reads as "ready" and stands clear of the icon art underneath.
+ flash.Flipbook:SetVertexColor(1, 0.92, 0.55)
+ end
+ flash.Flipbook:SetAlpha(0)
+ end
+ -- Normalise the inconsistent baked borders on 3.3.5a icon art (8% zoom). This is also what stands
+ -- in for the removed rounded mask.
+ if NE.tex.CropIcon then
+ NE.tex.CropIcon(item.Icon)
+ elseif item.Icon and item.Icon.SetTexCoord then
+ item.Icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
+ end
+end
+
+NE_CooldownViewerItemMixin = {}
+local ItemMixin = NE_CooldownViewerItemMixin
+
+function ItemMixin:OnLoad()
+ applyItemAtlases(self)
+ if self.OutOfRange then self.OutOfRange:Hide() end
+
+ -- Countdown numbers. On 3.3.5a the Cooldown widget draws no text at all, so NE.cd owns a
+ -- FontString + throttled OnUpdate (core/CooldownNumbers.lua). Same call shape as the source.
+ NE.cd.ApplyNumbers(self.Cooldown, {
+ font = self.cooldownFont or NE.cd.FONT.viewerEssential,
+ })
+
+ -- Retail renders drawSwipe=true, drawEdge=false, drawBling=true. 3.3.5a has none of these
+ -- setters; guarded so this is a no-op here and correct if a future client gains them.
+ if self.Cooldown then
+ if self.Cooldown.SetDrawSwipe then self.Cooldown:SetDrawSwipe(true) end
+ if self.Cooldown.SetDrawEdge then self.Cooldown:SetDrawEdge(false) end
+ if self.Cooldown.SetDrawBling then self.Cooldown:SetDrawBling(true) end
+ end
+end
+
+local BOOKTYPE_SPELL_ = BOOKTYPE_SPELL or "spell"
+
+-- Every rank spellID of an ability, so the cooldown read can find whichever rank is actually
+-- ticking. Our curated lists key each ability by its RANK 1 spellID (Devouring Plague = 2944,
+-- Mind Blast = 8092), but the client tracks a cooldown on the EXACT rank cast — GetSpellCooldown
+-- on the rank-1 id (or the bare name) reads 0 once you cast a higher rank, and players routinely
+-- cast several ranks (down-ranking for mana). WotLK still has ranks, so this matters here exactly
+-- as it did on 1.15.
+--
+-- DOWNPORT: primary source is now NE.spellbook (core/SpellRanks.lua), built by scanning the live
+-- spellbook, rather than NewEra's generated db2 dump. The book-scan fallback below still covers
+-- anything the table missed (racials, item use-spells).
+local function knownRankIDs(spellID, name)
+ local ids, seen, n = nil, {}, 0
+ local function add(id)
+ if id and not seen[id] then seen[id] = true; ids = ids or {}; n = n + 1; ids[n] = id end
+ end
+ add(spellID)
+
+ local list = SB and SB.KnownRankIDs and name and SB.KnownRankIDs(name)
+ if list then for _, id in ipairs(list) do add(id) end end
+
+ if name and GetNumSpellTabs and GetSpellBookItemName and GetSpellBookItemInfo then
+ for tab = 1, (GetNumSpellTabs() or 0) do
+ local _, _, offset, numSlots = GetSpellTabInfo(tab)
+ if offset and numSlots then
+ for s = offset + 1, offset + numSlots do
+ if GetSpellBookItemName(s, BOOKTYPE_SPELL_) == name then
+ local _, sid = GetSpellBookItemInfo(s, BOOKTYPE_SPELL_)
+ add(sid)
+ end
+ end
+ end
+ end
+ end
+ return ids
+end
+
+-- Highest rank of an ability the player has learned, so the tile shows the live rank's icon/name/
+-- tooltip rather than the curated rank-1 seed.
+--
+-- DOWNPORT: see the header. `select(7, GetSpellInfo(name))` is castTime on this client, so that
+-- approach is not merely unavailable — it silently returns a wrong-but-plausible number. All
+-- resolution goes through the spellbook-derived rank table.
+local function highestKnownRankID(spellID, name)
+ if not (spellID and name) then return spellID end
+ if SB and SB.HighestKnownRankID then return SB.HighestKnownRankID(spellID, name) end
+ return spellID
+end
+
+-- Public wrapper so other surfaces (the settings catalog) can resolve a curated rank-1 id to the
+-- player's highest learned rank for display.
+function M.HighestKnownRank(spellID)
+ if not spellID then return spellID end
+ return highestKnownRankID(spellID, GetSpellInfo(spellID))
+end
+
+function ItemMixin:SetSpell(spellID)
+ self._equipSlot = nil -- spell-sourced: clear any prior trinket binding (pool slot reuse)
+ local itemID, track = M.GetItemMeta(spellID)
+ self._itemCDID = (track == "item") and itemID or nil
+
+ local name = GetSpellInfo(spellID)
+ -- A pure spell displays its highest learned rank; an on-use item entry keeps its own id (its
+ -- "rank" is meaningless — icon and cooldown come from the item).
+ local displayID = (not itemID) and highestKnownRankID(spellID, name) or spellID
+ self.spellID = displayID
+
+ -- The id the spell was LISTED under, kept alongside the displayed one. `spellID` is whatever rank
+ -- the player currently knows, so it changes under the tile the moment they train the next rank —
+ -- fine for reading a cooldown, wrong as a settings key. Per-spell preferences (alerts, ready
+ -- sounds) hang off this stable id instead, or training a rank would silently orphan them.
+ self._baseSpellID = spellID
+
+ local icon
+ name, _, icon = GetSpellInfo(displayID)
+ self.spellName = name
+
+ self._iconItemID = itemID or nil
+ if itemID then
+ local itemIcon = M.ResolveItemIcon(itemID)
+ if itemIcon then icon = itemIcon end
+ end
+
+ self._rankCDIDs = (not self._itemCDID) and knownRankIDs(spellID, name) or nil
+ if self.Icon then self.Icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark") end
+ self:RefreshCooldown()
+end
+
+-- Equipped on-use trinket. Cooldown is read live from the inventory slot; the use-spell drives the
+-- usable tint and aura precedence, while the icon prefers the equipped item texture.
+function ItemMixin:SetEquipSlot(slot, itemID, useSpellID)
+ self._equipSlot = slot
+ self._equipItemID = itemID
+ self._itemCDID = nil
+ self._rankCDIDs = nil
+ self.spellID = useSpellID
+ self._baseSpellID = useSpellID -- item use-spells have no ranks, so this is already stable
+ self.spellName = useSpellID and GetSpellInfo(useSpellID) or nil
+ self._iconItemID = itemID or nil
+ local icon = (GetInventoryItemTexture and GetInventoryItemTexture("player", slot))
+ or M.ResolveItemIcon(itemID)
+ or (useSpellID and select(3, GetSpellInfo(useSpellID)))
+ if self.Icon then self.Icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark") end
+ self:RefreshCooldown()
+end
+
+-- Bag consumable (potion / healthstone). Cooldown comes from the ITEM, not the use-spell.
+function ItemMixin:SetBagItem(itemID, useSpellID)
+ self._equipSlot = nil
+ self._equipItemID = nil
+ self._bagItemID = itemID
+ self._itemCDID = itemID
+ self._rankCDIDs = nil
+ self.spellID = useSpellID
+ self._baseSpellID = useSpellID
+ self.spellName = useSpellID and GetSpellInfo(useSpellID) or nil
+ self._iconItemID = itemID or nil
+ local icon = M.ResolveItemIcon(itemID)
+ if self.Icon then self.Icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark") end
+ self:RefreshCooldown()
+end
+
+-- Re-resolve the icon for an item-backed entry once the server delivers its data.
+function ItemMixin:RefreshIcon()
+ if not (self.Icon and self._iconItemID) then return end
+ local icon
+ if self._equipSlot then
+ icon = (GetInventoryItemTexture and GetInventoryItemTexture("player", self._equipSlot))
+ or M.ResolveItemIcon(self._iconItemID)
+ else
+ icon = M.ResolveItemIcon(self._iconItemID)
+ end
+ if icon then self.Icon:SetTexture(icon) end
+end
+
+function ItemMixin:SetTimerShown(shown)
+ self.timerShown = shown
+ NE.cd.ApplyNumbers(self.Cooldown, { show = shown })
+end
+
+function ItemMixin:SetTooltipsShown(shown)
+ self.tooltipsShown = shown
+end
+
+function ItemMixin:SetHideWhenInactive(hide)
+ self.hideWhenInactive = hide
+ self:UpdateShownState()
+end
+
+-- Retail swipe colours. DOWNPORT: 3.3.5a Cooldown has no SetSwipeColor, so these are applied only
+-- when the setter exists (i.e. never on this client). Kept so the aura/cooldown distinction is
+-- still expressed in code and lights up automatically if the surface ever gains it.
+M.COLOR_COOLDOWN = { 0, 0, 0, 0.7 }
+M.COLOR_AURA = { 1, 0.95, 0.57, 0.7 }
+M.COLOR_WHITE = { 1, 1, 1, 1 }
+
+local function setSwipeColor(cooldown, c)
+ if cooldown and cooldown.SetSwipeColor then
+ cooldown:SetSwipeColor(c[1], c[2], c[3], c[4])
+ end
+end
+
+-- ── The buffed-spell glow (§H.2 8c) ─────────────────────────────────────────────────────────────
+-- The guide's second headline feature: "puts glow effects around buffed spells". Retail expresses it
+-- as COLOR_AURA on the swipe — which, per the note above, this client cannot set. So we substitute
+-- the signal rather than the colour: a static gold halo on the tile while the spell's buff is up.
+--
+-- Not LibCustomGlow, which §H.2 nominated. Every glow that library renders MOVES (marching ants,
+-- pulsing button glow, orbiting sparkles), and motion on this addon already means something else —
+-- it is the alert vocabulary (Alerts.lua), fired when something needs attention NOW. A buff being up
+-- is a steady state, and retail draws it as one.
+--
+-- The FIRST attempt drew a gold additive copy of the tile's own IconOverlay art, on the reasoning
+-- that lighting the existing frame would align perfectly and cost nothing. It rendered as absolutely
+-- nothing in game, and the art says why: 6704514's overlay cell is pure black — every texel (0,0,0)
+-- carrying only an alpha ramp, peaking at a=104 on the border line. It is a SHADOW, not a metal
+-- frame. ADD blending adds src.rgb x alpha, so black source emits zero light no matter what vertex
+-- colour is multiplied over it. The atlas resolved, the region was shown, and the maths produced an
+-- invisible texture — a different route to 8a's exact failure mode.
+--
+-- So: the stock soft-glow ring, which is what this repo already uses for the bag rarity glow, the
+-- auction detail ring and the profession reagent slots (modules/bags/BagSkin.lua:29). It is present
+-- on every 3.3.5a client, it is pale rather than black, and it is built to be tinted.
+M.BUFF_GLOW_TEXTURE = "Interface\\Buttons\\UI-ActionButton-Border"
+
+-- Oversize, as a fraction of the tile edge; 35% is the figure the call sites above converged on, and
+-- for the reason documented there: the texture keeps its glow INSET within a transparent margin, so
+-- at the tile's own rect the visible ring falls inside the icon rather than on its edge.
+--
+-- That is what "icon glow effect inside frame" was — the ring landing too far in. I read it as a
+-- FILLED texture veiling the art and moved the region behind the icon for two passes, where the frame
+-- shadow then suppressed it. It draws on top now (Viewers.lua), which is how every other use of this
+-- texture in the addon draws it.
+M.BUFF_GLOW_OVERSIZE = 0.35
+M.BUFF_GLOW_MIN_OVER = 6
+
+function M.BuffGlowInset(size)
+ local over = math.floor((size or 0) * M.BUFF_GLOW_OVERSIZE + 0.5)
+ if over < M.BUFF_GLOW_MIN_OVER then over = M.BUFF_GLOW_MIN_OVER end
+ return over
+end
+
+-- The halo, in its OWN FRAME. This is the part no draw layer could have fixed, and it is why moving
+-- the region from BACKGROUND to OVERLAY 7 changed nothing the owner could see.
+--
+-- `Cooldown` is a CHILD FRAME of the tile, and a child frame draws above EVERY layer of its parent —
+-- BACKGROUND and OVERLAY 7 alike. The halo was therefore under the sweep no matter which layer it was
+-- given, and the sweep is a rotating wedge, so which part of the halo survived depended on where the
+-- sweep happened to be. That is every symptom reported against this glow, in one mechanism: "doesn't
+-- align to the top or right hand sides", "the swipe covers the glow on the right but not the left",
+-- and a snapped-to-pixel inset that "didn't fix anything" — because pixels were never the problem
+-- on this one.
+--
+-- A sibling frame with a higher frame level is the only thing that outranks a child frame. The
+-- texture inside it is unchanged: same art, same ADD, same rect.
+function M.BuildBuffGlow(parent, size)
+ if not parent then return nil end
+ local go = M.BuffGlowInset(size)
+ local f = CreateFrame("Frame", nil, parent)
+ -- Anchored to the ICON, not the tile, so the two are concentric by construction and the halo
+ -- follows the inset slider without being told about it.
+ f:SetPoint("TOPLEFT", parent.Icon, "TOPLEFT", -go, go)
+ f:SetPoint("BOTTOMRIGHT", parent.Icon, "BOTTOMRIGHT", go, -go)
+ local base = (parent.Cooldown and parent.Cooldown.GetFrameLevel and parent.Cooldown:GetFrameLevel())
+ or (parent.GetFrameLevel and parent:GetFrameLevel()) or 1
+ f:SetFrameLevel(base + 3)
+
+ f.Texture = f:CreateTexture(nil, "OVERLAY")
+ f.Texture:SetAllPoints(f)
+ f.Texture:SetTexture(M.BUFF_GLOW_TEXTURE)
+ f.Texture:SetBlendMode("ADD")
+ f.Texture:SetVertexColor(M.COLOR_BUFF_GLOW[1], M.COLOR_BUFF_GLOW[2], M.COLOR_BUFF_GLOW[3],
+ M.COLOR_BUFF_GLOW[4])
+ f:Hide()
+ return f
+end
+
+-- Tunable: warm gold, the colour retail gives the swipe it will not let us tint here.
+M.COLOR_BUFF_GLOW = { 1, 0.82, 0.30, 0.9 }
+
+function ItemMixin:SetBuffGlow(on)
+ local glow = self.BuffGlow
+ if not glow then return end
+ on = on and true or false
+ if on and not M.IsBuffGlowEnabled() then on = false end
+ if self._buffGlow == on then return end
+ self._buffGlow = on
+ if on then glow:Show() else glow:Hide() end
+end
+
+-- GCD detection. Retail reads C_Spell.GetSpellCooldown(spellID).isOnGCD; we replicate the manual
+-- probe: query the sentinel "Global Cooldown" spell and compare start+duration.
+local GCD_PROBE_SPELL = 61304
+
+local function isSpellOnGCD(spellID, start, duration)
+ if not (start and start > 0 and duration and duration > 0) then return false end
+ local gcdStart, gcdDuration = GetSpellCooldown(GCD_PROBE_SPELL)
+ if not (gcdStart and gcdDuration and gcdDuration > 0) then return false end
+ return math.abs(start - gcdStart) < 0.05 and math.abs(duration - gcdDuration) < 0.05
+end
+
+-- Cast-lockout: mid-cast/channel, GetSpellCooldown reports the cast duration as the "cooldown" of
+-- the locked spells.
+local function isCastLockoutCooldown(start, duration)
+ if not (start and duration) then return false end
+ local _, _, _, csMS, ceMS = UnitCastingInfo("player")
+ if csMS and ceMS then
+ local castStart = csMS / 1000
+ local castDur = (ceMS - csMS) / 1000
+ if math.abs(start - castStart) < 0.05 and math.abs(duration - castDur) < 0.1 then return true end
+ end
+ local _, _, _, hsMS, heMS = UnitChannelInfo("player")
+ if hsMS and heMS then
+ local chStart = hsMS / 1000
+ local chDur = (heMS - hsMS) / 1000
+ if math.abs(start - chStart) < 0.05 and math.abs(duration - chDur) < 0.1 then return true end
+ end
+ return false
+end
+
+-- A player buff/debuff matching this spell name. DOWNPORT: backed by NE.aura (core/AuraSnapshot.lua)
+-- so the player's aura list is walked once per frame addon-wide. The returned entry keeps this
+-- module's legacy field names (.expirationTime / .debuffType / .isHarmful).
+local adaptedPool = {}
+
+function M.findPlayerAuraDataByName(name)
+ if not (name and NE.aura and NE.aura.FindByName) then return nil end
+ local row, harmful = NE.aura.FindByName("player", name)
+ if not row then return nil end
+ local e = adaptedPool[name]
+ if not e then e = {}; adaptedPool[name] = e end
+ e.name = row.name
+ e.count = row.count or 0
+ e.duration = row.duration or 0
+ e.expirationTime = row.expiration or 0
+ e.isHarmful = harmful or nil
+ e.debuffType = harmful and row.dispelType or nil
+ return e
+end
+
+-- Live totem lookup (Shaman): a tracked totem spell sources its swipe from the real totem timer.
+-- 3.3.5a exposes GetTotemInfo(slot) -> haveTotem, name, startTime, duration, icon and fires
+-- PLAYER_TOTEM_UPDATE, same as the source assumed.
+function M.FindTotemByName(name)
+ if not name then return nil end
+ if not GetTotemInfo then return nil end
+ local slots = (GetNumTotemSlots and GetNumTotemSlots()) or MAX_TOTEMS or 4
+ for slot = 1, slots do
+ local haveTotem, totemName, startTime, duration = GetTotemInfo(slot)
+ if haveTotem and totemName and totemName ~= "" and duration and duration > 0 then
+ if totemName == name or totemName:find(name, 1, true) or name:find(totemName, 1, true) then
+ return startTime, duration
+ end
+ end
+ end
+ return nil
+end
+
+-- Duration-based GCD threshold, used when the 61304 probe is unavailable.
+M.GCD_MAX = 1.51
+
+-- Rank-safe cooldown read. Try the keyed id, then every known rank (returning the one with the
+-- longest remaining cooldown — the rank actually cast, regardless of up/down-ranking), then name.
+function M.SpellCD(spellID, spellName, rankIDs)
+ local start, duration, enabled = GetSpellCooldown(spellID)
+ if start and start > 0 then return start, duration, enabled end
+ if rankIDs then
+ local bestStart, bestDur, bestEnab, bestEnd = nil, nil, nil, 0
+ for i = 1, #rankIDs do
+ local s, d, e = GetSpellCooldown(rankIDs[i])
+ if s and s > 0 and d and d > 0 and (s + d) > bestEnd then
+ bestStart, bestDur, bestEnab, bestEnd = s, d, e, s + d
+ end
+ end
+ if bestStart then return bestStart, bestDur, bestEnab end
+ end
+ if spellName then
+ local s2, d2, e2 = GetSpellCooldown(spellName)
+ if s2 and s2 > 0 then return s2, d2, e2 end
+ end
+ return start, duration, enabled
+end
+
+-- Read this item's cooldown, returning ALL THREE values.
+--
+-- MUST stay an explicit branch. The tempting one-liner
+-- local start, duration, enabled = self._itemCDID and M.ItemCooldown(...) or M.SpellCD(...)
+-- is the multi-return truncation trap: Lua adjusts an `or` operand to ONE value, so duration and
+-- enabled arrive nil and every refresh computes cooldownIsActive=false. That was the whole
+-- "doesn't show cooldowns on cooldown" bug upstream.
+function ItemMixin:ReadCooldown()
+ if self._equipSlot then return GetInventoryItemCooldown("player", self._equipSlot) end
+ if self._itemCDID then return M.ItemCooldown(self._itemCDID) end
+ return M.SpellCD(self.spellID, self.spellName, self._rankCDIDs)
+end
+
+function ItemMixin:UpdateShownState()
+ if not (self.spellID or self._bagItemID) then self:Hide(); return end
+ -- Retail's ShouldBeShown: items whose template doesn't set allowHideWhenInactive ALWAYS show.
+ -- Essential and Utility both fall in that category — they show every configured cooldown the
+ -- player knows, on or off cooldown. Only BuffIcon/BuffBar honour hideWhenInactive.
+ if not self.allowHideWhenInactive then self:Show(); return end
+ if not self.hideWhenInactive then self:Show(); return end
+
+ local aura = M.findPlayerAuraDataByName(self.spellName)
+ if aura and aura.duration > 0 and aura.expirationTime > GetTime() then
+ self:Show()
+ return
+ end
+ local start, duration = self:ReadCooldown()
+ local hasCooldown = start and start > 0 and duration and duration > 0
+ if hasCooldown
+ and not isSpellOnGCD(self.spellID, start, duration)
+ and not isCastLockoutCooldown(start, duration)
+ and duration > M.GCD_MAX then
+ self:Show()
+ else
+ self:Hide()
+ end
+end
+
+-- ── Ready flash ─────────────────────────────────────────────────────────────────────────────────
+-- The cue that an ability has just come off cooldown. Retail plays a 22-frame flipbook sprite; the
+-- source steps it by hand (the animation system was unavailable to it too), and that stepper ports.
+--
+-- TWO FAULTS FIXED HERE, both of which made the flash silently never appear:
+--
+-- 1. `NE.tex.GetAtlasRect` returns FOUR NUMBERS (left, right, top, bottom), not a table. The
+-- stepper indexed the result as `atlas.right`, so `atlas` was the number `left` and every
+-- access errored inside an OnUpdate.
+-- 2. The "art not shipped, degrade quietly" guard was `if not getFlashAtlas()`. GetAtlasRect
+-- returns `0, 1, 0, 1` for an UNKNOWN atlas, and `0` is truthy in Lua — so the guard passed
+-- precisely when the art was missing. `NE.tex.HasAtlas` is the correct test.
+--
+-- The retail flipbook art is not registered on this client, so the flipbook path is dormant. Rather
+-- than leave the feature invisible, an undecorated fallback pulses the square button highlight —
+-- a base-client texture DragonUI itself uses, so it cannot fail to load. If the flipbook atlas is
+-- ever registered, the sprite path takes over with no further change.
+local FLASH_DURATION = 0.75
+local FLASH_FRAMES = 22
+local FLASH_ROWS = 11
+local FLASH_COLS = 2
+
+-- A base-client texture DragonUI itself uses (modules/bagsort.lua et al), so it cannot fail to load.
+M.FLASH_FALLBACK_TEXTURE = "Interface\\Buttons\\ButtonHilight-Square"
+
+local function hasFlipbook()
+ return NE.tex and NE.tex.HasAtlas and NE.tex.HasAtlas("UI-HUD-ActionBar-GCD-Flipbook")
+end
+
+-- left, right, top, bottom as a table, or nil when the atlas is unknown.
+local function getFlashAtlas()
+ if not hasFlipbook() then return nil end
+ local l, r, t, b = NE.tex.GetAtlasRect("UI-HUD-ActionBar-GCD-Flipbook")
+ return { left = l, right = r, top = t, bottom = b }
+end
+
+local function flashOnUpdate(flash)
+ local now = GetTime()
+ local start = flash._flashStartTime
+ if not start then flash:SetScript("OnUpdate", nil); flash:Hide(); return end
+ if now < start then return end
+ local progress = (now - start) / FLASH_DURATION
+ if progress >= 1 then
+ flash:SetScript("OnUpdate", nil)
+ flash:Hide()
+ if flash.Flipbook then flash.Flipbook:SetAlpha(0) end
+ flash._flashStartTime = nil
+ return
+ end
+ if not flash.Flipbook then return end
+
+ local atlas = getFlashAtlas()
+ if not atlas then
+ -- Fallback burst: snap to full brightness, then expand outward past the icon while fading.
+ -- A pure alpha blink on an icon-sized quad is nearly invisible against the art underneath —
+ -- the outward growth is what makes it read at a glance, which was the reported problem.
+ local tex = flash.Flipbook
+ local alpha
+ if progress < 0.15 then
+ alpha = progress / 0.15 -- fast ramp in
+ else
+ alpha = 1 - (progress - 0.15) / 0.85 -- long fade out
+ end
+ if alpha < 0 then alpha = 0 elseif alpha > 1 then alpha = 1 end
+ tex:SetAlpha(alpha)
+
+ -- Grow from the icon's own bounds to ~1.7x across the burst.
+ local w = flash:GetWidth() or 30
+ local pad = w * 0.35 * progress
+ tex:ClearAllPoints()
+ tex:SetPoint("TOPLEFT", flash, "TOPLEFT", -pad, pad)
+ tex:SetPoint("BOTTOMRIGHT", flash, "BOTTOMRIGHT", pad, -pad)
+ return
+ end
+
+ flash.Flipbook:SetAlpha(1)
+ local frame = math.floor(progress * FLASH_FRAMES)
+ if frame >= FLASH_FRAMES then frame = FLASH_FRAMES - 1 end
+ local col = frame % FLASH_COLS
+ local row = math.floor(frame / FLASH_COLS)
+ local frameW = (atlas.right - atlas.left) / FLASH_COLS
+ local frameH = (atlas.bottom - atlas.top) / FLASH_ROWS
+ local l = atlas.left + col * frameW
+ local t = atlas.top + row * frameH
+ flash.Flipbook:SetTexCoord(l, l + frameW, t, t + frameH)
+end
+
+function ItemMixin:ScheduleFlash(start, duration)
+ local flash = self.CooldownFlash
+ if not (flash and flash.Flipbook) then return end
+ -- No atlas gate here any more: the fallback pulse needs no art, so the flash is always available.
+ -- The old guard could not detect a missing atlas anyway (see the note above).
+ if self._flashScheduledFor == start and self._flashScheduledDur == duration then return end
+ self._flashScheduledFor = start
+ self._flashScheduledDur = duration
+ local playStart = (start + duration) - FLASH_DURATION
+ if playStart <= GetTime() then
+ flash:Hide()
+ flash:SetScript("OnUpdate", nil)
+ return
+ end
+ flash._flashStartTime = playStart
+ if flash.Flipbook then flash.Flipbook:SetAlpha(0) end
+ flash:Show()
+ flash:SetScript("OnUpdate", flashOnUpdate)
+end
+
+function ItemMixin:ClearFlash()
+ self._flashScheduledFor = nil
+ self._flashScheduledDur = nil
+ local flash = self.CooldownFlash
+ if flash then
+ flash:SetScript("OnUpdate", nil)
+ flash:Hide()
+ flash._flashStartTime = nil
+ if flash.Flipbook then
+ flash.Flipbook:SetAlpha(0)
+ -- The burst grows the texture past the frame; put it back or the next play starts oversized.
+ flash.Flipbook:ClearAllPoints()
+ flash.Flipbook:SetAllPoints(flash)
+ end
+ end
+end
+
+-- ── Icon tint ───────────────────────────────────────────────────────────────────────────────────
+-- Priority: out-of-range red > usable white > not-enough-mana blue > not-usable grey, plus the OOR
+-- shadow overlay. Composes with SetDesaturated (orthogonal).
+M.ICON_USABLE = { 1.0, 1.0, 1.0 }
+M.ICON_OOM = { 0.5, 0.5, 1.0 }
+M.ICON_UNUSABLE = { 0.4, 0.4, 0.4 }
+M.ICON_OUTOFRANGE = { 0.64, 0.15, 0.15 }
+
+-- DOWNPORT: SetShown does not exist on 3.3.5a (CONTRACTS §0).
+local function setShown(region, shown)
+ if not region then return end
+ if shown then region:Show() else region:Hide() end
+end
+
+function ItemMixin:RefreshIconColor()
+ if not self.Icon then return end
+
+ -- Equipped trinket / bag consumable: tint from ITEM usability. IsUsableSpell on a use-spell
+ -- returns false (it isn't a known player spell), which would wrongly grey a usable trinket.
+ local itemForUsability = self._equipSlot and self._equipItemID or self._bagItemID
+ if itemForUsability or self._equipSlot then
+ local usable = true
+ if IsUsableItem and itemForUsability then usable = IsUsableItem(itemForUsability) and true or false end
+ local c = usable and M.ICON_USABLE or M.ICON_UNUSABLE
+ self.Icon:SetVertexColor(c[1], c[2], c[3])
+ setShown(self.OutOfRange, false)
+ return
+ end
+
+ if not self.spellID then return end
+
+ -- DOWNPORT: C_Spell.IsSpellUsable is absent on this client (neither ClassicAPI nor compat provides
+ -- it), so we use the 3.3.5a global directly — and it must be given the spell NAME.
+ --
+ -- 3.3.5a's IsUsableSpell takes a name, or a spellbook INDEX with a bookType. It does not take a
+ -- spellID. Passing one is read as an index far past the end of the book, so it returns nil rather
+ -- than erroring — and nil is neither usable nor out-of-mana, so every icon fell through to
+ -- ICON_UNUSABLE and the whole viewer rendered permanently grey. DragonUI's own action bars pass a
+ -- name for this reason (modules/actionbars/extrabar.lua:1049). The id is kept as a fallback for a
+ -- nameless entry only.
+ local usable, oom
+ if IsUsableSpell then usable, oom = IsUsableSpell(self.spellName or self.spellID) end
+
+ -- Range only matters with a live target and a spell that actually range-checks. 3.3.5a's
+ -- IsSpellInRange takes a NAME and returns 1/0/nil (nil = no range check applies).
+ local outOfRange = false
+ if UnitExists("target") and IsSpellInRange and self.spellName then
+ local r = IsSpellInRange(self.spellName, "target")
+ if r ~= nil then outOfRange = (r == 0) end
+ end
+
+ local c = outOfRange and M.ICON_OUTOFRANGE
+ or usable and M.ICON_USABLE
+ or oom and M.ICON_OOM
+ or M.ICON_UNUSABLE
+ self.Icon:SetVertexColor(c[1], c[2], c[3])
+ setShown(self.OutOfRange, outOfRange)
+end
+
+-- ── The refresh ─────────────────────────────────────────────────────────────────────────────────
+-- Retail contract:
+-- isOnActualCooldown = not isOnGCD and cooldownIsActive
+-- cooldownDesaturated = isOnActualCooldown -- bright on GCD, grey on a real CD
+-- cooldownPlayFlash = isOnActualCooldown
+-- Aura precedence: an active self-aura overrides with the (retail: golden) swipe and no desaturation.
+function ItemMixin:RefreshCooldown()
+ if not self.spellID or not self.Cooldown then return end
+
+ self:RefreshIconColor()
+
+ -- 1. Aura precedence.
+ --
+ -- Retail shows the AURA's remaining time here, because its only way to say "this spell's buff is
+ -- up" is to tint that swipe gold. Reported as wrong on Prayer of Mending: the tile showed the 30s
+ -- buff while the player wanted the cooldown, and the two numbers counting down over the same icon
+ -- are indistinguishable. Since 8c the glow says "buffed" on its own, so the swipe no longer has to,
+ -- and the timer can be whichever the player finds useful. Default: the cooldown.
+ --
+ -- The glow is set from `aura` either way — the SIGNAL is not the setting, only the number is.
+ local aura = M.findPlayerAuraDataByName(self.spellName)
+ local auraUp = aura and aura.duration > 0 and aura.expirationTime > GetTime()
+ self:SetBuffGlow(auraUp and true or false)
+
+ if auraUp and M.BuffShowsAuraTime() then
+ local auraStart = aura.expirationTime - aura.duration
+ setSwipeColor(self.Cooldown, M.COLOR_AURA)
+ if self.Cooldown.SetDrawSwipe then self.Cooldown:SetDrawSwipe(true) end
+ CooldownFrame_Set(self.Cooldown, auraStart, aura.duration, 1)
+ if self.Icon then self.Icon:SetDesaturated(false) end
+ self:ClearFlash()
+ if self.hideWhenInactive then self:Show() end
+ return
+ end
+
+ -- 1b. Totem precedence (Shaman): source the swipe from the live totem timer; falls through to the
+ -- normal cooldown path once the totem is gone, so the cast CD still shows.
+ local totemStart, totemDur = M.FindTotemByName(self.spellName)
+ if totemStart and totemDur and totemDur > 0 then
+ setSwipeColor(self.Cooldown, M.COLOR_AURA)
+ if self.Cooldown.SetDrawSwipe then self.Cooldown:SetDrawSwipe(true) end
+ CooldownFrame_Set(self.Cooldown, totemStart, totemDur, 1)
+ if self.Icon then self.Icon:SetDesaturated(false) end
+ -- A live totem is not an aura, but it is the same STATE — this tile's effect is currently up —
+ -- and the branch above already gives it COLOR_AURA for that reason. Glowing one and not the
+ -- other would make a shaman's totem tiles the only active tiles on screen that look inactive.
+ self:SetBuffGlow(true)
+ self:ClearFlash()
+ if self.hideWhenInactive then self:Show() end
+ return
+ end
+
+ -- No glow clear here. It USED to sit on this line, on the reasoning that everything below is "no
+ -- effect of ours is up" — which stopped being true the moment the aura timer became optional: with
+ -- the cooldown preferred, a genuinely buffed spell falls straight through to the cooldown path and
+ -- would have its glow wiped on the way past. The one call above covers both readings.
+
+ -- 2. Spell cooldown.
+ local start, duration, enabled = self:ReadCooldown()
+ local cooldownIsActive = start and start > 0 and duration and duration > 0 and enabled == 1
+
+ if cooldownIsActive then
+ -- No isOnGCD flag on this client. Use the duration heuristic, then catch cast/channel lockout
+ -- (while mid-cast, all OTHER spells report a "cooldown" matching the cast duration).
+ local isOnGCD = duration <= M.GCD_MAX
+ if not isOnGCD then isOnGCD = isCastLockoutCooldown(start, duration) end
+ local isOnActualCooldown = not isOnGCD
+
+ -- Arm the ready-transition flag. ARM-ONLY here — never clear on this path — so a frequent
+ -- refresh can't wipe a pending transition before the detector observes it.
+ if isOnActualCooldown then self._wasOnRealCD = true end
+
+ -- Re-run ourselves the moment the cooldown ends.
+ --
+ -- 3.3.5a fires NO event when a cooldown expires — every event the viewer listens for
+ -- (SPELL_UPDATE_COOLDOWN, UNIT_SPELLCAST_*, BAG_UPDATE_COOLDOWN) marks a cooldown STARTING or
+ -- changing. The swipe still finishes, because the Cooldown widget animates itself in C, but no
+ -- Lua re-runs, so the desaturation set below stayed on until some unrelated event happened to
+ -- refresh the tile — usually the player's next cast. That is the reported "icons stay grey
+ -- after coming off cooldown".
+ --
+ -- One timer per cooldown, keyed on its end time so repeated refreshes during the same cooldown
+ -- don't stack. The small margin keeps us just past the boundary, where GetSpellCooldown has
+ -- certainly cleared.
+ if isOnActualCooldown then
+ local endsAt = start + duration
+ if self._cdRefreshFor ~= endsAt then
+ self._cdRefreshFor = endsAt
+ local remaining = endsAt - GetTime()
+ if remaining > 0 and C_Timer and C_Timer.After then
+ C_Timer.After(remaining + 0.05, function()
+ -- Only act if this is still the cooldown we scheduled for; a recast supersedes us.
+ if self._cdRefreshFor == endsAt then
+ self._cdRefreshFor = nil
+ self:RefreshCooldown()
+ end
+ end)
+ end
+ end
+ end
+
+ if self.Cooldown.SetDrawSwipe then self.Cooldown:SetDrawSwipe(true) end
+ setSwipeColor(self.Cooldown, M.COLOR_COOLDOWN)
+ CooldownFrame_Set(self.Cooldown, start, duration, enabled)
+
+ if self.Icon then self.Icon:SetDesaturated(isOnActualCooldown) end
+
+ if isOnActualCooldown then
+ self:ScheduleFlash(start, duration)
+ else
+ self:ClearFlash()
+ end
+ else
+ CooldownFrame_Clear(self.Cooldown)
+ if self.Icon then self.Icon:SetDesaturated(false) end
+ self:ClearFlash()
+ end
+
+ if self.hideWhenInactive then self:UpdateShownState() end
+end
+
+-- Is this item on a REAL cooldown right now — not the GCD, not a cast/channel lockout?
+function ItemMixin:IsOnRealCooldown()
+ if not self.spellID then return false end
+ local start, duration, enabled = self:ReadCooldown()
+ if not (start and start > 0 and duration and duration > 0 and enabled == 1) then return false end
+ if duration <= M.GCD_MAX then return false end
+ if isSpellOnGCD(self.spellID, start, duration) then return false end
+ if isCastLockoutCooldown(start, duration) then return false end
+ return true
+end
+
+-- True EXACTLY ONCE per real-cooldown -> ready transition. Alerts.lua's ticker consumes this.
+function ItemMixin:ConsumeReadyTransition()
+ if self:IsOnRealCooldown() then
+ self._wasOnRealCD = true
+ return false
+ end
+ if self._wasOnRealCD then
+ self._wasOnRealCD = false
+ return true
+ end
+ return false
+end
+
+-- Fire everything assigned to this spell's "ability is ready" event: the visual alert flash and the
+-- ready sound. Called once per transition by the alert ticker, which owns the edge detection.
+--
+-- Self-gating: both halves no-op unless the player assigned something, so an unconfigured spell
+-- costs two table lookups and makes no sound.
+function ItemMixin:FireReadyAlerts()
+ local key = self:GetSettingsKey()
+ if not key then return end
+ if M.alerts and M.alerts.OnAvailable then
+ M.alerts.OnAvailable(self)
+ end
+ if M.GetReadySoundKit and M.PlayReadySound then
+ local kit = M.GetReadySoundKit(key)
+ if kit then M.PlayReadySound(kit) end
+ end
+end
+
+-- The id every per-spell PREFERENCE is stored under. See the note in SetSpell: never use
+-- `spellID` for this, it tracks the learned rank and moves.
+function ItemMixin:GetSettingsKey()
+ return self._baseSpellID or self.spellID
+end
+
+-- ── Tooltip ─────────────────────────────────────────────────────────────────────────────────────
+-- DOWNPORT: 3.3.5a GameTooltip has no SetSpellByID. !!!ClassicAPI adds SetItemByID (WidgetAPI.lua)
+-- but not the spell equivalent, so we fall back to the spell hyperlink, which 3.3.5a does support.
+local function tooltipSetSpell(tip, spellID)
+ if tip.SetSpellByID then
+ local ok = pcall(tip.SetSpellByID, tip, spellID)
+ if ok then return true end
+ end
+ return pcall(tip.SetHyperlink, tip, "spell:" .. spellID)
+end
+M.TooltipSetSpell = tooltipSetSpell -- reused by the aura item mixins (AuraItemMixins.lua)
+
+-- The same thing, with the row's own name as a floor.
+--
+-- `SetHyperlink` on an id this client cannot resolve does not error — it SUCCEEDS and leaves the
+-- tooltip empty, so the pcall above returning true is not evidence that anything was drawn. Aura
+-- rows hit that constantly: the catalog stores rank-1 ids straight from Spell.dbc and plenty of
+-- aura-only ids have no linkable spell behind them. The result is a tile whose tooltip reads
+-- "Lasts 30 sec" under no name at all, which on a 38px icon grid leaves nothing to identify it by.
+--
+-- The caller always knows a name — the catalog and the seen registry both store one, precisely
+-- because GetSpellInfo cannot be relied on here — so it becomes the title when the client has
+-- nothing. NumLines is the test rather than the pcall's word for it, and ClearLines is what makes
+-- that number mean "this hover", not "whatever was up last".
+function M.TooltipSetSpellNamed(tip, spellID, name)
+ if tip.ClearLines then tip:ClearLines() end
+ if spellID then tooltipSetSpell(tip, spellID) end
+ if tip.NumLines and tip:NumLines() > 0 then return true end
+ if name and name ~= "" then
+ tip:AddLine(name, 1, 1, 1)
+ return true
+ end
+ return false
+end
+
+-- Shared icon-crop helper: what stands in for the removed rounded MaskTexture.
+function M.CropIcon(tex)
+ if not tex then return end
+ if NE.tex and NE.tex.CropIcon then
+ NE.tex.CropIcon(tex)
+ elseif tex.SetTexCoord then
+ tex:SetTexCoord(0.07, 0.93, 0.07, 0.93)
+ end
+end
+
+-- The other half of what the removed MaskTexture did, and the half that was missed.
+--
+-- Retail anchors the Icon with setAllPoints and masks it with 6707800. That mask is easy to think of
+-- as "rounds the corners", but decoding it shows it does TWO things: its outer 3px of 64 are FULLY
+-- TRANSPARENT, so it also insets the icon by 3/64 = 4.7% on every side, and only then rounds what is
+-- left with a corner radius of 4/64.
+--
+-- Dropping the mask therefore dropped the inset, and the inset is the part that matters here. The
+-- numbers, for a 50px Essential tile:
+--
+-- IconOverlay's crisp border line falls at item x 4.05 .. 45.56 (the art's line is at 14-19 and
+-- 67-71 of an 86px cell, mapped through the -9/+8 outward anchor)
+-- a MASKED icon spans 2.34 .. 47.66, so the frame overlaps its edge by ~1.7px — a frame sitting on
+-- the icon, which is what retail looks like
+-- an UNMASKED icon spans 0 .. 50, so it overshoots the border line by ~4px on every side and lands
+-- out in the soft halo instead
+--
+-- Four pixels does not sound like much, but it is the whole difference between "framed" and "too big
+-- for its frame", which is how it was reported. The corner rounding we still cannot reproduce, and it
+-- turns out not to matter much: at radius 4/64 a square corner overshoots the arc by only
+-- 4*(1-1/sqrt2) = 1.17px of 64, under a pixel on a real tile.
+--
+-- iconSize is applied with SetScale (Viewers.lua), not SetSize, so a fixed pixel inset computed from
+-- the tile's authored size stays correct at every size setting.
+--
+-- The mask's edge is HARD, incidentally: texels 3..60 of 64 at alpha 255, the rest at 0, no
+-- antialiasing. So 3/64 is exact rather than a reading off a ramp.
+M.ICON_MASK_INSET = 3 / 64
+
+-- One further pixel, and this one is a JUDGEMENT rather than a derivation — kept separate so the two
+-- are not confused.
+--
+-- The mask inset alone is faithful, and still reads a pixel too big, for two compounding reasons.
+-- First, the overlay is anchored +-9 horizontally but +-8 vertically, so its border line falls at a
+-- different tile offset per axis; the mask is square, so on the VERTICAL axis the icon's edge lands
+-- just outside the border band on Essential, Utility and the bar's icon (2.34 against a band of
+-- 2.74..6.58 on Essential). Second, our icon has square corners where retail's is rounded, so its
+-- edge reads harder against the same soft border.
+--
+-- FRACTIONAL, not flat pixels: the frame art scales with the tile, so a flat budget generous enough
+-- for a 50px Essential tile pushes a 30px Utility icon clean through its own opening.
+--
+-- And it defaults to ZERO, which is the correction that matters, because the relationship runs the
+-- OPPOSITE way to how three rounds of this assumed. Rendering the overlay cell shows what it actually
+-- is: an INNER SHADOW, black, peaking at alpha 108/255 — 42% — with its dark core on a line at art
+-- x=14 of 86 and a corner radius of about 4 art px.
+--
+-- An inner shadow has to fall ON something to be seen. Inset the icon and the shadow's core slides
+-- off it onto empty space, where black-at-42%-over-the-world is nothing at all. So every step of
+-- "make the icon smaller so it fits its frame" was quietly DELETING the frame, which is exactly how
+-- the last pass ended: at +4% the whole dark core sat off the icon, and the report came back as "it
+-- looks like the icon border is missing".
+--
+-- essential (50px, -9/+8): shadow core at item x 2.07; mask inset alone puts the icon edge at 2.34
+-- utility (30px, -6/+5): core at 0.84; mask inset 1.41
+--
+-- Retail's masked icon sits at 2.34 with the band running out to 6.02, so almost all of the shadow
+-- lands on the icon. That is the look, and 0 extra reproduces it. The slider still exists — "too
+-- large" was reported three times and a square corner does read bigger than a rounded one — but it
+-- now carries a real cost, and MAX is the point past which the dark core leaves the icon entirely.
+--
+-- The rounding itself is answered by FRAME STRENGTH below, not by this.
+M.ICON_INSET_EXTRA = 0 -- percent of the tile edge, on top of the derived mask inset
+M.ICON_INSET_EXTRA_MAX = 4 -- past this the shadow's core slides off the icon and the frame fades
+
+function M.GetIconInsetExtra()
+ local cd = M._store and M._store(false)
+ local v = cd and cd.iconInset
+ if type(v) ~= "number" then return M.ICON_INSET_EXTRA end
+ if v < 0 then return 0 end
+ if v > M.ICON_INSET_EXTRA_MAX then return M.ICON_INSET_EXTRA_MAX end
+ return v
+end
+
+-- ── Frame strength ──────────────────────────────────────────────────────────────────────────────
+-- The other half of "the border is missing", and the half that answers "no rounded edges" too.
+--
+-- The frame art tops out at 42% black. On a bright icon over a dark world that is a soft bevel, not a
+-- border, and its rounded corners are far too faint to make a square icon read as a rounded one. We
+-- cannot raise a texture's alpha — SetAlpha only scales down — but we CAN draw it more than once:
+-- stacking N copies gives 1-(1-a)^N, so 42% becomes 66% at two and 80% at three. Same art, same rect,
+-- same corner radius, just deeper. The corners darken with it, which is what makes the square icon
+-- underneath stop announcing itself.
+--
+-- Two by default: enough to read as a frame at a glance, short of the heavy vignette three gives.
+M.FRAME_STRENGTH = 2
+M.FRAME_STRENGTH_MAX = 3
+
+function M.GetFrameStrength()
+ local cd = M._store and M._store(false)
+ local v = cd and cd.frameStrength
+ if type(v) ~= "number" then return M.FRAME_STRENGTH end
+ if v < 1 then return 1 end
+ if v > M.FRAME_STRENGTH_MAX then return M.FRAME_STRENGTH_MAX end
+ return math.floor(v)
+end
+
+-- Build the extra copies for one tile. Registered so the setting can be applied live, the same way
+-- the inset is — and for the same reason: these are created once, at tile construction.
+function M.BuildFrameStack(parent, ox, oy)
+ if not (parent and parent.IconOverlay) then return end
+ local stack = {}
+ for i = 1, M.FRAME_STRENGTH_MAX - 1 do
+ local t = parent:CreateTexture(nil, "OVERLAY")
+ t:SetPoint("TOPLEFT", parent, "TOPLEFT", -ox, oy)
+ t:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", ox, -oy)
+ t:SetDrawLayer("OVERLAY", i)
+ t:Hide()
+ stack[i] = t
+ end
+ parent.IconOverlayStack = stack
+ M._frameStacks = M._frameStacks or {}
+ M._frameStacks[#M._frameStacks + 1] = parent
+ M.ApplyFrameStrength(parent)
+ return stack
+end
+
+function M.ApplyFrameStrength(parent)
+ local stack = parent and parent.IconOverlayStack
+ if not stack then return end
+ local extra = M.GetFrameStrength() - 1
+ for i = 1, #stack do
+ if i <= extra then stack[i]:Show() else stack[i]:Hide() end
+ end
+end
+
+function M.RefreshFrameStrength()
+ for _, parent in ipairs(M._frameStacks or {}) do M.ApplyFrameStrength(parent) end
+end
+
+function M.SetFrameStrength(v)
+ local cd = M._store and M._store(true)
+ if cd then cd.frameStrength = tonumber(v) or M.FRAME_STRENGTH end
+ M.RefreshFrameStrength()
+end
+
+function M.SetIconInsetExtra(v)
+ local cd = M._store and M._store(true)
+ if cd then cd.iconInset = tonumber(v) or M.ICON_INSET_EXTRA end
+ M.ReanchorIcons()
+end
+
+-- SNAPPED TO WHOLE PIXELS, and this is not tidiness — it is the fix for a real, asymmetric artefact.
+--
+-- The derived inset is fractional: 50 x 3/64 = 2.34375 on an Essential tile. Anchored at that, the
+-- icon's left edge lands at 2.34 and its right edge at 50 - 2.34 = 47.66, and the renderer resolves
+-- each to a whole pixel independently — 2 on one side, 48 on the other. The two margins then differ
+-- by a pixel, against a frame whose own anchors (+-9/+-8) are integers. That is what shows as a
+-- sliver of icon art escaping on one side and not the other, and as the cooldown sweep covering the
+-- glow on the right but not the left: the sweep shares the icon's rect exactly, so it inherits the
+-- same split, and half a pixel is the whole difference between "covers" and "doesn't".
+--
+-- It reads like a mask artefact and it is not. Retail's mask does not centre anything; it clips an
+-- icon that was already anchored on whole pixels. Snapping here is what we lost, not the rounding.
+--
+-- Deliberately whole pixels rather than "whole pixels at the current UI scale": iconSize is applied
+-- with SetScale, so a tile can sit at a fractional scale anyway, and chasing that would need the
+-- effective scale at anchor time and a re-anchor on every scale change. At scale 1 — the default, and
+-- where this was reported — integer offsets land on integer pixels on both sides.
+function M.IconInset(size)
+ local px = (size or 0) * (M.ICON_MASK_INSET + M.GetIconInsetExtra() / 100)
+ return math.floor(px + 0.5)
+end
+
+-- The frame's opening, in tile pixels, on the axis whose outward overhang is `o`. The ceiling for any
+-- inset: past this the icon no longer touches the frame at all.
+function M.IconAperture(size, o)
+ local ext = (size or 0) + 2 * (o or 0)
+ return -o + 20 * ext / 86
+end
+
+-- Anchor a region to the visible ICON's rect. Used for the icon itself and for everything drawn over
+-- it: on this client the cooldown sweep, the out-of-range shade and the ready flash are all
+-- unshaped substitutes, so any of them anchored to the TILE would sit proud of the icon by the inset
+-- above and betray the old, larger footprint. Retail can anchor them to the tile because its swipe
+-- and shadow are themselves rounded art.
+function M.AnchorMaskedIcon(region, parent, size)
+ if not (region and parent) then return end
+ local pad = M.IconInset(size)
+ region:ClearAllPoints()
+ region:SetPoint("TOPLEFT", parent, "TOPLEFT", pad, -pad)
+ region:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -pad, pad)
+ -- Remember every anchoring, so the inset can be re-applied live. These anchors are set ONCE at tile
+ -- construction; without this, moving the slider would change nothing until the next login, which is
+ -- not a slider anyone can use.
+ --
+ -- A flat list rather than a field on the parent, because the parent is not always a pooled tile:
+ -- the BuffBar row anchors its icon to a NESTED frame (BuffViewers.lua:84), which walking
+ -- viewer.items would never reach. Tiles are pooled and never destroyed, so this only ever grows to
+ -- the size of the pool.
+ M._insetAnchors = M._insetAnchors or {}
+ local list = M._insetAnchors
+ for i = 1, #list do
+ if list[i][1] == region then return pad end -- re-anchoring an already-tracked region
+ end
+ list[#list + 1] = { region, parent, size }
+ return pad
+end
+
+-- Re-apply the inset to every region already anchored, in all four viewers.
+function M.ReanchorIcons()
+ for _, a in ipairs(M._insetAnchors or {}) do
+ M.AnchorMaskedIcon(a[1], a[2], a[3])
+ end
+end
+
+-- ── BuffBar background: a horizontal 3-slice ────────────────────────────────────────────────────
+--
+-- Bar-BG is 132x19 and gets anchored to a region 6px wider than the bar (retail's own -2/+4), so at
+-- the 100% bar width it stretches 1.47x horizontally and at the 200% end of the width slider it
+-- stretches 3.1x. Its interior survives that — cols 8..120 are dead uniform, black at alpha 173 —
+-- but the two 1px light edge lines at x=1 and x=126 do not: they smear to 3px and the bar loses its
+-- edge. Reading the cell's columns gives the cut points, and they are unambiguous:
+--
+-- x=0..7 alpha 96, 253, 235, 220, 188, 179, 174, 173 — the left cap, with its edge line at x=1
+-- x=8..120 alpha 173 flat, luminance 0 — uniform, safe to stretch by any factor
+-- x=121..131 alpha 179..253 (edge line at x=126) then 172, 90, 35, 11, 5 — cap plus drop shadow
+--
+-- The right cap is wider than the left because the art carries a shadow down and to the right; that
+-- asymmetry is also why the anchors are -2/+2 and +4/-7 rather than symmetric.
+--
+-- HORIZONTAL ONLY. There is no flat band vertically — rows 3..11 run 209, 185, 165, 153, 153, 160,
+-- 173, 194, 228, a continuous bevel — so the vertical axis has nothing to slice and every piece
+-- stretches down the same 1.47x it always has. A nine-slice here would be inventing a seam.
+M.BARBG_NATIVE_W = 132
+M.BARBG_NATIVE_H = 19
+M.BARBG_CAP_LEFT = 8
+M.BARBG_CAP_RIGHT = 11
+
+-- The region's inset from the bar, retail's own numbers (CooldownViewer.xml:307-311). Asymmetric
+-- because the shadow falls down and to the right.
+M.BARBG_PAD_L, M.BARBG_PAD_T = -2, 2
+M.BARBG_PAD_R, M.BARBG_PAD_B = 4, -7
+
+-- THREE TEXTURES ON THE BAR, not a child frame holding them. A child frame draws above EVERY layer
+-- of its parent, so a Frame-based group would put the background over the fill, the pip and the
+-- name — the same rule that hid the buff glow under the cooldown sweep in §H.2.8. Textures on the
+-- bar itself sit at BACKGROUND and stay there.
+--
+-- Caps scale with the region's HEIGHT, not its width. The vertical stretch is a constant 28/19
+-- whatever the bar width, so scaling the caps by it keeps the corner bevel uniformly scaled instead
+-- of squashing it into an ellipse — the flat middle absorbs all the extra width, which is the whole
+-- point of slicing.
+local function barBGCapScale(bg)
+ local bar = bg and bg.bar
+ local h = bar and bar:GetHeight() or 0
+ if h <= 0 then return 1 end
+ return (h + M.BARBG_PAD_T - M.BARBG_PAD_B) / M.BARBG_NATIVE_H
+end
+
+local function barBGWidth(bg)
+ local bar = bg and bg.bar
+ local w = bar and bar:GetWidth() or 0
+ return w - M.BARBG_PAD_L + M.BARBG_PAD_R
+end
+
+-- The registry entry, or nil if 8a's registration ever goes missing (in which case the caller
+-- leaves the pieces untextured rather than drawing three copies of the whole sheet).
+local function barBGEntry()
+ return NE.tex and NE.tex._atlasEntry and NE.tex._atlasEntry("UI-HUD-CoolDownManager-Bar-BG")
+end
+
+function M.ApplyBarBGAtlas(bg)
+ if not (bg and bg.Left and bg.Middle and bg.Right and bg.bar) then return false end
+ local rect = barBGEntry()
+ if not rect then return false end
+ local bar = bg.bar
+
+ local u = function(x) return rect.left + (rect.right - rect.left) * (x / M.BARBG_NATIVE_W) end
+ local l, r = M.BARBG_CAP_LEFT, M.BARBG_NATIVE_W - M.BARBG_CAP_RIGHT
+
+ local scale = barBGCapScale(bg)
+ local leftW, rightW = M.BARBG_CAP_LEFT * scale, M.BARBG_CAP_RIGHT * scale
+
+ bg.Left:ClearAllPoints()
+ bg.Left:SetPoint("TOPLEFT", bar, "TOPLEFT", M.BARBG_PAD_L, M.BARBG_PAD_T)
+ bg.Left:SetPoint("BOTTOMLEFT", bar, "BOTTOMLEFT", M.BARBG_PAD_L, M.BARBG_PAD_B)
+
+ -- A bar narrow enough that the caps would overlap has no middle to speak of; fall back to the
+ -- plain single stretch rather than drawing the two caps over each other.
+ --
+ -- A width of ZERO is not that case. The bar takes its width from its anchors, which the client has
+ -- not resolved when the row is built, so the first call here reads 0 — and treating that as "too
+ -- narrow" would collapse every row to a single stretch and leave it there until something happened
+ -- to resize it. Unresolved is not narrow; slice, and let the first real OnSizeChanged re-measure.
+ --
+ -- The test is the BAR's own width, not the padded region's: the padding is +6, so an unresolved
+ -- bar still reports a 6px region and would sail past a `regionW > 0` guard into the fallback.
+ local barW = bar:GetWidth() or 0
+ if barW > 0 and leftW + rightW >= barBGWidth(bg) then
+ bg.Left:SetPoint("TOPRIGHT", bar, "TOPRIGHT", M.BARBG_PAD_R, M.BARBG_PAD_T)
+ bg.Left:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", M.BARBG_PAD_R, M.BARBG_PAD_B)
+ bg.Left:SetTexCoord(rect.left, rect.right, rect.top, rect.bottom)
+ bg.Middle:Hide(); bg.Right:Hide()
+ return true
+ end
+
+ bg.Left:SetWidth(leftW)
+ bg.Left:SetTexCoord(u(0), u(l), rect.top, rect.bottom)
+
+ bg.Right:ClearAllPoints()
+ bg.Right:SetPoint("TOPRIGHT", bar, "TOPRIGHT", M.BARBG_PAD_R, M.BARBG_PAD_T)
+ bg.Right:SetPoint("BOTTOMRIGHT", bar, "BOTTOMRIGHT", M.BARBG_PAD_R, M.BARBG_PAD_B)
+ bg.Right:SetWidth(rightW)
+ bg.Right:SetTexCoord(u(r), u(M.BARBG_NATIVE_W), rect.top, rect.bottom)
+
+ bg.Middle:ClearAllPoints()
+ bg.Middle:SetPoint("TOPLEFT", bg.Left, "TOPRIGHT", 0, 0)
+ bg.Middle:SetPoint("BOTTOMRIGHT", bg.Right, "BOTTOMLEFT", 0, 0)
+ bg.Middle:SetTexCoord(u(l), u(r), rect.top, rect.bottom)
+
+ bg.Middle:Show(); bg.Right:Show()
+ return true
+end
+
+-- Build the three pieces on the bar. Returns a plain table, not a frame — see above. It carries the
+-- same key the single texture did, so AuraItemMixins keeps addressing it as `bar.BarBG`.
+function M.BuildBarBG(bar)
+ if not bar then return end
+ local bg = { bar = bar }
+
+ -- Prefer the shipped BLP over the raw FDID, the same order entrySource uses in core/Texture.lua.
+ local rect = barBGEntry()
+ local source = rect and ((NE.tex.Local and NE.tex.Local(rect.file)) or rect.file)
+ local function piece()
+ local t = bar:CreateTexture(nil, "BACKGROUND")
+ if source then t:SetTexture(source) end
+ return t
+ end
+
+ bg.Left, bg.Middle, bg.Right = piece(), piece(), piece()
+
+ M.ApplyBarBGAtlas(bg)
+ -- The width slider re-sizes the row long after construction, and the caps are sized off the bar.
+ -- HookScript chains, so this coexists with the fill overlay's own OnSizeChanged hook.
+ bar:HookScript("OnSizeChanged", function() M.ApplyBarBGAtlas(bg) end)
+ return bg
+end
+
+function ItemMixin:OnEnter()
+ if self.tooltipsShown == false then return end
+ if self._equipSlot then
+ GameTooltip:SetOwner(self, "ANCHOR_TOP")
+ GameTooltip:SetInventoryItem("player", self._equipSlot)
+ GameTooltip:Show()
+ return
+ end
+ if self._bagItemID then
+ GameTooltip:SetOwner(self, "ANCHOR_TOP")
+ if GameTooltip.SetItemByID then
+ GameTooltip:SetItemByID(self._bagItemID)
+ else
+ GameTooltip:SetHyperlink("item:" .. self._bagItemID)
+ end
+ GameTooltip:Show()
+ return
+ end
+ if not self.spellID then return end
+ GameTooltip:SetOwner(self, "ANCHOR_TOP")
+ if M.TooltipSetSpellNamed(GameTooltip, self.spellID, self.spellName) then GameTooltip:Show() end
+end
+
+function ItemMixin:OnLeave()
+ GameTooltip:Hide()
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/PORT_PLAN.md b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/PORT_PLAN.md
new file mode 100644
index 0000000..9b4417e
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/PORT_PLAN.md
@@ -0,0 +1,3445 @@
+# Cooldown Manager — Port Plan (Build Contract)
+
+Downport of `ReferenceAddons/NewEra/CooldownViewer/` + `CooldownViewerSettings/` (Classic 1.15 /
+TBC 2.5.x) onto 3.3.5a. Read `CONTRACTS.md` §0 first — every global convention there applies.
+
+**Status: the port is complete.** Phases 0-4a, 4b-1 through 4b-5, 4c (the Settings tab), 5a (on-use
+trinkets), 6 (loose ends), 7 (the tracked-aura catalog), 8 (the art) and 9 (the guide audit) are
+implemented — the whole of both phasing tables except the deliberate cuts. Offline harnesses pass
+(`qa/offline/`, 635 boot assertions). Phases 1-3 and the 4b-1 window shell are confirmed working
+in-game; 4b-2 and 4b-3 are confirmed in-game (three faults found and fixed, §G.7.1/§G.7.2).
+
+**Phase 8 is DONE** (§H.2), one item at a time: **8a** registered six atlases the viewers had been
+setting by name and that existed nowhere, so every one rendered as an invisible texture — which is why
+the icons looked like bare spellbook icons. **8c** lights a spell's frame gold while its own buff is
+up, closing the last headline feature the retail guide lists and we did not have. **8d** is the bar
+3-slice and **8e** the pandemic ring. **8b** — the gold cooldown swipe — was measured, attempted and
+declined (§H.2.11): the ClassicAPI capture rig it needs is unexercised code that costs ~200 frames and
+takes our countdown numbers out with it, for a capability 8c already substitutes for.
+
+**Phase 9 is DONE** (§H.3): every feature the retail guide describes now has a verdict. Two of its
+three open items had already been paid for by earlier phases — the buffed-spell glow by 8c, and target
+DoT timers by Phase 7, which is what made `ScanTargetTrackedAuras` reachable at all. The third was
+the "Not Displayed" rename, which is the only code Phase 9 shipped.
+
+Phase 7 is **DONE** (§H.1, notes in §H.1.1): the Tracked Buffs tab now has two real sources — a
+generated per-class catalog gated on the player's actual talents, and a registry of auras the scan has
+met.
+
+Also outstanding: consumables (§G.9, parked as a stretch goal by the owner — blocked on a generator
+pass, not on effort), and three items needing the game rather than the harness — §F4 taint under a
+combat restack, §F5's four-movers-versus-one call, and an in-game pass over 4b-4, 4b-5, 5a and 4c. See
+the end of §G.13.
+
+---
+
+## A. What the source actually is
+
+Retail's Cooldown Manager is `C_CooldownViewer` / `C_CooldownViewerSettings` — client-side C++ added
+in 11.0 (TWW). That API does **not** exist on 3.3.5a, and it does not exist on Classic Era or TBC
+Classic either: `C_CooldownViewer.IsCooldownViewerAvailable()` returns false there.
+
+So NewEra never ported Blizzard's Lua. It built a **visual port with a custom data driver** — retail's
+frame stack (icon viewers, cooldown swipe, GCD flash, out-of-range shadow) fed by hand-curated
+per-class spell lists plus live `GetSpellCooldown` / `UNIT_AURA` / `GetInventoryItemCooldown` reads.
+The source header states this explicitly (`CooldownViewer.lua:1-17`).
+
+**This is the single most important fact for planning:** the hard part — inventing a cooldown data
+model for a client with no spec system and no curated cooldown sets — is already solved and shipped.
+`CdmSeedTBC.lua` is a working precedent for layering one flavour's data on top of the vanilla base,
+which is exactly the shape WotLK needs. Our work is the **platform gap**, not the design.
+
+Size: **5,444 lines** across 12 files in `CooldownViewer/`, plus **2,139 lines** in
+`CooldownViewerSettings/`.
+
+| File | Lines | Ports as-is? |
+|---|---|---|
+| `CooldownViewer.lua` | 1,715 | mostly — `getOpt` retarget + `RegisterUnitEvent` |
+| `ItemMixins.lua` | 1,289 | mostly — mask + swipe strip |
+| `Alerts.lua` | 520 | partial — pandemic FX unportable (§E6) |
+| `ClassData.lua` | 392 | yes (vanilla base) |
+| `SoundAlertData.lua` | 349 | partial — kit IDs inert, ships extracted OGGs (§E6) |
+| `EditModeRegister.lua` | 213 | **no — replace wholesale** |
+| `CooldownViewerEquip.lua` | 182 | yes |
+| `CdmSeedTBC.lua` | 131 | template for a new WotLK seed |
+| `AlertData.lua` | 100 | regenerated for WotLK, not copied (§E6) |
+| `RacialsTBC.lua` | 55 | template |
+| `Assets.lua` | 50 | partial (swipe art unused in v1) |
+| `CooldownViewer.xml` | 448 | needs `MaskTexture` + `GridLayoutFrame` surgery |
+
+---
+
+## B. Architect decisions (locked — flag before deviating)
+
+### B1. No Edit Mode. Movers + options tab instead.
+
+`EditModeRegister.lua` is written against `NE.editmode` — a **6,441-line retail Edit Mode
+reimplementation** (`ReferenceAddons/NewEra/EditMode/`) that this addon does not have and will not
+port. It uses `EM.Register` with a full settings codec (`system` / `systemIndex` / `defaultSettings` /
+`settings` / `options`), `EM.GetFrameSettingStored`, `EM.const.StoredToDisplay`,
+`EM.RegisterBottomManaged`, `EM.UpdateBottomManagedFrames`, `EM.RegisterToggleableFrames`,
+`EM.ShouldShowActivity`, `EM.RepositionHandle`, `EM.AtLogin`.
+
+DragonUI's equivalent is **position-only**: `core/movers.lua` is a drag handle that persists a point
+string; `modules/editor_mode.lua` adds a grid overlay and a reset button. No per-frame settings popup,
+no options metadata, no bottom-managed stack, no settings codec. **DragonUI is read-only** (CONTRACTS
+§0) so we cannot extend it.
+
+> **CORRECTION — found on the first in-game test, after this section was written.** DragonUI has
+> **two independent** positioning systems and only one is what `/dui edit` drives:
+>
+> | system | registered via | shown by |
+> |---|---|---|
+> | `addon.MoversSystem` (`core/movers.lua`) | `NE.RegisterPanel` | **nothing** — `ToggleConfigMode` sits behind a dead `elseif` in `core/commands.lua:31`; `addon.EditorMode` always exists and wins the first branch |
+> | `addon.EditableFrames` (`core/api.lua:551`) | `addon:RegisterEditableFrame` | `/dui edit` → `EditorMode:Show` → `addon:ShowAllEditableFrames` |
+>
+> An always-on HUD frame MUST use the second, or it registers cleanly and is then silently never
+> shown. `NE.RegisterHUDFrame` (integration/Register.lua) is the seam; `NE.RegisterPanel` stays
+> correct for toggled WINDOWS, where a MoversSystem handle is fine.
+>
+> **And `RegisterEditableFrame` alone is not enough** — it only records metadata. The drag
+> affordances (`RegisterForDrag`, `OnDragStart`/`OnDragStop` with its auto-save, the green nineslice
+> overlay, the label) are attached by DragonUI's frame FACTORY, `addon.CreateUIFrame`
+> (`core/api.lua:255`). `addon.HideUIFrame`, which the editor calls on each registered frame, only
+> does `SetMovable(true)` / `EnableMouse(true)` and shows an overlay a plain `CreateFrame` does not
+> have. So the required pattern — the one `modules/castbar.lua` uses with `CastbarModule.anchor` —
+> is: build a `CreateUIFrame` **anchor**, register the ANCHOR, and pin the real content to it.
+> `NE.RegisterHUDFrame` does this and keeps the anchor's size synced to the content via
+> `OnSizeChanged`.
+>
+> Two further gotchas found with it: DragonUI's `ApplyUIFramePosition` reads `x`/`y` gated on an
+> `override` flag that nothing sets, while `SaveUIFramePosition` writes `anchor`/`posX`/`posY` —
+> they do not round-trip, so position restore is ours. And registration must happen at
+> `PLAYER_LOGIN`, not file load, matching every other NewEra module.
+
+**Decision:** delete `EditModeRegister.lua`. Replace with:
+- one DragonUI mover per viewer, via the existing `NE.RegisterPanel` path in `integration/Register.lua`
+- the ten settings rendered in the DragonUI_Options "New Era" tab
+
+`getOpt` (`CooldownViewer.lua:141`) is the single chokepoint every setting reads through, so
+retargeting it at our own DB table is a contained change. The whole `M.CDV_CODEC` retail-int mapping
+(`CooldownViewer.lua:126-139`) and the `sliderToStored` / `StoredToDisplay` conversions go away —
+we store display values directly.
+
+Cost: ~300 lines of new glue instead of 6,441 lines of ported Edit Mode.
+
+**Note on precedent:** `modules/character/EditModeRegister.lua` declines Edit Mode on the grounds
+that it is "scoped to always-on HUD frames, NOT toggled windows." The Cooldown Manager *is* a
+persistent HUD frame — it is the first module that rule would say *should* have one. We are declining
+anyway, because the cost is the entire Edit Mode system and DragonUI's movers cover the actual need
+(position + scale). Revisit only if per-viewer settings prove unusable in the options tab.
+
+### B2. Four independent frames, not a combined group.
+
+Follow the source's current SPLIT model (`CooldownViewer.lua:240-245`): Essential / Utility /
+BuffIcon / BuffBar are four independently positioned frames. The older combined-container approach is
+already reverted upstream; do not resurrect it. Default anchors (BOTTOM of UIParent):
+Utility y=240, Essential y=310, BuffIcon y=370, BuffBar x=420 y=430.
+
+### B3. No bottom-managed stacking.
+
+`EM.RegisterBottomManaged` reflows the viewers above the action-bar tower as bars are added. There is
+no DragonUI equivalent. Viewers sit where the user puts them. Drop the `OnSizeChanged` restack hook
+(`EditModeRegister.lua:159-164`) with it.
+
+---
+
+## C. Platform gaps (verified against ClassicAPI + DragonUI, 2026-07-25)
+
+### C1. `GridLayoutFrame` — MISSING, must build
+
+All four viewer frames inherit it (`CooldownViewer.xml:348,374,400,427`). The layout code depends on
+`:Layout()`, `layoutIndex`, `stride`, `childXPadding`/`childYPadding`, `layoutFramesGoingRight`,
+`layoutFramesGoingUp`, `alwaysUpdateLayout`, `ignoreInLayout`, `ResizeLayout`.
+
+ClassicAPI ships `AnchorUtil.CreateGridLayout` / `GridLayoutMixin` **helpers** (`Util/AnchorUtil.lua:49-101`)
+but **not** the `GridLayoutFrame` XML template. Grep confirms the only occurrence of the string in the
+whole AddOns tree is the NewEra source XML itself.
+
+→ Write `core/GridLayout.lua` (~150 lines) implementing LayoutFrame / GridLayoutFrame mixin semantics,
+applied in Lua at frame creation rather than via XML `inherits`. Reusable by any future HUD module.
+
+### C2. `` — MISSING, cannot polyfill
+
+Used in all four item templates for the rounded icon mask (`CooldownViewer.xml:56,134,208,279`).
+MaskTexture is a Legion widget. It does not exist on 3.3.5a, no polyfill is possible in Lua, and an
+unknown XML node risks failing the entire file's parse. CONTRACTS §0 already lists `SetMask` as a hard
+rule, and `qa/staticcheck.sh` greps for it.
+
+→ Drop the mask. Keep the `iconoverlay` art layered on top, crop the icon with `SetTexCoord`
+(~0.07–0.93). `core/ButtonSkin.lua` already establishes this pattern.
+
+### C3. `Cooldown:SetSwipeTexture` / `SetEdgeTexture` / `SetUseCircularEdge` — MISSING
+
+WoD+ APIs, called at `ItemMixins.lua:70-71,917-918`. ClassicAPI's `Util/Cooldown.lua` is a private
+quadrant-based `CooldownCapture` renderer reached through `Private`, **not** a widget-method polyfill —
+it exports nothing that satisfies these calls.
+
+→ Accept the engine's built-in sweep. Guard the calls (`if self.Cooldown.SetSwipeTexture then`) — the
+source already does at line 70. The DF swipe/edge art registered in `Assets.lua` (FDIDs 6731092,
+5423465) goes unused in v1. Cosmetic only.
+
+**Unverified:** whether 3.3.5a's Cooldown widget has `SetDrawEdge`. ClassicAPI's `CooldownFrame_Set`
+calls it unconditionally (`Util/Cooldown.lua:12`). Confirm before relying on it.
+
+### C4. `self:RegisterUnitEvent(...)` — MISSING as a widget method
+
+Called throughout the viewer `OnLoad`s (`CooldownViewer.lua:307-324`, `1321`). ClassicAPI provides
+`EventHandler.RegisterUnitEvent(Object, Event, ...)` as a **namespaced function**
+(`Util/EventHandler.lua:189`, exported at `:270`), not a method on the frame metatable.
+
+→ Small shim in `compat/Compat.lua` adding the method to a frame, delegating to the ClassicAPI
+dispatcher. Cheap and reusable.
+
+### C5. `CooldownFlash` flipbook — ALREADY SOLVED
+
+Good news: `ItemMixins.lua:463` implements the flipbook as a hand-rolled OnUpdate texcoord stepper,
+written precisely because Era lacked retail's animation system. Ports as-is.
+
+### C6. Present and usable
+
+`NE.tex` / `RegisterLocal` / `SetAtlas` (`core/Texture.lua`), `NE.FrameUtil.PinPixelPerfect`,
+`NE.Log`, `NE.flavor`, `CreateFromMixins` (ClassicAPI `Util/Mixin.lua`), `C_Timer`, `C_Item`,
+`C_Container`. The settings panel builds on `ButtonFrameTemplate`, which ClassicAPI provides
+(`Templates/UIPanelTemplates.xml`).
+
+### C7. Missing NewEra helpers — small substitutions
+
+| Symbol | Used for | Substitution |
+|---|---|---|
+| `NE.EV_LEARNED_SPELL` | learn-spell event name | one-line const, `LEARNED_SPELL_IN_TAB` |
+| `NE.spellbook.SPELLID` | rank expansion (`CdmSeedTBC.lua:95-109`) | build by scanning the spellbook at login — do **not** ship a db2 dump |
+| `NE.actionbar1.procglow` | proc/activation glow (`CooldownViewer.lua:1026-1028`) | LibCustomGlow (already embedded, see `modules/spellbook/Spellbook.lua`) + 3.3.5a's `IsSpellOverlayed` / `SPELL_ACTIVATION_OVERLAY_GLOW_SHOW` |
+| `NE_CDM_HIDDEN` | generated all-cooldowns superset | optional — code fails open (`CooldownViewer.lua:622`) |
+| `NE_SPELL_RACEMASK` | race-gating (`CooldownViewer.lua:654`) | optional — fails open by design |
+
+---
+
+## D. Data work (WotLK)
+
+The engineering above is bounded; **this is the main authoring cost.**
+
+`CdmSeedTBC.lua` (131 lines) + `RacialsTBC.lua` (55) are the templates. Both are flavour-guarded
+(`if not (NE and NE.flavor == "tbc") then return end`) and purely additive over the vanilla
+`ClassData.lua` base. A `CdmSeedWotLK.lua` follows the identical shape.
+
+Needed:
+- **Death Knight in full** — absent from every table; new class in 3.3.5a.
+- **WotLK additions for the other nine.** Non-exhaustive: Warrior Bladestorm/Shockwave; Paladin
+ Divine Storm/Hand of Protection-Freedom-Sacrifice; Hunter Explosive Shot/Call of the Wild; Rogue
+ Fan of Knives/Killing Spree/Shadow Dance; Priest Penance/Dispersion/Hymn of Hope; Mage Deep
+ Freeze/Mirror Image/Living Bomb; Warlock Metamorphosis/Haunt/Demonic Circle; Shaman Feral
+ Spirit/Thunderstorm/Riptide; Druid Starfall/Berserk/Wild Growth/Survival Instincts.
+- **Buff-viewer seed.** Per `CdmSeedTBC.lua:83-89`, seed only *permanent state toggles* (armors,
+ aspects, forms, shields) as `icon`. The ≤120s auto-window (`BUFF_TRACK_MAX_DURATION`,
+ `CooldownViewer.lua:1115`) handles short procs. Do **not** seed long maintenance buffs — upstream
+ removed those as clutter.
+
+**Sourcing rule (CLAUDE.md prime directive, restated in `CdmSeedTBC.lua:21-26`):** every spellID must
+come from the 3.3.5a client data, not memory. Store one representative rank per ability; the runtime
+rank resolver (`highestKnownRankID`) picks the learned rank live.
+
+---
+
+## E. Phasing
+
+| Phase | Scope | Exit criterion |
+|---|---|---|
+| **0** | `core/GridLayout.lua`; `RegisterUnitEvent` shim; `EV_LEARNED_SPELL`; spellbook rank table. No CDM code. | A throwaway grid of 12 test frames lays out, wraps at stride, and reflows on padding change. |
+| **1** | Essential + Utility viewers. Mask and swipe stripped. One mover each. Settings in options tab. | Icons appear, swipe on cast, timer counts down, `/dnetest` PASS. The "does it feel right" checkpoint. |
+| **2** | `CdmSeedWotLK.lua` + `RacialsWotLK.lua` + Death Knight. | Every class shows a sensible default set at 80. |
+| **3** | ~~BuffIcon + BuffBar (aura-driven; more moving parts).~~ **DONE** — see §E2. | Buff bars track, auto-window catches procs, no empty-row churn. |
+| **4a** | `Alerts.lua` + `SoundAlertData.lua` + `AlertData.lua` — the ENGINE and its stores. **DONE** — see §E6. | Assigning an alert or sound programmatically makes it fire. |
+| **4b** | The `CooldownViewerSettings/` panel: spell picker, per-spell alert/sound assignment, drag reorder, presets. Re-scoped in §E6. | Alerts, sounds and spell visibility are all settable from the UI. |
+
+Phases 1 and 2 are the shippable unit. Phase 3 onward is optional polish.
+
+---
+
+## E1. Implementation notes (added after building Phases 0–1)
+
+Three things the scope did not anticipate, all resolved:
+
+1. **`GetSpellInfo` position 7 is `castTime` on 3.3.5a, not `spellID`.** The WotLK signature is
+ `name, rank, icon, cost, isFunnel, powerType, castTime, minRange, maxRange` (confirmed by
+ `!!!ClassicAPI/Util/C_Spell.lua:39`). The source's `highestKnownRankID` is built entirely on
+ `select(7, GetSpellInfo(name))`, so ported verbatim it would return e.g. `1500` for a 1.5s cast
+ and use it as a spell ID — silently, since that is a plausible-looking number. This promoted
+ `core/SpellRanks.lua` from a convenience to **load-bearing**: it is the only correct source of
+ "highest rank the player knows" on this client.
+
+2. **`RegisterUnitEvent` could not be delegated.** !!!ClassicAPI implements the semantics
+ (`Util/EventHandler.lua:189`) but only inside its `Private` namespace — never exported — so
+ `compat/Events.lua` implements it on the Frame metatable instead (RegisterEvent + a per-frame
+ allow-set enforced by wrapping that frame's OnEvent).
+
+3. **`Rebuild → RefreshLayout → UpdateVisibility → Show → OnShow → Rebuild` is a re-entrancy
+ cycle.** The client only fires `OnShow` on a hidden→shown transition, so it settles at depth two
+ rather than recursing forever — but that is an implicit dependency on client behaviour. Guarded
+ explicitly in `Viewers.lua:Rebuild`.
+
+Also deferred deliberately in Phase 1: `CooldownViewerEquip.lua` (trinket/potion discovery) is
+stubbed to return empty, with the consuming loop left in place so the Phase 2 port drops in.
+
+## E2. Phase 3 notes (aura viewers)
+
+Built out of order, ahead of Phase 2, because the buff viewers need **no class data**: they read
+live auras, not curated cooldown lists. Three parts:
+
+- `AuraItemMixins.lua` — the BuffIcon tile (reverse swipe = aura elapsed, stack count, dispel
+ border) and the BuffBar row (depleting StatusBar + pip, driven by a per-frame OnUpdate off cached
+ expiration/duration rather than a per-frame aura scan).
+- `BuffViewers.lua` — both viewer frames plus the shared aura-scan rebuild.
+- Tracked-aura pool in `CooldownViewer.lua` — ONE pool as retail models it: each aura is `icon`,
+ `bar` or `hidden`, so a bar-assigned aura never also renders as an icon.
+
+Model: any player buff with `0 < duration <= 120s` auto-tracks, which is what makes trinket, potion
+and proc buffs work without enumerating them. Longer buffs and permanent toggles are excluded, so
+the viewers stay quiet out of combat. Explicit assignments override the window in both directions.
+Target auras are a secondary source for EXPLICITLY tracked entries only — never the auto window,
+which would flood the viewer with every enemy debuff.
+
+**The trap worth naming:** the 1.15 source scans with
+`local name, icon, count, _, duration, expiration, _, _, _, spellID = UnitBuff("player", i)` — the
+MODERN return layout. On 3.3.5a `rank` occupies index 2 and shifts everything after it, so ported
+verbatim `icon` receives the rank string and `duration` the caster. All scanning here goes through
+`NE.aura` (core/AuraSnapshot.lua), which owns the correction in one place. `test_boot.lua` asserts
+the icon comes from index 3 specifically, as a regression guard.
+
+Two stride subtleties preserved from upstream, both of which otherwise wrap the stack into a
+phantom extra column: `GetStride` counts LAYOUT children (has `layoutIndex`, not `ignoreInLayout`)
+rather than SHOWN ones, and `BuffBarItem:UpdateShownState` keeps `ignoreInLayout` mirroring
+visibility.
+
+## E3. Phase 2 notes (WotLK data)
+
+131 abilities across all ten classes, including the whole of Death Knight (9 Essential, 15 Utility)
+which had no vanilla base at all. Additive appends onto `ClassData.lua`, same shape as
+`CdmSeedTBC.lua`.
+
+**Every ID is generated from this client's own data, not typed from memory** — see
+`tools/cdm-spellgen/`. `Spell.dbc` and `SkillLineAbility.dbc` are extracted from the locale MPQs
+(`Data/enUS/`, not the base archives), spells are attributed to classes via each skill line's
+ClassMask, and each authored ability NAME resolves to the rank-1 castable ID. "Castable" is defined
+as the lowest-rank, lowest-ID entry carrying a real cooldown (> 1.5s) — which is what separates an
+ability from its triggered sub-spells: Penance resolves to 47540, not its 47666/47750 heal and
+damage triggers; Death Grip to 49576, not 49560/49575.
+
+Only the curation is hand-authored: which ability is Essential vs Utility. An unresolvable name is
+a hard error rather than a silent omission, and `verify.py` independently re-reads the DBC to assert
+every emitted ID exists, matches its comment, has a cooldown, is rank 1, and is unique. That gate
+caught three entries that had no business in a pressable-cooldown list — Immolation Aura and Demon
+Charge (rank `Demon`, Metamorphosis-form only) and Reincarnation (rank `Passive`).
+
+Column positions were located empirically rather than assumed: `Spell.dbc` name 136, rank 153,
+RecoveryTime 29, CategoryRecoveryTime 30 (cooldown is the max of the last two). An early attempt to
+find the cooldown column by matching remembered durations failed outright — a small reminder of why
+the sourcing rule exists.
+
+**Known gap:** `Data/patch-4.MPQ` and `Data/patch-S.mpq` are encrypted and unreadable. If the server
+overrides spell data there, these IDs reflect the stock client. Repeated in the generated file's
+header.
+
+## E4. Learn-gate fix (after first in-game test of Phases 2-3)
+
+Reported: a Disc priest's **Penance** and a Holy priest's **Guardian Spirit** never appeared despite
+both talents being taken, and **Divine Hymn** never appeared on a level-squished server where the
+character knows it at 60. All three are one fault — the learn-gate, not the data.
+
+The source checks `IsSpellKnown(spellID)` then falls back to `GetSpellInfo(name)`. Both fail here:
+
+- `IsSpellKnown(id)` tests **one exact rank**. Our curated lists key each ability by its rank-1 id,
+ so the check goes false the moment the player trains rank 2 — which is most talent abilities.
+- `GetSpellInfo(name)` is a spell-database lookup on this client, not a membership test.
+- Neither consults the spellbook, and neither is level-aware — which matters on a squished server.
+
+Fixed by asking the **spellbook, by name** (`SB.IsSpellNameKnown`, core/SpellRanks.lua). That is
+authoritative, rank-agnostic and level-agnostic. The name set is built from `GetSpellBookItemName`
+alone — deliberately without id resolution — so an entry whose `GetSpellLink` can't be parsed still
+counts as known. The old checks remain as fallbacks, so the change can only widen what shows.
+
+Two supporting changes: the rank table now builds lazily on first use (viewers rebuild on
+`PLAYER_ENTERING_WORLD`, which can precede the deferred `SPELLS_CHANGED` build), and
+`SB.OnRebuilt` lets the viewers re-source after training or a spec switch.
+
+`/necdm` prints the gate's decision per curated spell — book / IsSpellKnown / byName — so the next
+report of a missing icon doesn't need guesswork.
+
+## E5. Custom-list shadowing (the second half of the "missing abilities" report)
+
+The learn-gate fix in §E4 was necessary but not sufficient. `/necdm` then showed the real fault:
+
+```
+hidden Holy Fire id=14914 book=true IsSpellKnown=true byName=true
+```
+
+Every check passing, still hidden — so the gate was never the filter. `10 curated, 3 shown` was the
+tell.
+
+`M.GetItemMeta` is a read-only query, called from `ItemMixins:SetSpell` for **every icon on every
+rebuild** — and it called `GetEditableList`, which SEEDS AND PERSISTS a custom list from the curated
+defaults. So the first time any viewer built, it froze the then-current spell list into
+SavedVariables; `GetActiveSpellList` took the `custom` branch from then on and ignored the curated
+tables permanently. Every ability added later — the entire WotLK seed — was invisible on any
+character that had run an earlier build.
+
+Fixes:
+1. `GetItemMeta` now uses `GetCustomList` (read-only). **A query must never pin persistent state.**
+2. `M.MigrateStaleCustomLists`, versioned and run once at boot, clears lists that were never
+ deliberately authored. Until the Phase 4 picker exists there is no way to author one, so every
+ stored list is an artifact and clearing it loses nothing the user chose.
+3. `/necdm` now reports when a custom list is shadowing the curated table. Its absence from the
+ first version is exactly why this took two rounds to find.
+
+Upstream carries the same guarded one-time reset, for the same reason — its boot block notes a
+stale snapshot was "hiding the new defaults". That block was dropped along with
+`EditModeRegister.lua`; this restores its intent.
+
+## E6. Phase 4a notes (alerts, sounds, and the assignment menu)
+
+§F2 asked for a re-scope before committing. Doing it changed the shape of the phase.
+
+**What did not port: the pandemic border FX** (~130 of `Alerts.lua`'s 520 lines). Upstream renders
+the `refresh` alert with a 1:1 port of retail's `CooldownPandemicFXTemplate` — a ring plus three
+cascading glows, every one clipped to the ring by a `MaskTexture`. Two independent blockers:
+
+- MaskTexture is not merely missing here. ClassicAPI defines `CreateMaskTexture` / `AddMaskTexture`
+ as `Private.Void` ("potentially impossible to implement", `WidgetAPI.lua:279/302/476`) and Cell's
+ polyfill returns an inert dummy. The calls would *succeed and clip nothing*, leaving three
+ full-quad glows scaling to 1.5x as square smears across the icon and its neighbours — worse than
+ no FX, and silently so.
+- `Animation:SetTarget` does not exist on 3.3.5a either (zero occurrences in the whole AddOns tree).
+ Animations act on the region owning the AnimationGroup, so one-group-drives-three-textures has no
+ equivalent.
+
+The atlases are absent too. So `refresh` renders through LibCustomGlow — already embedded, already
+in the TOC, and what §C7 nominated for the proc-glow substitution — tinted pandemic-orange to keep
+the one legible part of the retail look.
+
+**What the sound port actually required.** Upstream's fallback path, `PlaySound(kit)`, is *dead* on
+3.3.5a: this client's `PlaySound` takes a name string, and `PlaySoundKitID` takes a 3.3.5a kit index,
+not a six-digit TWW id. Nothing retail-numbered can make a sound. The only working route is playing
+audio by file path, so the 67 OGGs upstream extracted are shipped in `Sounds/cdm/` and played with
+`PlaySoundFile` (`.ogg` is fine here — DBM has shipped them on this client for years). The retail kit
+id survives purely as the stable assignment key. The whole `Short` category (26 entries) is dropped:
+upstream never had extractable audio for it, so here it could only ever be 26 menu entries that play
+nothing.
+
+**`AlertData.lua` is regenerated, not copied.** Upstream's tables are vanilla-only. WotLK adds Kill
+Shot (a genuine third execute ability) and Victory Rush, and *removes* Mongoose Bite's dodge gate in
+3.1.0 — listing it would flash the icon every time it came up, the exact behaviour the data gate
+exists to prevent. `tools/cdm-spellgen/gen_alertdata.py` resolves every rank from the client DBCs.
+Two impostor classes had to be filtered, both found by inspecting output rather than by assumption:
+
+- Rank text cannot discriminate. 3.3.5a gives Overpower rank 1 (7384) an **empty** rank string while
+ its ranks 2-4 are labelled — and those higher ranks appear in no skill line at all, so Overpower is
+ single-rank on this client. A "keep the ranked rows" filter drops the real ability and keeps four
+ NPC copies of Riposte.
+- `SkillLineAbility` class attribution fixes both. Applied *first*, the "drop unranked siblings" rule
+ then safely removes triggered sub-spells like Execute's damage component (20647).
+
+**The bug the harness caught: preferences were keyed on a moving id.** `ItemMixin.spellID` holds the
+*learned rank*, not the listed one — Mind Blast's tile reports 10947, not the curated 8092. Keying
+alerts and sounds on it would have silently orphaned every assignment the moment the player trained
+the next rank. `_baseSpellID` / `GetSettingsKey()` now carries the stable id, and the harness asserts
+the two differ. This is the same rank gotcha the cooldown path already documents, resurfacing in a
+new place.
+
+**The assignment surface was tried as a right-click menu, and removed.** Alerts and sounds are
+strictly opt-in per spell, so 4a otherwise ships dormant. `ItemMenu.lua` put the choices on the icon
+itself. It did not open in-game, and rather than debug a surface the Phase 4b panel replaces, the
+owner called it: delete it and do assignment in the settings panel. So the ENGINE and its stores
+ship here and nothing drives them yet — the options tab says so rather than advertising a way in
+that does not exist.
+
+`M.SetSpellEnabled` / `M.IsSpellEnabled` survive that removal and are the panel's seam for showing
+and hiding a spell. `SetSpellEnabled` is the only caller of `GetEditableList`, which is the point of
+the §E5 fix: seeding a custom list is correct exactly when the user has just chosen something.
+
+**Phase 4b, re-scoped.** `CooldownViewerSettings/` is 2,139 lines across six files, and its retail
+dependencies are heavier than §F3 assumed: `MenuUtil` (absent), `GLOBAL_MOUSE_UP` (retail 9.x,
+absent — `Reorder.lua` is built entirely on it), `LargeSideTabButtonTemplate` (absent), clipboard
+export (no `CopyToClipboard` on 3.3.5a), and the CDM side-tab art. It is a phase in its own right,
+not a tail on this one. The per-spell menu covers the assignment need in the meantime.
+
+## E7. Faults found on the first in-game tests of Phase 4a
+
+All of these predate this phase. Each had been invisible because a harness stub was more generous
+than the real client — a pattern worth noting in its own right: every one of these was found by a
+player looking at the screen, not by the 156 assertions.
+
+1. **Every icon rendered grey.** `RefreshIconColor` called `IsUsableSpell(self.spellID)`. On 3.3.5a
+ that function takes a spell NAME, or a spellbook INDEX with a bookType — never a spellID. Given
+ one it reads it as an index far past the end of the book and returns **nil**, without erroring.
+ nil is neither usable nor out-of-mana, so every icon fell through to `ICON_UNUSABLE` and the
+ whole viewer dimmed to 40%. DragonUI's own action bars pass a name
+ (`modules/actionbars/extrabar.lua:1049`). The harness stub returned a blanket `true` regardless
+ of argument, which is exactly why it never caught this; it now mimics the client.
+
+2. **The ready flash never played, for two compounding reasons.** `NE.tex.GetAtlasRect` returns four
+ NUMBERS, not a table, and the flipbook stepper indexed the result as `atlas.right` — so `atlas`
+ was the number `left` and every access inside an OnUpdate was an error waiting to happen. It
+ never got that far, because the "art not shipped, degrade quietly" guard was
+ `if not getFlashAtlas()`, and `GetAtlasRect` returns `0, 1, 0, 1` for an unknown atlas — `0` is
+ truthy in Lua, so the guard passed precisely when the art was missing and `ScheduleFlash` bailed
+ for the wrong reason. `NE.tex.HasAtlas` is the correct test. The retail GCD flipbook is not
+ registered on this client, so the sprite path stays dormant and an undecorated alpha pulse on
+ `Interface\Buttons\ButtonHilight-Square` (a base texture DragonUI itself uses) provides the cue.
+ The sprite path takes over automatically if the atlas is ever registered.
+
+3. **Icons went grey on cast and STAYED grey.** Distinct from fault 1, and only visible once that
+ was fixed. Every event the viewer listens for — `SPELL_UPDATE_COOLDOWN`, `UNIT_SPELLCAST_*`,
+ `BAG_UPDATE_COOLDOWN` — marks a cooldown STARTING or changing. **3.3.5a fires nothing when one
+ expires** (`Alerts.lua`'s header already said so, in the course of explaining why the ready
+ transition has to be polled). The swipe still completed, because the Cooldown widget animates
+ itself in C, but no Lua re-ran, so the `SetDesaturated(true)` from the start of the cooldown
+ persisted until some unrelated event refreshed the tile — usually the player's next cast. Each
+ item now schedules its own refresh for the moment its cooldown ends, keyed on the end time so
+ repeated refreshes during one cooldown don't stack timers.
+
+ The harness could not have caught this: its `C_Timer.After` stub ran every callback immediately
+ at the next drain, regardless of delay, so a scheduled refresh would have "worked" no matter
+ what. The stub now honours the delay, and the regression test asserts the tile un-desaturates
+ **with no event fired at all**.
+
+4. **The ready flash was too faint to notice.** Fixing the plumbing (fault 2) was not enough: an
+ alpha blink on an icon-sized quad barely registers against the icon art beneath it. The fallback
+ burst now snaps to full brightness and expands to ~1.7x the icon while fading, in warm gold on an
+ ADD blend, and sits two frame levels above the Cooldown swipe it plays over.
+
+5. **`AceLocale: Missing entry for 'CooldownViewerBuffBar'` on every login.** DragonUI's
+ `CreateUIFrame` labels the editor handle with `addon.L[frameName]`, and AceLocale's read metatable
+ fires a non-breaking error for any undefined key. Harmless but noisy, once per registered frame.
+ The table `GetLocale` returns has an `__index` hook but **no `__newindex`**, so `NE.RegisterHUDFrame`
+ now seeds its own key — a plain assignment into a runtime table, not a change to DragonUI — which
+ both silences the warning and upgrades the handle's label from `CooldownViewerBuffBar` to
+ "Buff Bars".
+
+## F. Open questions / unverified
+
+1. ~~**`Cooldown:SetDrawEdge` on 3.3.5a**~~ — **answered on the evidence, and `/necdm` now prints the
+ runtime answer.** ClassicAPI stubs `SetEdgeTexture` / `SetEdgeColor` / `SetEdgeScale` as
+ "Incompatible (3.3.5)" (`Util/WidgetAPI.lua:1024-1026`) but does NOT stub `SetDrawEdge`, and its own
+ cooldown-capture path calls `Self:GetDrawEdge()` unguarded (`Util/Cooldown.lua:204`) on every
+ captured cooldown's OnShow — which would error constantly on every action button if the method were
+ missing. `Process` injects unconditionally rather than filling gaps (`WidgetAPI.lua:1338`), so an
+ absence from its table means native, not forgotten. Both call sites of ours guard anyway
+ (`ItemMixins.lua:81`), so nothing depends on the answer; the `/necdm` line settles it rather than
+ leaving an inference from someone else's code standing as fact.
+2. ~~`Alerts.lua` and `SoundAlertData.lua` not dependency-mapped.~~ **Done — see §E6.**
+3. ~~`CooldownViewerSettings/` blockers named but not designed.~~ **Done — scoped in §G.** Two of
+ the blockers §E6 listed were wrong; see §G.1.
+4. **Taint.** The viewers are pure display frames with no secure attributes, and `EnsureGroup`
+ reparents them freely, so taint should be a non-issue — but Phase 1 must verify no combat-lockdown
+ errors on `SetPoint` during a live restack.
+5. **Whether four separate movers is tolerable UX** in DragonUI's editor mode versus one grouped
+ handle. Decide during Phase 1 with the frames on screen.
+
+---
+
+# G. Phase 4b — the settings panel (`/cdm`)
+
+Scope for porting `ReferenceAddons/NewEra/CooldownViewerSettings/` (2,139 lines / 6 files) as a
+**standalone window opened with `/cdm`**, matching upstream's own choice of a free dialog rather
+than a managed UIPanel.
+
+This is where spell picking and per-spell alert/sound assignment live. Phase 4a shipped the engines
+and their stores with nothing driving them; this closes that.
+
+## G.1. Corrections to §E6
+
+Two blockers named there do not exist. Checking beats remembering:
+
+- **`LargeSideTabButtonTemplate` is not a problem.** Upstream synthesized it because Era lacks it —
+ and so did we, already, for another module: `NE.tabs.MakeSideTab` (`core/Tabs.lua:222`) is the
+ same substitution, complete with tooltip wiring. The Spells/Auras side tabs use it as-is.
+- **Clipboard export is not a problem.** `CopyToClipboard` appears nowhere in the source. Export
+ goes through a `StaticPopup` with a pre-selected edit box (`Presets.lua:260`) — the classic
+ manual-copy pattern, native on 3.3.5a.
+
+Also better than expected: **no `WowScrollBox` / `ScrollUtil` anywhere.** The body is a plain
+`UIPanelScrollFrameTemplate` with a `SetScrollChild` (`Panel.lua:377`), which is native here and
+which `NE.scrollbar.Reskin` already knows how to restyle.
+
+## G.2. What already exists
+
+| Need | Have | Where |
+|---|---|---|
+| Side tabs | `NE.tabs.MakeSideTab` | `core/Tabs.lua:222` |
+| Window chrome | `NE.chrome.Apply` | `core/PanelChrome.lua:277`; precedent `modules/collections/Window.lua:264` |
+| Scrollbar restyle | `NE.scrollbar.Reskin` | `core/ScrollbarReskin.lua:156` |
+| Portrait cutout | `NE.portrait.ApplyCutout` | `core/Portrait.lua` |
+| Open/close sounds | `NE.FrameUtil.WirePanelSounds` | `core/FrameUtil.lua:246` |
+| Templates | `ButtonFrameTemplate`, `SearchBoxTemplate` (ClassicAPI); `UIPanelScrollFrameTemplate`, `UIPanelButtonTemplate`, `StaticPopupDialogs` (native) | — |
+| Panel background art | `character-panel-background` (5882640) already registered | `modules/character/Assets.lua:35` |
+| Alert + sound stores | shipped in Phase 4a | `Alerts.lua`, `SoundAlertData.lua` |
+| Show/hide a spell | `M.SetSpellEnabled` / `M.IsSpellEnabled` | `CooldownViewer.lua` |
+| ESC-to-close, slash command | `UISpecialFrames` + `SLASH_*` | precedent `modules/encounterjournal:643` |
+
+## G.3. What must be built
+
+1. **`core/Menu.lua` — a MenuUtil-shaped builder over `UIDropDownMenu`.** The highest-leverage
+ piece by far. All three menu sites (`Categories.lua:42` item menu, `Panel.lua:192` settings menu,
+ `Presets.lua:348` layout menu) are written against MenuUtil's builder API — `root:CreateTitle`,
+ `CreateButton`, `CreateRadio`, `CreateDivider`, arbitrarily nested. Reimplementing that API over
+ 3.3.5a's `UIDropDownMenu` (~120 lines) lets all three port close to verbatim, instead of
+ rewriting each by hand. Use ClassicAPI's `C_UIDropDownMenu`, which grows
+ `C_UIDROPDOWNMENU_MAXLEVELS` on demand — the sound menu needs four levels
+ (item → Ready Sound → category → entry) and the native one caps at two.
+
+ This is also the correct home for the Phase 4a right-click menu that was deleted: upstream's
+ `showItemMenu` already carries Move-to / Remove / Ready Sound / Alert (type, FX, window), and it
+ maps onto the 4a API almost one-to-one. Only the FX enum differs — upstream uses `1 = ants`,
+ `6 = flash`; ours is `1/2/3` over LibCustomGlow (`AL.FX`), so drive the submenu off `AL.FX`.
+
+2. **Drag reorder without `GLOBAL_MOUSE_UP`** (`Reorder.lua`, 6 uses). That event is retail 9.x.
+ The file already runs an `OnUpdate` driver while a drag is active, so the substitution is to
+ watch `IsMouseButtonDown` transitions there and call the existing `endChange` / `CancelOrderChange`
+ on release. ~30 lines changed, not a rewrite. `GetMouseFoci` → `GetMouseFocus` is already
+ fallback-handled at `Reorder.lua:23`.
+
+3. **`NE.listheader`** — the collapsible category header. Absent, but with in-repo precedent:
+ `modules/character/Reputation.lua:68` notes the same gap and builds one inline.
+
+4. **Small shims:** `NE.button.Skin` and `NE.dropdown.SkinStyle` (cosmetic — may no-op initially),
+ `NE.OpenOptions` (point at our `NE.optionSections` tab), `SetShown` → the local `setShown`
+ helper (10 sites, CONTRACTS §0).
+
+5. **Art:** copy `7289697-cdmadvanced.blp` from the reference `Art/CooldownViewerSettings/`. Only
+ two of its three glyphs are needed (`icon_cooldownmanager`, `icon_trackedbuffs`).
+
+## G.4. Deliberate cuts
+
+- **The Group Buffs side tab.** 12 references to `NE.groupbuff.filter`, a module this addon does not
+ have. Two tabs, not three — `CDS.UpdateGroupBuffsTabState` goes with it.
+- ~~**`NE.editmode.Toggle` / `SelectFrame`** (4 refs)~~ — **routed, in Phase 6 (§G.13).** Upstream's cog
+ menu carried an "Edit Mode" entry that hid the window and toggled retail Edit Mode
+ (`Panel.lua:211`); ours is a **per-viewer** "Position this viewer" button on the Settings tab, going
+ through the new `NE.OpenFrameEditor` seam. Per viewer rather than once globally because DragonUI's
+ editor can be told which frame to select, so the button under Buff Bars' sliders opens the editor
+ with Buff Bars selected. The fourth ref, `RepositionHandle` (`CooldownViewer.lua:977` — re-anchor the
+ drag rect as items come and go), needs no port: `RegisterHUDFrame` syncs the anchor to the content's
+ size from the content's own `OnSizeChanged`.
+- **`NE.OpenOptions`** (upstream's "Show Options" jump out to the addon's option surface) — **not
+ ported, and now pointless.** The traffic runs the other way since §G.12: DragonUI's section is an
+ enable toggle and a button that opens `/cdm`. A link back to a page holding one checkbox would be
+ worse than its absence.
+- ~~**Presets / layouts**~~ — deferred to last as planned, and **shipped as 4b-5** (§G.11) once the
+ picker it sits on top of was working.
+
+**Resolved — the Equip categories. DONE for trinkets (§G.9).** Owner's call was to port
+`CooldownViewerEquip.lua` so on-use trinkets and potions are trackable. Trinkets shipped as Phase 5a
+with one source pool ("Trinkets"), not two: the passive pool is cut because on this client it can
+only ever contain on-use trinkets already listed in the active pool. Consumables are deferred on a
+**data** blocker, not effort — `Item.dbc` carries neither item names nor item→spell links, so a
+bucketed potion list cannot be generated from client data. §G.9 scopes the route through.
+
+## G.5. Phasing
+
+| Step | Scope | Exit criterion |
+|---|---|---|
+| **4b-1** | Window shell: chrome, Spells/Auras side tabs, scroll body, search box, `/cdm` toggle, ESC-close. **DONE** | `/cdm` opens and closes a correctly-chromed empty window |
+| **4b-2** | `SettingsAdapter` + `SettingsCategories` grids, read-only. **DONE** | All categories render the player's real spells |
+| **4b-3** | `core/Menu.lua` + the item context menu. **DONE** | **The payoff.** Spell visibility, alerts and sounds are all settable, and Phase 4a stops being dormant |
+| **4b-4** | Drag reorder. **DONE** | Items can be dragged between categories and reordered |
+| **4b-5** | Presets / import / export, and the snapshot pair that makes Revert real (§G.11). **DONE** | Layouts save, load, import and export; Revert undoes an apply |
+| **5a** | On-use trinket discovery + the Trinkets source pool (§G.9). **DONE** | An equipped on-use trinket appears in `/cdm`, drags into Essential, and shows on the bar |
+| **4c** | Third `/cdm` tab hosting every viewer setting; the DragonUI section shrinks to an enable toggle plus an Open button (§G.10 scope, §G.12 notes). **DONE** | Every setting is editable from `/cdm`, and no stored value is rendered in two windows |
+| **6** | Loose ends (§G.13): the edit-mode affordance §G.4 never decided, §F1, and the 4b-1 vestiges. **DONE** | Every viewer is positionable from `/cdm`; no open question left that offline work can close |
+| **5b** | Consumables, via a generated Spell.dbc effect table (§G.9) | **Parked as a stretch goal** (owner, this pass). Blocked on the generator, not on effort |
+
+4b-3 is the milestone that matters; 4b-4 and 4b-5 are polish. Rough size: ~1,400 lines adapted from
+the source plus ~200 of new shim, against 2,139 in the original — the difference being the Group
+Buffs tab, Edit Mode wiring, and the retail menu framework.
+
+## G.6. 4b-2 notes (adapter + grids)
+
+**The Hidden section needed data that did not exist.** Upstream's Hidden is the opt-in catalog, and
+it is driven by a generated `NE_CDM_HIDDEN` global — every class ability with a real cooldown.
+Without an equivalent, Hidden could only ever re-offer spells the player had removed, which is an
+undo list, not a picker. `tools/cdm-spellgen/gen_arsenal.py` emits `CdmArsenal.lua`
+(`M.ARSENAL_BY_CLASS`, 303 abilities across ten classes) from the same `resolved.json` the Phase 2
+seed came from, so it costs one thin emitter rather than new analysis.
+
+Expect Hidden to look SHORT in game. Our curation is deliberately broad, so most of a class's
+cooldown abilities already sit in Essential or Utility; what remains is the difference. That is the
+intended behaviour — the section fills up as the player moves things out.
+
+**`NE.listheader` substituted inline**, the same call `modules/character/Reputation.lua` made for the
+same missing Core helper, using the client's own +/- collapse buttons.
+
+**Bug worth recording: a tri-state predicate that only ever returned two.** `listHasEnabled` answers
+"is this spell listed and enabled?" with true / false / **nil**, where nil means *not mentioned* and
+is what sends `isPlaced` to the curated defaults. It returned `false` for a missing list instead of
+nil, so the curated fallback was unreachable and every curated spell appeared in Hidden alongside
+itself. Caught by asserting the two sets do not overlap — a property that is obvious to state and
+was not obvious to eyeball.
+
+## G.7. 4b-3 notes (the menu shim and the item menu)
+
+**`core/Menu.lua` reimplements the MenuUtil builder API, not the menus.** Every menu in the source
+is written as a generator that receives a root description and calls `root:CreateTitle`,
+`CreateButton`, `CreateRadio`, `CreateCheckbox`, `CreateDivider`, nesting by adding children to a
+returned description. Rebuilding that API once over `UIDropDownMenu` (~250 lines) means the three
+menu sites port close to verbatim. Rewriting each by hand into the init-callback idiom would have
+been more code in total and a fresh chance to get the level plumbing wrong at every site.
+
+**Why ClassicAPI's `C_UIDropDownMenu` and not the native one.** The native 3.3.5a
+`UIDROPDOWNMENU_MAXLEVELS` is a hard 2. The ready-sound menu is three deep — item → Ready Sound →
+category → entry — and the alert menu is three as well (item → Alert → FX Style). ClassicAPI's copy
+grows the cap inside `C_UIDropDownMenu_CreateFrames`. The native API is kept as a fallback (same
+shape, different list-frame name prefix), so the shim still works two levels deep if ClassicAPI is
+ever absent. `compat/COVERAGE.md` previously recorded this symbol as having "no current consumer";
+`core/Menu.lua` is now that consumer.
+
+**The tree is built separately from the render.** `NE.menu.BuildRoot` runs a generator and returns a
+plain node tree with no widget touched. That is what makes menu CONTENT testable offline: the
+harness builds the item menu, walks it, and invokes a leaf's callback to assert that selecting
+"Cat" writes kit 316401 and previews it — none of `UIDropDownMenu` is stubbed. The 4b-3 block adds
+26 assertions on that basis.
+
+**Two client-behaviour traps in `UIDropDownMenu`, both worth remembering.**
+
+1. *A submenu parent must be `notClickable`, not a no-op `func`.* `UIDropDownMenuButton_OnClick`
+ toggles the row's Check texture **before** it looks at `func`, so a clickable-but-inert parent
+ paints a stray checkmark on itself. Disabling the row leaves `OnEnter` — which is what actually
+ opens the submenu — firing normally.
+2. *`C_UIDropDownMenu_Refresh` cannot refresh our radios.* It keys off
+ `frame.selectedName/selectedID/selectedValue`, which say nothing about a function-valued
+ `checked`. Calling it would hide every check. The shim repaints a level's marks itself, from the
+ predicates, after any radio or checkbox fires.
+
+**One deliberate divergence from upstream: the FX enum.** Upstream's fx values index
+`NE.groupbuff.VISUAL_ALERT` (`1` = marching ants, `6` = flash), an enum this addon does not have.
+Ours is `AL.FX` — 1/2/3 over LibCustomGlow. The FX submenu is therefore *generated from* `AL.FX`
+rather than hardcoding upstream's pair; porting those two lines verbatim would have silently
+written `6`, for which there is no renderer. Pinned by a test that compares the submenu against
+`AL.FX` rather than against a literal.
+
+**The grid now shows its own state.** A tile with an alert or a ready sound configured carries a
+corner badge, and its tooltip names both. Without that, the only way to read the configuration is
+to right-click every icon in turn. Upstream's `common-icon-visual` glyph is not registered here, so
+the badge uses upstream's own fallback (a gold dot) — asked via `HasAtlas` rather than by letting
+`SetAtlas` fail, because a failed `SetAtlas` logs an ATLAS MISS and this runs once per tile per
+rebuild.
+
+**Both cog resets confirm first.** "Reset Spell Lists" and "Clear All Alerts" are irreversible until
+4b-5 brings a snapshot store, so both route through a `StaticPopup`. The harness asserts the click
+raises the popup rather than acting, then drives `OnAccept` separately.
+
+### G.7.1 In-game faults found on the 4b-3 pass
+
+Three reports, three unrelated causes. All three were invisible to the harness as it stood, and all
+three are now covered — including negative checks confirming each new assertion fails against the
+unfixed code.
+
+**"Icons in the cooldown manager window are all greyed out."** Not the learn tint, and not the icon
+textures: **alpha 0.25 on every tile, from the search filter.** ClassicAPI's `SearchBoxTemplate` has
+**no `Instructions` FontString** — its placeholder *is* the edit box's text (`SearchBoxTemplate_OnLoad`
+does `SetText(SEARCH)`, and `OnEditFocusLost` puts it back). So an untouched box reads back
+`"Search"`, `RefreshLayout` handed that straight to `ApplyItemFilter`, and every tile whose spell is
+not named "Search" dimmed. The 4b-1 line `if f.search.Instructions then … end` was a no-op guarding
+a field that never exists, which is exactly why it looked fine. Every read now goes through
+`CDS.GetSearchText`, which maps the placeholder to `""`. The harness sets `SEARCH` and seeds the box
+with it before asserting the tiles stay at full alpha.
+
+**"The selected alert or sound doesn't get reflected in the menu on reopening."** A **Lua idiom bug
+in ClassicAPI's `C_UIDropDownMenu_AddButton`**:
+
+```lua
+local checked = type(info.checked) == "function" and info.checked() or info.checked
+```
+
+When the predicate returns **false**, `(true and false)` is false, so the `or` falls through to
+`info.checked` — the function object, which is truthy. Every function-valued radio therefore
+rendered as selected, and a menu where *everything* is ticked communicates nothing. DragonUI and
+ClassicAPI are read-only (§0), so the fix is on our side: `core/Menu.lua` snapshots the predicate to
+a **boolean** at build time. Nothing is lost — the tree is rebuilt from the generator on every open,
+and `refreshChecks` re-reads the predicate on every click. Pinned by two assertions: exactly one
+radio in a group reads selected, and `info.checked` is never a function *at any rendered level*
+(the first version of that check scanned level 1 only, which holds no radios, and passed
+vacuously).
+
+**Panel size.** Owner asked for +30%. Applied as `PinPixelPerfect(f, 1.3)` — a **scale**, not larger
+`PANEL_W`/`PANEL_H`. The grid geometry (38px tiles, 46px pitch, 7 per row, 344-wide category) is
+upstream's probe-confirmed layout; growing the frame around unchanged tiles would only add margin.
+`PinPixelPerfect` folds the multiplier into its pixel-snap target and re-applies it whenever the UI
+scale changes.
+
+**What this cost, and the lesson.** The harness could assert menu *content* but nothing about
+*rendering*, so it could not have caught either of the first two. It now carries a small
+`C_UIDropDownMenu` stand-in — deliberately **not** a reimplementation. Copying ClassicAPI's `and`/`or`
+bug into a stub in order to "catch" it would be circular. What the stub records is the shape of the
+`info` tables *we* produce, and the invariant that keeps us clear of the bug ("never hand the client
+a predicate") is checkable without reproducing it.
+
+### G.7.2 Alert engine faults (second in-game pass)
+
+**"The FX come out green when actually used on the bars."** The preview was lying. `AL.Preview`
+hardcoded the alert type `"usable"`, so every preview flashed the usable YELLOW regardless of which
+event the player had just chosen — then the live icon glowed in that event's real tint (available
+green, refresh pandemic-orange). `Preview` now takes the type and the menu passes the one being
+configured. A preview whose whole job is "see it before you commit" has to show the colour you will
+actually get.
+
+**"The only FX Alert type that seems to do anything is available."** Two compounding causes.
+
+1. **`IsUsableSpell(spellID)` again, in `Alerts.lua:262`.** The same fault fixed in
+ `ItemMixins.lua:545` — 3.3.5a's `IsUsableSpell` takes a NAME or a spellbook index, never an id;
+ given an id it reads it as an index past the end of the book and returns nil. `isSpellUsableNow`
+ therefore returned false unconditionally, so the Usable event could never fire for anybody. The
+ ItemMixins fix did not sweep for other call sites. It should have.
+
+2. **The curated gate made Usable a dead entry for six classes.** `inUsableState` returned false for
+ any spell not in AlertData's EXECUTE or REACTIVE tables — eight abilities across Hunter, Paladin,
+ Warrior and Rogue. A Priest or a Mage could select "Usable" and nothing could ever happen. That
+ gate is defensible upstream, where the alert can be attached to spells with no cooldown at all;
+ here it contradicts the label. **Semantics changed:** Usable now means castable right now.
+ EXECUTE stays as the stricter condition where it applies, because target health is something
+ `IsUsableSpell` knows nothing about — without it Kill Shot would glow all fight instead of in
+ execute range. `A.IsReactive` is no longer consulted at all: the client already reports Overpower
+ and Revenge as unusable outside their proc window, so the curated check only restated it.
+ `A.REACTIVE` stays in AlertData as data with no reader.
+
+**The test that shielded the bug.** The 4a suite asserted, in as many words, *"a spell with no
+execute/reactive entry never glows on 'usable'"* — and it passed, for the wrong reason: with
+`isSpellUsableNow` hard-false, nothing glowed whatever the data said. The assertion agreed with the
+bug and hid it until the owner reported the feature did nothing. **A test that encodes "this feature
+is off here" cannot tell you the feature is broken.** Where the intended behaviour is negative,
+assert the positive case somewhere too, or the negative one proves nothing.
+
+## G.8. 4b-4 notes (drag reorder)
+
+**The one real downport is the drag's ending.** `GLOBAL_MOUSE_UP` is retail 9.x and upstream uses it
+six times. But a drag already runs an `OnUpdate` to follow the cursor, so the release is detected
+there instead, by watching `IsMouseButtonDown` transitions: left down→up commits, right down at any
+point cancels. Same two outcomes, no event. `GetMouseFoci` → `GetMouseFocus` was already
+fallback-handled upstream. Nothing else about the file needed changing.
+
+**`Adapter.ReorderTo` / `Adapter.AssignAt` are new.** Our storage model keeps a per-category editable
+list of `{spellID, enabled}`, so ordering is that list's order; the aura pool orders itself the same
+way, which lets one function serve both. `AssignAt` is `Assign` followed by a `ReorderTo`, which is
+what carries the drop POSITION across a category boundary — dropping onto a tile inserts at that
+caret rather than appending.
+
+**The off-by-one that every drag reorder has.** `ReorderTo` must recompute the target index AFTER
+removing the dragged entry: pulling it out shifts everything below it up by one, so an index taken
+beforehand overshoots by one whenever the item moved DOWN the list. Both drop-direction assertions
+fail against the pre-removal version, which is the point of having two.
+
+**Cuts.** `CDS.SetDragActive`, which illuminates the "+" drop slots this panel does not have. (The
+equip-token branches were also cut here and have since been restored by §G.9.) The
+consequence is that dropping on empty space does nothing; drop onto a TILE, or onto a category's
+HEADER, which resolves to that category on the walk up. Enabling mouse on the category container
+would make empty-area drops work too, but it risks the scroll frame's wheel handling for an
+affordance the header already provides.
+
+**Sounds.** 3.3.5a's `SOUNDKIT` (ClassicAPI `Util/SoundKit.lua`) is a dozen entries and carries none
+of retail's cursor kits. The three names used here are confirmed present in this client's own
+FrameXML, and each play is `pcall`ed and checks the `willPlay` return, so an unknown name is silence
+rather than an error.
+
+**Panel close cancels the drag**, hooked on `OnHide` rather than in `CDS.HidePanel`: the close button
+and ESC both call `Hide()` directly. Without it a closed window would strand the cursor icon
+(parented to UIParent) and leave the source tile dimmed and locked. The offline harness now fires
+`OnHide` on a shown→hidden transition, matching the client — it previously fired only `OnShow`, so
+this whole class of teardown bug was invisible to it.
+
+## G.9. Phase 5a notes (on-use trinkets)
+
+Port of `CooldownViewerEquip.lua` (182 lines) → `modules/cooldownviewer/Equip.lua`. This is the
+owner-approved item of §G.4, and it lands the *trinket* half in full.
+
+**The model is opt-in, and that is the point.** An item cooldown has no viewer by default. It is
+discovered, keyed by a stable token (`item:`), and parked in a source pool — the "Trinkets"
+section in `/cdm` — showing nowhere on screen until the player drags it into Essential or Utility.
+Assignment is persisted per token in `cooldownviewer.equipAssign`, so re-equipping the same trinket
+restores the choice. `"hidden"` has to be a *stored* value rather than a synonym for unassigned:
+`GetEquipAssignment` falls back to the class default only when a token is genuinely absent, so an
+explicit hide must occupy the key.
+
+**Trinkets cost nothing in curation.** `GetInventoryItemID` gives the item in each trinket slot and
+`GetItemSpell` gives its on-use spell; both come from the client at runtime. Nothing is typed.
+
+### The token is the move key, not the spellID
+
+`Adapter.GetItems` now returns a **mixed** list — numbers are spellIDs, tables are equip entries.
+That is upstream's shape and it keeps the change small: a tile handed a table calls `SetEquipEntry`,
+a tile handed a number calls `SetSpell`, and the menu and drag paths branch on `item.token`. The
+alternative (promoting every spell to an entry table) would have touched the menu, the drag path,
+the filter and the tests for no gain.
+
+Routing an equip row through the spellID path is not a near-miss, it is a data corruption: it would
+write the trinket's use-spell into the editable spell list, where it *survives unequipping the
+trinket* and points at nothing. `Adapter.Assign` therefore refuses outright when either side of the
+move is a source pool, and there is an assertion on that refusal.
+
+**Pooled tiles must drop the binding.** A tile that held a trinket can be handed a plain spell on
+the next rebuild. `clearEquipBinding` runs at the top of both `SetSpell` bodies; without it the
+stale token keeps routing that row's right-click and drag through the equip path.
+
+**An empty source pool is not rendered at all** — not as "(empty)". A player with no on-use trinket
+should not be told about a Trinkets section; that would read as a broken feature rather than an
+absent input. Stored categories still show when empty, because there an empty list is a state the
+player chose.
+
+**Reorder does not apply to an equip row.** It has no stored position — the viewer appends
+discovered items after the spells — so dropping one inside its own category is a deliberate no-op.
+Dragging a *spell* onto an equip row is also a no-op for the same reason: there is no index to
+reorder against. Cross-category drops work normally.
+
+**Events.** `UNIT_INVENTORY_CHANGED` triggers a full `Rebuild` on the viewers (a swap changes the
+discovered set, not just a cooldown) and a `RefreshLayout` on the panel.
+
+### Cut: the passive pool, for a data reason
+
+Upstream's `GetEquipPassiveItems` walks the same two trinket slots and surfaces each one's **use**
+spell as a trackable aura. So a proc trinket — the only kind for which a passive aura row would mean
+anything — returns nil from `GetItemSpell` and never appears, and the pool can only ever contain
+on-use trinkets that are already rows in the active pool. Upstream flags this itself ("Era has no
+on-equip-aura data layer", `TODO(hydrate)`). Shipping it would add a second source section whose
+contents duplicate the first. `equipPassive` and its half of the legal-target matrix go with it.
+
+### Deferred: consumables (potions, healthstones, runes)
+
+Upstream's `M.POTION_CATEGORIES` is a hand-curated table of ~45 **vanilla** item ids bucketed into
+Healing / Mana / Healthstone / Soulstone / Combat, best-rank-first, so the runtime can show one slot
+per bucket for the best item held. WotLK adds a tier to every one of those buckets. Typing those ids
+in violates the prime directive, and **the 3.3.5a client cannot supply them**: `Item.dbc` carries no
+item names and no item→spell link — both live server-side in `item_template` — so there is nothing
+to generate a bucketed list *from*.
+
+The way through is to classify at runtime by the use-spell's **effect**, which *is* client data:
+
+1. Extend `tools/cdm-spellgen/dbc.py` to keep `Effect[0..2]` and `EffectBasePoints[0..2]` alongside
+ the name/rank columns it already parses. It reads every field as `uint32` today, so this is a
+ wider tuple, not a new parser. Locate the columns the way `name_col` is located — empirically,
+ against anchors (spell 2050 Lesser Heal has `SPELL_EFFECT_HEAL`).
+2. Emit `M.CONSUMABLE_SPELLS = { [spellID] = { bucket = "health"|"mana", magnitude = } }`
+ for every spell whose effect is Heal (10) or Energize (30, mana).
+3. At runtime, scan the bags once per `BAG_UPDATE` (dirty-flagged), map each item through
+ `GetItemSpell` → spellID → that table, and keep the highest-magnitude item per bucket.
+
+That covers health/mana potions, healthstones and runes across every tier automatically, with no id
+typed anywhere. Combat potions (Free Action, Mighty Rage) apply auras rather than heals and are not
+reachable this way; they would need either a third bucket keyed on aura-applying effects or a
+deliberate cut. `M.DEFAULT_EQUIP_BY_CLASS` is already in place and empty so that the Warlock
+Healthstone/Soulstone defaults become a data change rather than a code change.
+
+## G.10. Scope: a third `/cdm` tab for the viewer settings
+
+**Where the settings live today.** Every non-per-spell setting is in the DragonUI options panel,
+registered by `modules/cooldownviewer/Register.lua` as section `cooldownviewer`. That is a different
+window, reached by a different path, and it is the one thing about the current `/cdm` that does not
+match retail — retail's Cooldown Manager settings sit *in* the Cooldown Manager.
+
+**What the tab would host.** Everything currently in the options section: per-viewer enable, scale,
+opacity, orientation/stride, visibility mode (always / in combat), show-timer and show-tooltip
+toggles, plus the "Show Unlearned" checkbox that is presently in the cog menu. Frame *position*
+stays with the movers (`/dui edit`) — §B1's decision, unchanged.
+
+**The pieces already exist, which is why this is small.**
+
+- `NE.tabs.MakeSideTab` builds the tab; `f.tabButtons` is already a list and `SetDisplayMode`
+ already iterates it. Adding a third entry is three lines plus an anchor.
+- The panel body is a `ScrollFrame` over `f.content`. `CDS.RefreshLayout` fills it from
+ `Adapter.MODE_ORDER[mode]`, which has no entry for a settings mode.
+- The side-tab glyph is the one asset gap: `SettingsAssets.lua` registers two
+ (`icon_cooldownmanager`, `icon_trackedbuffs`) from the reference sheet, which carries a third.
+
+**The one real decision — and the reason this is scoped rather than done.** The current body is
+owned end-to-end by the category grids: `RefreshLayout` rebuilds `f.content` from the adapter on
+every call, and there is nowhere for a page of controls that is *not* a category list to live. Two
+ways out:
+
+- **(a) A second content frame.** Build a `f.settingsContent` sibling, and have `SetDisplayMode`
+ show one and hide the other via `f.scroll:SetScrollChild`. `RefreshLayout` early-returns for the
+ settings mode and never touches it. Cheap, and no risk to the grids.
+- **(b) A "category" whose renderer is a control list.** More uniform, but it puts widget layout
+ behind an adapter whose whole contract is "return a list of spellIDs", and every consumer
+ (`ApplyItemFilter`, the drag path, `RestackCategories`) would need a "not a grid" branch.
+
+**(a) is the recommendation** — the settings page has nothing in common with a grid of tiles, and
+pretending otherwise buys uniformity at the cost of a branch in four files.
+
+Two smaller consequences fall out of (a) and should be handled with it: the **search box and cog**
+are meaningless on a settings page and should hide with the grids, and the panel is 399x609 at 1.3
+scale, so a long control list needs the scroll frame it already has rather than a taller window.
+
+**Duplication is the thing to decide, not the layout.** The options section cannot simply be moved:
+DragonUI's options panel is where a user goes to turn the module *off*, and it is read-only to us
+(CONTRACTS §0 — we register a section, we do not own the window). So either
+
+- the tab and the options section both read and write the same `NE.Config().cooldownviewer` store
+ and stay in sync by construction (each rebuilds on show; no shared widget state), or
+- the options section shrinks to an enable toggle plus an "Open Cooldown Manager" button, and the
+ tab becomes the single home for the rest.
+
+The second is retail's shape and the better end state. It is also the one that changes behaviour a
+user may already rely on, so it is an owner's call rather than an implementation detail.
+
+**RESOLVED (owner): the second.** The options section keeps the master enable toggle and an "Open
+Cooldown Manager" button; everything else moved to the tab. Implementation notes in §G.12.
+
+**Estimated size:** ~40 lines of panel wiring, ~180 lines for the control page (reusing the same
+slider/checkbox helpers `Register.lua` already builds for the options section), plus harness
+coverage that the tab switches modes and that a write from the tab is visible to the options
+section. No new platform gap — nothing here needs an API 3.3.5a lacks.
+
+
+## G.11. 4b-5 notes (layouts, import/export, and the real Revert)
+
+Downport of `CooldownViewerSettings/Presets.lua` (378 lines) → `SettingsPresets.lua`.
+
+**The snapshot pair came with it, and that is the point.** Upstream's `Panel.lua` owns
+`snapshotState`/`restoreState` and exposes them as `CDS.SnapshotState` / `CDS.RestoreState` /
+`CDS.DeepCopy`; 4b-1 deferred the Revert button precisely because that pair did not exist. It lives
+here now, so Revert is real. Applying a layout and undoing one are the same operation with a
+different source snapshot — building two mechanisms for that would have been the mistake.
+
+A layout is the five editable leaves plus the class it was captured on: `customLists`,
+`trackedAura`, `equipAssign`, `alerts`, `sounds`, `class`. Restore is **whole-leaf assignment, not a
+merge**: a layout is a complete state, so a spell the snapshot does not mention has to end up
+unmentioned rather than surviving from whatever was there before.
+
+`InvalidateCuratedCache` on restore is load-bearing. `GetActiveSpellList` caches the resolved curated
+list, and replacing `customLists` underneath it leaves the viewers rendering the *previous* layout
+until something else happens to dirty the cache — the same shape as the §E5 shadowing bug.
+
+**Undo is one step, and only for this session.** The panel's edits are individually reversible by
+hand (move it back, pick None), so what a player actually needs is "undo the layout I just applied",
+not a history. The snapshot is taken before any apply / starter / import, and cleared on `OnHide`:
+reverting an hour-old change is not an undo. The selected layout NAME is tracked *beside* the
+snapshot rather than inside it — it is session bookkeeping, and putting it in the snapshot would bake
+it into every export string, so a shared layout would arrive carrying the name of the layout its
+author happened to have selected.
+
+### Downport changes
+
+**No `WowStyle1DropdownTemplate`.** The footer control is a plain `UIPanelButtonTemplate` opening
+`CDS.BuildLayoutMenu` through `core/Menu.lua`. Same menu tree; only the widget differs. Its label is
+the selected layout's name, so the footer still answers "which layout am I on" at a glance.
+
+**Import is class-checked, which upstream is not.** A snapshot's spell lists are keyed by class, so
+restoring a Priest layout on a Mage writes into the Priest's slot and appears to do *nothing at all*.
+A silent no-op is the worst outcome for a paste, because the player cannot tell it from a broken
+feature. `Decode` now refuses with a reason naming the class the layout was built for.
+
+**Nothing else.** The codec is upstream's and was already 3.3.5a-clean — `string.byte`/`char`,
+`math.floor`, `string.find`, no `bit` library anywhere.
+
+### Safety
+
+Import never executes pasted input: a hand-rolled typed reader, no `loadstring`, no `setfenv`, every
+parse wrapped in `pcall`. Strings are length-prefixed so a spell name containing a tag character is
+still safe. Plain SavedVariables plus StaticPopups throughout — nothing here can taint the combat
+path.
+
+**Two bad-input findings, both from writing the negative test rather than the guard:**
+
+1. **The over-long string length is the one bad input that would pass silently.** `string.sub` clamps,
+ so `t1;s5:class` + `s99:PRIEST` parses as `{ class = "PRIEST" }` — a well-formed layout built from a
+ truncated read — and every later check (is it a table, is `.class` a string, does the class match)
+ then passes. The explicit bounds check in the `s` branch is what stops it. The test pairs it with
+ the same payload at the correct length, so the rejection is provably about the length and not the
+ shape.
+
+2. **The table pair-count cap was dead code, and was removed.** A header claiming a billion pairs is
+ the obvious DoS shape, so the first version capped `count`. The negative test showed the cap
+ changed nothing: every loop iteration must consume at least one byte of payload or raise, so the
+ parser is self-limiting and such a header fails on the first pair. The assertion now states *that*
+ — which is what lets the cap stay out instead of sitting there as a guard no input can reach.
+
+Both are the §G.7.2 lesson again: a guard whose test passes with the guard removed is telling you
+something, and it is usually that the guard or the test is wrong.
+
+## G.12. 4c notes (the Settings tab)
+
+§G.10 scoped this; the owner picked the second option, so the DragonUI options section is now an
+enable toggle and an "Open Cooldown Manager" button, and every viewer setting lives on a third `/cdm`
+tab. Two new files: `SettingsControls.lua` (the widget kit) and `SettingsOptions.lua` (the page).
+
+**Option (a) as scoped: a second scroll child.** `panel.settingsContent` is a sibling of
+`panel.content`, and `SetDisplayMode` swaps which one the ScrollFrame holds. The category grids own
+`panel.content` end to end — `RefreshLayout` rebuilds it from the adapter on every call — so a page of
+controls could not have shared it without an adapter category whose "list of spellIDs" contract it
+cannot satisfy.
+
+`CDS.RefreshLayout` gained a settings-mode early return, and that guard is load-bearing rather than an
+optimisation. The panel refreshes on `SPELL_UPDATE_ICON`, `GET_ITEM_INFO_RECEIVED` and
+`UNIT_INVENTORY_CHANGED` whenever it is shown; `MODE_ORDER` has no `settings` entry, so without the
+return each of those events would walk the deactivate loop, switch every category off, and find
+nothing to switch back on — leaving the grids empty behind a page the player is not looking at, to be
+discovered on the next tab switch.
+
+### Why a widget kit at all
+
+DragonUI's `Controls:AddToggle` / `AddSlider` / `AddDropdown` are AceGUI widgets that call
+`parent:AddChild(...)` and lay themselves out only inside an AceGUI container. Our body is a plain
+Frame in a plain ScrollFrame, and DragonUI is read-only (CONTRACTS §0), so the choice was a kit or an
+AceGUI container inside our panel. The kit is ~330 lines over the client's own
+`UICheckButtonTemplate` and `OptionsSliderTemplate`, and it keeps the page looking like the rest of
+the window rather than like the options tab embedded in it.
+
+**Three downport points inside the kit:**
+
+1. **No `WowStyle1DropdownTemplate`** — same absence the footer works around (§G.11). A dropdown is a
+ button labelled with the current value, opening a radio menu through `core/Menu.lua`. It also takes
+ an ORDERED array where the options tab takes a `value -> label` map: AceGUI sorts a map for you, and
+ a radio menu has to choose. "Always / In Combat / Hidden" is a progression; alphabetised, Hidden
+ lands in the middle.
+
+2. **`SetObeyStepOnDrag` does not exist here**, and `SetValueStep` alone only governs the arrow keys,
+ so a drag delivers continuous values. The kit snaps on the way in — the same thing Blizzard's own
+ option sliders do in their `OnValueChanged`.
+
+3. **The write gate on that same handler.** A drag fires `OnValueChanged` on every mouse move, and
+ every write runs `M.SetOpt`, which re-runs the viewer's `RefreshLayout` and relays out every icon.
+ Only a value that crosses into the next step may write. Verified by removing it: the "stays inside
+ one step" assertion goes from 0 writes to 2.
+
+### What is NOT on the page
+
+**Frame position.** Still the movers (`/dui edit`), §B1's decision unchanged. It is the one setting
+that is not a value in this store.
+
+**Hide When Inactive on Essential and Utility.** Retail's templates for those two do not set
+`allowHideWhenInactive`, so `UpdateShownState` ignores the setting and they always show every known
+cooldown. The options tab shipped the control anyway with a description explaining that it did
+nothing. The page offers it only where `GetOpt(frameID, "allowHideWhenInactive")` is true: a control
+that has to explain its own inertness is worse than an absent one.
+
+**The master enable.** It stays in DragonUI's options and appears nowhere here. That panel is where a
+player goes to turn a module off, and nobody looks inside a window for the way to make that window's
+frames stop existing.
+
+### Duplication: values versus actions
+
+The rule the §G.10 decision enforces is that no stored VALUE has two editors. A setting with two
+editors is consistent only while both rebuild on show, and the first time one does not, the player is
+reading a stale control and cannot tell that from a setting that failed to apply. Hence exactly one
+home for each of the ten per-viewer settings, the two bar-only ones, and buff auto-tracking.
+
+The two resets are deliberately in two places — the cog menu and the page's Reset section. They are
+ACTIONS routed to the same `StaticPopup`s, with no stored state to fall out of sync: the cog is at
+hand while working the lists, and a player looking for a reset looks under Settings. "Show Unlearned"
+stays in the cog alone, because it filters the grids rather than configuring a viewer, which is also
+why the cog and the search box both hide on this tab.
+
+### Art
+
+The tab glyph is the client's own `questlog-icon-setting` at 18px, not a third glyph from the CDM
+sheet. That sheet's spare is `icon_buffreorder` — "reorder group buffs" — which would be a lie here,
+and reusing the cog ties the tab to the cog beside the search box, which is what a player already
+reads as "options for this window". `SettingsAssets.lua` now registers that rect itself rather than
+depending on `modules/spellbook/Assets.lua` having loaded; the harness asserts it resolves, because an
+unregistered atlas is a transparent gap rather than an error.
+
+### Harness
+
+47 new/changed assertions (382 at that point; 394 after Phase 6). Five guards were confirmed by removal: the slider write gate,
+the step snap, the settings-mode return in `RefreshLayout`, the `allowHideWhenInactive` gate, and the
+chrome hiding. Two stub notes:
+
+- `frameMeta:SetValue` now fires `OnValueChanged` on a real change, as the client does. The sliders
+ re-seat their own thumb from inside that handler and re-read their getter on every page refresh, so
+ a stub that swallowed those calls could not tell a working re-entrancy guard from a missing one.
+- `SetObeyStepOnDrag` is deliberately still absent from the stub. Adding it would hide the reason the
+ kit snaps values itself.
+
+The control finder in the test scopes by section title as well as label, and that is not tidiness:
+"Icon size" exists four times, once per viewer, so an unscoped search always answers with Essential's
+row — and a test meaning to prove Buff Bars' slider works would have proved nothing. Same failure
+shape as §G.7.2 and the 5a pooled-tile test.
+
+## G.13. Phase 6 notes (loose ends)
+
+With 4c shipped and consumables parked as a stretch goal, both phasing tables were complete and there
+was no next *port* phase — what remained was a short list of things the plan had deferred rather than
+decided. This closes them.
+
+### 1. The edit-mode affordance (§G.4, undecided since the §G scope was written)
+
+`NE.editmode.Toggle` / `SelectFrame` were listed as "drop the affordance or route it at `/dui edit`",
+and neither was ever chosen — so the Settings tab shipped in 4c telling players to type `/dui edit`.
+Routed now, through a new integration seam:
+
+```
+NE.OpenFrameEditor(frame) -> true | false, reason
+```
+
+**It lives in `integration/Register.lua`, not in the module** (CONTRACTS §4: panel modules never reach
+into DragonUI internals directly), and **it takes the CONTENT frame while selecting the ANCHOR.** That
+is the trap the seam exists for. `RegisterHUDFrame` registers the `CreateUIFrame` anchor as the editable
+frame and hangs the viewer off it, so DragonUI's editor knows the anchor and nothing at all about the
+viewer. Handing `SelectEditorFrame` a content frame would put the coordinate readout and the Reset
+button on a frame the editor cannot move — and it would look like it worked. `.editorAnchor`, which
+`RegisterHUDFrame` already set, is the bridge.
+
+**Failure returns a reason instead of printing one**, because the caller knows where its message
+belongs. Two of the three failure paths matter:
+
+- **In combat.** `EditorMode:Show()` returns silently in combat — an empty branch, no message
+ (`DragonUI/modules/editor_mode.lua:334`) — so a button wired straight to it appears to do nothing
+ mid-fight. `OpenFrameEditor` re-checks `IsActive()` rather than trusting the call, and the explicit
+ combat branch exists purely to name combat in the reason. That distinction is now asserted: without
+ the branch the refusal still happens, but the player is told "editor mode declined to open", which is
+ not actionable. A guard whose only product is a better message still needs a test that reads the
+ message.
+- **No `EditorMode` at all**, on a DragonUI build that predates it. Returns a reason; does not error.
+
+**Per viewer, not once globally.** Upstream's version was a single cog-menu entry. Because DragonUI's
+editor takes a frame to select, ours is a "Position this viewer" button inside each viewer's section, so
+the one under Buff Bars' sliders opens the editor with Buff Bars selected. The panel closes only on
+success — hiding first and then failing would take away the only surface the reason could appear on.
+
+The fourth upstream ref, `RepositionHandle` (`CooldownViewer.lua:977`, re-anchor the drag rect as items
+are learned), needs no port at all: `RegisterHUDFrame` syncs the anchor to the content's footprint from
+the content's own `OnSizeChanged`.
+
+### 2. §F1, `Cooldown:SetDrawEdge`
+
+Open since Phase 0. The static evidence is strong enough to call it — ClassicAPI stubs the three
+`SetEdge*` methods as "Incompatible (3.3.5)" but not `SetDrawEdge`, injects unconditionally rather than
+filling gaps, and calls `GetDrawEdge` unguarded on every captured cooldown's OnShow — but that is an
+inference from someone else's code, so `/necdm` now prints the runtime answer for the four cooldown
+methods we care about. Both of our call sites guard regardless, so nothing depends on it; what changes
+is that the question stops being open.
+
+The probe frame is created on demand and cached, not at load: frames cannot be destroyed, so a
+diagnostic-only frame should not exist on a character who never runs the diagnostic.
+
+`/necdm` is also now smoke-tested. It is ~70 lines of formatting that nothing else touches, and a bad
+`select()` or a nil in a format string would otherwise surface only when someone reached for it to debug
+something else — the worst possible moment.
+
+### 3. 4b-1 vestiges
+
+The "Spell list coming in the next step." placeholder outlived 4b-2 by four phases: still built, still
+anchored, hidden on the first `RefreshLayout` and never seen again, with two live references keeping it
+alive. Removed, along with the stub `RefreshLayout` body that painted it. The stub itself stays as a
+genuine no-op, so a load failure in `SettingsCategories.lua` costs the grids rather than every
+`SetDisplayMode` call.
+
+Also corrected: `SettingsAssets.lua` now registers sheet 5684744 itself instead of relying on
+`modules/spellbook/Assets.lua` having shipped it. The side-tab BODY has depended on that all along and
+worked, because both files load from the same TOC — but the dependency was implicit and the harness was
+quietly logging the sheet as unshipped. One path registered twice, not one file shipped twice.
+
+### What is left, and it is not code
+
+Three items, all needing the game rather than the harness:
+
+- **§F4 taint.** The viewers are unsecure display frames with no attributes, so `SetPoint` during a live
+ restack should be a non-issue — but "should be" is what §F4 asks to confirm, and only a combat restack
+ confirms it.
+- **§F5, whether four separate movers is tolerable** versus one grouped handle. An owner call with the
+ frames on screen. The new per-viewer Position buttons make the four-mover shape easier to live with,
+ which is worth re-judging before changing it.
+- **In-game confirmation of 4b-4, 4b-5, 5a and 4c.** 1-3, 4b-1, 4b-2 and 4b-3 have had a pass; the four
+ most recent phases have not. Every in-game pass so far has found something (§G.7.1, §G.7.2), and none
+ of those faults were the kind an offline harness can reach — three of them were art and event-order
+ problems visible only on a real frame.
+
+---
+
+# H. Phases 7-9 — the Tracked Buffs tab, retail theming, and the guide audit
+
+Written after an in-game report ("Tracked buffs never shows anything") and a re-read of Wowhead's
+current Cooldown Manager guide (`wowhead.com/guide/ui/cooldown-manager-setup`, updated 2026-06-16).
+Consumables stay parked (§G.9).
+
+## H.1. Phase 7 — the Tracked Buffs tab is structurally empty — **DONE**
+
+**The report is not a rendering fault. The tab cannot ever show anything.**
+
+`store().trackedAura[CLASS]` is an OVERRIDE registry, and its header says so: "It starts EMPTY by
+design: the auto window provides the defaults, this is the override registry"
+(`CooldownViewer.lua:534`). The Auras tab renders that registry and nothing else — all three of its
+categories are one filtered pass over the same list:
+
+```
+trackedBuff -> entries with assignment == "icon"
+trackedBar -> entries with assignment == "bar"
+hiddenAura -> entries with assignment == "hidden"
+```
+
+The only writer is `M.SetAuraAssignment`, whose only caller is `Adapter.Assign` — reached by dragging
+or right-clicking **a row on that tab**. So the registry is a store whose sole editor is a view of the
+store: nothing can enter it, because entering requires a row that only entry could create. Three
+empty sections, forever, on every character.
+
+**It disables a second feature too.** `ScanTargetTrackedAuras` deliberately honours explicit
+assignments only, never the auto window (`BuffViewers.lua:171`) — which is the right call, since
+auto-tracking every debuff on your target would be noise. But its input is the same unreachable
+registry, so **target DoT tracking has never been reachable either.** The guide lists DoT timers as a
+headline feature.
+
+Note what is NOT broken: the buff *viewers* work. The auto-track window drives them off a live scan
+that never consults this registry, so Buff Icons and Buff Bars have been showing procs correctly all
+along. The bug is confined to the picker and to overrides.
+
+### Why upstream doesn't have this bug, and we do
+
+Retail's Buffs menu lists a Blizzard-curated per-spec aura set from `C_CooldownViewer` — the guide's
+categories are populated before the player touches anything, and the comments complain about exactly
+that ("You can only pick from the list Blizzard feels are the relevant abilities"). Upstream on Era
+has no such data either, so it drives the viewers from the auto window — but it also never had to make
+the *picker* work, because §G.4 cut its Group Buffs tab and its aura tab inherits the same emptiness.
+We shipped the tab; the data source was never built.
+
+### 7a. A seen-aura registry (the fix)
+
+`rebuildFromAuras` already walks every player buff on `UNIT_AURA` and asks `ShouldTrackBuff` about
+each one. Record what it sees:
+
+```
+store().seenAura[CLASS] = { [spellID] = { name =, icon =, duration =, last = } }
+```
+
+- **Recorded before the include/exclude decision, not after.** An aura the player has hidden must stay
+ in the list or Hidden becomes a one-way door — hide it, the row vanishes, and there is nothing left
+ to unhide.
+- **Record the name and icon, not just the id.** `GetSpellInfo(spellID)` fails for auras whose id the
+ client doesn't know, and a nameless row cannot be searched or labelled.
+- **Cap it** (~60/class) with eviction by `last`. Unbounded, a long-lived character accumulates every
+ proc, trinket and consumable buff it has ever carried into a SavedVariables table that is also
+ copied into every layout snapshot (§G.11).
+- **Bound it by the same window the auto-tracker uses**, so permanent toggles and hour-long food buffs
+ never enter — matching the guide's "does not track out-of-combat buffs such as food buffs, phials
+ and flasks, world events, zone buffs".
+- Cost is one table write in a path that already runs, gated on `last` being stale enough to bother.
+
+### 7b. Category contents become "override ∪ seen"
+
+`Adapter.GetItems` for an aura category returns explicit entries as today, plus seen auras with no
+explicit assignment routed by the auto-track destination (`M.AutoTrackDest`): `both` puts a row in each
+of Tracked Buffs and Tracked Bars, `icon`/`bar` in one. That makes the tab a description of what the
+bars are actually doing, which is what a player opening it is asking.
+
+Auto rows need to read as auto — retail has no equivalent because its list is curated, so this is ours
+to design. Cheapest honest option: the existing unlearned-tint machinery already proves per-tile state
+is easy; a tooltip line plus a subtle desaturation is enough, and `_applyAlertBadge` shows where a
+corner badge would go if that reads better in practice.
+
+**Dragging an auto row is what makes it explicit.** No new write path: the drag already calls
+`SetAuraAssignment`, which is exactly "stop auto-deciding this one".
+
+### 7c. An empty state, not three "(empty)" sections
+
+A character who has not had a short buff yet should get one line — "Buffs you've had appear here.
+Nothing tracked yet." — rather than three empty headers, which is what the report describes and reads
+as broken. §G.9 already established the pattern for this (an empty *source* pool is skipped outright
+rather than shown empty); the same judgement applies.
+
+### 7d. A generated per-class aura catalog (the retail shape)
+
+**Promoted from optional to primary by the owner, and built first** — which reverses the ordering
+argued below. See §H.1.1 for what the data turned out to be.
+
+7a-7c fix the tab with zero new data, but they are retrospective: the list is what you have had, not
+what you *can* have. Retail's is a catalog. The same generator that produced `CdmArsenal.lua` can
+produce one: `Spell.dbc` effect `ApplyAura` + a duration via `DurationIndex` → `SpellDuration.dbc`,
+filtered to the class's own spells and to durations inside the auto window.
+
+Two unknowns, both the kind §E3 already solved once: the effect and DurationIndex column positions
+(locate empirically against known anchors, never assume), and whether a self-buff can be distinguished
+from a debuff by effect target without guessing. Worth a spike before committing, and strictly after
+7a-7c — a catalog is a nicer picker, but the empty tab is the bug.
+
+### Verification
+
+- Offline: assert the tab is non-empty after a simulated `UNIT_AURA` carrying one short buff; assert a
+ hidden aura stays listed under Hidden (the one-way-door regression); assert the cap evicts oldest
+ first; assert `ScanTargetTrackedAuras` now has reachable input.
+- Negative: remove the "record before the decision" ordering and the Hidden test must fail.
+- In-game: log in, take any short buff, open `/cdm` → Tracked Buffs.
+
+### H.1.1 What shipped, and what the data turned out to be — **DONE**
+
+Built as one phase rather than 7a-7c-then-maybe-7d: the owner asked for the catalog up front ("It
+should be curated for spec, I think its worth generating a catalog"), which makes 7d the primary
+source and the seen registry the safety net rather than the other way round.
+
+**The two unknowns the plan flagged were both answered by measurement, and the first answer was
+wrong in the way that matters.** `locate()` — one column per anchor set, ambiguity is a hard error —
+reported *no* match for `DurationIndex` and `Effect[0]` on the first run. The columns were right
+(40 and 71); the ANCHORS were wrong. Bloodrage has no duration of its own, because its aura lives on
+the spell it triggers (`29131`), and its first effect is ENERGIZE rather than APPLY_AURA. Anchoring on
+a spell that looks ordinary and is not would have "disproved" two correct columns — which is the whole
+argument for locating rather than assuming, arriving from the opposite direction to the usual one.
+
+Located, each proved against 3-6 independently-known anchors: `DurationIndex` 40, `Effect[]` 71-73
+(contiguity checked, not assumed), `EffectImplicitTargetA[]` 86, `EffectApplyAuraName[]` 95,
+`EffectTriggerSpell[]` 116, `SkillLine.Name` 3, `TalentTab.ClassMask` 20 / `OrderIndex` 22.
+
+**Self-buff vs debuff was answered by effect target, as hoped:** `EffectImplicitTargetA == 1`
+(UNIT_CASTER). No guessing needed.
+
+**Three filters the plan did not anticipate, all found by reading output:**
+
+- **Channelled spells read as buffs.** A channel's duration sits in the same column as an aura's, so
+ Blizzard, Evocation, Mind Control, Tranquility and Arcane Missiles all arrived as 3-60s "self
+ buffs". `AttributesEx & 0x44` (CHANNELED_1 | CHANNELED_2) separates them, verified clean across 19
+ spells in both directions.
+- **The skill-line class vote drags in `GENERIC (DND)`.** resolve.py's majority vote is fine there
+ because its >1.5s cooldown filter hides the collateral; here it produced Grovel and Honorless
+ Target for every class, from skill line 183, whose rows carry a class mask of 0 and vote ~10% for
+ each of the ten classes. Fixed by preferring the per-ROW class mask and only falling back to the
+ line's vote when the line is >=90% one class. Marksmanship (85 rows, all HUNTER) qualifies; the
+ racial lines, First Aid, Cloth and Engineering do not.
+- **A racial can wear a class mask.** Blood Fury's SkillLineAbility row carries a WARRIOR bit, so it
+ survived the rule above and would have been offered to every warrior alive. Race-gating is not
+ something a per-class catalog can express, so the six racial skill lines are excluded outright and
+ the generator's verify pass now asserts no racial reached the output. Racials are covered by the
+ seen registry instead, which is exactly what a safety net is for.
+- **A floor as well as a ceiling.** Heroic Fury's aura lasts 100ms: real, self-targeted, inside the
+ window, and unreadable as a bar. Anything under a second is a mechanic, not a buff.
+
+**Spec attribution beat the plan's own idea.** §H.1 proposed a per-class catalog; what shipped is
+per-TALENT, which on this client is strictly better — it follows respecs and dual spec, where a
+static per-spec list cannot. `Talent.dbc` → `TalentTab.dbc` gives each talent's class and tree, and
+the runtime asks `GetTalentInfo` whether the player actually has it. 96 of 162 rows are gated.
+
+The tie-break that decides it was settled by measurement rather than by argument. An aura is often
+reachable BOTH as a talent's triggered proc AND as a row in a class skill line — the client indexes
+Lock and Load under Survival, Improved Steady Shot under Marksmanship — so "it is in a skill line" is
+not evidence of being baseline. Preferring the ungated reading left HUNTER with 1 gated row out of 18.
+Preferring the talent reading was checked by printing all 35 both-ways conflicts and reading them:
+every single one is a genuine talent (Fingers of Frost, Bloodsurge→`Slam!`, Arcane
+Concentration→Clearcasting, Martyrdom→Focused Casting, Ice Barrier, Metamorphosis, Flurry). Not one
+false gate, so the rule is empirical, not merely plausible.
+
+**The gate fails OPEN.** An empty talent table means the API has not answered yet — early login, or a
+build without the globals — not that the player has spent nothing. Reading it the other way would hide
+every gated row and reproduce the exact bug this phase exists to fix. Confirmed by mutation.
+
+**Matching became name-based, which is the catalog's price.** A catalog row holds the rank-1 id; the
+aura the player gets is whatever rank they cast. So `GetBuffOverrides` now keys include/exclude by id
+AND by lowercased name in the same table (numbers and strings cannot collide), `ShouldTrackBuff` takes
+an optional trailing `name`, and `ScanTargetTrackedAuras` accepts both key types. This also fixes a
+pre-existing latent fault: before, an aura assigned at one rank and re-cast at another silently stopped
+matching. `ScanTargetTrackedAuras` had already been forced into name matching for DoTs; this makes it
+the rule rather than the exception.
+
+**Where an unassigned candidate lands depends on what is actually happening to it** (7b): with
+auto-track on it appears wherever `AutoTrackDest` sends it, marked `auto` and desaturated; with
+auto-track off it appears under **Hidden**, because that is the truth and it gives that section a
+purpose beyond force-excludes. Dragging one out is what makes it explicit — no new write path, since
+the drag already calls `SetAuraAssignment`.
+
+**Show Unlearned is reused rather than twinned.** It already means "show me what I cannot use yet" for
+spells; a second setting saying the same thing about auras is how a settings page stops making sense.
+A revealed spec-gated row tints red and its tooltip names the talent required, because "Not yet
+learned" on a proc you can only get by respeccing is not actionable.
+
+**A test that proved nothing, twice.** The one-way-door assertion — a hidden aura must stay listed —
+passed with the guard removed, so the guard looked decorative. It was the test: `ResetTracking`
+rebuilds the shown viewers itself, so the aura was scanned and recorded while the pool was still empty,
+before the hidden assignment existed. Fixed by clearing the auras first, resetting through `settle`
+(NE.aura caches its snapshot per frame, so a same-frame rebuild re-reads auras that are already down),
+and only then raising an aura that was hidden before it was ever seen. All five new guards are now
+confirmed by mutation: the record-before-decide ordering, the fail-open gate, name matching, candidate
+dedupe, and the aura/equip tile dispatch.
+
+**Known limitation, deliberate:** a newly seen aura does not appear in an open picker until the next
+refresh. The panel does not listen to `UNIT_AURA` — it is the hottest event in the game and
+`RefreshLayout` rebuilds every grid — and with a catalog carrying the list, a row arriving a tab-switch
+later is a nicety rather than the feature.
+
+**Also worth knowing:** the offline harness keeps its own file list, separate from the .toc. The
+catalog was in the .toc and passing for a whole run before I noticed the harness had never loaded it —
+`GetAuraCatalog` was returning `{}` and every assertion was green. A file added to one and not the
+other is silently untested.
+
+## H.2. Phase 8 — theming, to match retail
+
+**The core finding: the art is already asked for, and nothing ships it.** Three atlases are set by name
+in the viewer code and registered nowhere, so every `SetAtlas` returns false and the region renders as
+nothing at all:
+
+| Atlas | Sheet | Used by | Effect of its absence |
+|---|---|---|---|
+| `UI-HUD-CoolDownManager-IconOverlay` | 6704514 | `ItemMixins:38`, `AuraItemMixins:178`, both `BuffViewers` item shapes | **No icon frame.** This is why the icons read as bare spellbook icons rather than Cooldown Manager tiles — the single biggest visual difference |
+| `UI-CooldownManager-OORshadow` | 6685874 | `ItemMixins:39` | No out-of-range shading |
+| `UI-HUD-ActionBar-GCD-Flipbook` | 5199404 | `ItemMixins:46` | No ready-flash (guarded by `HasAtlas`, so it silently no-ops) |
+
+Every BLP is already on disk in `ReferenceAddons/NewEra/Art/Common/`, alongside two more the port never
+wired: `6731092-ui-hud-cooldownmanager-icon-swipe` and `5423465-ui-hud-actionbar-secondarycooldown`.
+
+**Why they were missed:** upstream only needs `RegisterLocal` on the sheet FDID at the CALL SITE.
+Classic Era ships the atlas *names* in client data even when the art differs, so `C_Texture.GetAtlasInfo`
+supplies the rect there. 3.3.5a has no atlas database at all, so our port must supply both — and for
+these three, only the `SetAtlas` calls were ported, not the data behind them. Nothing errored, because a
+missed atlas is an invisible texture.
+
+**CORRECTION (found while building 8a):** the sentence above originally read "upstream never registers
+rects for them". That is false, and it mattered — `Generated/AtlasData.lua` carries rects for all six,
+and they are NOT full-file rects. 6704514 is a shared 256x128 sheet with the icon overlay in its left
+third and the three bar pieces down its right, so the 0→1 rect this section guessed at would have
+stretched the overlay across the bar art. The rects were transcribed from that file and then verified
+arithmetically against each BLP's real dimensions; see §H.2.1.
+
+### 8a. Ship and register the art — **DONE**
+
+Copy the BLPs to `Textures/CooldownViewer/`, `RegisterLocal` each FDID, and add the rects. The rects
+must be **derived, not assumed**: each of these is a single-purpose sheet, so a full-file 0→1 rect is
+likely, but "likely" is how you ship a texture stretched to the wrong aspect. Decode each BLP for its
+real dimensions first (`node` + ImageMagick, per the repo's BLP workflow) and record the measured size
+as the atlas `width`/`height`.
+
+Harness: `HasAtlas` assertions for all five, mirroring the side-tab-art assertions that caught
+`core/Tabs.lua`'s stale "sheet not shipped" note. That check is exactly what would have caught this.
+
+### H.2.1 What 8a actually took
+
+Shipped as `modules/cooldownviewer/Assets.lua` (the viewers' art; `SettingsAssets.lua` remains the
+/cdm window's own) plus three BLPs under `Textures/CooldownViewer/`. **Six** atlases, not three — the
+three bar pieces `AuraItemMixins` sets on the BuffBar were unregistered for the same reason and live on
+a sheet 8a ships anyway, so registering them was three lines. What remains of 8d is the visual
+judgement (nine-slice insets, colour), not the data.
+
+**The rects came from upstream's generated data, and the check that matters is that they belong to the
+sheets WE ship.** Each BLP's header was read for its real dimensions and the rect fractions multiplied
+back out: every product lands on the declared atlas size to the pixel (6704514 = 256x128 → 86x86,
+124x10, 132x19, 10x46; 6685874 = 512x1024 → 43x43; 5199404 = 2048x1024 → 94x517). The harness now
+asserts that arithmetic, and mutating the overlay rect to 0→1 fails it with "rect gives 256.0x128.0,
+declares 86x86" — the precise mistake this section had been braced for.
+
+That check is also the guard against the failure `SettingsAssets.lua` records: retail repacked sheet
+7289697, so Era-generated rects were wrong for the 12.1.0 BLP. Transcription alone would not have
+caught it; transcription plus arithmetic does.
+
+**Size:** 10.5MB for three sheets, of which 8.4MB is the GCD flipbook (2048x1024 uncompressed ARGB for
+a 94x517 strip). Kept whole rather than cropped and re-encoded: the repo already ships 861 BLPs / 237MB
+including a 16MB roleicons sheet and 4MB Professions flipbooks of exactly this kind, so cropping would
+be a bespoke BLP *encoder* and a divergence from upstream's rects, in service of a size this repo does
+not treat as a problem.
+
+**One behaviour change, and it turns on code that had never run.** Registering the flipbook flips
+`hasFlipbook()` true, retiring the fallback ready-flash burst in favour of the 22-frame sprite stepper
+— a path dormant since Phase 1 because the atlas it gates on was never registered. Its frame maths is
+consistent with the art (94/2 = 517/11 = 47), and the harness now drives the stepper at four points
+across the flash and asserts each texcoord is inside the strip, exactly one cell, and grid-aligned. If
+it still looks wrong in game, deleting the one flipbook registration reverts to the fallback with no
+other change — the fallback was written as a permanent alternative, not a stopgap.
+
+**Three harness stub gaps, found because 8a made real code paths reachable.**
+`NE.tex.SetAtlasOnStatusBar` had never executed — it bails when its atlas is unknown — and the moment
+the bar atlas was registered it hit `SetAlpha` on a string. The stub's `GetStatusBarTexture` returned
+the *path* it had been handed; the real API always returns a texture OBJECT. That one is worth noting
+beyond the harness, because `AuraItemMixins:193` anchors the Pip to that return value. Also added:
+`GetMinMaxValues`, `GetVertexColor`, `GetStatusBarColor`, `hooksecurefunc`, and texcoord recording.
+
+**THE FIT, found on the first in-game look at 8a.** With the overlay finally visible, the icons read as
+too large for their frames — the reported words were "the icons are just too large and so sit outside
+the new framing", and they were exactly right.
+
+The cause is the other thing the removed MaskTexture was doing. Retail anchors the Icon with
+`setAllPoints` and masks it with 6707800; it is natural to read that mask as "rounds the corners", but
+decoding it shows its outer **3px of 64 are fully transparent**. So it also insets the icon by
+3/64 = 4.7% on every side, and only then rounds what is left, at a corner radius of 4/64.
+
+§C2 recorded that the mask could not be polyfilled and the port moved on. What went unnoticed is that
+the *inset* needs no mask at all — it is an anchor offset. In tile coordinates, for a 50px Essential
+tile:
+
+| | spans |
+|---|---|
+| IconOverlay's crisp border line | 2.07 .. 4.44 (art x 14-19 of the 86px cell, through the -9/+8 anchor) |
+| a MASKED icon (retail) | 2.34 .. 47.66 — edge sits inside the border band, so the frame overlaps it |
+| an UNMASKED icon (ours) | 0 .. 50 — overshoots the line by ~4px into the soft halo |
+
+Fixed with `M.AnchorMaskedIcon` (ItemMixins.lua), applied to all four tile shapes. The corner rounding
+is still absent and turns out to be the cheap half: at radius 4/64 a square corner overshoots the arc
+by 4*(1-1/√2) = 1.17px of 64, under a pixel on a real tile. The four missing pixels of inset were the
+whole visible problem.
+
+The harness asserts the border line lands ON the icon's edge, derived from the art rather than
+restated, and the magnitude is mutation-checked as well as the presence — 1/64 instead of 3/64 fails.
+This was unassertable before: the stub discarded texture anchors entirely, so a texture's geometry
+could not be seen. It records them now.
+
+**SECOND PASS, from the next look in game: "still 1px too large in all directions", and the swipe
+"sits at the old icon sizes".** Both right, and they are two different faults.
+
+*The swipe* was mine to have caught. Upstream anchors the Cooldown, the out-of-range shade and the
+flash to the TILE with `setAllPoints`, and I kept that, reasoning that retail's mask only masks the
+Icon. It does — but retail's swipe and shadow are themselves rounded art cut to the masked icon, while
+ours are the engine's plain sweep, a flat shade and a squarish sprite. At tile size each one draws
+proud of the inset icon and re-advertises the old footprint. All three now anchor to the icon's rect;
+this is a deliberate divergence from upstream's anchors, for a reason that only exists on this client.
+
+*The pixel* is the more interesting one, because the mask value was not wrong. Re-measured, the mask's
+edge is HARD — texels 3..60 of 64 at alpha 255, nothing in between — so 3/64 is exact, not a reading off
+a ramp. What was wrong is that the mask is SQUARE while the overlay is anchored **±9 horizontally and
+±8 vertically**, so the border band sits at a different tile offset per axis:
+
+| tile | axis | border band | mask inset alone | +1px |
+|---|---|---|---|---|
+| essential | x | 2.07..6.02 | 2.34 in | 3.34 in |
+| essential | y | 2.74..6.58 | 2.34 **out** | 3.34 in |
+| utility | y | 1.51..3.84 | 1.41 **out** | 2.41 in |
+| buffIcon | y | 1.79..4.93 | 1.88 in | 2.88 in |
+| bar icon | y | 1.51..3.84 | 1.41 **out** | 2.41 in |
+
+On three of the four shapes the icon's top and bottom edges were outside their band — the icon
+overshot the frame line vertically while sitting correctly inside it horizontally, which is precisely
+"1px too large in all directions" to look at. `M.ICON_ROUNDING_INSET = 1` puts every shape inside the
+band on both axes, and it is kept as a SEPARATE constant from `ICON_MASK_INSET` because it is a
+judgement (it also absorbs our square corners reading harder than retail's rounded ones), not a
+derivation. Mutating it to 0 fails the vertical assertion specifically.
+
+**The harness had checked one axis.** The first version of the fit assertion derived the band from the
+art and compared the horizontal one only, so it went green with the vertical edges still outside. It
+now checks per axis, and separately that each icon-space overlay shares the icon's rect — the
+assertion the swipe fault needed. Four mutations confirm the lot.
+
+**And a note on the ready flash, since it reads as unchanged.** The retail GCD flipbook is a thin pale
+rounded-square OUTLINE that traces the tile edge over 22 frames — not a burst. It is genuinely subtler
+than the gold expanding highlight it replaced, so "that still looks like the fallback" is a fair
+reading of a correctly working sprite. `/necdm` now reports which path is live, so the question is
+answerable without guessing.
+
+**A generalised assertion, because the specific one would not have caught the original fault.** Listing
+six names and checking each is registered only tests the names someone thought to list — and the fault
+was six names nobody had listed anywhere. So the harness reads the atlas names back out of the viewer
+sources (skipping comment lines, since these files also name atlases in prose to explain why they are
+absent) and requires every one to resolve. A seventh `SetAtlas` added later fails without anyone
+remembering to update a list. First attempt at this matched on the function name and found 3 of 6: the
+call sites go through a local alias, `local set = NE.tex.SetAtlas`.
+
+### 8b. The cooldown swipe
+
+§C3 recorded `SetSwipeTexture` / `SetEdgeTexture` / `SetUseCircularEdge` as MISSING. That was right
+about the native widget and wrong about the platform: **ClassicAPI implements `SetSwipeTexture` and
+`SetUseCircularEdge`** over a four-quadrant ScrollFrame capture rig (`Util/WidgetAPI.lua:1076-1135`,
+`Util/Cooldown.lua`), and only `SetEdgeTexture`/`SetEdgeColor`/`SetEdgeScale` are stubbed as
+"Incompatible (3.3.5)". So the retail radial sweep is reachable.
+
+It is not free: the rig builds five frames and a rotating animation per cooldown, and it hijacks
+`SetAlpha` on the Cooldown frame. Wire it behind a setting, default on, measured on a full Essential
+row before it is trusted — §C3's caution was about the wrong thing but it was not baseless.
+
+### H.2.11 The 8b measurement — **DO NOT ADOPT** (investigated, declined)
+
+The plan above set the gate: *measured before it is trusted*. Measured. It should not be wired, and
+"default on" would have been actively wrong. Five findings, in descending order of how decisive:
+
+**1. It deletes the countdown numbers.** `CooldownCaptureShow` calls `SetCooldownAlpha(Self, 0)` on
+the Cooldown frame to hide the native sweep it is replacing (`Util/Cooldown.lua:210`). Our countdown
+FontString is created ON that frame (`core/CooldownNumbers.lua:151-156`, deliberately — "parented to
+the cooldown itself so it inherits the cooldown's show/hide and frame level"), and frame alpha is
+inherited by regions. So the timer reads alpha 0 for the entire cooldown, restored to 1 only in the
+bling branch as it ends. The number on the tile is the single most important thing a Cooldown Manager
+draws, and this rig turns it off for exactly the interval it matters.
+
+**2. Zero callers, anywhere.** `SetSwipeTexture` is invoked by nothing in the whole AddOns tree
+outside ClassicAPI's own definition of it. This is not a well-worn path with a known quirk; it is
+unexercised code, and we would be its first user on this client.
+
+**3. The cost is ten frames per Cooldown, not five.** `CooldownCapture` builds the Swipe (1), four
+quadrants that are a ScrollFrame *plus* an Anchor frame each (8), and the Wedge (1) — plus an
+AnimationGroup and a Rotation. With `DrawEdge` on, two more frames and another AnimationGroup. At our
+defaults (Essential 12, Utility 7, plus the BuffIcon pool) that is roughly 190-250 frames for a
+feature the client already draws natively.
+
+**4. It fights the icon inset.** `Swipe:SetParent(Self:GetParent())` then `Swipe:SetAllPoints(Attach)`
+anchors the sweep to the TILE, not to the Cooldown's icon-inset rect. That reintroduces precisely the
+artefact §H.2.7 removed — a sweep sitting proud of the icon, betraying the larger footprint.
+Workaroundable by re-anchoring `cd._Swipe` after capture, but that is reaching into ClassicAPI's
+internals to undo its own layout.
+
+**5. And the headline capability is not what it looks like.** `SetSwipeTexture(path, ...)` forces
+`SetVertexColor(0, 0, 0, .65)` on all five textures and discards the r/g/b/a you passed
+(`Util/WidgetAPI.lua:1128-1136`). Retail's swipe art therefore comes through as a black 65%
+silhouette — which is what the native 3.3.5a sweep already is. We also do not ship 6731092, and
+Assets.lua deliberately did not ("shipping art ahead of the code that uses it").
+
+**What the rig genuinely adds is one thing: `SetSwipeTexture("", r, g, b, a)`, a solid COLOURED
+sweep.** That is the capability §C3 and 8c wanted and could not have, because `SetSwipeColor` is
+WoD+. But 8c already shipped a substitute for it — the static gold halo (§H.2.2) — and the owner has
+it in game and approved it. So the one real gain is a second solution to a solved problem, bought
+with ~200 frames, a hijacked `SetAlpha`, an unexercised code path and the loss of the timers unless
+two pieces of shared code are reworked first.
+
+**Decision: keep the native sweep.** §C3's original answer — "accept the engine's built-in sweep" —
+was right, for a better reason than it knew.
+
+If the gold *swipe* is ever wanted over the gold *halo*, the shape of that work is recorded here so
+it does not need re-deriving: reparent `_neText` off the Cooldown frame, re-anchor `cd._Swipe` to the
+Cooldown's own rect, enable capture only on tiles whose aura is up, and leave every other tile on the
+native sweep. It would be opt-in and default off.
+
+### 8c. The glow on a buffed spell — **DONE**
+
+The guide's second headline feature: "Tracks combat buffs and puts glow effects around buffed spells."
+
+We have half of it. `ItemMixins:568-580` already gives an active self-aura precedence over the
+cooldown, showing the aura's remaining time on the spell's own icon — the behaviour one commenter
+describes on Keg Smash. What is missing is the visual distinction: retail tints that swipe gold, and
+`SetSwipeColor` is WoD+ (the file says so at line 13).
+
+LibCustomGlow is already a dependency and already drives the alert FX (§E6), so a glow while a linked
+aura is active is a small, contained addition — and it substitutes for the colour we cannot set rather
+than approximating it.
+
+### H.2.2 What 8c shipped, and why it is not LibCustomGlow
+
+The plan above nominated LibCustomGlow because it was already there. Building it made the objection
+obvious: **every glow that library renders moves.** Marching ants, a pulsing button glow, orbiting
+sparkles — and on this addon motion already has a meaning. It is the alert vocabulary (§E6): something
+needs attention *now*. "Your buff is up" is a steady state, and retail draws it as one, with a flat
+gold swipe tint. Reusing the alert language for it would make every buffed spell read as an alarm, and
+would collide visually with a real alert on the same tile.
+
+What shipped instead is a **static gold halo on the tile**: one texture, no OnUpdate where the lib's
+renderers each drive one, and no possible collision with an alert glow because it is not the same
+mechanism.
+
+#### The first version rendered nothing, and the art says why
+
+It drew a gold additive copy of the tile's **own `IconOverlay` art**, in the same rect — the reasoning
+being that lighting the existing frame would align to the pixel at both tile sizes for free, with no
+constant to get wrong. Reported from the game as "I dont see this in effect", and it was not a wiring
+fault. Decoding the cell:
+
+```
+mid-row across the left border, 6704514 overlay cell (86x86 at 1,1):
+ x0..13 (0,0,0) a1 -> a48 the soft outer falloff
+ x14 (0,0,0) a104 the "crisp border line" 8a measured
+ x15..19 (0,0,0) a93 -> a7
+ x20+ (255,255,255) a0 fully transparent interior
+cell texels with alpha>8: 2795; mean effective luminance: 0.0 / 255
+```
+
+**Every texel is pure black.** The frame is a *drop shadow*, not a metal border — the "crisp line" 8a
+found at art x14-19 is just where the black is least transparent. `ADD` blending emits `src.rgb x
+alpha`, so a black source emits zero light no matter what vertex colour is multiplied over it. The
+atlas resolved, the region was shown, the blend was `ADD`, the tint was gold, and the arithmetic
+produced an invisible texture — 8a's exact failure mode reached by a completely different route.
+
+The replacement is `Interface\Buttons\UI-ActionButton-Border`, the stock 3.3.5a soft-glow ring this
+repo already uses for the bag rarity glow, the auction detail ring and the profession reagent slots
+(`modules/bags/BagSkin.lua:29`). Its rect is **oversized by 35%** of the tile edge — the figure those
+three call sites converged on, because the texture carries a wide transparent margin and at a tight
+rect the visible ring falls inside the icon rather than on its edge. That is the reverse of the 8a
+rule (anchor to the *icon* rect) and of the first version's rule (match the frame), so it is a
+constant, `M.BUFF_GLOW_OVERSIZE`, with the reasoning next to it.
+
+**What the harness now asserts is the source, not the mechanism.** Everything about the broken version
+passed — atlas resolved, region shown, blend `ADD`, tint gold — because each of those was true. The
+assertion that catches it is that an additive region is given art that can *carry* light, expressed
+as: the glow's texture is the stock ring, and is not from the CoolDownManager sheet at all. Mutating
+it back to a sheet path fails on exactly that line.
+
+Deliberately **not** aura-only: the totem branch (`RefreshCooldown` 1b) glows too. A live totem is not
+an aura, but it is the same state — this tile's effect is currently up — and that branch already
+carries `COLOR_AURA` for exactly that reason. Glowing one and not the other would leave a shaman's
+totem tiles as the only active tiles on screen that look inactive.
+
+Two clears matter more than the light itself, and both are pinned by a named assertion:
+
+1. **The cooldown path.** The aura branch returns early, so a glow left un-cleared there would stay up
+ for the rest of the session. One `SetBuffGlow(false)` sits below both precedence branches rather
+ than being repeated in each of the paths under it.
+2. **The pooling loop** (`Viewers.lua`). `RefreshCooldown` returns early with no `spellID`, so nothing
+ on the refresh path can clear a recycled tile — the next spell to reuse it would inherit a gold
+ frame from the spell that used to live there.
+
+Settings: "Glow while buffed" under a new **Buffed spells** section, default ON (it is a retail
+feature, not an addition of ours). `SetBuffGlowEnabled` refreshes rather than waiting to be noticed —
+turning it off has to reach a tile glowing *right now*, and for a buff already up the next natural
+refresh can be a minute away.
+
+`/necdm` reports the glow: on/off, the texture path, and how many of the shown tiles are lit right
+now. The glow has two innocent ways to look broken — the setting is off, or nothing is currently in
+the aura branch — and neither is distinguishable from a fault by looking. The first version added a
+third, which is why the texture is named in the output.
+
+Harness: 19 assertions, and two stub fidelity fixes — `SetBlendMode` and `SetDrawLayer` were both
+discarding their argument, and both carry meaning here (without `ADD` the copy *darkens* the tile
+instead of lighting it; without the sublevel it draws under the art it is meant to light). Seven
+guards confirmed load-bearing by removal, each failing on its own named assertion: the aura-branch
+light, the cooldown-path clear, the pooling clear, the setting's live refresh, the additive blend,
+the glow's source art, and the oversized rect.
+
+### H.2.3 The in-game pass on 8c — four bugs and two features
+
+Six items off one session in the game. Two were about 8c itself, two were older faults that 8c's
+attention surfaced, and two were features the port had simply never had.
+
+**The glow was drawing over the icon** ("icon glow effect inside frame"). `UI-ActionButton-Border` is
+not a hollow ring — it is a filled square glow, brightest near its edge — so drawn over the tile it
+washes light straight across the icon art. Fixed by DRAW LAYER rather than by geometry: the region
+moved to `BACKGROUND`, where the opaque icon masks its interior and only the halo that reaches past
+the icon survives. That is also the closest this client has to the mask it does not have, and it makes
+the oversize forgiving — too small now shows less halo instead of veiling the icon.
+
+**"Icons too large still and no rounded edges."** The third report on this, at the third value, which
+is the signal that it is not a number to be found. The mask inset is faithful to retail and retail's
+icon is ROUNDED; ours cannot be (§C2), and a square corner reads larger than its own edge does. So it
+became a setting — with a measured ceiling. Decoding the overlay's aperture (the first fully
+transparent texel inward of the border line) puts it at art 20 of 86:
+
+| tile | anchor | aperture x | aperture y | as a fraction |
+|---|---|---|---|---|
+| essential 50px | -9/+8 | 6.81 | 7.35 | 13.6% |
+| buffIcon 40px | -8/+7 | 5.02 | 5.24 | 12.6% |
+| utility 30px | -6/+5 | 3.77 | 4.30 | 12.6% |
+
+Inset that far and the icon clears the frame's opening entirely, so every square corner sits inside
+the rounded aperture — the closest this gets to rounded edges, at the cost of a visibly smaller icon.
+The slider spans from retail-faithful to that limit.
+
+The correction inside the correction: the extra had to become a **fraction of the tile**, not flat
+pixels. The frame art scales with the tile, so its opening does too, and a flat budget generous enough
+for a 50px Essential tile pushes a 30px Utility icon clean through its own opening. The first draft
+used flat pixels and the harness failed on exactly that — utility at 3.41 against an inner limit of
+3.28. The invariant it now checks is not a value but a range: the frame always covers the icon's edge,
+and the icon never shrinks past the opening, at every legal setting on every shape.
+
+**Prayer of Mending showed the 30s buff, not the cooldown.** Retail puts the aura's remaining time on
+a buffed tile because tinting that swipe gold is its ONLY way to say "buffed" — it has no spare
+channel. Since 8c we do, so the number is free to be the useful one. Aura precedence became a setting
+defaulting to the cooldown, and the glow carries the signal on both settings. This is the first thing
+8c has paid for rather than cost.
+
+It also moved a guard. `SetBuffGlow(false)` used to sit below both precedence branches, on the
+reasoning that everything past them meant "no effect of ours is up" — which stopped being true the
+moment the timer became optional, since a genuinely buffed spell now falls straight through to the
+cooldown path. One call above both branches covers both readings.
+
+**Old-spec abilities stayed in the window until touched.** Nothing the viewers listened for fires on a
+talent-group change, and any interaction that rebuilt a viewer hid it. `ACTIVE_TALENT_GROUP_CHANGED`
+and `PLAYER_TALENT_UPDATE` now rebuild and invalidate both the curated-list cache and the talent gate.
+
+**Per-spec layouts (new).** Dual talent specialisation is a 3.1 feature this port had ignored: one
+character genuinely wants Mind Blast on Essential in Discipline and not in Holy. The three tables that
+describe a layout — spell lists, aura assignments, trinket placement — moved into a per-talent-group
+bucket. Deliberately NOT bucketed: `seenAura`, which is knowledge rather than layout (an aura met in
+Holy is still an aura this character can get), and everything under `frames`, since a viewer that
+jumped across the screen on a respec would read as broken — retail's Edit Mode is not spec-aware
+either. Keyed by talent GROUP, not by inferred spec name: the group is what the client actually gives
+us, and inferring "Discipline" from the heaviest tree would be a guess that changes mid-levelling and
+collides outright when two groups share a tree.
+
+The harness caught a real one here. The first draft stored buckets under `cd.layouts` — which is
+already SettingsPresets' saved-layout table, so `ResetTracking` silently deleted every layout the
+player had saved. Renamed to `specLayouts`, with an assertion that a saved layout survives a tracking
+reset.
+
+**New buffs are hidden by default (new).** Auto-track flipped OFF. A newly met aura is still recorded
+and still listed under Hidden — discovery is worth keeping — but nothing reaches the screen until it
+is assigned. On by default meant a raid arrived as other people's cooldowns, food and flasks on a
+viewer the player had carefully curated.
+
+Harness: 519 → 565. Two stub fidelity fixes: `CreateTexture` was discarding its LAYER argument, so a
+region created on the wrong layer was unassertable, and `SetDrawLayer`/`SetBlendMode` now record.
+Eight guards confirmed load-bearing. Two mutations came back green first time and both were the test's
+fault, not the guard's: the spec-swap test set its scene with `SetSpellEnabled`, which rebuilds the
+viewer itself, so the scene-setting was doing the work the event was meant to do; and the glow-layer
+test could not see `CreateTexture`'s argument at all. Both rewritten, both now load-bearing.
+
+### H.2.4 The frame is an inner shadow, and that inverts the icon fit
+
+"It looks like the icon border is missing?" — after a pass whose whole point was to make the icons fit
+their frames better. Rendering the overlay cell instead of measuring it in columns explains it, and
+overturns the rule three previous passes had been working to.
+
+```
+6704514 overlay cell, 86x86: BLACK throughout, alpha only.
+ dark core on a line at art x=14, peak alpha 108/255 = 42%
+ soft falloff outward to x=0, fading inward to 0 at x=20
+ corner radius of the core ~4 art px
+```
+
+It is an **inner shadow**, not a frame in the sense of a drawn border. A shadow shows only where there
+is something beneath it to darken. So insetting the icon does not tighten the frame around it — it
+slides the icon out from under the shadow, and black-at-42%-over-the-world is nothing at all. Every
+round of "the icons are too large, inset them further" was quietly *deleting* the frame, and the last
+one crossed the line where the dark core sat entirely off the icon.
+
+| | icon edge | dark core | band | covered |
+|---|---|---|---|---|
+| essential x, shipped default | 2.34 | 2.07 | 2.07 – 6.02 | 93% |
+| essential x, at +4% (last pass) | 4.34 | 2.07 | 2.07 – 6.02 | **42%** |
+| utility x, at +4% | 2.61 | 0.84 | 0.84 – 3.28 | **27%** |
+
+The harness had this exactly backwards. Its assertion was that the icon's edge must land INSIDE the
+border band — which is what drove the shrinking, and which is satisfied most comfortably by the
+settings that hide the frame. It now asserts the opposite and measures it as **coverage**: how much of
+the dark band has icon under it, ≥70% at the shipped default. "Some overlap" is not enough to
+distinguish the two states — at +4% the Essential tile still overlapped the band, and that is the
+state that was reported as missing.
+
+So the inset default went back to 0 — retail-faithful — and its ceiling dropped to +4%, where the
+shadow is weak but not gone.
+
+**Frame strength** is the actual answer to both "the border is missing" and "no rounded edges". The
+art tops out at 42% black, which reads as a bevel rather than a border, and its rounded corners are
+far too faint to stop a square icon announcing itself. `SetAlpha` only scales down, so the alpha
+cannot be raised — but the texture can be drawn more than once: stacking N copies gives 1-(1-a)^N, so
+42% becomes **66%** at two and **80%** at three. Same art, same rect, same corner radius, just deeper,
+and the corners deepen with it. Default 2, exposed as a 1-3 slider next to the inset.
+
+This is the third distinct fault in this port traceable to reading a texture's *metadata* instead of
+its *pixels* — after the six unregistered atlases (§H.2.1) and the black additive glow (§H.2.2). All
+three were invisible to every assertion that checked wiring. Rendering the cell to a PNG and looking
+at it took two minutes and would have pre-empted all three.
+
+### H.2.5 The spec swap is an ORDER problem, not a staleness one
+
+"Switched from Holy to Disc and can still see Circle of Healing. It only disappears when I try to
+move it." The previous pass had already added `ACTIVE_TALENT_GROUP_CHANGED` handling, and it was
+working — the fault is that it runs too EARLY.
+
+`core/SpellRanks.lua` owns the learn gate's source of truth, and rebuilds its table on a **deferred**
+timer (`C_Timer.After(0)`), because SPELLS_CHANGED bursts during login and talent swaps and a full
+book walk is ~200 string matches. So the sequence on a dual-spec swap was:
+
+1. `ACTIVE_TALENT_GROUP_CHANGED` fires; consumers refresh **from the old book**
+2. `SPELLS_CHANGED` fires; a rebuild is queued for the next frame
+3. next frame: the table catches up, and `SB._onRebuilt` fires
+
+The viewers subscribe at step 3 (`Register.lua`), so they self-corrected. **The picker never
+subscribed** — it refreshed at step 1 and then had no reason to look again. Dragging a tile refreshes
+the layout, which is why interacting with it "fixed" it.
+
+Three changes, and the ordering one is the real fix:
+
+- `SpellRanks` now also rebuilds on `ACTIVE_TALENT_GROUP_CHANGED`. SPELLS_CHANGED does fire for a
+ swap, but its order against the talent event is not guaranteed, and consumers refreshing on the
+ talent event were reading a table that had not been told yet. The coalescing already there means
+ the extra registration costs nothing when both arrive together.
+- The settings panel subscribes to `OnRebuilt`, like the viewers do.
+- The viewers' subscription also drops the talent-gate cache, which it should have been doing.
+
+The test worth keeping is the one that models the ORDER rather than the outcome: fire the talent
+event, assert the gate still passes (that is the stale window, and it is correct at that instant),
+then drain the deferred timers and assert it flips — with no interaction. Written as "after a swap
+the spell is gone" it would have passed against the broken build, because the harness drains timers
+between most steps anyway.
+
+One guard here is deliberately NOT load-bearing: the panel's own `InvalidateCuratedCache`. Register's
+subscription runs first and already clears it — but "first" holds only while those two register in
+their current order, and a refresh reading a stale cache is the exact failure this callback exists to
+prevent. Noted in place rather than dressed up as verified.
+
+### H.2.6 Halo alignment, and cut corners tried and rejected
+
+"Is there any way to improve the alignment of the icon to the gold background?"
+
+The halo was anchored to the TILE, which is the larger rect, so it sat wide of the icon it was meant
+to be lighting. It now anchors to the **icon's own region**, which makes the two concentric by
+construction and means it follows the inset slider without being told about it.
+
+**Cut corners: attempted, and reverted at the owner's call.** "Can we make the corners not square? The
+spellbook successfully does this." The spellbook does — and not by masking. §C2 still holds
+(`CreateMaskTexture` is dead; `modules/spellbook/Spellbook.lua:160` says so outright). It covers the
+corners with something **opaque**: its frame is solid metal with cut corners drawn over the icon's
+edge, so the square underneath never shows.
+
+That also explains why the previous pass's frame-strength work could not finish this. Our frame is a
+translucent shadow, 42% at its darkest; stacking makes the corners *darker* and cannot make them
+*absent*. No see-through texture hides a corner.
+
+Building it confirmed the mechanism works — the square talent-node socket (`talents-node-square-gray`,
+alpha 0 at the corner texel, 255 along the mid-edge) turns the tile into a clean octagon. Sizing had
+one real decision, since its band is 13 texels of 80, 16.3% of the edge: flush with the icon costs
+that much of the art around the rim, while growing it so its opening clears the art overhangs the
+*tile* by the same amount and makes neighbouring icons collide. Flush was the better of the two, and
+is what the spellbook itself does.
+
+**Rejected on look** — the spellbook's socket is a heavier, greyer frame than the Cooldown Manager's
+own art, and next to the CDM shadow it reads as a different UI. Reverted whole; the halo change was
+kept. What this leaves on the table is that cut corners ARE achievable here — the blocker is art that
+suits these tiles, not the technique. If retail's own CDM sheet turns out to carry an opaque frame
+cell, that is the piece to look for.
+
+### H.2.6 Two rejected passes at the glow, and where it actually stands
+
+**Cut corners — built, reverted.** The spellbook makes square icons stop looking square without a
+mask (§C2 holds; `CreateMaskTexture` is dead) by covering the corners with something OPAQUE. That is
+also why frame strength could not finish the job: our frame is translucent, so stacking makes corners
+darker and never absent. The square talent-node socket does work, and flush with the icon is the right
+sizing — growing it so its band clears the art overhangs the tile by 16% and collides neighbouring
+icons. Rejected on look: it reads as a different UI beside the CDM art. The blocker is art that suits
+these tiles, not the technique.
+
+**The flat edge ring — built, reverted.** Reported as "the glow doesn't align to the top or right hand
+sides". The diagnosis was right and worth keeping: both glow versions were anchored symmetrically, so
+the RECT was always centred; what was not centred was what you could SEE, because a soft glow's
+visible extent is wherever its falloff survives whatever is drawn over it — here the frame shadow at
+66% black across the whole border band. What escaped was a gold edge on the two sides where the shadow
+had faded most.
+
+The fix I built — four flat strips on the icon's rect, above the shadow and above the sweep — solved
+the alignment completely and looked considerably worse: a hand-drawn rectangle where Blizzard's art
+had been. Owner's verdict, and correct. **I optimised for the property I could assert over the one
+that mattered**, and shipped it without rendering it first, having twice that same session established
+that rendering the art was the thing that settled these questions.
+
+**Where it stands, after the owner asked the obvious question I had talked myself out of:** on
+`UI-ActionButton-Border`, anchored to the icon's rect, drawn at **OVERLAY sublevel 7** — on top of the
+frame's shadow and its stacked copies.
+
+It spent two passes on BACKGROUND because of a claim I made and never checked. When the halo was
+first reported as appearing "inside the frame", I concluded the texture must be a FILLED square glow
+that washes light across the icon, and moved it behind so the opaque icon would mask its middle. It is
+not filled. It is a **ring with a transparent centre** — which the repo's own notes say in three
+places, and which four other modules rely on by drawing this exact texture straight over an item icon;
+`modules/professions/Crafting.lua:463` uses this precise layer and sublevel. The real cause of "inside
+the frame" was the ring landing too far in, which is a SIZE question, and the oversize constant already
+existed for it.
+
+Having put it behind the shadow, I then diagnosed the shadow suppressing it as an inherent trade-off
+between frame strength and glow evenness, and proposed knobs for balancing two things that never
+needed balancing. The trade-off was manufactured by the previous mistake.
+
+Kept from the reverted work: the harness now records frame levels. "This draws above that" was
+unassertable, the same way `CreateTexture`'s dropped LAYER argument made "this draws behind that"
+unassertable. Three stub parameters accepted and quietly forgotten have each now hidden something real.
+
+### H.2.7 A sliver on one side: the inset was landing on a half pixel
+
+Reported as icon art escaping to the left and right of the frame, and — the more diagnostic half —
+"with the cooldown swipe the right hand side is great and the swipe covers the glow correctly. However
+the left hand side the swipe doesn't cover the glow." Guessed at the time to be mask fallout. It is
+not; it is arithmetic, and it is fixable.
+
+The derived inset is fractional: `50 x 3/64 = 2.34375`. Anchored at that, the icon's left edge sits at
+2.34 and its right edge at `50 - 2.34 = 47.66`, and the renderer resolves each to a whole pixel
+**independently** — 2 on one side, 48 on the other. The two margins end up a pixel apart, against a
+frame whose own anchors (+-9 / +-8) are integers. One side shows a sliver of icon and the other does
+not.
+
+The swipe follows exactly because it is *supposed* to: §H.2.1 pinned it to the icon's rect through the
+shared helper, so it inherits the same split. Half a pixel is the entire difference between "covers
+the glow" and "doesn't", which is why that was the clearest symptom of the two.
+
+`M.IconInset` now snaps to whole pixels. Not "whole pixels at the current UI scale" — iconSize is
+applied with `SetScale`, so a tile can sit at a fractional scale regardless, and chasing that needs the
+effective scale at anchor time plus a re-anchor on every scale change. At scale 1, where this was
+reported, integer offsets land on integer pixels on both edges.
+
+Two harness changes fall out. The "inset is a fraction of the tile, not flat pixels" assertion — which
+exists because a flat budget pushes a 30px tile through its own opening — would forbid snapping if
+left as exact equality, so it now asserts the inset tracks the fraction to within half a pixel. And a
+new one asserts the result is integral, on the tile AND on every region that shares its rect, which is
+the property that actually fixes the artefact.
+
+Retail's mask is not what we lost here. A mask clips an icon that was already anchored on whole
+pixels; it does not centre anything. What we lost was the snapping, and that we could always have had.
+
+### H.2.8 The glow was under the sweep the whole time
+
+Four passes were spent on this glow, and every reported symptom had one cause.
+
+**`Cooldown` is a CHILD FRAME of the tile, and a child frame draws above EVERY layer of its parent.**
+BACKGROUND, OVERLAY 0, OVERLAY 7 — all of them are below it. The halo was under the rotating sweep no
+matter which draw layer it was given, and the sweep is a *wedge*, so which part of the halo survived
+depended on where the sweep had got to. Read back with that in hand, the reports are one fault:
+
+| reported | actually |
+|---|---|
+| "icon glow effect inside frame" | the swept wedge covering part of the ring |
+| "doesn't align to the top or right hand sides" | the wedge's current position, not an anchor |
+| "the swipe covers the glow on the right but not the left" | said outright, and I read it as two faults |
+| pixel-snapping "didn't fix anything" | correct: pixels were never this one's problem |
+
+The third row is the uncomfortable one. The owner described the mechanism exactly, and I treated the
+swipe and the glow as two separate misalignments to be reconciled rather than one drawing over the
+other.
+
+The fix is a **sibling frame with a higher frame level**, which is the only thing that outranks a
+child frame. Art, blend and rect are unchanged from the version that was already there. Note this is
+the same structural insight as the reverted flat-ring experiment (§H.2.6) — that version hosted its
+strips in a frame *and* replaced the art, so when the art was rejected the insight went with it. It
+was the frame that mattered.
+
+What the detours were worth keeping: the region is on top of the frame shadow rather than behind it
+(§H.2.6 — a ring with a transparent middle, not a filled glow), and the icon inset snaps to whole
+pixels (§H.2.7 — a genuine artefact, just not this one).
+
+The assertion that closes it compares the glow's frame level against the Cooldown's. A draw-layer
+assertion could never have caught this, because the layer was never wrong in the terms it could see.
+
+### 8d. Bar theming
+
+The bar rows are functional: icon, name, depleting fill. Against retail they want the fill texture, the
+spark, and the name/timer typography and alignment checked side by side. Lowest-confidence item here,
+because it is the one that needs a screenshot comparison rather than a code reading — scope it after
+8a, when the icons are right and the bars are the remaining mismatch.
+
+### H.2.9 What 8d actually took
+
+**Three of the four questions closed by looking, and the fourth was a real defect.** The owner's
+screenshots (edit mode with looping previews, and a live Power Word: Shield mid-depletion) answered:
+the fill IS orange, so `SetStatusBarColor` reaches the visible overlay through the `hooksecurefunc`
+in `SetAtlasOnStatusBar` — worth confirming, because the engine's own bar texture is deliberately
+alpha-0 and a missed hook would render a plain grey bar with nothing else looking wrong. The pip
+reads as a spark, not the oversized blob its 10x46 cell suggested (the art is only ~6x30 inside it).
+Typography and alignment match upstream's XML, which our Lua transcribes literally.
+
+**The defect was `Bar-BG` stretching bodily.** Decoding the cell settles where it slices:
+
+| cols | content |
+|---|---|
+| 0-7 | alpha 96, 253, 235, 220, 188, 179, 174, 173 — left cap, edge line at x=1 |
+| 8-120 | alpha 173 flat, luminance 0 — uniform, safe to stretch by any factor |
+| 121-131 | alpha 179..253 (edge line at x=126) then 172, 90, 35, 11, 5 — cap plus drop shadow |
+
+The right cap is wider because the art's shadow falls down and to the right, which is also why
+retail's anchors are `-2/+2, +4/-7` rather than symmetric. Stretched whole, the two 1px edge lines
+smear: 1.47x at the 100% bar width, **3.1x at the 200% end of the width slider**. That last figure is
+what the owner's screenshot was actually showing, since they had widened the bars for legibility.
+
+**Horizontal only.** Vertically there is no flat band to slice — rows 3-11 run 209, 185, 165, 153,
+153, 160, 173, 194, 228, a continuous bevel — so every piece stretches down the same constant 28/19
+it always has. A nine-slice here would be inventing a seam. Cap widths therefore scale with the
+region's HEIGHT, so widening the bar moves the extra pixels into the middle and leaves the corner
+bevel uniformly scaled instead of squashed into an ellipse.
+
+**`BarBG` is three textures on the bar, held in a plain table — not a child frame.** A child frame
+draws above every layer of its parent (§H.2.8), so a `Frame`-based group would have put the
+background over the fill, the pip and the name. The same rule, one phase later, in a place it could
+have bitten again.
+
+Two things the harness taught us:
+
+* **A zero width is "not laid out yet", not "too narrow for caps".** The bar takes its width from its
+ anchors, unresolved when the row is built, so the first call reads 0. Conflating the two collapsed
+ every row to a single stretch until something happened to resize it. The guard also has to test the
+ BAR's width rather than the padded region's — padding is +6, so an unresolved bar still reports a
+ 6px region and sails past a `regionW > 0` check.
+* **`HookScript` in the stub replaced instead of chaining**, so the last hook on a script silently
+ won — and these rows hook `OnSizeChanged` twice, once for the fill's texcoord and once for the cap
+ widths. Fixed. It mattered: under the replacing stub the cap-width assertion passed **vacuously**,
+ because nothing re-ran at all and "the caps did not change" is satisfied by doing nothing. The
+ assertion now blanks the middle's texcoord first, so the re-apply is itself observable.
+
+What 8d did NOT need: a retail comparison shot. Upstream's XML is vendored at
+`ReferenceAddons/NewEra/CooldownViewer/CooldownViewer.xml` and our geometry matches it line for line,
+so any mismatch had to be in the texture treatment rather than the numbers.
+
+`/necdm` now reports the stretch factor the middle is carrying and the cap widths it is holding off.
+
+### 8e. The pandemic ring
+
+`Alerts.lua`'s `refresh` type is already upstream's `CooldownPandemicFXTemplate` in behaviour, tinted
+pandemic-orange. The retail art (`PandemicFX-Icon01/02/03`, `PandemicFX-Bar`) is on sheet 6685874,
+which 8a ships anyway. The two clip masks (7552325, 7553101) are unusable — §C2, `` cannot
+be polyfilled — so the ring needs the unmasked art plus a crop, the same substitution the icons use for
+their rounded corners.
+
+### H.2.10 What 8e actually took
+
+**The premise above was wrong, and decoding the cells is what showed it.** "The ring needs the unmasked
+art plus a crop" assumed everything in retail's pandemic FX depends on the two clip masks §C2 rules
+out. It does not. `UI-CooldownManager-PandemicBorder` is **already a hollow rounded-square ring**:
+alpha 0 through the centre, material in two bands at cols 2-8 and 52-58 of 61, real RGB at full alpha
+(peak 255,48,48). It needs no mask, no crop and no substitute. It is finished art, on a sheet 8a
+already ships, and it ported as one atlas registration plus a texture.
+
+This is the fourth time this phase that reading a texture's metadata instead of its pixels produced a
+wrong plan — after the six unregistered atlases (8a), the black additive glow (8c) and the
+shadow-vs-border frame (§H.2.5). The correction cost about ten minutes with `blp.js`.
+
+**What genuinely needed the masks was the CASCADE, and it is the half that did not port.**
+`PandemicFX-Icon01/02/03` are FILLED 128x128 quads (centre alpha 61/81/24), not rings — retail clips
+them to the ring shape and *then* scales them 0.25 → 1.5. Unmasked they are square smears across the
+icon and its neighbours, so those three atlases are deliberately **not** registered. `Animation:SetTarget`
+is independently absent too, so the template's one-group-drives-three-textures structure has no
+equivalent regardless.
+
+**Substitution: the ring pulses.** One texture on one frame, so the animation acts on the region that
+owns the group — which is exactly what this client's animation model does — and no mask is involved at
+any point. Same signal as the cascade, real art, no faked clip.
+
+Details worth keeping:
+
+* **A fourth FX, not a special case.** `AL.FX` already drives the settings submenu by generation, so
+ adding `{ id = 4, name = "Pandemic Border" }` gave it a menu entry, a preview and idempotent
+ start/stop for free. `refresh` defaults to it via `AL.DefaultFX`; the three LibCustomGlow renderers
+ stay selectable.
+* **Untinted, alone among the FX.** The other three renderers are colourless and take meaning from
+ `TINT`. This art already carries the colour, and vertex colour multiplies — TINT.refresh's
+ (1, 0.5, 0.1) over a ring that is (1, 0.19, 0.19) lands on (1, 0.09, 0.02), a muddier red than the
+ artist chose, arrived at by tinting something that needed no tint.
+* **The opening lands on the VISIBLE icon, which is not the same rect on every tile.** Retail anchors
+ this ring with `SetAllPoints(item)`; copying that put it inside the icon on a picker tile, and
+ anchoring to `.Icon` instead then pushed it too far out on a viewer tile. Both reports were correct,
+ because the two tiles disagree about where the icon ends: a picker tile has no frame art, so the
+ visible icon IS the `.Icon` rect; a framed viewer tile's gold `IconOverlay` opens `M.IconAperture`
+ inside the tile (6.8px on a 50px Essential) and covers the icon's outer edge, so the 46px `.Icon`
+ rect shows as ~36px. The art settles what to do with that. Its mid-row alpha runs 5, 15, 30, 50,
+ 75, 106, 141, 181, **255** across cols 0-8, then 0 through col 51, then mirrors — a hard edge at
+ col 8 bleeding OUTWARD. So the 43-pixel OPENING is what must land on the visible icon and the 9 per
+ side belong outside it. 43 is not a coincidence: it is the size of `OORshadow` on the same sheet,
+ the icon-sized cell of this art family. Deriving from the aperture makes both tiles one formula —
+ and on a framed tile it lands within a pixel of the tile rect, which is *why* retail's
+ `SetAllPoints` works for retail: 43/61 = 70.5% is where a frame of these proportions opens.
+ `.Icon` was wrong for a framed tile in a second way too — it moves with the icon-inset slider while
+ the frame does not, so a ring tied to it would drift off the opening it is meant to trace.
+ Re-anchored on every SHOW, because these rects come from anchors the client resolves late
+ (the §H.2.9 lesson).
+* **Levelled above the cooldown sweep**, for the reason §H.2.8 cost four passes to find: the sweep is a
+ child frame, so it outranks every draw layer of the item and a ring on any layer would sit under it.
+* **Alpha animations are DELTAS on this client.** ClassicAPI's `SetFromAlpha`/`SetToAlpha` are a
+ polyfill over native `SetChange` (WidgetAPI.lua:414-436) and only emit it once both halves are set —
+ hence from-then-to on each animation, and hence `startPandemic` setting alpha to 1 before `Play`.
+* The **bar** ring (`pandemicborderbar`, 32x32, nine-slice 15/15/15/15) is registered nowhere: the
+ alert ticker walks only the icon viewers, so nothing could read it. Upstream's own bar path is
+ dormant for the same reason, by its own comment.
+
+The harness gained an AnimationGroup stub, without which the builder's feature gate would have skipped
+the pulse and the ring would have tested green while sitting static. Four mutations verified negatively:
+never playing the pulse, levelling the ring at the sweep's level, tinting it, and reverting the anchor
+to `SetAllPoints`. The last of those first ABORTED the harness rather than failing by name — `GetPoint`
+returns no offsets for an all-points anchor, so the comparison hit nil — which is barely a catch at
+all; the assertion now reads defensively and names the defect.
+
+### Verification
+
+Screenshots, into `screenshots/`, per the repo's existing practice: an Essential row on cooldown, a
+Utility row with one spell buffed, a Buff Bar mid-depletion, and an out-of-range target. Offline
+harness can only assert the atlases resolve — the rest is the eye.
+
+## H.3. Phase 9 — audit against the current retail guide — **DONE**
+
+Every feature and setting the guide describes, with a verdict. Consumables excluded by owner decision.
+
+Phase 9 closes the port. Of the three items that were open when it was written, two had already been
+paid for by earlier phases and only needed confirming; the third was the rename, which is the only
+code this phase actually shipped.
+
+**Have it:**
+
+- Enable checkbox + a button through to the settings window ("Enable Cooldown Manager", "Advanced
+ Cooldown Settings") — §G.12, as an enable toggle plus "Open Cooldown Manager".
+- Two picker tabs, spells and buffs, with icon tooltips on hover. (We have three; the Settings tab is
+ ours. Retail keeps its settings in Edit Mode.)
+- Essential / Utility / Not Displayed, per mode — and since Phase 9, under retail's own name.
+- Not-talented spells greyed out — our unlearned tint, which goes further: it explains itself in the
+ tooltip.
+- Right-click to move between categories.
+- Sound and visual alerts via right-click, choosing trigger and effect — §E6/§G.7. **We are ahead
+ here:** the guide says alerts are "only available with castable spells, and not buffs".
+- Drag to reorder, and to reassign — §G.8. **Also ahead:** the guide's comments are three people asking
+ for reordering and being told it does not exist.
+- All four elements movable — four movers plus, since Phase 6, a per-viewer Position button.
+- Buff timers on tracked buffs; out-of-combat buffs excluded.
+
+**Missing, and worth doing — all three now closed:**
+
+1. ~~**The glow around a buffed spell** → 8c. The only headline feature from the guide's own list that
+ we do not have in some form.~~ **DONE** (§H.2.2). Nothing on the guide's feature list is now
+ missing; what remains under Phase 9 is the "Not Displayed" naming preference and the two deliberate
+ cuts below.
+
+2. ~~**Target DoT timers.** The guide lists "timers for ... damage-over-time effects". The code exists
+ (`ScanTargetTrackedAuras`) and has never been reachable~~ — **DONE, by Phase 7**, and confirmed
+ rather than re-fixed. `ScanTargetTrackedAuras` reads the `include` set that `GetBuffOverrides`
+ builds from the tracked-aura pool, and that pool was empty on every character until 7a/7d gave it
+ a source. One empty table was disabling two features.
+
+ Confirmed end-to-end, not by reading: `test_boot.lua` assigns Shadow Word: Pain through the same
+ `SetAuraAssignment` the picker calls, puts a **Rank 10** SW:P on the target — a different spellID
+ from the assigned one, and outside the auto-track window, so nothing but the name match can show
+ it — and asserts the tile appears. The negative half is asserted separately: an *unassigned* target
+ debuff stays invisible, because the auto window must never reach targets or every enemy debuff in
+ the room arrives uninvited.
+
+3. ~~**"Not Displayed"** is retail's name for what we call "Hidden", in both modes.~~ **DONE.** The
+ two `CATS` labels, plus the three other places the old word reached the player: the empty-state
+ line for the aura catalog ("Everything recorded is displayed."), the trinket tooltip, and the
+ buff-tracking help text on the Settings tab. The right-click menu needed nothing — it builds
+ "Move to …" from `Adapter.Label`, so it followed.
+
+ **The rename stops at the display string.** The category ids (`hiddenSpell` / `hiddenAura`) and
+ the stored assignment value (`"hidden"`) are unchanged, because that value is what every saved
+ layout already on disk contains — renaming it would silently un-hide every aura a player has ever
+ hidden. Both halves are asserted, so a future tidy-up that "finishes" the rename fails loudly.
+
+**Deliberately not doing:**
+
+- **Charges / recharge** ("Max 2 Charges ... 12 sec recharge"). No charge system exists on 3.3.5a.
+- **Pet and AoE-spell timers.** Totems are already trackable as ordinary cooldowns via the arsenal;
+ a totem's *remaining duration* would need `GetTotemInfo` polling and a per-class totem→spell mapping,
+ which is a feature in its own right rather than a gap in this one. Flag for a later decision.
+- **Retail's curated per-spec buff list.** 7d is the closest we can get from client data, and the
+ guide's comment thread is largely people complaining that the curated list omits their abilities —
+ a generated catalog plus the seen registry is arguably a better answer than the thing being copied.
+
+### H.3.1. In-game fault: rows with no name on hover
+
+Found immediately after Phase 9, on the surface Phase 9 sent the owner to look at. Most rows under
+Not Displayed showed no name when hovered.
+
+`tooltipSetSpell` reports whether its `pcall` errored, and callers were reading that as "a tooltip
+was drawn". It is not. 3.3.5a has no `GameTooltip:SetSpellByID` and `!!!ClassicAPI` does not add one
+(it adds `SetItemByID` and stops), so every spell tooltip goes through `SetHyperlink` — and
+`SetHyperlink` on an id the client cannot resolve **succeeds and draws nothing**. Aura rows hit that
+constantly, because the generated catalog stores rank-1 ids straight from `Spell.dbc` and plenty of
+aura-only ids have no linkable spell behind them. The tile then showed "Lasts 30 sec" and "Tracked
+automatically" under no title at all, on a 38px grid icon that carries no label of its own.
+
+The name was never missing — the catalog and the seen registry both store one, precisely because
+`GetSpellInfo` cannot be relied on for aura ids. It simply was not used. `M.TooltipSetSpellNamed`
+adds it as a floor: `ClearLines`, ask the client, and if `NumLines()` is still 0, put the row's own
+name up as the title. All four hover paths route through it — the two viewer aura mixins, the spell
+tiles, and the picker.
+
+**The stub was hiding it.** `test_boot.lua` defined a `SetSpellByID` the client does not have, so the
+harness exercised a branch no player reaches, and its `SetHyperlink` was a bare no-op that could not
+model "succeeds, draws nothing". Both are fixed, and the stub now gives a resolvable spell a title
+**and a body** — without a body, "the client answered" and "we wrote the name ourselves" produce
+identical tooltips and the guard passes just as happily when the fallback has eaten every tooltip in
+the addon. That was caught by a mutation that failed to fail; it is the fourth vacuous assertion this
+phase and the same shape every time.
+
+Five mutations, each failing by name: the picker reverted to the unnamed helper, the viewer mixin
+reverted, the fallback fired regardless of `NumLines`, the fallback emptied, and the client never
+asked.
+
+### H.3.2. In-game fault: alerts never fired on tracked buffs
+
+Reported as "the FX on tracked buffs doesn't work". It was not the FX — it was the ticker's viewer
+list. `Alerts.lua` walked `viewers.essential` and `viewers.utility` only, justified in a comment as
+"the aura viewers have no ready transition and no curated usable state".
+
+Both halves of that are true and neither was a reason to skip them. `available` is edge-triggered off
+`ConsumeReadyTransition`, which aura items do not define, so `checkReadyTransition` returns at its
+first guard and costs one lookup. `usable` reads `IsUsableSpell`, which answers for a self-buff's own
+spell exactly as it does for a cooldown. What the omission actually cost was **`refresh`** — the one
+alert whose trigger *is* an aura decaying, and therefore the most useful of the three on a tracked
+buff. It could be assigned from the menu, it lit the badge, it previewed on the settings tile, and
+then it never fired in play.
+
+**The asymmetry that hid it:** `AL.Stop` and `M.ResetAlerts` have always cleared FX across all four
+viewers via `M.ForEachViewer`. Only the code that *set* it was narrower, so the teardown looked
+complete. The ticker now uses `ForEachViewer` too, which is also why it cannot go stale against
+`M.viewers` a second time.
+
+**Two triggers were being offered where they cannot fire**, which is the same defect one layer up.
+`Available`'s own tooltip promised "Works for every spell" on rows that hold no spell, and a ready
+sound assigned to a buff lit the badge permanently with nothing able to play it — `FireReadyAlerts`
+is a cooldown-edge path. `SettingsMenu.lua` now gates both on the row's category `mode`. `None` still
+sits above `Available`, so a layout that stored it before the gate is still clearable; a stored ready
+sound gets a single `Clear Ready Sound` entry rather than vanishing with the submenu, because a
+setting you can reach only to regret is worse than one you were never shown. `Refresh` also drops its
+"a cooldown that applies no aura can never trigger it" caveat on aura rows — there, the row *is* the
+aura.
+
+Four mutations, each failing by name: the ticker reverted to the two spell viewers (three
+assertions), the `Available` gate removed, the ready-sound gate removed, and `isAuraRow` forced true
+to prove the gate does not fire on spell rows.
+
+### H.3.3. In-game fault: the alert triggers were all cooldown questions
+
+Reported after §H.3.2 shipped: "the tracked buffs still don't work with effects", with an alert
+reading `Alert: usable (Button Glow)` on **Surge of Light**. The ticker was reaching the aura viewers
+by then. The trigger could still never fire.
+
+All three ported triggers ask questions about a **cooldown** — has it finished (`available`), is it
+castable (`usable`), is the buff it applies running out (`refresh`). A proc is none of those. Surge
+of Light is a 10s aura with no castable spell of its own name, so `IsUsableSpell` answers nil,
+`available` has no cooldown edge to consume, and `refresh` covers the last 3 seconds of the 10. The
+question a tracked buff actually asks — *is this proc up* — had no trigger at all.
+
+**`active` is the fourth type**, ours and not retail's, offered on aura rows only. It reads the
+item's own `_auraActive`, which both aura mixins already maintain, so it costs nothing on the ticker
+path. **`usable` is now gated** on the client knowing a castable spell of that name — probed with
+`GetSpellInfo`, not `IsUsableSpell`, because the latter returns nil both for an unknown name and for
+a known spell you merely cannot afford right now, and reading it would have hidden the entry from
+anyone who opened the menu out of mana. A self-buff row keeps it, with a line saying it is about
+re-casting rather than about the buff being up.
+
+**A second fault, found while fixing the first.** Per-spell settings are stored under the id the
+PICKER ROW carries — rank 1 from the catalog, or whatever rank the scan first met — while a live
+viewer item carries the id the scan returned this time. For any ranked buff those differ, so
+`AL.Get(item.spellID)` found nothing: stored, badged, silently dead. `GetBuffOverrides` had already
+solved this for *assignments* by keying on name as well as id; `M.SettingsKeyForAura` applies the
+same rule to the settings stores, and the aura mixins gained the `GetSettingsKey` the spell items
+have always had. It did not bite Surge of Light (catalog id 33151 is also the applied id) which is
+why the report was about `usable` and not about everything.
+
+**Three harness gaps, each of which had been hiding one of these.**
+`IsUsableSpell` returned `true` for **any** string, so a proc row looked castable offline — the exact
+opposite of the client's answer, and the reason `usable`-on-an-aura had a passing test.
+`CreateTexture` **discarded the subLevel**, so "created underneath the thing it was meant to cover"
+had no offline symptom; that is the same fault §H.3.2's stone note describes, and it had gone
+unmodelled for four phases. And the earlier `refresh`-on-a-buff test used one spellID for both the
+stored alert and the scanned aura, so it could not have caught the rank mismatch.
+
+Six mutations, each failing by name: `active` never evaluated, `active` sharing the usable tint,
+`Active` not offered, `Usable` offered regardless, the settings key falling back to the live id, and
+the Inset fill returned to subLevel -5.
+
+### H.3.4. The window body, and the inset that was never visible
+
+The `/cdm` window read as a dark wash beside every other standalone window: `PC.ApplyModernChrome`
+tints its `f.Bg` to 0.32 grey — a downport fix for a different frame's first-paint colour flash — and
+this was the last window still wearing it. Stripped to full brightness, as Collections does.
+
+The Inset fill it carried was `character-panel-background` at BACKGROUND subLevel **-5**, while
+`f.Bg` sits at subLevel **0** and spans the whole frame. It drew underneath and **was never once
+visible**. ClassicAPI's `PortraitFrameTemplate` declares its own `$parentBg` with no parentKey
+(`UIPanelTemplates.xml:1517`), so PanelChrome builds `f.Bg` fresh at the default subLevel instead of
+re-texturing the template's at -6 — which is what put the two on the wrong sides of each other. The
+Inset now carries the near-black recessed fill every other inset in this addon uses (Collections'
+`buildInset`, LFG's rail), at a subLevel **above** the body stone.
+
+The rock FDID was never registered in the offline harness, so PanelChrome took its graceful-degrade
+branch and applied no tint at all — a guard on the body brightness passed against the very regression
+it exists to catch, and a mutation removing the untint failed to fail. Registering it fixed that, the
+registration is now itself asserted, and the stub gained `SetHorizTile`/`SetVertTile` (whose absence
+was what forced the degrade branch) plus `ButtonFrameTemplate`'s `Inset` child.
+
+### H.3.5. The scrollbar was never reskinned, and the row highlight was the wrong shape
+
+**The scrollbar.** `/cdm` called `NE.scrollbar.Reskin`, which looked applied and could never have done
+anything here. Reskin reaches for `scroll.ScrollBar`; 3.3.5a's `UIPanelScrollFrameTemplate` declares
+its slider as `$parentScrollBar` with **no parentKey**, so that field is nil and Reskin returns at its
+second line. Exactly the shape of the `PortraitFrameTemplate` `$parentBg` trap in §H.3.4, and it has
+no symptom beyond "the bar looks stock" — which is what it looked like.
+
+Switched to `NE.scrollbar.BuildCustomPixel`, the hand-built track+thumb every other list in this addon
+uses. It is the right variant for a real ScrollFrame: it drives `SetVerticalScroll` directly and sizes
+the thumb from visible:total, rather than `BuildCustom`'s row-step heuristic for FauxScrollFrames.
+
+`BuildCustomPixel` had to grow the stock-bar handling `BuildCustom` has always had. Its only previous
+caller (the character stats sidebar) is a bare `CreateFrame("ScrollFrame")` with no template and so no
+stock bar to get out of the way of; a templated ScrollFrame has one, complete with arrows, and without
+this the player gets **two** bars side by side. It now hides the stock slider with a re-show guard,
+reparents the arrows off it (a hidden parent hides its children whatever their own state), reskins and
+re-anchors them to our track, and replaces their `OnClick` — the template's inline handler drives
+`self:GetParent()` and assumes that is the slider, so after the reparent every click throws.
+
+**The row highlight.** Reported from the game: the blue highlight over-stretched on Tracked Bars.
+`ButtonHilight-Square` is a 64×64 glow drawn for a square button; across a 344px row it smears — bright
+over the icon, bleeding away to the right. Wide rows take `UI-QuestTitleHighlight`, which is built to
+stretch and is what the character Sidebar, EquipmentManagerPane and TitlesPane rows already use. The
+square tiles keep the square texture, which is the half a blanket swap would have broken.
+
+**Harness.** `core/ScrollbarReskin.lua` was not loaded at all, so none of this had coverage. Loading it
+needed four stub fixes, each of which had been hiding something: `GetVerticalScrollRange` was a hard 0
+(the one value that makes every scrollbar hide itself, so a bar built wrong in every respect still
+looked right), `SetFrameStrata` was write-only with no getter (code that re-reads a strata to match it
+died inside a `pcall`, visible only as a widget that quietly failed to build), the four button state
+textures returned a fresh region per call (so "did this button get retextured" was unanswerable), and
+`UIPanelScrollFrameTemplate` supplied no slider — modelled now by global name and deliberately
+**without** a `.ScrollBar` parentKey, because that absence is the bug.
+
+### H.3.6. In-game fault: the per-character reset was resetting every character
+
+Reported as "the reset character button in the settings actually resets all characters", and that is
+exactly what it did.
+
+The store is **one shared DragonUI profile**, keyed by class all the way down —
+`customLists[][]`, `trackedAura[]`, `seenAura[]`. That class key is the
+only thing separating one character's data from another's. `M.ResetTracking` blanked
+`cd.specLayouts = {}` and `cd.seenAura = {}` **whole**, taking every class with it: a Priest pressing
+reset cleared the Warlock, the Druid and the Mage as well.
+
+Now scoped. Each layout bucket loses only this class's slice — the "every bucket, not just the active
+talent group" half was right and is unchanged. The one-time migration is forced first, so the legacy
+flat keys are gone by the time the scrub runs and there is one shape to handle rather than two.
+
+**`equipAssign` could not be sliced the same way**: it is keyed `"item:"` with no class
+dimension at all. Scoped instead to the tokens this character can actually reach — the trinkets it has
+equipped — which leaves another character's placements alone. A token for an item nobody has equipped
+is unreachable in the picker regardless.
+
+**The copy was wrong too, in the same direction.** The button promised "clearing per-character spell
+lists" and the confirm popup named no scope whatever, which is the last thing read before an
+irreversible action. Both now say *this class*, which is the honest word: another class is untouched,
+and another character of the same class shares these lists and will see them reset.
+
+Three mutations, each failing by name: the unscoped wipe restored verbatim, `seenAura` blanked whole
+again, and `customLists` left alone — the last proving the fix did not quietly turn the reset into a
+no-op, which is the failure mode a scoping change invites.
+
+### H.3.7. Viewer positions are per character
+
+Owner request: moving the viewers in `/dui edit` should save per character. It did not — DragonUI's
+profile is shared across the account (the same fact behind §H.3.6), so all four viewers sat wherever
+the last character to touch them left them.
+
+**Done by KEY, not by a parallel store.** DragonUI's editor takes exactly one thing from us — the
+`configPath = { section, key }` it reads and writes `profile[section][key]` through. Varying the key
+therefore buys per-character positions with no change to DragonUI at all (CONTRACTS §0).
+`NE.FramePositionKey` appends name and realm, and it is **opt-in per frame**: a module that wants one
+placement across the account keeps it by saying nothing. Only the four Cooldown Manager viewers ask.
+The fallback when the client cannot yet name the player is the shared key — a shared position is a
+mild surprise, a position filed under `-nil` is one nothing will ever read back.
+
+**Migration matters more than the feature here.** A character upgrading into this must not watch its
+viewers jump to the default; that reads as data loss. The existing shared entry seeds each character's
+slot on first registration, as a **copy**, and is deliberately left in place — deleting it would hand
+the upgrade to whoever logged in first and a default layout to everybody else.
+
+Four mutations, each failing by name: the viewers no longer asking, the key builder ignoring the flag,
+the seed skipped, and the seed moving the shared entry instead of copying it.
+
+### H.3.8. Per-character appearance, as a tickbox
+
+Follow-up to §H.3.7. Position went per character unconditionally; everything else under `cd.frames` —
+orientation, icons per row, icon direction, size, padding, opacity, visibility, timer, tooltips, bar
+content, bar width — stayed account-wide. The owner asked for that to be opt-in rather than automatic.
+
+**"Separate appearance per character", off by default.** With it off nothing about the previous
+behaviour changes: reads and writes go to `cd.frames` exactly as before, and no per-character bucket is
+created at all.
+
+**On, a character's values are OVERRIDES on the shared table, not a copy of it.** `getOpt` consults the
+character bucket first and falls through to `cd.frames`. That structure is doing the work:
+
+* **Nothing moves when the box is ticked.** A character that has changed nothing reads what the account
+ reads, because the fall-through *is* the seed. There is no copy step, so there is no copy step to get
+ wrong — contrast §H.3.7's position migration, which needed one.
+* **Unticking is not destructive.** It stops consulting the bucket; the overrides survive, so the box is
+ free to try in both directions.
+* **An untouched key keeps tracking the account.** Change icon size account-wide and every character
+ that never overrode it follows.
+
+`ResetOpts` clears **both** levels: the button says "defaults", and clearing only the override would
+land on the account-wide setup while promising something else. Other characters' buckets are untouched.
+
+Five mutations, each failing by name: the read never consulting the bucket, writes always going
+account-wide, the option ignored, the bucket not keyed by character, and reset clearing only the
+override. The third of those initially aborted the run instead of failing by name — one assertion
+indexed `frames[FID]` without a nil guard, which is a caught regression that looks like a truncated
+harness.
+
+**Not covered, and worth knowing:** `CDS.SnapshotState` captures five leaves — `customLists`,
+`trackedAura`, `equipAssign`, `alerts`, `sounds`. Appearance has never been among them, so import/export
+does not carry icons-per-row on any setting of this box.
+
+### H.3.9. Layouts carry appearance, behind their own checkbox
+
+§H.3.8 closed by noting that `CDS.SnapshotState` captured five leaves and appearance was not among
+them, so a share string carried *what* you track but not *how it looks*. The owner asked for that,
+gated.
+
+**Capture is unconditional; applying is the gate.** A share string always carries appearance — a few
+hundred bytes — so the decision belongs to whoever *applies* it, not whoever exported it. The new
+"Layouts include appearance" checkbox is off by default: an imported layout that silently resizes and
+reorients four viewers is how "load a layout" stops being an action anyone trusts.
+
+**Captured RESOLVED, not as a table copy.** What the exporting character sees is the account-wide
+table with their own per-character overrides on top (§H.3.8), so copying either half alone would export
+something nobody is looking at. Resolving through `GetOpt` also keeps the per-character bucket out of
+share strings entirely — its keys are a character name and a realm, meaningless on another account.
+
+**Applied through `SetOpt`**, not by assigning the leaf: `SetOpt` is what knows whether this character
+writes to its own bucket or the shared one, and it re-applies each viewer as it goes.
+
+**Revert is not governed by the box.** `CDS.RestoreState(snap, opts)` takes `opts.appearance` —
+true forces, false forces skip, nil follows the setting — and Revert passes true. Its contract is to
+put things back exactly, and the box may have been ticked for the apply and unticked before the undo;
+a revert that left a resized viewer behind is a partial undo, which is no undo.
+
+**The codec needed no change**, because the serializer is generic over tables — a claim worth checking
+rather than assuming, so the round-trip test now asserts the appearance leaf survives it.
+
+Five mutations, each failing by name: capture removed, the box ignored in both directions, appearance
+never applied, Revert no longer forcing it, and the option defaulting on.
+
+### H.3.10. The settings move onto the frame, in edit mode
+
+Retail puts a system's settings **on the frame**: enter Edit Mode, select the Cooldown Manager, and
+orientation / icon limit / size / opacity are right there beside it. Ours were reachable only from the
+`/cdm` Settings tab — correct, complete, and a different window from the one showing the thing you want
+to change. The owner asked for the retail shape.
+
+**§B1's decision is not reopened.** Upstream's 6,441-line Edit Mode reimplementation stays unported;
+what changes is that DragonUI's editor now carries the *settings* as well as the *position*.
+
+**The seam is `NE.RegisterHUDFrame`'s new `editorSettings` field** (integration/Register.lua), not the
+module. Every DragonUI touch — the mouse handler, the edit-mode gate, `SelectEditorFrame`,
+`EditorMode:Hide` — lives there. The field is a **callback**, not a widget: what a module opens when its
+frame is clicked is the module's business, and the host glue's business is only deciding when it may
+(CONTRACTS §4).
+
+**Nothing in DragonUI is edited (CONTRACTS §0).** `CreateUIFrame` sets `OnMouseDown` (left-click
+select), `OnDragStart` and `OnDragStop`, and *none* of `OnMouseUp` / `OnEnter` / `OnLeave` — so ours are
+plain `SetScript` calls on a frame we asked the factory to build, overwriting nothing. Out of edit mode
+the anchor is `EnableMouse(false)`, so the settings are unreachable without the gate even existing.
+
+**Either button opens it**, because retail opens a system's dialog on *selection* and left-click is what
+selects. **Our handler also selects the frame**, which matters most on right-click: `CreateUIFrame` only
+selects on `LeftButton`, so without it the editor's coordinate readout and Reset button would describe
+whatever was clicked last while the dialog edits something else.
+
+#### The first pass was a context menu, and that was the wrong shape
+
+It shipped as a right-click menu: every setting a submenu of radios. It worked. Then the owner showed
+what NewEra's own 1.15 edit mode does — a small **dialog** beside the selected frame with every slider
+and dropdown visible at once — and the menu's two compromises stopped being defensible:
+
+* **A menu shows one setting at a time**, and hides the value you are trying to match while you pick it.
+* **A numeric setting inside a menu has to become a list of discrete rows.** That is why the menu shipped
+ opacity in steps of 5 where the tab steps 1, and why it needed a tooltip explaining that a value could
+ legitimately tick *nothing at all*. A dialog takes a real slider and both compromises simply go away.
+
+So `EditorMenu.lua` was replaced by `EditorPanel.lua`. The seam survived the swap unchanged, which is
+the argument for having made it a callback rather than a menu generator.
+
+#### What the dialog is
+
+**The /cdm control kit, not a second set of widgets** (SettingsControls.lua). One new control was needed:
+`AddCompactSlider`, a one-line slider with nudge arrows. The tab's tall two-line slider is right for a
+page you scroll and wrong for a dialog that has to sit on screen next to the thing it edits without
+burying it. **The arrows are not decoration** — a drag reports continuous values on this client
+(`SetObeyStepOnDrag` is retail-only), so they are the only precise way to land on an exact value.
+
+**`FULLSCREEN_DIALOG` strata.** `CreateUIFrame` puts the editor handles at `FULLSCREEN`; a settings
+dialog that renders behind the frame it configures is not a settings dialog.
+
+**One dialog, not four.** Selecting another viewer swaps the page and retitles. Pages are built lazily
+and kept, so switching costs nothing after the first visit.
+
+**Re-read on open.** Something else may have moved these underneath it — the `/cdm` tab, a layout apply,
+a reset — and a dialog that opens on a stale value is indistinguishable from one whose settings did not
+take.
+
+**Revert vs Reset, deliberately not the same button.** *Revert Changes* goes back to how the viewer was
+when the editor was opened; *Reset to Default* goes to defaults. Conflating them is how someone loses a
+setup they spent ten minutes on. The snapshot is taken on first open per editor session and dropped when
+the editor closes — keeping it would silently arm the button with an hour-old state. Revert is disabled
+until there is something to revert, because a button that is always live and usually does nothing teaches
+you to ignore it, and this one is the undo.
+
+**Leaving edit mode takes the dialog with it**, via the viewer spec's `onHide`. `HideAllEditableFrames`
+calls that for every registered frame, so it fires whichever viewer was selected.
+
+**Two editors for one value**, which SettingsOptions.lua's header forbids. The rule is about *staleness*,
+so both halves are closed rather than the rule waived: the dialog re-reads on open, and every write it
+makes calls `CDS.RefreshSettingsPage`, which no-ops when that tab was never built. The test asserts the
+tab's slider actually moves.
+
+**Not carried:** `Scale`, the first row in NewEra's own dialog. There is no scale value in this port's
+store — `iconSize` scales the icons and nothing scales the frame — so adding it is a new stored setting
+(defaults, per-character, layout capture) plus a fix to the editor anchor, which sizes itself from the
+content's *unscaled* `GetWidth`. That is a feature, not a look change, and is left for the owner to call.
+
+Fourteen mutations, each failing by name and none aborting the run: the opener never attached, the
+edit-mode gate dropped, the tab never notified, no re-read on open, Revert going to defaults instead of
+the snapshot, snapshots surviving the session, Revert always armed, the arrows not stepping, the arrows
+not clamping, `onHide` not wired, `Hide When Inactive` offered everywhere, the title never updated, pages
+never hidden on switch, and the strata left below the handles.
+
+#### The art, second pass
+
+The first render was the right dialog wearing the wrong clothes: `PC.Apply` is PANEL chrome, so a
+small floating dialog arrived with the rock fill and — despite `noPortrait` — the portrait ring, which
+lives in the `PortraitFrameTemplate` nineslice, not in the portrait texture that flag suppresses. It
+read as a window that had lost its contents.
+
+The reference is `ReferenceAddons/NewEra/EditMode/SettingsPopup.lua`, itself a transcription of
+retail's `EditModeSystemSettingsDialog`. Taken from it:
+
+* **Chrome** — `DialogBorderTranslucentTemplate`: black at 0.8 inset 7, under the **DiamondMetal
+ "Dialog"** nineslice. That layout is now registered in `core/NineSliceLayouts.lua` (verbatim from
+ NewEra, including its `D = 32`, since the corner square and the edge thickness must match or they do
+ not line up), and its two sheets — 3056750 frame, 3056755 side edges — ship in `Textures/Common`.
+ Core art, not the module's: anything dialog-shaped needs it, and the Metal family is panel chrome.
+* **Metrics** — 343×32 rows spaced 2, a 100px label column, 200px controls, 20px content inset,
+ `GameFontHighlightMedium` labels, a 32px checkbox, `GameFontHighlightLarge` title at `TOP -15`.
+* **Buttons region** — Revert at 180 wide, the `UI-FriendsFrame-OnlineDivider` strip, then full-width
+ Reset. "Cooldown Manager Settings" moved OUT of that region and onto the end of the options stack,
+ which is where retail's `AddExtraButtons` puts a system's own extra actions.
+* **Dropdown** — retail's `WowStyle1Dropdown` paints a STATIC body and puts the gradient on the arrow.
+ 3.3.5a has no such template, so the trigger is assembled from four pieces off sheet 5390329
+ (`common-dropdown-textholder` plus the `a-button` states) on a bare Button — fighting
+ `UIPanelButtonTemplate`'s own art for a look this different is more code than drawing it.
+
+**A THEME, NOT A SECOND KIT.** All of that lands in `SettingsControls.lua` as a table passed to
+`Kit.New`, with every default equal to what the `/cdm` tab already had. Same widgets, two sets of
+metrics; the tab is a scrolling page of sections and this is a fixed panel, and asserting the tab kept
+its own is part of the suite.
+
+**`Scale` is still not carried** — see above; unchanged by this pass.
+
+**Three assertions in the first art pass were vacuous, and the mutations found all three.** Checking
+that the `Dialog` layout table names a DiamondMetal atlas passed just as happily with the dialog
+applying `PortraitFrameTemplate`, and with the BLPs never copied: `NE.tex.SetAtlas` reports a miss and
+leaves the texture blank in both cases. Reading the file path off the piece the dialog actually wears
+answers all three questions at once. The harness now loads `core/NineSlice.lua`,
+`core/NineSliceLayouts.lua` and `Textures/Assets.lua` for the same reason — without them it could not
+tell a registered atlas from one whose art nobody shipped, and PanelChrome takes its graceful-degrade
+path silently either way.
+
+Ten further mutations, each failing by name: the panel chrome layout swapped back in, the nineslice
+never applied, each of the three sheets unshipped (frame, edges, dropdown — three separate mutations,
+since they fail independently), the theme ignored, the modern trigger never built, the theme leaking
+into the tab, the hover state never swapping the arrow, and the bare button left without `SetText`.
+
+#### The red 3-slice button, and the bug it was hiding
+
+`core/ButtonSkin.lua` has reskinned buttons to retail's `BigRedThreeSliceButton` since Sprint 0 — off
+the `128-RedButton` sheet, which this port **never shipped**. So every call fail-safed to native art
+and returned `false` to say so, and nothing read the return: the LFG role buttons and this dialog's
+Revert/Reset were all silently unskinned. Sheet 1536801 now ships, with its ten rects (three states ×
+three pieces, plus the highlight strip).
+
+**Shipping it turned on a code path that had never once run, and it was broken.** `updateScale` is a
+port of retail's `ThreeSliceButtonMixin:UpdateScale`, which does `self.Left:SetScale(scale)` —
+**Texture has no `SetScale` on 3.3.5a.** ClassicAPI has to synthesise even `GetEffectiveScale` for a
+Region by delegating to the parent (`Util/WidgetAPI.lua:205`) and adds no `SetScale` at all. Every
+call site wraps `Skin` in `pcall`, so in game this would have thrown *after* the three textures were
+created and the native art hidden — a button wearing nothing at all, with no error in the log. The
+caps are now sized in real pixels instead, and the trim branch no longer divides back out by a scale
+that was never applied.
+
+Two harness gaps fell out of chasing it, both now closed: `Enable`/`Disable` did not fire their
+scripts (so a skin that repaints on `OnEnable`/`OnDisable` was never asked to, and a disabled button
+would have tested as wearing the normal art), and textures had `SetHorizTile` with no getter (so
+"does the centre tile?" could only be written with a nil escape hatch that passed either way — it
+did, until the getter existed).
+
+Six more mutations, each failing by name: the sheet unshipped, the left cap's rect dropped, the
+footer never skinned, the pressed state never applied, `SetScale` put back, and the centre stopping
+tiling.
+
+**One button was missed on that pass:** "Cooldown Manager Settings" is built by the control kit
+(`Column:AddButton`), not by the dialog, so the dialog's own `NE.button.Skin` call never reached it —
+and it sat directly above two buttons that *were* skinned, which is far more obviously wrong than
+three unskinned buttons would have been. The skin is now a theme flag (`buttonArt`, off by default,
+like the rest of the theme) so the `/cdm` tab's buttons keep their look. Three mutations: the kit
+never skinning, the dialog not asking, and the flag defaulting on so it leaks into the tab.
+
+#### H.3.11. Three corrections from the owner
+
+**The buttons pair up, and the destructive one asks.** Revert and Reset were stacked with a divider
+between them, which is retail's shape but costs 46px of a dialog whose whole job is to sit beside the
+frame it edits without burying it — and stacked they read as a list of two things to work down rather
+than the pair of alternatives they are. They are now one row, `(343 - 3) / 2` each, one anchored to
+each side, under the divider.
+
+That puts them three pixels apart, which is why Reset now confirms. The asymmetry between the two is
+easy to miss: Revert is *bounded* — it undoes this editor session and greys itself out when there is
+nothing to undo — while Reset throws away every layout choice ever made for that viewer and nothing in
+the addon can put it back. `StaticPopupDialogs["NE_CDM_EDITOR_RESET"]` rewrites its `text` per viewer
+at click time, so the confirm names what it is about to wipe; a generic "are you sure?" asked over four
+differently-configured viewers is the kind of prompt people learn to click through. It also bumps its
+own strata to `FULLSCREEN_DIALOG` on show — StaticPopups live at `DIALOG`, and this dialog sits above
+the editor's own handles, so left alone the confirm would open *behind* the button that raised it.
+
+**The dropdown body is a 3-slice.** `common-dropdown-textholder` is not a bar: decoded off sheet
+5390329 it is a **54×41 rounded rect with a ~12px bevelled corner and a soft outer glow**. Stretched
+to a 200×26 row — one texture, `SetAllPoints` — the corners smear into horizontal blobs and the bevel
+squashes to a blur. That is what "the dropdowns are formatted very weirdly" was looking at, and it was
+only visible because the art finally shipped. Columns 13..42 of the source are a flat vertical run, so
+an 18px cut lands well inside it on both sides: the two caps carry the corners with their width
+following the row height (`18 × h/41`), and only the middle stretches, invisibly. The arrow is a
+separate 27×27 square button in the same sheet and is now drawn square and inset, instead of
+`h-2` straddling the right cap's bevel.
+
+The same pass fixed the *alignment*, which was the other half of "weirdly": the dropdown was a fixed
+200 anchored at `labelW + 6`, so its left edge sat 4px right of the compact slider's nudge arrow and
+its right edge 37px short of the slider's value text. Down a stack of alternating rows, nothing lined
+up with anything. The control column is now defined once, by the slider, and the dropdown fills it.
+
+**The red 3-slice is the addon's standard button.** Not the Cooldown Manager's look — the addon's.
+Roughly sixty `CreateFrame(..., "UIPanelButtonTemplate")` calls across a dozen modules build the stock
+grey button, and the mechanism for this is deliberately *not* sixty edits:
+
+* `NE.buttonskin.SkinPanelButtons(root)` walks a frame tree and skins what it finds. The predicate
+ reads the **art**, because a running frame cannot be asked which template built it: on 3.3.5a
+ `UIPanelButtonTemplate`'s entire runtime identity is that it `SetNormalTexture`s
+ `Interface\Buttons\UI-Panel-Button-Up`. It is self-limiting — `Skin` clears that texture, so a
+ skinned button stops matching.
+* `NE.buttonskin.Watch(root)` keeps it true as the tree grows. A window is registered before its
+ contents exist and its panes are built on the tab that first needs them, so one sweep at
+ registration would miss most of the addon. Three passes cover it: now, next frame, and on show —
+ plus an `OnShow` hook on frames within two levels of the root, which is where panes live.
+* Twelve windows call `Watch` — auction house, bags, character, collections, encounter journal, guild,
+ LFG, professions, social, spellbook, talents, and the `/cdm` window.
+* **Opt-out** is `_nePlain` (already the codebase's marker for a plain button standing in for a tab —
+ those sit in a tab strip and would read as loose buttons in red) and `_neNoSkin` for anything else.
+* `DEFAULT_THEME.buttonArt` flips to **on**, so the `/cdm` Settings tab's buttons come along too. The
+ suite's old assertion that the tab was untouched now asserts the opposite, deliberately.
+
+This resolves the question left open when the sheet shipped: the LFG role buttons and everything else
+change to the red art, by the owner's call.
+
+**A harness gap, and a vacuous assertion, both found by mutation.** `SetNormalTexture` was a no-op in
+the stub, which would have made every assertion about the sweep pass by describing an empty tree — it
+now writes to the memoized state texture, and `CreateFrame` models the template's normal texture. And
+the predicate test only had a button with *no* normal texture, so loosening it to `return true` still
+passed; a button carrying art of its own (an item slot, an icon button — the case where a false match
+costs something, since `hideNativeArt` would blank it) is now in the test. The dropdown slicing had
+the same shape of hole: all three pieces resolve to the same file and the cap widths come from a Lua
+constant, so nothing read the atlas at all. The texcoords are asserted contiguous and left-to-right.
+
+Thirteen mutations, each failing by name: `buttonArt` defaulting off, the dropdown back to a fixed 200,
+the caps collapsed, the caps unscaled, the atlas rect covering the whole textholder, all three pieces
+using the left cap, Reset firing without confirming, the confirm text left generic, Reset back on its
+own row, the sweep ignoring `_nePlain`, the sweep not recursing, `Watch` never hooking a pane's
+`OnShow`, and the predicate matching any button.
+
+#### H.3.12. Three more from the same pass
+
+**The confirm opened behind the edit-mode buttons.** Edit mode stacks three things over `DIALOG`,
+where StaticPopups live: the editor handles at `FULLSCREEN` (`addon.CreateUIFrame`), this dialog at
+`FULLSCREEN_DIALOG` to clear them, and DragonUI's own Exit Edit Mode / Reset All Positions panel at
+**`TOOLTIP` frame level 200** (`DragonUI/core/api.lua:971`). `TOOLTIP` is the top strata, so clearing
+that last one is a *level* question, not a strata one — the confirm goes to `TOOLTIP`/300 on show and
+puts the shared popup frame back on hide, since the next dialog to use that slot is not ours.
+
+(The "Error Messages" title above the confirm in the report is not ours either: it is DragonUI's own
+editor handle for `UIErrorsFrame`, which parks at screen centre-top under where the popup lands.)
+
+**The dialog slid sideways while a slider was being dragged.** `place` anchored it *to* the viewer —
+`SetPoint("TOPLEFT", anchor, "TOPRIGHT", ...)` — so Icon Size or Icon Limit resized the frame and the
+dialog moved out from under the cursor mid-drag. It now resolves the position into `UIParent`
+coordinates once, on open, and holds still until another frame is selected. The trade is that a viewer
+which grows a lot can end up overlapping its own dialog; a dialog that holds still is worth more.
+
+**Menus close on a click outside.** They had two ways out — pick a row, or Escape — and nothing in the
+backend supplies a third: ClassicAPI's `C_DropDownList` frames carry only the mouse-off timer that
+applies to menus opened in `MENU` mode from a hovered parent, not to one opened by clicking a button.
+So `core/Menu.lua` grew a full-screen mouse catcher, armed by both `OpenContext` and `ToggleAnchored`,
+parked **strictly one frame level under** the open list so the menu keeps its own clicks. It disarms
+from the list's `OnHide`, which covers every other way a menu closes (a row picked, Escape, a
+`CloseAll` from elsewhere) — a catcher left up would silently eat the next click anywhere on screen.
+The click is consumed, which is what Blizzard's dropdowns do.
+
+**The level clamp was wrong, and the test caught it before the game did.** The first version clamped
+the *catcher* up (`math.max(1, lvl - 1)`), which ties with a list at level 1 — and at equal levels the
+click resolves by creation order, so the catcher would sometimes have swallowed the row the player was
+aiming at. It clamps the *list* up instead.
+
+Three harness gaps closed alongside, each of which would have made an assertion vacuous: the dropdown
+stub's `Toggle` never showed or hid anything (so "is a menu open?" was unanswerable, and a catcher
+armed over an empty screen tested identically to one that was not); frames had no `GetLeft`/`GetTop`,
+so a placement resolving screen coordinates could only ever take its "I cannot measure this" fallback;
+and `UIParent` was 0×0, so a branch on which half of the screen a frame sits in always went the same
+way — which is exactly why the first mutation of the new placement came back green.
+
+Eight mutations, each failing by name: the popup left at `FULLSCREEN_DIALOG`, the popup never
+restoring the shared frame, each placement branch pinned back to the frame, the catcher never armed,
+armed with nothing open, never disarmed on close, and sitting on top of the list instead of under it.
+
+#### H.3.13. The confirm again, and the tab loses its per-viewer half
+
+**The strata fix above was aimed at the wrong frame.** `DragonUI_EditorPanel` — the coordinate
+readout, `TOOLTIP` level 200, `core/api.lua:971` — is not what covers the confirm. **Exit Edit Mode**
+and **Reset All Positions** are separate frames on `UIParent` at `TOOLTIP` level **1000**
+(`modules/editor_mode.lua:190, 210`), so 300 cleared something nobody was complaining about and the
+popup was still unreadable. It now READS the level off those frames and adds 10, because a constant
+picked to beat 1000 is a constant that stops working the next time DragonUI moves theirs, and nothing
+would say so. The test models `DragonUIExitEditorButton` for exactly this reason: without it, "the
+confirm is above the edit-mode buttons" is a comparison against nothing.
+
+**The per-viewer settings leave the `/cdm` Settings tab (owner steer).** They had been in two places
+since the dialog shipped — thirteen controls rendered twice, held together by a refresh contract that
+this file's own header calls out as the thing not to do. The dialog is the better half: you are
+looking at the frame while you change its layout, rather than at a list of numbers in a window that
+covers it. So the tab's four viewer sections are gone, and with them `buildViewerSection`, the
+`ORIENTATION` / `DIRECTION` / `VISIBILITY` / `BAR_CONTENT` lists, and `EditorPanel`'s `notifyPanel` —
+which was not merely dead but ran on every tick of a slider drag.
+
+What replaces them is one **Viewer layout** section of four buttons. Each opens edit mode with that
+viewer selected *and its dialog already up*, through the same seam a click on the handle uses — since
+this is now the only route from the tab to those settings, landing the player in edit mode to go
+hunting for the right handle would be a worse tab than the one it replaced.
+
+The tab keeps everything that was never per-viewer: buffed-spell glow, icon fit, talent specs, buff
+tracking, resets.
+
+**Six tests moved rather than being deleted, because the behaviour did not go away.** Step snapping,
+write-once-per-step, checkbox row-versus-box and dropdown ordering were all asserted against the tab's
+copies of these controls. They now sit in the dialog block, against `AddCompactSlider` — a genuinely
+different commit path from `AddSlider` — while the tab's own widget kinds keep their coverage through
+the controls it still has (`Icon inset` for the tall slider, `Glow while buffed` for the checkbox,
+`Show them as` for the plain dropdown). The theme-isolation assertions moved the same way, and the
+tab's absence of per-viewer settings is asserted **by control name over every viewer**, not by the
+sections being gone — the latter would still pass with the controls re-homed under another header.
+
+Four mutations, each failing by name: the popup level hardcoded again, the popup clearing only the
+coordinate panel, the viewer link stopping at edit mode, and a per-viewer section reappearing on the
+tab.
+
+#### H.3.14. The confirm stops being a StaticPopup
+
+Three attempts to out-stack DragonUI's Exit Edit Mode and Reset All Positions buttons, and the third
+one was to stop trying. In order: `FULLSCREEN_DIALOG`/300 (aimed at the coordinate panel, the wrong
+frame); `TOOLTIP`/300 (right strata, still short of their 1000); reading their level at show time
+(did not work in game either, and from this side there was nothing to see — a `StaticPopup` frame is
+shared, re-parented and repositioned by code we do not own, and every hook we had went through it).
+
+**So the confirm is our own frame, parented to the dialog.** That settles the stacking by construction
+rather than by arithmetic — a child draws inside its parent's stacking, above it, with no number to
+get wrong — and it settles the *position* too, which was the other half of the problem: those buttons
+park at screen centre, and a `StaticPopup` lands in exactly that spot. A confirm inside the dialog
+appears beside the viewer being edited, where nothing else is competing for the space.
+
+It is **modal to the dialog**: a blocker fills the panel behind it, dimmed, eating clicks. The question
+is about the very settings underneath it, and nudging a slider behind an unanswered "are you sure?"
+means answering it about a different state than the one that was read. It wears the same DiamondMetal
+chrome and red buttons as the dialog, and it is dropped whenever the dialog switches viewers — it names
+one viewer and acts on whichever is current, so those must not be allowed to differ.
+
+One thing did not survive first contact: the message came out as "…orientation and vis...".
+The FontString was sized by anchoring TOPLEFT and TOPRIGHT, which does give it a width — but on 3.3.5a
+that width TRUNCATES with an ellipsis rather than wrapping. An explicit `SetWidth` wraps, which is why
+`SettingsControls`' own `AddText` has always set one. The height then follows from `GetStringHeight`,
+so the frame grows around whatever the text turns out to be.
+
+Nine mutations, each failing by name: the confirm parented to `UIParent`, its level not raised above
+the dialog, the blocker never shown, the confirm placed under its own blocker, Reset firing without
+asking, the confirm surviving a viewer switch, No doing what Yes does, the two-anchor sizing put back,
+and the height returned to a constant. A tenth — raising the blocker — turned out not to be a
+regression at all: the confirm's level is derived from the blocker's, so they move together, which is
+the property that makes the ordering structural rather than a pair of constants to keep in step.
+
+#### H.3.15. The sliders stop being 2004, and Revert learns to explain itself
+
+Two reports in one pass. **"The revert button on the CDM window does nothing. What is it currently
+designed to do?"** — and the answer is that it was doing exactly what it was built to do, which turned
+out to be the problem. That Revert (the /cdm footer, next to the layout picker) is a ONE-STEP session
+undo for *layout* operations only: apply, import, starter reset. It is disabled the rest of the time,
+and cleared when the window closes so a stale undo from an hour ago can never fire. So for a player who
+has never touched layouts it is permanently grey — and, because **a disabled Button eats OnEnter on
+3.3.5a**, permanently mute. The tooltip that would have answered "what would this even undo?" only
+existed in the one state nobody needed it. `SetMotionScriptsWhileDisabled(true)` gives it back, and
+the tooltip now adds a reason line while it is unavailable, naming what it covers (layouts) and what it
+does not (the per-viewer settings, which revert from the edit-mode panel instead). The button's
+*behaviour* is unchanged; it was never broken, it was illegible.
+
+**"Can we get the horizontal scrollbars in the new settings matching the earlier NewEra screenshots?"**
+— the sliders. NewEra builds every edit-mode slider from retail's `MinimalSliderWithSteppersTemplate`
+(`EditMode/SettingsPopup.lua:173`), whose art is sheet 4567914, the `minimal_sliderbar_*` family.
+
+**This shipped twice, and the second version is the one to read.** The sheet was not in the NewEra Art
+set, so the first build substituted: the minimal *scrollbar* art IS shipped, and it is the same rounded
+bar at right angles, so the horizontal slider borrowed the vertical one's pieces and turned them 90°
+via a new `NE.tex.SetAtlasRotated` — the **eight-argument** `SetTexCoord`, which names all four display
+corners independently and is the only way to rotate art on 3.3.5a (`Texture:SetRotation` is retail).
+Then the owner supplied 4567914, and the whole workaround came out: rotation helper deleted with it,
+since a primitive whose only caller has gone is dead code. *For future missing art, ask — the owner
+supplies it.*
+
+The sheet is 32×128 and carries the entire widget: both rounded track caps (11×17), the **one-pixel**
+run that tiles between them, the little **diamond** thumb (20×19), and both chevron steppers (11×19 and
+9×18). `NE.scrollbar.SkinSlider` rebuilds it piece by piece at those native sizes. Only the run
+stretches; the caps keep their own width, which is the rule the dropdown's textholder learned the hard
+way when stretching a rounded rect smeared its corners. The Slider is sized to the **knob**, not the
+track, so the diamond stands two pixels proud of the bar and the whole of it falls inside the frame's
+own mouse region.
+
+`OptionsSliderTemplate`'s groove goes whole — it is a *backdrop* of `UI-SliderBar-Background` inside a
+beveled border, not a region that could be re-pointed. The compact slider's nudge arrows became the
+sheet's own chevrons: they were the spellbook's page-turn glyphs, which is the mismatch the whole pass
+is about. The button stays 18px, because the row's arithmetic is written against `ARROW_W`; only the
+glyph takes the atlas's size.
+
+**No hover or pressed art, deliberately.** The sheet has one diamond and one chevron per direction,
+because retail's minimal slider does not change art on either — the feedback is the thumb moving. The
+first build *did* swap three states per piece, but only because the scrollbar art it was borrowing has
+them; carrying that over would have been this addon's idea wearing the reference's clothes. The
+steppers keep a one-pixel pressed offset, which is the only feedback this art affords, and a test
+asserts the sheet has no `-over` variant so nobody re-adds a tint and calls it fidelity.
+
+Both slider kinds are skinned, the tall two-line one on the tab as well as the dialog's compact one:
+the skin belongs to the kit, not to the dialog that asked for it first.
+
+Assertions read the rendered **texcoords**, never the atlas name: all six pieces come off one file, so
+the texture path is identical whichever rect a piece ends up wearing, and "did the left cap get the
+left cap's rect" is a question only the coordinates can answer. Seven mutations fail by name — the
+track caps swapped, the thumb wearing a cap instead of the diamond, the groove left in place, the caps
+stretching with the bar, the steppers swapped, the pressed glyph not nudged, and the Slider sized to
+the track instead of the knob. That last one took two attempts: the first assertion read the *thumb's*
+height against a constant, which cannot see a change to the *frame's*, so it passed the mutation
+unmoved. Rewritten to compare the two heights directly.
+
+One mutation from the first build was **not** caught, and was right not to be: `applyState` re-asserted
+the caps' drawn size in case `useAtlasSize` snapped them back to native — but those caps were 8×8
+*squares*, so rotation returned exactly the size they already had, and the only caller that could have
+made the guard matter was a `thickness` option nobody passed. Guard and option deleted before the art
+arrived and made the whole path moot.
+
+#### H.3.16. Submenus stop demanding you aim at the arrow
+
+> "The effects dropdown menu has issues where i have to specifically mouse over the arrow which makes
+> navigating annoying"
+
+The FX Style submenu on the alert menu — and every other submenu we build, the ready-sound tree
+included. Hovering the row did nothing; only the 16×16 `$parentExpandArrow` at the right edge opened
+it, and a diagonal approach to it across the row shut whatever had just been opened.
+
+**Same 3.3.5a quirk as §H.3.15's Revert button, wearing a different hat: a disabled Button eats
+OnEnter.** Submenu parents are `notClickable` — deliberately, and §H.3.15's reason still holds, because
+UIDropDownMenu's OnClick ticks the row's Check texture before it ever looks at `func`. But
+`C_UIDropDownMenu.lua:172` turns `notClickable` into `info.disabled`, and disabling costs the row two
+things at once:
+
+1. its own `OnEnter` — which for a `hasArrow` row is the thing that calls `C_ToggleDropDownMenu` on
+ level+1. Silenced.
+2. `$parentInvisibleButton` goes **up** (line 179). It covers the row, and its `OnEnter` calls
+ `C_CloseDropDownMenus(level + 1)`. So the row body did not merely fail to open the submenu — it
+ actively closed one.
+
+Between them, the arrow was the only door and the row was a wall in front of it.
+
+`NE.menu` cannot fix this in ClassicAPI — read-only, §0 — so `openOnHover` does it per row, after
+`AddButton` has written it: `SetMotionScriptsWhileDisabled(true)` and take the invisible button down.
+The tooltip that button existed to serve is on the row's own `OnEnter` too (`C_UIDropDownMenu.xml:157`),
+so nothing is lost, and `AddButton` re-shows it on the next build regardless.
+
+**It is set on every row, `false` included.** These list buttons are shared with every
+`C_UIDropDownMenu` in the game; one left motion-enabled is a disabled title in some later menu that
+hover-highlights for no reason. Handing it back on non-submenu rows is what keeps the change from
+leaking, and is its own assertion.
+
+The harness gained `$parentInvisibleButton` and the one rule that governs it — shown on any disabled
+row, and `notClickable`/`isTitle` both *become* disabled two lines before that check. Without it in the
+stub, taking the button down would have tested identically to forgetting to. Three mutations fail by
+name: the row never told to take OnEnter, the invisible button left up, and the motion flag set
+blanket-true.
+
+#### H.3.17. …and the submenu opens where the row is
+
+> "The submenu opens up right next to the main menu then when i move my mouse over to it, the frame
+> drops down to where it actually should be"
+
+Letting the row open its own submenu uncovered a second fault that had been sitting under the first,
+harmless only because nothing ever exercised it. ClassicAPI picks a level-2+ list's anchor like this
+(`C_UIDropDownMenu.lua:397`):
+
+```lua
+local anchorFrame = (strsub(button:GetParent():GetName(), 1, 12) == listFramePrefix)
+ and button or button:GetParent()
+```
+
+Blizzard's original compares against the **literal** `"DropDownList"` — exactly 12 characters.
+ClassicAPI renamed the prefix to `"C_DropDownList"`, which is 14, and left the 12 alone. The test can
+therefore never be true, and the anchor is always `button:GetParent()`.
+
+That was invisible while the arrow was the only way in, because an arrow's parent **is** its row —
+the right answer, reached by accident. A row's parent is the whole **list**, so the submenu hung off
+the top of the parent menu. Crossing the arrow then re-opened it from the arrow, and it dropped into
+place: exactly the two-step the owner described.
+
+The correction re-anchors to the **arrow**, not to the row that was hovered. The arrow is a 16×16
+child centred in a 16px row, so `TOPRIGHT` is the same point either way — but the arrow's own OnEnter
+re-opens the menu unless it finds itself as the anchor already (`C_UIDropDownMenu.xml:94`), and
+anchoring to the row would rebuild the whole submenu every time the mouse crossed the arrow on its way
+there. Same pixels, no rebuild under the cursor.
+
+The screen-bounds flip from `ToggleDropDownMenu`'s own tail is re-run here too, against the right
+anchor this time; a submenu that re-anchored correctly and then fell off the bottom of the screen would
+be no improvement.
+
+It hooks `OnEnter` on the row, which cannot be un-hooked, on a button shared with every menu in the
+game — so the hook asks whether `C_UIDROPDOWNMENU_OPEN_MENU` is ours before it moves anything, and
+that guard is its own assertion. The level is closed over rather than read back off the parent: a
+button belongs to exactly one list forever and its name already says which.
+
+918 assertions. Six mutations fail by name: no fix at all, anchored to the row, the ownership guard
+dropped, the wrong corner, the old points left in place, and the off-screen flip removed.
+
+#### H.3.18. The module ships disabled
+
+> "I want this module to be set to disabled by default."
+> "When disabled the cdm window shouldnt be loadable via settings or /cdm."
+> "When going to the window via the settings menu it should go to the Spells tab by default."
+
+Owner's decision, and the only NewEra module that ships off. The reasoning is worth recording because it
+does not generalise: everything else here **replaces** something the player already had — a nicer
+character sheet, a nicer guild window — so shipping it on ships an improvement. Four viewers in the
+middle of the screen replace nothing. They are new HUD furniture, sized and placed by us, over whatever
+the player has already arranged there, and deciding that for them on first login is the one thing a HUD
+addon should not do.
+
+`M.IsEnabled` fell through to `true`; it now falls through to `false`. That single flag is the whole
+gate, deliberately — nothing else keys off it, so a profile that switches it on later reads exactly the
+same curated lists, layouts and alerts as one that had it on all along. There is no seed to miss and no
+first-run path that only happens while it is on.
+
+Flipping the default turned four places that had been merely *quiet* while disabled into places that were
+**wrong**, because "disabled" had never been the state anyone actually shipped in:
+
+* **Edit mode offered four handles for it.** `NE.RegisterHUDFrame` defaults `editorVisible` to "always
+ offer it", which is right for a HUD frame that is only *empty* right now — that is what `showTest` is
+ for — and wrong for one the player has never turned on. Out of the box `/dui edit` would have opened
+ onto four green rectangles full of demo icons for a feature that has never been on screen. DragonUI
+ honours a false `editorVisible` by hiding the anchor and skipping `showTest` (`core/api.lua:1239`).
+* **The viewers kept working.** Frame events do not care whether a frame is shown, so every player who
+ never turned the Cooldown Manager on would still have paid for `SPELL_UPDATE_COOLDOWN`, `UNIT_AURA`,
+ `BAG_UPDATE_COOLDOWN` and the rest for the whole session, re-reading cooldowns to repaint tiles nobody
+ can see. Gated at both `OnEvent` entry points — the base's, and `auraViewerOnEvent`, which answers the
+ two hottest events on this client itself and never reaches the base. Safe to **drop** events rather
+ than queue them: switching on runs through `UpdateVisibility`, whose `Show` fires `OnShow`, and
+ `OnShow` is `Rebuild`, so the catch-up is a full re-read from scratch. Not gated on `IsShown()` — a
+ viewer hidden by its own category or by "In Combat" still has to track state or it comes back wrong.
+* **The ready sounds kept playing.** The alert ticker early-outs when nothing is assigned, which is not
+ the same question. A player who assigns sounds, switches the module off, and then keeps hearing it
+ announce cooldowns for viewers that are not on screen has a haunted UI. (A latent fault before this,
+ since the toggle already existed; off-by-default is what made it worth fixing.)
+* **The window was still reachable, and configured viewers it could not show.** The owner's answer, on
+ the second message: the window is part of the module, so an off Cooldown Manager has no window. Every
+ route in — `/cdm`, the options button, `OpenTo` — funnels through `ShowPanel`, so one gate covers it,
+ and it sits **before** `build()`: the panel is several hundred frames built lazily on first open, and
+ a player who never turns the module on should never pay for them. `M.SetEnabled` closes a window that
+ is already up, for the one case the player can reach — open `/cdm`, then untick the toggle. Closing is
+ never gated, so `/cdm` stays the way to shut it either way.
+
+That last one also settles a question off-by-default would otherwise leave open: **where** a player goes
+to switch it on. Not the window, which would be a dead end configuring viewers it cannot show — the
+switch is in DragonUI's options, where every other module's is, and the refusal message names it.
+
+And the options button now opens on **Spells**, not Settings. The first thing anyone wants after turning
+the module on is to see and change what it tracks; Settings is one click away on a tab already on screen.
+The link on the edit-mode dialog still opens on Settings — that one exists to reach the non-per-viewer
+settings by name, so it means it.
+
+**Existing profiles:** one that never touched the toggle stored nothing, so it lands on the new default
+and the viewers are hidden after this change. One tick brings back the exact setup that was there —
+disabling has never deleted anything — and the branch is unreleased, so no adoption migration was added.
+
+934 assertions, and the boot block asserts the *off* state before anything turns it on, since that is the
+state the rest of the file spends its time not being in. Nine mutations fail by name: the default back to
+`true` (10 failures), `editorVisible` dropped, either event gate dropped, the ticker gate dropped, the
+window gate always allowing, `OpenTo` gated only via `ShowPanel`, `SetEnabled` not closing the window,
+and the options button back on Settings. The aura-viewer gate is probed through `_lastShownCount` rather
+than the item count — a profile with nothing tracked yet has zero items either way, so asserting on the
+count would have passed against the missing gate.
+
+#### H.3.19. The rotation spells, and why no class had any
+
+> "Lots of classes are missing default abilities that dont have cooldowns. Such as druids with wrath,
+> which i have added myself. Can you do a pass over classes and make sure the default rotation spells
+> are in for eahc spec?"
+
+Not an oversight in the curation — **the generator could not express one.** `resolve.py` defines a
+"castable" id as the lowest-rank entry carrying a cooldown `> 1.5s`, which is a good test for telling
+an ability from its triggered sub-spells and a useless one for asking whether something is an ability.
+Wrath has no cooldown. Neither does Frostbolt, Shred, Steady Shot, Shadow Bolt or Sinister Strike. Every
+one of them resolved to nothing, and `gen_wotlk.py` treats an unresolvable name as a hard error, so no
+rotational spell could ever have reached the seed. The owner's four hand-added Druid ids were the only
+ones in the file, and the next `gen_wotlk.py` run would have deleted them — `verify.py` rejects a
+no-cooldown id outright.
+
+**A second resolver, not a relaxed one.** `resolved_any.json` drops the cooldown test and replaces it
+with the two rules that test was standing in for, both picked by reading candidate dumps
+(`explore_nocd.py`, new, prints each class's no-cooldown non-passive non-talent abilities — the menu the
+curation is authored from) rather than by argument:
+
+* **Prefer an explicit `Rank 1`; fall back to unranked only when nothing is ranked.** Rejuvenation
+ forces it — fifteen ranks plus an *unranked* sixteenth (`64801`, the glyph's), and sorting by rank
+ number puts that one first, because an unparseable rank string scores 0. It is the same trap
+ `gen_alertdata.py` documents for Overpower, in the other direction.
+* **Drop passives.** Without a cooldown filter, nothing else keeps talent passives out of a list of
+ things to press.
+
+Emitted as its own `ROTATION_ADD` table rather than more rows in `ESSENTIAL_ADD`, which is what lets
+`verify.py` keep asserting "carries a real cooldown" of the two tables where that holds. In the rotation
+table the assertion is inverted — an entry that *does* carry a cooldown is an error — and that is how
+Crusader Strike (4s) and Flame Shock (6s) got moved up into Essential where they belong.
+
+**The class attribution was wrong, and the cooldown filter had been hiding it.** `skill2class` was a
+bare `most_common(1)` over every skill line. Skill line 202 is **Engineering**: 321 rows, 287 with no
+class mask, and the 34 that have one split 10 PALADIN / 9 SHAMAN / 9 DRUID — class-restricted engineering
+items — so `most_common` broke the tie for PALADIN and handed a profession's inventory to paladins. Line
+777 is **Mounts**, category 7 like the real class lines, unanimously PALADIN on the strength of 4 rows
+out of 315. Invisible while every answer had to carry a cooldown; it surfaced the moment the rotation
+resolver stopped filtering, as PALADIN reporting **661** abilities against 60–100 for every other class.
+Fixed with category 7 + Mounts excluded by name + 90% dominance, and verified to change **not one id**
+in the existing curation. Per-row class masks instead of the vote was tried and is worse: it loses every
+talent-granted ability (Penance, Starfall, Dispersion, all of DEATHKNIGHT), whose rows carry mask 0.
+
+**Two collision guards**, because the seed appends to `ClassData.lua` and `appendAll` dedupes by **id**,
+which is not the same as deduping by ability. `ClassData` lists Multi-Shot as `14288`; the resolver
+answers `2643`. Both real, both survive an id dedupe, and the viewer shows one spell twice. So the
+comparison is by NAME with every vanilla id mapped back through `Spell.dbc` — same id is redundant and
+merely printed, a different id is a hard error. Nine rotation names were dropped that way.
+
+**And it found live bugs in `ClassData.lua`.** The guard's first version matched the two table headers
+on entry and nothing on exit, so it ran on into the buff tables and raised a false alarm — which was
+worth having. Auditing every id in the file against its own comment turned up 23 disagreements. Most are
+vanilla→WotLK renames where the id is right (Blessing → Hand of Protection/Freedom, Soulstone → Create
+Soulstone, Arcane Intellect (Greater) → Arcane Brilliance). Twelve are genuinely wrong ids, and all
+twelve sit in `BUFFICON_BY_CLASS` / `BUFFBAR_BY_CLASS`, which `CooldownViewer.lua:789` records as unused
+— the aura viewers auto-track instead. Left alone as dead data, listed in the session notes.
+
+The two that were **not** dead are Mage's, and between them they cost mages a core button: `12472` was
+listed in Essential as "Cold Snap" and is **Icy Veins**; `11958` was listed in Utility as "Ice Block" and
+is **Cold Snap**. Two wrong labels, and Ice Block absent from every list. Both entries are dropped from
+`ClassData.lua` and all three abilities are now curated by name in the generator, which resolves them
+from the DBC.
+
+Curation is per CLASS, not per spec, because that is all the tables can express — the runtime gate is
+"has the player learned it" plus the talent gate, and every druid has learned Wrath. So each list is the
+union across a class's specs, and the per-spec layouts (§F2) are where a player prunes it. A Feral druid
+finding Balance's nukes in the picker is the cost of a Balance druid finding them there at all.
+
+232 ids verified against the DBC. Two mutations fail by name: the rotation table never appended (4
+failures), and Wrath listed at its max-rank id instead of rank 1.
+
+#### H.3.20. Starter layouts per spec, and how to tell what spec you are
+
+> "Yes lets do starter layouts per spec. Although im not sure the besty way to decide what spec for
+> lower levels? Would it be just talent points spent?"
+
+Yes — and `GetTalentTabInfo(tab, false, false, group)` takes a **talent group**, which is what makes it
+usable for the inactive spec's bucket as well as the active one. Without that argument dual spec would
+detect the same tree twice.
+
+The honest limits, since the question was about low levels:
+
+* **Levels 1–9 there is no signal at all.** Zero points; a pick would be one in three.
+* **At 10+ the first point is already a declaration of intent.** Someone who spends point one in Balance
+ is levelling Balance. It is weak evidence about power and strong evidence about intent — and intent is
+ what a starter layout needs, so a single point is enough.
+* **An exact tie (5/5/0) has no right answer.**
+
+**The failure that actually matters is not guessing wrong, it is guessing repeatedly.** A layout that
+re-derives itself as points are spent would silently overwrite curation every few levels. So detection
+runs at the moment of application and nowhere else, which reduces the low-level problem to being right
+*once*, with the player present. Owner's calls on the two forks: apply on demand **and** once on a fresh
+spec bucket; and when the spec is unreadable, **do not guess** — offer the three trees by name.
+
+`M.DetectSpec(group)` therefore returns **nil** for no-points and for a tie, rather than tab 1. The menu
+marks the detected tree with "(your spec)" instead of pre-selecting it — detection is derived data and
+can be wrong, the same reasoning that keeps "Show Unlearned" on the aura picker.
+
+**Once per bucket is the entire safety argument for the auto path.** `SeedStarterIfFresh` marks the
+bucket in the same breath as seeding it, so it can never run twice over curation the player has since
+done. It is hung on `NE.spellbook.OnRebuilt` rather than `PLAYER_LOGIN` because that fires once the
+client has finished answering about talents — at login and after a spec swap — and it is idempotent, so
+firing often is free. A character with no points is skipped **without being marked**, so the first point
+they spend still seeds them a starter.
+
+**Disabled, not deleted.** The starter writes a custom list holding every spell in the class's Essential
+list, the spec's own on and the rest off, so off-spec spells land under Not Displayed one drag from
+returning. Deleting them would make the starter the one action in the window you cannot undo by hand.
+**Essential only** — Utility is defensives, interrupts and escapes any spec might press, and the bloat
+this answers is all in Essential.
+
+30 lists, authored by name in `gen_wotlk.py` and resolved against **the ids the curated list actually
+holds**, not against a fresh resolve: `ClassData` keeps some abilities at a higher rank (Multi-Shot at
+`14288`), and a starter carrying the rank-1 `2643` would enable a row the picker has never heard of and
+appear to do nothing. A starter name that is not in that class's Essential list is a hard error — and it
+is checked against Essential specifically, not Essential+Utility, or a name living only in Utility would
+pass and then enable nothing. Tab order is from `TalentTab.dbc`, not memory.
+
+964 assertions. Seven mutations fail by name: no-points falling back to tree 1 (5 failures), a tie
+resolving to the first tree, the group argument ignored, the starter deleting instead of disabling, the
+seed marker never set, the marker set even when the spec is unknown, and an invalid tab falling back to
+tab 1.
+
+#### H.3.21. Unlearned spells are hidden, not tinted
+
+> "I can see spells I havent learnt yet on my druid. For instance Mangle which is a talent i dont have
+> yet, it lists under not displayed. I want spells that are not known to be hidden entirely"
+
+One report, three faults. What hid all three for this long is that **no consumer ever hid an unlearned
+row — they only tinted it**, so a learn-gate that answered "learned" for nearly everything looked
+exactly like a learn-gate that worked.
+
+**1. The gate barely gated.** §E4 replaced an id-keyed check with the spellbook, correctly, but kept the
+old checks chained after it "so the change can only widen what shows, never narrow it". The last link is
+`GetSpellInfo(name)`, which on this client answers from the spell **database** — §E4's own bullet list
+says so two lines earlier. A chain whose last link says yes to every ability in the game is not a gate.
+The fallbacks are now reached only when `SB.EnsureBuilt()` reports no book at all; with a book, the book
+is the answer. The condition is deliberately "built", not "non-empty": an empty book is a real answer for
+a character with no spells, whereas *not built yet* is the absence of one, and there the gate still fails
+**open** — showing an ability the player lacks beats hiding one they have because a scan had not run.
+
+**2. Not Displayed ignored the gate entirely**, by an explicit decision recorded in the code: a fresh
+character seeing an empty picker reads as broken. True, but the price was worse than the disease.
+`Mangle (Bear)` sat in a section whose name means *you chose not to show this*, one drag from a row that
+could never light up, and the red tint's "not yet learned" contradicted the heading rather than
+qualifying it. The catalog now honours **Show Unlearned** like every other spell section — the setting
+already existed and already meant this; it simply did not reach here. The empty-picker worry is answered
+where it belongs, in the section's empty text, which names the setting instead of saying "(empty)".
+
+**3. `IsTrackable` waved the whole arsenal through.** Its escape hatch — *an id outside the curated set
+is a user-added external, so always trackable* — was written when the curated lists were the only class
+data. `M.ARSENAL_BY_CLASS` is 296 more ids that are nothing but class abilities, and `Mangle (Bear)` is
+one of them (the seed curates the Cat form). So the gate was never consulted for most of the picker, and
+the moment a player dragged an arsenal-only ability into a viewer it rendered whether they could cast it
+or not. The arsenal now counts as class data for the gate. The hatch still exists, and still has to —
+delete it and every user-added trinket use-spell vanishes, which is its own mutation below.
+
+The picker surfaces ask `M.IsSpellLearned` (new, the raw test) rather than `M.IsTrackable`: on a picker
+row every entry is a class ability by construction, so "not in the class tables" means nothing there.
+Gating the catalog on `IsTrackable` would have hidden Mangle (Cat), which the seed curates, and kept
+Mangle (Bear), which it does not — the exact inversion of the report.
+
+Regenerating `CdmArsenal.lua` for the new footer also collected the §H.3.19 attribution fix, which had
+never been applied to this table: seven ids leave, all of them the bug that fix describes — `Stuck` and
+`Summon Friend` under DRUID, `Wormhole: Gadgetzan` under PALADIN, and four racials filed under ROGUE and
+WARRIOR. Its header had also drifted, still saying "Hidden" where the file on disk had been hand-edited
+to the §H.3 rename; the generator now carries the current wording so the next regen keeps it.
+
+984 assertions. Ten mutations fail by name: the database fallback restored unconditionally (2 failures),
+the gate failing closed with no book, the arsenal dropped from the class-ability set (2), the external
+hatch deleted, the catalog skipping the learn gate (2), the catalog ignoring Show Unlearned (3), the
+empty text back to "(empty)", and the tile tint stuck on, stuck off, or flagged without desaturating.
+Two mutations are **equivalent, not uncaught**, and worth naming so nobody re-tests them: gating the
+catalog on `IsTrackable` now behaves identically, because widening the class-ability set to include the
+arsenal made the two agree for every id the catalog can hold; and dropping `CdmArsenal.lua`'s
+`InvalidateCuratedCache()` footer changes nothing in a harness that loads every file before the first
+assertion, which is the one ordering it exists to protect against.
+
+#### H.3.22. Not Displayed is the catalog minus what is on screen
+
+Reported as "Gift of the Naaru is showing twice — under Utility and Not Displayed". It was every racial,
+for every race that has one, on every class: Blood Fury for Orcs, Berserking for Trolls, Stoneform for
+Dwarves, Cannibalize and Will of the Forsaken for the Forsaken, and so on. Only Human was clean, and only
+because its racial lists in `ClassData.lua` are deliberately empty.
+
+The catalog is defined as *the arsenal minus what is placed*, and `SettingsAdapter.lua` computed the
+subtrahend with its own `isPlaced` — a second implementation of the placement rules that read the custom
+list and fell back to the curated defaults. `GetActiveSpellList` answers the same question, and it does
+one more thing: `appendRacials` merges the player's racials into both lists at render time, because
+racials are class-agnostic and were never in the editable seed. The copy had no idea that merge existed,
+so a racial was placed and unplaced simultaneously — rendered by the viewer, and offered again by the
+catalog as something the player had chosen not to display.
+
+`isPlaced` is replaced by `placedSet`, which asks `GetActiveSpellList` for each display category and
+unions the results. Two properties follow that the old shape could not have:
+
+* **Duplication becomes impossible by construction.** `GetItems` renders `GetActiveSpellList(cat, showAll)`,
+ and `placedSet` subtracts `GetActiveSpellList(cat, true)`. The `includeUnlearned` list is a strict
+ superset of the gated one — the only difference is the `IsTrackable` filter — so anything on screen is
+ necessarily subtracted. Any *future* merge into a display list is inherited rather than re-missed.
+* **The round trip survives.** Suppressing racials in the catalog would have fixed the duplicate and made
+ a removed racial unrecoverable. Instead, removing one writes it disabled into the custom list, which is
+ exactly what `appendRacials`' `exclude` set reads, so it leaves the viewer and appears in the catalog —
+ once — ready to drag back.
+
+`includeUnlearned` is `true` deliberately: this asks about PLACEMENT, not castability. Gating it would
+call an unlearned-but-placed spell unplaced and hand it back to the catalog, restoring the double listing
+for a second, unrelated reason. `M.GetActiveSpellList` gained an optional third `class` argument for this
+caller, which carries a class down from `Adapter.GetItems`; every viewer still gets the player's.
+
+The suite could not have caught this. The pre-existing overlap check runs as a Human — the one race with
+nothing to duplicate. The new §H.3.22 block switches race, asserts the merge/subtract/round-trip cycle,
+and then asserts the invariant itself across both spell categories with Show Unlearned each way. 992
+assertions. Two mutations fail by name: `placedSet` re-deriving placement instead of asking (the original
+bug, reproduced exactly), and placement gated on learnedness.
+
+#### H.3.23. One tile per ability, not per spell ID
+
+Reported as "two versions of Icy Touch, the correct one and another with just the name and icon", after
+§H.3.22 had removed the id-level duplicates. This is the other half: an **ability is not an id**.
+`Spell.dbc` holds a row per rank plus variants that never reach a spellbook, so one ability can enter a
+list under two ids and render as two identical tiles — the tile draws `GetSpellInfo`'s name and icon, and
+by construction those match.
+
+Five abilities are in that state, because the arsenal is generated and the curated tables are authored
+and for these five they picked different rows for the same thing:
+
+| Class | Ability | arsenal | curated |
+|---|---|---|---|
+| DEATHKNIGHT | Icy Touch | 52372 | 45477 (essential) |
+| HUNTER | Multi-Shot | 2643 (R1) | 14288 (R2) |
+| PALADIN | Hammer of Justice | 853 (R1) | 10308 (R4) |
+| PALADIN | Judgement of Wisdom | 53408 | 20186 (R1) |
+| ROGUE | Sprint | 2983 (R1) | 11305 (R3) |
+
+**The data is deliberately left alone.** Every runtime read resolves through `highestKnownRankID`
+(ItemMixins), which maps a spellID to the player's highest KNOWN rank BY NAME — so any id for an ability
+lands on the same cooldown, icon and tooltip, and none of these five misbehaves in a viewer. Rewriting
+curated ids would churn saved layouts for no functional gain. Two are worth recording anyway, as data
+notes rather than bugs to fix here: `52372` is the untrained Icy Touch variant (`classmask=0`) against
+curated `45477` (`classmask=0x20`, the trained DK rank 1); and curated `20186` is the *vanilla* Judgement
+of Wisdom (Rank 1, skill line Holy) where WotLK's player-cast version is `53408` (skill line
+Retribution) — the arsenal has the better id there, and name resolution is why it has never shown.
+
+`classmask = 0` is **not** a "not a player ability" signal and must not be used as one: 72 of the 296
+arsenal ids have it, and most are talents (Starfall, Penance, Preparation, Last Stand).
+
+The dedupe lives in **`GetActiveSpellList`**, not in the picker, because the live viewer reads that same
+function — a picker-only fix would have left two identical tiles on the cooldown bar itself. The catalog
+adds the other half in `hiddenSpells`: a name already on screen, or already emitted during the walk, is
+not offered again. Three guards, each load-bearing in a different situation:
+
+* `ownerByName` — the CURATED id wins a collision even though the catalog walks the arsenal first.
+ Nothing functional rides on it; consistency does, since the seed, starter layouts and presets all name
+ the curated id.
+* `placedNames` — the placed id is not always in a pool the catalog walks. Regenerating `CdmArsenal.lua`
+ drops ids (seven left it in §H.3.21), and a saved layout goes on holding one; nothing then marks its
+ name in passing.
+* `seenNames` — two generated ids for one ability, neither placed nor curated. Nothing outside the walk
+ can arbitrate, so the walk keeps the first.
+
+An id the client cannot name is never collapsed: `GetSpellInfo` returns nil, there is nothing to compare,
+and treating "no name" as one shared name would merge unrelated abilities into a single tile.
+
+1001 assertions. Six mutations fail by name: the dedupe removed from `GetActiveSpellList`; the catalog
+ignoring what is on screen under another id; the generated twin winning the collision; no within-walk
+name memory; and `or ""` for a missing name in either file, which collapses every unnamed id together.
+The fixture builds its own twins rather than asserting against the five above, so it keeps meaning the
+same thing after any regeneration.
+
+Verified in bulk out-of-band as well: a probe that feeds the harness all 49,839 real `Spell.dbc` names
+and walks 10 races × 10 classes reports one tile per ability everywhere, and reports exactly the five
+collisions above when the dedupe is disabled.
+
+#### H.3.24. A short trailing row centres under the full ones
+
+12 essential icons at an icon limit of 9 wrap to 9 + 3, and the 3 were flush against the start edge —
+visibly off-centre under the row above. `core/GridLayout.lua` now takes `centerPartialLines`, and every
+viewer sets it in `BaseViewerMixin:RefreshLayout`.
+
+A *line* is the axis `stride` counts along: a row when horizontal, a column when vertical. The offset is
+half the line's shortfall against the laid-out bounds, measured as the furthest child edge rather than a
+sum of cell sizes — so a line of mixed-width children (the BuffBar's bars) measures what it actually
+occupies. A line that fills `stride` measures the full extent and gets 0, which is what keeps the columns
+of a multi-row grid aligned with one another.
+
+The offset is folded into `x`/`y` **before** the `layoutFramesGoingRight` mirror, not after. Mirroring
+reflects a line about the frame's centre and the centring offset is symmetric about that same centre, so
+ordering it first is exactly what flips its sign for a leftward grid — there is no separate mirrored case
+to get wrong. Vertically there is nothing to mirror at all: `rowY` is always measured from whichever edge
+`layoutFramesGoingUp` picked, so the same `+off` serves both stack directions.
+
+The flag defaults off in the mixin, so the alert and preview grids that also use it are untouched.
+
+`test_gridlayout.lua` covers the owner's 9+3 case, the flag being off, the mirrored grid, an evenly
+dividing grid getting no offset at all, vertical short columns in both stack directions, and the offset
+being converted back into child units under `iconSize` scaling.
+
+## H.4. Suggested order
+
+**Phase 7 is done; what follows applies to 8 and 9.**
+
+**Phase 7 first**, and not because it is a bug: 8a is a bigger visible change, but the Tracked Buffs
+tab is currently a feature that appears broken on every character, and it is also blocking target-DoT
+tracking. 7a-7c are contained (one registry, one adapter branch, one empty state).
+
+**Then 8a**, which is the largest look-and-feel change per line of code in the whole port — three
+registrations turning bare icons into Cooldown Manager tiles. **Then 8c** (the glow, small and it
+closes the last guide feature), **8b** (the swipe, needs measuring), **8d/8e** (screenshot work).
+
+8a, 8c, 8d (§H.2.9) and 8e (§H.2.10) are done. Both of the "screenshot work" items turned out to have
+one real defect each behind a pile of things that were already correct, and in both cases the defect
+was found by decoding the art rather than by looking at a screenshot: `Bar-BG` stretching its 1px edge
+lines up to 3.1x, and 8e's premise that the pandemic ring needed a mask substitute when the art is
+already a hollow ring.
+
+§H.2 is closed. **8b was measured and declined** (§H.2.11): it was indeed the one item that could make
+things worse, and it would have — `CooldownCaptureShow` zeroes the Cooldown frame's alpha, which takes
+our countdown numbers with it, for ~200 frames' cost and a capability 8c already substitutes for.
+
+**Phase 9 is done** (§H.3), and the prediction held: it was bookkeeping absorbed by the phases above,
+apart from the "Not Displayed" rename. What the estimate missed is that "a one-line owner preference"
+was five strings, not one — the label reached the player through the empty-state text, a trinket
+tooltip and the Settings tab's help text as well as the two category headers, and the right-click
+menu's "Move to …" only followed for free because it already read `Adapter.Label`. The single line it
+was *not* was the stored assignment value, which had to stay `"hidden"`.
+
+Phase 7 and 8a are also the two that most want an in-game pass immediately after, which argues for
+doing them before the deferred verification of 4b-4 / 4b-5 / 5a / 4c rather than after — one trip
+through the game covering all of it.
+
+**Nothing in the phasing tables is left.** What remains is not port work: §G.9 consumables (parked by
+the owner), and the in-game passes listed at the top — §F4, §F5, and 4b-4 / 4b-5 / 5a / 4c / Phase 6.
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Register.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Register.lua
new file mode 100644
index 0000000..efe47ad
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/Register.lua
@@ -0,0 +1,378 @@
+-- DragonUI_NewEra/modules/cooldownviewer/Register.lua — DragonUI wiring for the Cooldown Manager.
+--
+-- Replaces NewEra/CooldownViewer/EditModeRegister.lua, which is written against `NE.editmode` — a
+-- 6,441-line reimplementation of retail Edit Mode this addon does not have and will not port (see
+-- PORT_PLAN.md §B1). The two things that file delivered are re-homed:
+--
+-- position -> DragonUI's MoversSystem, reached through NE.RegisterPanel (CONTRACTS §4: panel
+-- modules never touch DragonUI internals directly)
+-- settings -> the /cdm window's own Settings tab (SettingsOptions.lua). They spent Phases 1-4b in
+-- the New Era options tab, for want of anywhere better; §G.10/§G.12 moved them, which
+-- is both where retail keeps them and one editor per stored value instead of two.
+-- What NE.RegisterOptionSection still registers is the master enable toggle and a
+-- button that opens the window.
+--
+-- Dropped with Edit Mode, deliberately: bottom-managed stacking (EM.RegisterBottomManaged — the
+-- viewers would rise above the action-bar tower as bars are added; DragonUI has no equivalent, so
+-- the viewers simply stay where the user puts them), and the retail settings-int codec.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+-- All four retail viewers. essential/utility read spell cooldowns from the curated class lists;
+-- buffIcon/buffBar read live auras (Phase 3) and need no class data.
+local VIEWERS = {
+ { category = "essential", id = "CooldownViewerEssential", label = "Essential Cooldowns",
+ desc = "Offensive burst and damage cooldowns.", order = 40 },
+ { category = "utility", id = "CooldownViewerUtility", label = "Utility Cooldowns",
+ desc = "Defensives, interrupts, CC and escapes.", order = 41 },
+ { category = "buffIcon", id = "CooldownViewerBuffIcon", label = "Buff Icons",
+ desc = "Short-duration buffs and procs, as icons.", order = 42, aura = true },
+ { category = "buffBar", id = "CooldownViewerBuffBar", label = "Buff Bars",
+ desc = "Short-duration buffs and procs, as depleting bars.", order = 43,
+ aura = true, bar = true },
+}
+M.VIEWER_SPECS = VIEWERS
+
+-- ── Frames + edit mode ──────────────────────────────────────────────────────────────────────────
+--
+-- TIMING: registration runs at PLAYER_LOGIN, not at file load. Every other NewEra module does the
+-- same (see modules/guild/Window.lua, modules/auctionhouse/Window.lua) because DragonUI's AceDB
+-- profile — which the position round-trip reads and writes — is not guaranteed ready while our
+-- files are still executing.
+--
+-- SEAM: NE.RegisterHUDFrame, not NE.RegisterPanel. RegisterPanel wires a MoversSystem mover, and
+-- `/dui edit` does not drive MoversSystem at all — it drives addon.EditableFrames. See the comment
+-- block on NE.RegisterHUDFrame in integration/Register.lua for the full detail. Registering a HUD
+-- frame with RegisterPanel produces a handle that is silently never shown.
+
+local booted = false
+
+local function boot()
+ if booted then return end
+ booted = true
+
+ -- Before anything reads a spell list: clear custom lists that were auto-seeded by the old
+ -- GetItemMeta side effect. They shadow the curated tables and would hide the whole WotLK seed on
+ -- any character that had run an earlier build. Versioned, so it runs once.
+ if M.MigrateStaleCustomLists then M.MigrateStaleCustomLists() end
+
+ for _, spec in ipairs(VIEWERS) do
+ local frame = spec.aura and M.CreateBuffViewer(spec.category) or M.CreateViewer(spec.category)
+ if not frame then
+ if NE.Log then NE.Log("CDM", "viewer '" .. spec.category .. "' failed to build") end
+ else
+
+ NE.RegisterHUDFrame({
+ name = spec.id,
+ label = spec.label, -- shown on the editor handle; also silences AceLocale's missing-key warning
+ frame = frame,
+ section = "widgets",
+ key = "ne" .. spec.id,
+ -- PER CHARACTER. DragonUI's profile is shared across the account, so without this all four
+ -- viewers sit wherever the last character to touch them left them. Where these belong is
+ -- downstream of WHAT they show, and that is per class already — a Warlock's buff bars have
+ -- no reason to inherit a Priest's placement. Existing shared positions are seeded into each
+ -- character's slot on first login, so nothing moves on upgrade.
+ perCharacter = true,
+ -- No handles while the module is off. RegisterHUDFrame defaults this to "always offer it",
+ -- which is right for a HUD frame that is merely EMPTY right now — that is what showTest is
+ -- for. It is wrong for one the player has not turned on: the Cooldown Manager ships disabled
+ -- (see M.IsEnabled), so out of the box `/dui edit` would open onto four green rectangles for
+ -- a feature that has never been on screen, and showTest would fill them with demo icons.
+ -- DragonUI honours a false here by hiding the anchor and skipping showTest entirely
+ -- (core/api.lua:1239).
+ editorVisible = function() return M.IsEnabled() end,
+ -- Retail keeps a system's settings ON the frame in Edit Mode, not in a separate options
+ -- window. EditorPanel.lua builds that dialog; integration/Register.lua owns when it opens.
+ editorSettings = function(editorAnchor)
+ if M.ShowEditorPanel then M.ShowEditorPanel(spec.category, editorAnchor) end
+ end,
+ -- Leaving edit mode takes the dialog with it. HideAllEditableFrames calls this for every
+ -- registered frame, so it fires whichever viewer was selected — and it is idempotent.
+ onHide = function()
+ if M.HideEditorPanel then M.HideEditorPanel() end
+ end,
+ defaultPoint = {
+ point = "BOTTOM", relativePoint = "BOTTOM",
+ -- BuffBar sits off to the side in retail's preset; the rest are centred.
+ x = (spec.category == "buffBar") and 420 or 0,
+ y = M.VIEWER_DEFAULT_Y[spec.category] or 300,
+ },
+ -- Demo content while the editor is open. Essential/Utility would otherwise be empty for a
+ -- character who hasn't learned those spells (or any Death Knight, pending Phase 2), and the
+ -- aura viewers are empty by nature whenever no short buff happens to be up.
+ showTest = function()
+ frame._editPreview = true
+ frame:Show()
+ frame:Rebuild()
+ end,
+ hideTest = function()
+ frame._editPreview = false
+ frame:Rebuild()
+ frame:UpdateVisibility()
+ end,
+ })
+
+ frame:RefreshLayout()
+ frame:UpdateVisibility()
+ end
+ end
+
+ -- The learn-gate reads the spellbook table (core/SpellRanks.lua). Re-source every viewer once
+ -- that table has been rebuilt — training a rank, switching spec or respeccing all change what
+ -- the player knows, and the viewers must follow.
+ if NE.spellbook and NE.spellbook.OnRebuilt then
+ NE.spellbook.OnRebuilt(function()
+ M.InvalidateCuratedCache()
+ -- The talent gate too: a spec swap changes which catalog rows are offered, and this is the
+ -- point at which the client has finished telling us what changed.
+ if M.InvalidateTalentCache then M.InvalidateTalentCache() end
+
+ -- Seed a per-spec starter into a layout bucket that has never had one. Hung HERE rather than on
+ -- PLAYER_LOGIN because this fires once the client has finished answering about talents — at
+ -- login AND after a spec swap — and GetTalentTabInfo is what the detection reads. It is
+ -- idempotent: the bucket is marked the moment it is seeded, so every later call is a no-op, and
+ -- a character with no talent points is simply skipped until they spend one.
+ local P = NE.cooldownviewersettings and NE.cooldownviewersettings.presets
+ if P and P.SeedStarterIfFresh then pcall(P.SeedStarterIfFresh) end
+
+ M.RefreshActiveViewer()
+ end)
+ end
+
+ -- Alert/ready-sound poll. Started unconditionally: it early-outs on its own gate every tick when
+ -- nothing is assigned, which is cheaper than watching the store for the first assignment.
+ if M.alerts and M.alerts.Start then M.alerts.Start() end
+end
+
+-- ── Diagnostic ──────────────────────────────────────────────────────────────────────────────────
+-- /necdm — report, per curated spell for the player's class, exactly what the learn-gate decided
+-- and why. Added after a report that talent-granted abilities (Penance on Disc, Guardian Spirit on
+-- Holy) and level-squished abilities (Divine Hymn at 60) were being filtered out: guessing at which
+-- of the three checks failed is far slower than printing all three.
+SLASH_NECDM1 = "/necdm"
+SlashCmdList["NECDM"] = function()
+ local _, class = UnitClass("player")
+ local SB = NE.spellbook
+ local function say(msg) DEFAULT_CHAT_FRAME:AddMessage("|cff1784d1CDM|r " .. msg) end
+
+ say(("class=%s spellbook built=%s names=%d"):format(
+ tostring(class),
+ tostring(SB and SB.built),
+ (function() local n = 0; for _ in pairs(SB and SB.KNOWN_NAMES or {}) do n = n + 1 end; return n end)()))
+
+ for _, category in ipairs({ "essential", "utility" }) do
+ local source = M.SPELL_DATA_BY_CATEGORY and M.SPELL_DATA_BY_CATEGORY[category]
+ local list = source and source[class] or {}
+ local shown = M.GetActiveSpellList(category)
+ local inShown = {}
+ for _, id in ipairs(shown) do inShown[id] = true end
+
+ -- Report whether a CUSTOM list is overriding the curated table. Its absence from the first
+ -- version of this command is why a spell could report book=true on every check and still be
+ -- hidden: the gate was fine, the list it was gating had been replaced.
+ local custom = M.GetCustomList(category, class)
+ say(("|cffffcc55%s|r — %d curated, %d shown%s"):format(
+ category, #list, #shown,
+ custom and (" |cffff5555[custom list active: %d entries — curated table IGNORED]|r"):format(#custom) or ""))
+ for _, id in ipairs(list) do
+ local name = GetSpellInfo(id)
+ local book = name and SB and SB.IsSpellNameKnown and SB.IsSpellNameKnown(name)
+ local known = IsSpellKnown and IsSpellKnown(id)
+ local byname = name and GetSpellInfo(name) ~= nil
+ if not inShown[id] then
+ say((" |cffff5555hidden|r %-24s id=%-6d book=%-5s IsSpellKnown=%-5s byName=%s"):format(
+ tostring(name or "?"), id, tostring(book), tostring(known), tostring(byname)))
+ end
+ end
+ end
+
+ -- Alerts and sounds, keyed by the LISTED spell id (never the learned-rank one — see
+ -- ItemMixin:GetSettingsKey). Printing the key is the point: a mismatch here is what an orphaned
+ -- assignment looks like.
+ local cd = M._store and M._store(false)
+ local alerts, sounds = (cd and cd.alerts) or {}, (cd and cd.sounds) or {}
+ local n = 0
+ for id, cfg in pairs(alerts) do
+ if cfg and cfg.type then
+ n = n + 1
+ say((" |cff55ff55alert|r %-24s key=%-6d type=%-9s fx=%s window=%d%%"):format(
+ tostring(GetSpellInfo(id) or "?"), id, cfg.type, tostring(cfg.fx), (cfg.window or 0.3) * 100))
+ end
+ end
+ for id, kit in pairs(sounds) do
+ if kit then
+ n = n + 1
+ say((" |cff55ff55sound|r %-24s key=%-6d kit=%-7d %s"):format(
+ tostring(GetSpellInfo(id) or "?"), id, kit, tostring(M.GetSoundKitName(kit))))
+ end
+ end
+ if n == 0 then say(" no alerts or ready sounds assigned (right-click an icon to add one)") end
+
+ -- §F1: which Cooldown widget methods this client actually has. The plan has carried "confirm
+ -- SetDrawEdge" as an open question since Phase 0, and the static evidence says it is native —
+ -- ClassicAPI stubs SetEdgeTexture / SetEdgeColor / SetEdgeScale as "Incompatible (3.3.5)" but does
+ -- NOT stub SetDrawEdge, and its own cooldown-capture path calls Self:GetDrawEdge() unguarded
+ -- (Util/Cooldown.lua:204), which would error on every captured cooldown if it were missing. That is
+ -- an inference from someone else's code, so this prints the answer instead of arguing it. One
+ -- throwaway frame, created on demand: frames cannot be destroyed, so it is not created at load.
+ local probe = M._widgetProbe
+ if not probe then
+ probe = CreateFrame("Cooldown", nil, UIParent)
+ probe:Hide()
+ M._widgetProbe = probe
+ end
+ local function has(name) return probe[name] ~= nil and "yes" or "NO" end
+ say(("cooldown widget: SetDrawEdge=%s GetDrawEdge=%s SetSwipeTexture=%s SetReverse=%s"):format(
+ has("SetDrawEdge"), has("GetDrawEdge"), has("SetSwipeTexture"), has("SetReverse")))
+
+ -- Phase 8a. Six atlases were being set by name with nothing registered, which renders as an
+ -- invisible texture and logs nothing — so the state is worth being able to READ rather than infer
+ -- from how the bars look. The ready flash in particular has two very different code paths, and
+ -- "is it the sprite or the fallback?" is otherwise a guess.
+ if NE.tex and NE.tex.HasAtlas then
+ local names = {
+ "UI-HUD-CoolDownManager-IconOverlay", "UI-CooldownManager-OORshadow",
+ "UI-HUD-ActionBar-GCD-Flipbook", "UI-HUD-CoolDownManager-Bar",
+ "UI-HUD-CoolDownManager-Bar-BG", "UI-HUD-CoolDownManager-Bar-Pip",
+ }
+ local missing = {}
+ for _, n in ipairs(names) do
+ if not NE.tex.HasAtlas(n) then missing[#missing + 1] = n end
+ end
+ if #missing == 0 then
+ say(("viewer art: all %d atlases registered"):format(#names))
+ else
+ say(("viewer art: MISSING %d of %d -- %s"):format(#missing, #names,
+ table.concat(missing, ", ")))
+ end
+ local flip = NE.tex.HasAtlas("UI-HUD-ActionBar-GCD-Flipbook")
+ say(("ready flash: %s"):format(flip and "retail flipbook sprite (22 frames)"
+ or "fallback highlight burst (flipbook atlas not registered)"))
+ end
+
+ -- Phase 8c. The glow has two independent ways to be invisible — the setting is off, or no tile is
+ -- currently in the aura branch at all — and neither one looks different from a broken glow. Report
+ -- both, plus a live count, so "I don't see it" is answerable rather than guessable. (The first
+ -- version had a third way: art that emits nothing under ADD. Hence naming the texture here.)
+ local lit, withSpell = 0, 0
+ for _, cat in ipairs({ "essential", "utility" }) do
+ local v = M.viewers and M.viewers[cat]
+ for _, it in ipairs((v and v.items) or {}) do
+ if it.spellID and it:IsShown() then
+ withSpell = withSpell + 1
+ if it._buffGlow then lit = lit + 1 end
+ end
+ end
+ end
+ say(("buff glow: %s, texture %s, lit on %d of %d shown tiles"):format(
+ M.IsBuffGlowEnabled() and "ON" or "off (Settings > Buffed spells)",
+ tostring(M.BUFF_GLOW_TEXTURE), lit, withSpell))
+
+ -- Phase 8e. The pandemic ring has the same "three ways to be invisible" problem the buff glow had:
+ -- no spell carries a refresh alert, none is inside its window, or the atlas failed to register.
+ -- Report which, plus whether the pulse is actually running — a built-but-never-played group is a
+ -- silent failure, because the ring still shows, just static.
+ local AL = M.alerts
+ if AL then
+ local assigned, lit, pulsing = 0, 0, 0
+ for _, cat in ipairs({ "essential", "utility" }) do
+ local v = M.viewers and M.viewers[cat]
+ for _, it in ipairs((v and v.items) or {}) do
+ local sid = it.GetSettingsKey and it:GetSettingsKey() or it.spellID
+ if sid and AL.GetType(sid) == "refresh" then assigned = assigned + 1 end
+ local ov = it._pandemicRing
+ if ov and ov:IsShown() then
+ lit = lit + 1
+ if ov.anim and ov.anim.IsPlaying and ov.anim:IsPlaying() then pulsing = pulsing + 1 end
+ end
+ end
+ end
+ say(("pandemic ring: art %s, %d spell(s) set to Refresh, %d ring(s) up, %d pulsing"):format(
+ (NE.tex and NE.tex.HasAtlas and NE.tex.HasAtlas("UI-CooldownManager-PandemicBorder"))
+ and "registered" or "MISSING", assigned, lit, pulsing))
+ end
+
+ -- Phase 8d. The 3-slice is invisible when it works and a soft edge when it does not, which is a
+ -- hard thing to eyeball at the 100% bar width. Report the stretch factor the middle is carrying:
+ -- that is the number the slice exists to keep off the caps.
+ local bar = M.viewers and M.viewers.buffBar and M.viewers.buffBar.items
+ and M.viewers.buffBar.items[1] and M.viewers.buffBar.items[1].Bar
+ local bg = bar and bar.BarBG
+ if bg and bg.Left then
+ local w = (bar:GetWidth() or 0) - M.BARBG_PAD_L + M.BARBG_PAD_R
+ say(("bar frame: %s, region %.0fpx wide vs %dpx art (%.2fx), caps %.1f/%.1f held"):format(
+ bg.Middle and bg.Middle:IsShown() and "3-slice" or "single stretch (bar too narrow)",
+ w, M.BARBG_NATIVE_W, w / M.BARBG_NATIVE_W,
+ bg.Left:GetWidth() or 0, bg.Right and bg.Right:GetWidth() or 0))
+ end
+end
+
+local bootFrame = CreateFrame("Frame")
+bootFrame:RegisterEvent("PLAYER_LOGIN")
+bootFrame:SetScript("OnEvent", function()
+ local ok, err = pcall(boot)
+ if not ok and NE.Log then NE.Log("CDM", "boot failed: " .. tostring(err)) end
+end)
+
+-- ── Options ─────────────────────────────────────────────────────────────────────────────────────
+--
+-- TWO CONTROLS, deliberately. This section used to carry every viewer setting — the ten per-viewer
+-- ones, the two bar-only ones, buff auto-tracking and both resets — because Phase 1 had nowhere else
+-- to put them. They now live on the Cooldown Manager's own Settings tab (SettingsOptions.lua), which
+-- is where retail keeps them and where a player already is when they care about them.
+--
+-- What stays is what only makes sense here: the master OFF switch, because DragonUI's options panel is
+-- where a player goes to turn a module off, and nobody looks inside a window for the way to make that
+-- window's frames stop existing — plus a button to reach the rest.
+--
+-- No stored VALUE is rendered in both places. A setting with two editors stays consistent only while
+-- both are rebuilt on show, and the first time one is not, the player is reading a stale control and
+-- cannot tell that from a setting that failed to apply. (EditorPanel.lua later added a third view of
+-- the per-viewer settings — retail's on-the-frame dialog — and pays that cost explicitly: it re-reads
+-- every control on open and refreshes the Settings tab on every write. See its header.)
+
+NE.RegisterOptionSection({
+ id = "cooldownviewer",
+ order = 40,
+ build = function(scroll, C)
+ if C.AddSpacer then C:AddSpacer(scroll) end
+ C:AddHeading(scroll, "Cooldown Manager")
+ C:AddDescription(scroll,
+ "Retail's Cooldown Manager, driven from curated per-class cooldown lists. |cffffcc55Off by "
+ .. "default|r — it adds four viewers to the middle of your screen, so it waits to be asked. Every "
+ .. "setting — which spells and buffs are tracked, each viewer's layout, size and visibility, alerts "
+ .. "and ready sounds — lives in the Cooldown Manager window itself (/cdm). Drag the viewers with "
+ .. "DragonUI's editor mode to reposition them, and right-click one there for its own layout settings.")
+
+ C:AddToggle(scroll, {
+ label = "Enable Cooldown Manager",
+ desc = "Off by default. Turn on to show the four viewers; turn off to hide them again. Takes "
+ .. "effect immediately either way, and nothing is forgotten — this switch stores one flag "
+ .. "and touches nothing else, so your setup comes back exactly as you left it.",
+ getFunc = function() return M.IsEnabled() end,
+ setFunc = function(v) M.SetEnabled(v) end,
+ })
+
+ if C.AddButton then
+ C:AddButton(scroll, {
+ label = "Open Cooldown Manager",
+ -- SPELLS, not Settings (owner's steer). This is the front door: the first thing anyone wants
+ -- after turning the module on is to see and change what it tracks, which is the Spells tab.
+ -- Settings is where you go once you already know what is in the viewers, and the tabs are right
+ -- there. (The link on the edit-mode dialog still opens on Settings — that one exists to reach
+ -- the non-per-viewer settings by name, so it means it.)
+ desc = "Opens the Cooldown Manager window (/cdm) on its Spells tab. Needs the module on — "
+ .. "the window configures the viewers, so it goes away with them.",
+ callback = function()
+ if M.OpenSettingsPanel then M.OpenSettingsPanel("spells") end
+ end,
+ })
+ else
+ -- An older DragonUI_Options with no AddButton. Name the command rather than leaving this section
+ -- as an off switch and no way through.
+ C:AddDescription(scroll, "Type /cdm to open the Cooldown Manager window.")
+ end
+ end,
+})
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsAdapter.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsAdapter.lua
new file mode 100644
index 0000000..a392fa9
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsAdapter.lua
@@ -0,0 +1,517 @@
+-- DragonUI_NewEra/modules/cooldownviewer/SettingsAdapter.lua — the category model the /cdm panel
+-- talks to. Downport of NewEra/CooldownViewerSettings/DataAdapter.lua.
+--
+-- The panel never touches the spell lists directly; everything goes through here. That indirection
+-- is upstream's design and it earns its keep twice over: our storage model differs from theirs (we
+-- keep a per-category editable list with `enabled` flags, they keep one list per bucket), and if a
+-- Classic Plus client ever ships C_CooldownViewer this is the single file that would swap.
+--
+-- CATEGORIES. Six, in two display modes:
+-- spells: Essential / Utility — the editable lists
+-- Not Displayed — the ARSENAL, i.e. every class ability with a real cooldown that
+-- is not currently in a viewer. This is what makes the panel a
+-- picker rather than an undo list.
+-- auras : Tracked Buffs / Tracked Bars / Not Displayed — the unified tracked-aura pool, keyed by
+-- each entry's assignment ("icon" / "bar" / "hidden").
+--
+-- ONE Equip source pool, not upstream's two. `equipActive` holds the discovered on-use trinkets
+-- that the player has not placed in a viewer yet. The passive pool is cut for a data reason, not a
+-- scheduling one — see Equip.lua's header.
+--
+-- "Not Displayed" is a CATALOG, not a bucket. For spells it is computed — arsenal minus what is
+-- placed — rather than stored, so a newly-generated ability appears in it automatically. For auras
+-- it IS a stored assignment, because an aura the player force-hides has to stay hidden against a
+-- live scan.
+--
+-- The LABEL is retail's ("Not Displayed", Phase 9 §H.3); the STORED assignment value is still
+-- "hidden" and the category ids are still hiddenSpell / hiddenAura. Renaming those would invalidate
+-- every existing saved layout for a cosmetic gain, so the rename stops at the display string.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+NE.cooldownviewersettings = NE.cooldownviewersettings or {}
+local CDS = NE.cooldownviewersettings
+
+local Adapter = {}
+CDS.adapter = Adapter
+
+-- Prefer a client GlobalString where 3.3.5a ships one, else an English literal — the house pattern.
+local function GS(key, fallback)
+ local v = _G[key]
+ return (v ~= nil and v ~= "") and v or fallback
+end
+
+-- kind: "icon" (grid of tiles) | "bar" (stacked rows).
+-- list: the editable-list key for spell categories. aura: the assignment value for aura categories.
+-- source: a discovery POOL rather than a stored list — it holds whatever is unassigned right now,
+-- and is skipped at render when empty so a player with no on-use trinket never sees the section.
+local CATS = {
+ essential = { mode = "spells", kind = "icon", label = GS("COOLDOWN_VIEWER_SETTINGS_CATEGORY_ESSENTIAL", "Essential"), list = "essential" },
+ utility = { mode = "spells", kind = "icon", label = GS("COOLDOWN_VIEWER_SETTINGS_CATEGORY_UTILITY", "Utility"), list = "utility" },
+ equipActive = { mode = "spells", kind = "icon", label = GS("COOLDOWN_VIEWER_SETTINGS_CATEGORY_EQUIP_ACTIVE", "Trinkets"), source = true },
+ hiddenSpell = { mode = "spells", kind = "icon", label = GS("COOLDOWN_VIEWER_SETTINGS_CATEGORY_NOT_IN_BAR", "Not Displayed") },
+ trackedBuff = { mode = "auras", kind = "icon", label = GS("COOLDOWN_VIEWER_SETTINGS_CATEGORY_TRACKED_BUFF", "Tracked Buffs"), aura = "icon" },
+ trackedBar = { mode = "auras", kind = "bar", label = GS("COOLDOWN_VIEWER_SETTINGS_CATEGORY_TRACKED_BARS", "Tracked Bars"), aura = "bar" },
+ hiddenAura = { mode = "auras", kind = "icon", label = GS("COOLDOWN_VIEWER_SETTINGS_CATEGORY_NOT_IN_BAR", "Not Displayed"), aura = "hidden" },
+}
+
+-- The source pool sits between the display categories and Not Displayed, which is upstream's order
+-- and retail's: "here is what you own but haven't placed", above "here is everything else".
+Adapter.MODE_ORDER = {
+ spells = { "essential", "utility", "equipActive", "hiddenSpell" },
+ auras = { "trackedBuff", "trackedBar", "hiddenAura" },
+}
+
+-- Settings category id → the assignment value M.SetEquipAssignment stores. The values are VIEWER
+-- category names, so M.GetEquipItemsForCategory can compare them directly against a live viewer's
+-- `category` field with no second mapping at runtime. Source cats map to nil, "back to the pool".
+local ASSIGN_FOR_CAT = {
+ essential = "essential", utility = "utility", hiddenSpell = "hidden",
+ equipActive = nil,
+}
+
+function Adapter.IsSourcePool(catID)
+ return (CATS[catID] and CATS[catID].source) and true or false
+end
+
+function Adapter.Meta(catID) return CATS[catID] end
+function Adapter.Label(catID) return CATS[catID] and CATS[catID].label or catID end
+function Adapter.Kind(catID) return CATS[catID] and CATS[catID].kind or "icon" end
+
+-- What an empty section should say (Phase 7c). "(empty)" is fine for a list the player emptied
+-- themselves; it reads as a fault in a section that has never been fillable, which is exactly how
+-- the aura tab looked before this phase. Deliberately static: the text has to be true whatever the
+-- auto-track destination happens to be, so it names the action rather than describing the state.
+--
+-- hiddenSpell names the setting rather than describing the state, because the state it is most
+-- likely reporting is a LOW-LEVEL character with nothing left to list — and "(empty)" there reads as
+-- a fault. Naming Show Unlearned turns the dead end into a signposted one.
+local EMPTY_TEXT = {
+ trackedBuff = "Nothing here yet — drag a buff in to pin it.",
+ trackedBar = "Nothing here yet — drag a buff in to pin it.",
+ hiddenAura = "Everything recorded is displayed.",
+ hiddenSpell = "Everything you have learned is displayed. Turn on Show Unlearned (cog) to see the rest.",
+}
+
+function Adapter.EmptyText(catID) return EMPTY_TEXT[catID] or "(empty)" end
+function Adapter.Mode(catID) return CATS[catID] and CATS[catID].mode or "spells" end
+
+-- ── Spell categories ────────────────────────────────────────────────────────────────────────────
+
+-- Every spellID currently placed in a spell viewer, as a set.
+--
+-- ASKED, NOT RE-DERIVED (§H.3.22). This used to reimplement the placement rules — read the custom
+-- list, fall back to the curated default — which is the same question GetActiveSpellList already
+-- answers, and the copy drifted from the original. GetActiveSpellList also merges the player's
+-- RACIALS in (appendRacials), and the copy never knew racials existed, so every racial was placed
+-- and unplaced at the same time: shown in Utility by the viewer, and listed again under Not
+-- Displayed by this catalog. Draenei saw Gift of the Naaru twice, Orcs Blood Fury, and so on for
+-- every race with a racial at all.
+--
+-- Not Displayed is defined as "the catalog minus what is on screen", so it has to be computed from
+-- what is on screen. Any future merge into a display list is then inherited rather than re-missed.
+--
+-- includeUnlearned is TRUE deliberately: this asks about PLACEMENT, not castability. Letting the
+-- learn-gate run here would call an unlearned-but-placed spell unplaced and hand it to the catalog,
+-- putting it in two places again for a second reason.
+--
+-- Returns ids AND names. The names are the §H.3.23 half: the catalog must not offer a SECOND id for
+-- an ability already on screen under a first one, and an id comparison cannot see that.
+local function placedSet(class)
+ local ids, names = {}, {}
+ for _, key in ipairs({ "essential", "utility" }) do
+ for _, id in ipairs(M.GetActiveSpellList(key, true, class) or {}) do
+ ids[id] = true
+ local name = GetSpellInfo(id)
+ if name then names[name] = true end
+ end
+ end
+ return ids, names
+end
+
+-- The Not Displayed catalog: every class ability with a real cooldown that is not currently placed,
+-- plus the player's racials. Race-impossible spells are dropped outright — that is not a "not yet"
+-- but a "never", and no setting should surface it.
+--
+-- The LEARN gate applies here too now (§H.3.21). It did not, on the reasoning that an empty picker
+-- reads as broken on a fresh character — but the cost was worse: an untalented druid's Mangle sat in
+-- the catalog indistinguishable from an ability deliberately left undisplayed, one drag from a row
+-- that could never light up. Tinting it red said "not yet learned" where the section's own name
+-- already said "you chose not to show this", and the two readings cancel.
+--
+-- The empty-picker worry is answered where it belongs: the section's empty text names Show Unlearned,
+-- so the full arsenal is one click away and the player is told where.
+local function hiddenSpells(class)
+ local showAll = M.GetShowUnlearned and M.GetShowUnlearned()
+ local placed, placedNames = placedSet(class)
+ local _, race = UnitRace("player")
+ local rb = race and M.RACIAL_BY_RACE and M.RACIAL_BY_RACE[race]
+
+ -- Which id OWNS an ability, when the pools disagree about it (§H.3.23). The arsenal is generated and
+ -- the curated tables are authored, and for five abilities they picked different Spell.dbc rows for
+ -- the same thing — the DK's Icy Touch is curated as 45477 and generated as 52372. The catalog walks
+ -- the arsenal first, so without this the row the player drags would be the generated twin.
+ --
+ -- Nothing functional rides on the choice: highestKnownRankID resolves any id for an ability to the
+ -- player's known rank by name, so both tiles behave identically. It rides on CONSISTENCY — the
+ -- curated id is the one the seed, the starter layouts and the presets all name, and a layout is
+ -- easier to reason about when one ability means one number everywhere.
+ local ownerByName = {}
+ local function claim(id)
+ local name = id and GetSpellInfo(id)
+ if name and ownerByName[name] == nil then ownerByName[name] = id end
+ end
+ for _, key in ipairs({ "essential", "utility" }) do
+ local src = M.SPELL_DATA_BY_CATEGORY[key] and M.SPELL_DATA_BY_CATEGORY[key][class]
+ if src then for _, id in ipairs(src) do claim(id) end end
+ if rb and rb[key] then for _, id in ipairs(rb[key]) do claim(id) end end
+ end
+
+ local seen, seenNames, out = {}, {}, {}
+ local function consider(id)
+ if not id or seen[id] then return end
+ seen[id] = true
+ if M.SpellAllowedForRace and not M.SpellAllowedForRace(id) then return end
+ -- IsSpellLearned, NOT IsTrackable: the latter waves through anything outside the curated tables,
+ -- which is most of this catalog — the arsenal is a separate generated pool. Gating on it would
+ -- have hidden Mangle (Cat), which the seed curates, and kept Mangle (Bear), which it does not.
+ if not showAll and M.IsSpellLearned and not M.IsSpellLearned(id) then return end
+ -- One tile per ABILITY. An id the client cannot name skips this and is judged by id alone —
+ -- there is nothing to compare it against, and dropping it would be a guess.
+ local name = GetSpellInfo(id)
+ if name then
+ local owner = ownerByName[name]
+ if (owner and owner ~= id) or placedNames[name] or seenNames[name] then return end
+ seenNames[name] = true
+ end
+ if not placed[id] then out[#out + 1] = id end
+ end
+
+ local arsenal = M.ARSENAL_BY_CLASS and M.ARSENAL_BY_CLASS[class]
+ if arsenal then for _, id in ipairs(arsenal) do consider(id) end end
+
+ -- Curated ids act as a floor: an ability the curation added but the generator missed still has to
+ -- be re-addable after the player removes it.
+ for _, key in ipairs({ "essential", "utility" }) do
+ local src = M.SPELL_DATA_BY_CATEGORY[key] and M.SPELL_DATA_BY_CATEGORY[key][class]
+ if src then for _, id in ipairs(src) do consider(id) end end
+ end
+
+ if rb then
+ for _, key in ipairs({ "essential", "utility" }) do
+ if rb[key] then for _, id in ipairs(rb[key]) do consider(id) end end
+ end
+ end
+ return out
+end
+
+-- ── Equip rows ──────────────────────────────────────────────────────────────────────────────────
+-- An equip row is an ENTRY TABLE, not a bare spellID, and GetItems returns a mixed list. That is
+-- upstream's shape and it is the reason the whole thing stays cheap: a tile that gets a table calls
+-- SetEquipEntry, a tile that gets a number calls SetSpell, and every other consumer keys off
+-- `item.token or item.spellID`. The alternative — promoting every spell to an entry table — would
+-- have touched the menu, the drag path, the filter and the tests for no gain.
+--
+-- The token, not the spellID, is the move key. It is stable across an unequip/re-equip, and it
+-- survives the case a use-spell cannot be resolved yet.
+local function equipEntry(e)
+ return { itemID = e.itemID, spellID = e.spellID, token = e.token,
+ label = e.label, source = e.source, kind = e.kind,
+ hidden = (M.GetEquipAssignment(e.token) == "hidden") }
+end
+
+-- Is this spell already a STORED entry in an editable list — i.e. the player added the on-use item
+-- through the picker as a spell? If so the discovery layer skips it, or a trinket whose use-spell is
+-- also a curated entry would render twice: once as the stored spell, once as the discovered item.
+local function editableHasSpell(spellID, class)
+ if not spellID then return false end
+ for _, key in ipairs({ "essential", "utility" }) do
+ local list = M.GetCustomList(key, class)
+ if list then
+ for _, e in ipairs(list) do
+ if e.spellID == spellID and e.enabled then return true end
+ end
+ end
+ end
+ return false
+end
+
+-- Discovered equip items whose assignment is exactly `assignVal` (nil = the unassigned source pool).
+local function equipItemsAssigned(assignVal, class)
+ local out = {}
+ if not M.GetEquipActiveItems then return out end
+ for _, e in ipairs(M.GetEquipActiveItems()) do
+ if M.GetEquipAssignment(e.token) == assignVal and not editableHasSpell(e.spellID, class) then
+ out[#out + 1] = equipEntry(e)
+ end
+ end
+ return out
+end
+
+-- ── Aura rows (Phase 7b) ────────────────────────────────────────────────────────────────────────
+--
+-- An aura category is now the union of two things: what the player has ASSIGNED by hand, and what
+-- the viewers are deciding for themselves. The second half is the point — before it, the only writer
+-- of the assignment pool was this picker, so the pool could never be anything but empty.
+--
+-- Where an unassigned candidate lands depends on what is actually happening to it:
+--
+-- auto-track ON it is being tracked, so it appears in whichever category the destination sends
+-- it to (M.AutoTrackDest: both / icon / bar) and is marked `auto`
+-- auto-track OFF nothing is showing it, so it appears under HIDDEN — which is the truth, and it
+-- also gives the section a purpose beyond force-excludes
+--
+-- Either way, dragging one out is what makes it explicit: the drag already calls SetAuraAssignment,
+-- and "stop deciding this one for me" is exactly what that write means. No new write path.
+local AUTO_DEST_CATS = {
+ both = { icon = true, bar = true },
+ icon = { icon = true },
+ bar = { bar = true },
+}
+
+local function auraRow(e, assignment)
+ local name = e.name or (e.spellID and M.ResolveAuraName and M.ResolveAuraName(e.spellID))
+ local icon = e.icon or (e.spellID and select(3, GetSpellInfo(e.spellID)))
+ return {
+ aura = true, -- the tile setter dispatches on this
+ spellID = e.spellID,
+ label = name,
+ icon = icon,
+ dur = e.dur,
+ assignment = assignment, -- nil for an auto row: it has no stored assignment yet
+ auto = (assignment == nil) or nil,
+ talent = e.talent,
+ untalented = e.untalented,
+ seen = e.seen,
+ }
+end
+
+local function auraItems(assignVal, class)
+ local out = {}
+
+ for _, e in ipairs(M.GetTrackedAuraList(class) or {}) do
+ if (e.assignment or "icon") == assignVal then
+ out[#out + 1] = auraRow(e, e.assignment or "icon")
+ end
+ end
+
+ local autoOn = (not M.IsAutoTrackBuffs) or M.IsAutoTrackBuffs()
+ local wanted
+ if autoOn then
+ wanted = AUTO_DEST_CATS[(M.AutoTrackDest and M.AutoTrackDest()) or "both"] or AUTO_DEST_CATS.both
+ wanted = wanted[assignVal]
+ else
+ wanted = (assignVal == "hidden")
+ end
+
+ if wanted and M.GetAuraCandidates then
+ -- Show Unlearned is reused rather than given an aura-specific twin: it already means "show me
+ -- what I cannot use yet" for spells, and a second setting saying the same thing about auras is
+ -- how a settings page stops making sense.
+ local showAll = M.GetShowUnlearned and M.GetShowUnlearned()
+ for _, e in ipairs(M.GetAuraCandidates(class, showAll and true or false)) do
+ out[#out + 1] = auraRow(e, nil)
+ end
+ end
+
+ return out
+end
+
+-- ── Items for a category ────────────────────────────────────────────────────────────────────────
+
+function Adapter.GetItems(catID, class)
+ if not class then local _; _, class = UnitClass("player") end
+ local meta = CATS[catID]
+ if not (meta and class) then return {} end
+
+ local out = {}
+
+ -- The source pool IS the unassigned discovery set — nothing stored, nothing curated.
+ if meta.source then
+ return equipItemsAssigned(nil, class)
+ end
+
+ if meta.aura then
+ return auraItems(meta.aura, class)
+ end
+
+ if meta.list then
+ -- The learn gate applies here: Essential/Utility should show what you can cast, unless the
+ -- player has asked to see everything.
+ local showAll = M.GetShowUnlearned and M.GetShowUnlearned()
+ for _, id in ipairs(M.GetActiveSpellList(meta.list, showAll and true or false, class)) do
+ out[#out + 1] = id
+ end
+ -- Trinkets placed in this viewer, listed after the spells — the same order the live viewer
+ -- builds them in, so the panel reads as a preview of the bar rather than a separate list.
+ for _, e in ipairs(equipItemsAssigned(ASSIGN_FOR_CAT[catID], class)) do out[#out + 1] = e end
+ return out
+ end
+
+ -- Not Displayed: the catalog minus whatever the display categories are showing right now. See
+ -- hiddenSpells and placedSet — the subtraction is against the real lists, not a re-derivation.
+ for _, id in ipairs(hiddenSpells(class)) do out[#out + 1] = id end
+ for _, e in ipairs(equipItemsAssigned("hidden", class)) do out[#out + 1] = e end
+ return out
+end
+
+-- ── Moves ───────────────────────────────────────────────────────────────────────────────────────
+-- Mirrors retail's legalOriginalSourceCategoryToTargetCategory. Same category is always legal
+-- (that is a reorder, not a move).
+-- Nothing lists equipActive as a target: a trinket leaves the pool by being placed, and comes back
+-- only by being unequipped. That asymmetry is retail's, and it is why the pool has no inbound edges.
+local LEGAL = {
+ essential = { utility = true, hiddenSpell = true },
+ utility = { essential = true, hiddenSpell = true },
+ equipActive = { essential = true, utility = true }, -- retail EquipSlotEssential → {Essential, Utility}
+ hiddenSpell = { essential = true, utility = true },
+ trackedBuff = { trackedBar = true, hiddenAura = true },
+ trackedBar = { trackedBuff = true, hiddenAura = true },
+ hiddenAura = { trackedBuff = true, trackedBar = true },
+}
+
+function Adapter.CanTarget(fromCat, toCat)
+ if not (fromCat and toCat) then return false end
+ if fromCat == toCat then return true end
+ local t = LEGAL[fromCat]
+ return (t and t[toCat]) and true or false
+end
+
+function Adapter.GetValidTargets(fromCat)
+ local meta = CATS[fromCat]
+ if not meta then return {} end
+ local out = {}
+ for _, id in ipairs(Adapter.MODE_ORDER[meta.mode]) do
+ if id ~= fromCat and Adapter.CanTarget(fromCat, id) then out[#out + 1] = id end
+ end
+ return out
+end
+
+-- Move a spell or aura between categories.
+function Adapter.Assign(spellID, fromCat, toCat, class)
+ if not (spellID and Adapter.CanTarget(fromCat, toCat)) then return false end
+ if not class then local _; _, class = UnitClass("player") end
+
+ local fromMeta, toMeta = CATS[fromCat], CATS[toCat]
+ if not (fromMeta and toMeta) then return false end
+
+ -- A source-pool row is an equip item and MUST route through AssignEquip. Falling through here
+ -- would write its use-spell into the editable list as if the player had picked a spell, which
+ -- looks right until you unequip the trinket and the entry stays behind, pointing at nothing.
+ if fromMeta.source or toMeta.source then return false end
+
+ -- Auras are one pool keyed by assignment, so a move is a single write.
+ if toMeta.aura then
+ M.SetAuraAssignment(class, spellID, toMeta.aura)
+ return true
+ end
+
+ -- Spells: clear the old placement, then set the new one. Not Displayed is the ABSENCE of a placement,
+ -- which is why it has no list of its own.
+ if fromMeta.list then M.SetSpellEnabled(fromMeta.list, spellID, false) end
+ if toMeta.list then M.SetSpellEnabled(toMeta.list, spellID, true) end
+ return true
+end
+
+-- Reassign an EQUIP item by TOKEN. Separate from Assign because an equip row's identity is its
+-- token: the spellID may be shared with a curated entry, and for a source-pool row there is no
+-- stored list to write into at all — the whole state is the one assignment value.
+function Adapter.AssignEquip(token, fromCat, toCat)
+ if not (token and toCat and CATS[toCat]) then return false end
+ if fromCat and not Adapter.CanTarget(fromCat, toCat) then return false end
+ M.SetEquipAssignment(token, ASSIGN_FOR_CAT[toCat])
+ if M.RefreshActiveViewer then M.RefreshActiveViewer() end
+ return true
+end
+
+-- Remove a user-added entry outright. Only meaningful for auras: a spell's "removal" is just
+-- returning it to the Not Displayed catalog, which Assign already does, and an equip row is discovered
+-- rather than stored — it leaves by being unequipped.
+function Adapter.IsRemovable(spellID, catID, class)
+ local meta = CATS[catID]
+ if not (meta and meta.aura and spellID) then return false end
+ if meta.source then return false end
+ if not class then local _; _, class = UnitClass("player") end
+ for _, e in ipairs(M.GetTrackedAuraList(class) or {}) do
+ if e.spellID == spellID then return true end
+ end
+ return false
+end
+
+function Adapter.Remove(spellID, catID, class)
+ if not Adapter.IsRemovable(spellID, catID, class) then return false end
+ if not class then local _; _, class = UnitClass("player") end
+ M.RemoveTrackedAura(class, spellID)
+ return true
+end
+
+-- The ordered backing list for a category: the editable spell list, or the tracked-aura pool.
+-- Not Displayed (spells) has neither — it is computed — so it returns nil and reorders there are no-ops.
+local function orderedList(catID, class)
+ local meta = CATS[catID]
+ if not meta then return nil end
+ if meta.list then return M.GetEditableList(meta.list, class) end
+ if meta.aura then return M.GetTrackedAuraList(class) end
+ return nil
+end
+
+local function indexOf(list, spellID)
+ for i, e in ipairs(list) do
+ if e.spellID == spellID then return i end
+ end
+ return nil
+end
+
+-- Move spellID to sit immediately before (offset 0) or after (offset 1) targetID, inside one
+-- category. This is drag-only: retail reorders by drag and has no menu equivalent, so neither do we.
+function Adapter.ReorderTo(catID, spellID, targetID, offset, class)
+ if not (spellID and targetID and spellID ~= targetID) then return false end
+ if not class then local _; _, class = UnitClass("player") end
+ local list = orderedList(catID, class)
+ if not list then return false end
+
+ local from = indexOf(list, spellID)
+ if not from then return false end
+ local entry = table.remove(list, from)
+
+ -- Recompute the target index AFTER the removal. Pulling the entry out shifts everything below it
+ -- up by one, so an index captured beforehand lands a slot too far whenever the item moved DOWN
+ -- the list — the classic off-by-one in every drag reorder.
+ local to = indexOf(list, targetID)
+ if not to then table.insert(list, from, entry); return false end
+ table.insert(list, to + ((offset == 1) and 1 or 0), entry)
+
+ local meta = CATS[catID]
+ if meta.list then M.RefreshActiveViewer(meta.list) else M.RefreshActiveViewer() end
+ return true
+end
+
+-- Cross-category move that lands at a POSITION rather than at the end of the destination.
+function Adapter.AssignAt(spellID, fromCat, toCat, dropID, offset, class)
+ if not Adapter.Assign(spellID, fromCat, toCat, class) then return false end
+ if dropID and dropID ~= spellID then
+ Adapter.ReorderTo(toCat, spellID, dropID, offset, class)
+ end
+ return true
+end
+
+-- Reorder within a category. Spell order is the editable list's order; aura order is the pool's.
+function Adapter.MoveWithin(catID, spellID, delta, class)
+ local meta = CATS[catID]
+ if not (meta and meta.list and spellID and delta ~= 0) then return false end
+ if not class then local _; _, class = UnitClass("player") end
+
+ local list = M.GetEditableList(meta.list, class)
+ if not list then return false end
+ for i, e in ipairs(list) do
+ if e.spellID == spellID then
+ local j = i + delta
+ if j < 1 or j > #list then return false end
+ list[i], list[j] = list[j], list[i]
+ M.RefreshActiveViewer(meta.list)
+ return true
+ end
+ end
+ return false
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsAssets.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsAssets.lua
new file mode 100644
index 0000000..617d99b
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsAssets.lua
@@ -0,0 +1,53 @@
+-- DragonUI_NewEra/modules/cooldownviewer/SettingsAssets.lua — art for the /cdm settings panel.
+--
+-- Two sheets, one shipped here and one already on disk:
+--
+-- 7289697 (Interface\CDM\CDMAdvanced) — the side-tab GLYPHS. TWW-era art this client's data has
+-- no entry for, so it must ship as a local BLP copy or SetAtlas renders nothing.
+-- 5684744 (questlog) — the side-tab BODY and its hover/selected glows, used by
+-- NE.tabs.MakeSideTab, plus the cog glyph used by this panel's options button and its Settings
+-- tab. The file is also shipped by modules/spellbook/Assets.lua; this file registers the path and
+-- the rects itself so the tabs do not depend on that module's asset file having run.
+--
+-- That second point corrects a stale note in core/Tabs.lua, which said the questlog-tab-side*
+-- atlases live on "a different (not-yet-shipped) sheet". They do not — they are on 5684744, the
+-- same sheet the spellbook already ships. MakeSideTab has therefore been rendering transparent
+-- everywhere for want of three rects, not for want of art.
+--
+-- Rects transcribed verbatim from NewEra/Generated/AtlasData.lua, except the two CDM glyphs, whose
+-- coords come from the reference CooldownViewerSettings/Assets.lua — that file re-derives them
+-- because retail 12.1.0 REPACKED this sheet, so the Era-pinned generated rects are wrong for the
+-- BLP we ship. We ship the 12.1.0 BLP, so we use the 12.1.0 coords.
+
+local NE = DragonUI_NewEra
+if not (NE and NE.tex and NE.tex.RegisterLocal) then return end
+
+NE.tex.RegisterLocal(7289697,
+ "Interface\\AddOns\\DragonUI_NewEra\\Textures\\CooldownViewerSettings\\7289697-cdmadvanced.blp")
+
+-- Sheet 5684744 carries the side-tab BODY and the cog glyph, and both the tabs and this panel's cog
+-- draw from it. modules/spellbook/Assets.lua already ships the same file for its own cog, so this is a
+-- second registration of one path, not a second copy of the art — RegisterLocal is a table write, and
+-- the last writer wins with the same value. Declared here so the tabs do not depend on the spellbook
+-- module's asset file having run, which is what the note below used to quietly assume.
+NE.tex.RegisterLocal(5684744,
+ "Interface\\AddOns\\DragonUI_NewEra\\Textures\\Spellbook\\5684744-questlog.blp")
+
+NE.tex.RegisterAtlases({
+ -- Side-tab body + states (sheet 5684744, registered by modules/spellbook/Assets.lua).
+ ["questlog-tab-side"] = { file = 5684744, left = 0.105469, right = 0.205078, top = 0.396484, bottom = 0.513672, width = 51, height = 60 },
+ ["questlog-tab-side-glow-hover"] = { file = 5684744, left = 0.001953, right = 0.101562, top = 0.396484, bottom = 0.513672, width = 51, height = 60 },
+ ["questlog-tab-side-glow-select"] = { file = 5684744, left = 0.001953, right = 0.101562, top = 0.578125, bottom = 0.695312, width = 51, height = 60 },
+
+ -- The Settings tab's glyph, and the cog beside the search box. Same rect modules/spellbook/Assets.lua
+ -- registers for its own cog, restated here alongside the sheet registration above so this module
+ -- declares every atlas it draws. RegisterAtlases is a plain table write, so the duplicate is
+ -- harmless either way round.
+ ["questlog-icon-setting"] = { file = 5684744, left = 0.138672, right = 0.167969, top = 0.035156, bottom = 0.066406, width = 15, height = 16 },
+
+ -- CDM side-tab glyphs (sheet 7289697, 512x128). icon_buffreorder is deliberately absent: it
+ -- belongs to upstream's Group Buffs tab, which this port does not have (PORT_PLAN §G.4) — and its
+ -- "reorder group buffs" meaning is why the Settings tab reuses the cog above instead.
+ ["icon_cooldownmanager"] = { file = 7289697, left = 0.330078, right = 0.455078, top = 0.085938, bottom = 0.585938, width = 32, height = 32 },
+ ["icon_trackedbuffs"] = { file = 7289697, left = 0.201172, right = 0.326172, top = 0.007813, bottom = 0.507813, width = 32, height = 32 },
+})
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsCategories.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsCategories.lua
new file mode 100644
index 0000000..4aebc83
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsCategories.lua
@@ -0,0 +1,473 @@
+-- DragonUI_NewEra/modules/cooldownviewer/SettingsCategories.lua — the collapsible category sections
+-- inside the /cdm panel's scroll body. Downport of NewEra/CooldownViewerSettings/Categories.lua.
+--
+-- Each category is a header over a grid: icon categories are a 7-wide grid of 38x38 tiles (8px pad,
+-- so 46px pitch), the bar category a single column of 317x38 rows (10px pad, 48px pitch). Those
+-- numbers are upstream's, probe-confirmed against retail, and are kept exactly.
+--
+-- DOWNPORT: upstream builds its header with NE.listheader.Build, a Core helper this addon does not
+-- have. Built inline here instead — the same substitution modules/character/Reputation.lua made for
+-- the same missing helper, using the client's own +/- buttons for the collapse affordance.
+--
+-- Phase 4b-2 rendered and tooltipped. 4b-3 made the tiles live: right-click routes to
+-- CDS.OnItemClick, and the alert badge / tooltip extras come from SettingsMenu.lua. Both are
+-- called through CDS with a nil-guard, so this file stays loadable on its own.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+local CDS = NE.cooldownviewersettings
+local Adapter = CDS.adapter
+
+local ICON, PAD = 38, 8
+local PITCH = ICON + PAD -- 46
+local STRIDE = 7
+local CAT_W = 344
+local CAT_GAP = 18
+local CONTAINER_X, CONTAINER_Y = 13, -15
+local HEADER_H = 26
+local BAR_W, BAR_H, BAR_PITCH = 317, 38, 48
+
+local QUESTION_MARK = "Interface\\Icons\\INV_Misc_QuestionMark"
+
+-- ── Item tiles ──────────────────────────────────────────────────────────────────────────────────
+
+-- Unlearned entries read as unavailable. Since §H.3.21 they are only ever LISTED with Show Unlearned
+-- on, so this tint is that setting's whole visual payload rather than a permanent fixture of the
+-- catalog — which is what it had degenerated into.
+--
+-- Asked through IsSpellLearned rather than IsTrackable for the reason SettingsAdapter's catalog gate
+-- gives: IsTrackable waves through every id outside the curated tables, so on a picker row it says
+-- "learned" for most of the arsenal and the tint lands on an arbitrary subset.
+local function applyLearnedTint(item)
+ local known = (not M.IsSpellLearned) or M.IsSpellLearned(item.spellID)
+ item._unlearned = not known
+ if item._unlearned then
+ item.Icon:SetDesaturated(true)
+ item.Icon:SetVertexColor(1.0, 0.4, 0.4)
+ else
+ item.Icon:SetDesaturated(false)
+ item.Icon:SetVertexColor(1, 1, 1)
+ end
+end
+
+-- Tiles are POOLED and never destroyed, so a tile that held a trinket last rebuild can be handed a
+-- plain spell this one. Every equip field has to be dropped on that transition or the stale token
+-- keeps routing the row's right-click and drag through the equip path — which would silently
+-- reassign a trinket the player is no longer even looking at.
+local function clearEquipBinding(item)
+ item.token, item._iconItemID, item._equipHidden = nil, nil, nil
+ item._aura = nil
+end
+
+-- Shared by both tile shapes: an equip row shows the real ITEM icon (async — nil until the server
+-- caches it, re-pulled on GET_ITEM_INFO_RECEIVED), never a learn tint (you either have the trinket
+-- equipped or it is not in the list at all), and greys when the player has hidden it.
+--
+-- The label is the use-spell's name, which is what a 38px tile can actually carry.
+local function setEquipEntry(self, entry)
+ self._aura = nil -- pooled tiles: see clearEquipBinding
+ self.spellID = entry.spellID
+ self.token = entry.token
+ self._iconItemID = entry.itemID
+ self._equipHidden = entry.hidden and true or false
+ self._unlearned = false
+
+ self.spellName = entry.label or (entry.spellID and GetSpellInfo(entry.spellID)) or ""
+ local icon = (M.ResolveItemIcon and M.ResolveItemIcon(entry.itemID))
+ or (entry.spellID and select(3, GetSpellInfo(entry.spellID)))
+ self.Icon:SetTexture(icon or QUESTION_MARK)
+ self.Icon:SetDesaturated(self._equipHidden)
+ self.Icon:SetVertexColor(1, 1, 1)
+ if self.Label then self.Label:SetText(self.spellName) end
+ if CDS._applyAlertBadge then CDS._applyAlertBadge(self) end
+end
+
+-- An aura row (Phase 7b). Three states worth telling apart on sight, because each answers a
+-- different question the player is asking:
+--
+-- assigned you put it here plain
+-- auto the viewer is deciding this one — drag it to take over desaturated
+-- untalented a spec-gated catalog row, visible only via Show Unlearned red, like a spell you
+-- have not learned
+--
+-- The icon comes from the entry rather than GetSpellInfo, because the seen registry stores one: an
+-- aura this client only ever hands us as an aura may have no spellbook entry to look up.
+local function setAuraEntry(self, entry)
+ clearEquipBinding(self)
+ self._aura = entry
+ self.spellID = entry.spellID
+ self.spellName = entry.label or ""
+ self._unlearned = entry.untalented and true or false
+
+ self.Icon:SetTexture(entry.icon or QUESTION_MARK)
+ if entry.untalented then
+ self.Icon:SetDesaturated(true)
+ self.Icon:SetVertexColor(1.0, 0.4, 0.4)
+ else
+ self.Icon:SetDesaturated(entry.auto and true or false)
+ self.Icon:SetVertexColor(1, 1, 1)
+ end
+ if self.Label then
+ self.Label:SetText(self.spellName ~= "" and self.spellName
+ or ("Aura " .. tostring(entry.spellID or "?")))
+ end
+ if CDS._applyAlertBadge then CDS._applyAlertBadge(self) end
+end
+
+local function itemOnEnter(self)
+ if not (self.spellID or self.token) then return end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ -- An equip row shows the ITEM tooltip: for a trinket that is the on-use text plus the equip
+ -- bonuses, which is what the player is choosing between. The use-spell tooltip alone would drop
+ -- the half of the trinket that is not the cooldown.
+ if self._iconItemID and GameTooltip.SetItemByID then
+ GameTooltip:SetItemByID(self._iconItemID)
+ elseif self._iconItemID and GameTooltip.SetHyperlink then
+ GameTooltip:SetHyperlink("item:" .. self._iconItemID)
+ elseif M.TooltipSetSpellNamed then
+ -- NAMED, because this is the surface where the failure hurts most: a 38px grid tile has no
+ -- label of its own, so a tooltip the client could not fill leaves the row unidentifiable. Most
+ -- of the catalog's aura ids cannot be hyperlinked on 3.3.5a.
+ M.TooltipSetSpellNamed(GameTooltip, self.spellID, self.spellName)
+ end
+ if self._unlearned then
+ -- For an aura the gate is a talent, not a level, so say which one. "Not yet learned" on a proc
+ -- you cannot ever learn without respeccing tells the player nothing they can act on.
+ if self._aura and self._aura.talent then
+ GameTooltip:AddLine("Requires the " .. self._aura.talent .. " talent", 1, 0.3, 0.3)
+ else
+ GameTooltip:AddLine("Not yet learned", 1, 0.3, 0.3)
+ end
+ end
+ if self._aura then
+ if self._aura.dur then
+ GameTooltip:AddLine(("Lasts %s sec"):format(tostring(self._aura.dur)), 0.7, 0.7, 0.7)
+ end
+ if self._aura.auto then
+ GameTooltip:AddLine("Tracked automatically. Drag it into a section to pin it there.",
+ 0.6, 0.8, 1)
+ end
+ end
+ if self.token then
+ GameTooltip:AddLine(self._equipHidden and "Not displayed on any viewer"
+ or "Drag onto Essential or Utility to track it.", 0.6, 0.8, 1)
+ end
+ -- What the corner badge means for this tile. Provided by SettingsMenu (4b-3).
+ if CDS._itemTooltipExtra then CDS._itemTooltipExtra(self, GameTooltip) end
+ GameTooltip:Show()
+end
+
+local function itemOnLeave() GameTooltip:Hide() end
+
+local function itemOnClick(self, button)
+ if CDS.OnItemClick then CDS.OnItemClick(self, button) end
+end
+
+local function wireItem(b)
+ b:SetScript("OnEnter", itemOnEnter)
+ b:SetScript("OnLeave", itemOnLeave)
+ b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
+ b:SetScript("OnClick", itemOnClick)
+ -- Left-drag is retail's primary way to reorder and reassign; the right-click menu is the
+ -- keyboard-free alternative. SettingsReorder (4b-4) provides BeginDrag.
+ b:RegisterForDrag("LeftButton")
+ b:SetScript("OnDragStart", function(self)
+ if CDS.BeginDrag then CDS.BeginDrag(self) end
+ end)
+end
+
+local function makeIconItem(parent)
+ local b = CreateFrame("Button", nil, parent)
+ b:SetSize(ICON, ICON)
+
+ b.Icon = b:CreateTexture(nil, "ARTWORK")
+ b.Icon:SetAllPoints()
+ b.Icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) -- same crop the live viewer uses
+
+ local hl = b:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetAllPoints()
+ hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
+ hl:SetBlendMode("ADD")
+
+ function b:SetSpell(spellID)
+ clearEquipBinding(self)
+ self.spellID = spellID
+ local name, _, icon = GetSpellInfo(spellID)
+ self.spellName = name
+ self.Icon:SetTexture(icon or QUESTION_MARK)
+ applyLearnedTint(self)
+ if CDS._applyAlertBadge then CDS._applyAlertBadge(self) end
+ end
+
+ b.SetEquipEntry = setEquipEntry
+ b.SetAuraEntry = setAuraEntry
+
+ wireItem(b)
+ return b
+end
+
+local function makeBarItem(parent)
+ local b = CreateFrame("Button", nil, parent)
+ b:SetSize(BAR_W, BAR_H)
+
+ local bg = b:CreateTexture(nil, "BACKGROUND")
+ bg:SetAllPoints()
+ bg:SetTexture(0, 0, 0, 0.35)
+
+ b.Icon = b:CreateTexture(nil, "ARTWORK")
+ b.Icon:SetSize(BAR_H - 4, BAR_H - 4)
+ b.Icon:SetPoint("LEFT", b, "LEFT", 2, 0)
+ b.Icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
+
+ b.Label = b:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+ b.Label:SetPoint("LEFT", b.Icon, "RIGHT", 8, 0)
+ b.Label:SetPoint("RIGHT", b, "RIGHT", -6, 0)
+ b.Label:SetJustifyH("LEFT")
+
+ -- A WIDE-ROW highlight, not the square one the icon tiles use. ButtonHilight-Square is a 64x64
+ -- glow drawn for a square button, and stretching it across a 344px row smears it into the
+ -- lopsided blue wash that was reported — bright over the icon, bleeding away to the right.
+ -- UI-QuestTitleHighlight is built to stretch horizontally, and is what every other wide list row
+ -- in this addon uses (character Sidebar, EquipmentManagerPane, TitlesPane), at the same alpha.
+ local hl = b:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetAllPoints()
+ hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
+ hl:SetBlendMode("ADD")
+ hl:SetAlpha(0.4)
+
+ function b:SetSpell(spellID)
+ clearEquipBinding(self)
+ self.spellID = spellID
+ local name, _, icon = GetSpellInfo(spellID)
+ self.spellName = name
+ self.Icon:SetTexture(icon or QUESTION_MARK)
+ self.Label:SetText(name or ("Spell " .. tostring(spellID)))
+ applyLearnedTint(self)
+ if CDS._applyAlertBadge then CDS._applyAlertBadge(self) end
+ end
+
+ b.SetEquipEntry = setEquipEntry
+ b.SetAuraEntry = setAuraEntry
+
+ wireItem(b)
+ return b
+end
+
+-- ── Category section ────────────────────────────────────────────────────────────────────────────
+
+-- Inline collapsible header. NE.listheader does not exist here; this is the same substitution
+-- modules/character/Reputation.lua makes, with the client's own +/- collapse buttons.
+local function makeHeader(parent, onToggle)
+ local h = CreateFrame("Button", nil, parent)
+ h:SetHeight(HEADER_H)
+
+ local bg = h:CreateTexture(nil, "BACKGROUND")
+ bg:SetAllPoints()
+ bg:SetTexture(1, 1, 1, 0.06)
+
+ h.Toggle = h:CreateTexture(nil, "ARTWORK")
+ h.Toggle:SetSize(16, 16)
+ h.Toggle:SetPoint("LEFT", h, "LEFT", 4, 0)
+
+ h.Text = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h.Text:SetPoint("LEFT", h.Toggle, "RIGHT", 4, 0)
+
+ h.Count = h:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
+ h.Count:SetPoint("RIGHT", h, "RIGHT", -6, 0)
+
+ h:SetScript("OnClick", onToggle)
+
+ function h:SetExpanded(on)
+ self.Toggle:SetTexture(on and "Interface\\Buttons\\UI-MinusButton-Up"
+ or "Interface\\Buttons\\UI-PlusButton-Up")
+ end
+ return h
+end
+
+local function makeCategory(parent, kind)
+ local c = CreateFrame("Frame", nil, parent)
+ c:SetWidth(CAT_W)
+ c.kind = kind
+ c._factory = (kind == "bar") and makeBarItem or makeIconItem
+ c.items = {}
+ c._expanded = true
+ c._count = 0
+
+ c.header = makeHeader(c, function() c:Toggle() end)
+ c.header:SetPoint("TOPLEFT")
+ c.header:SetPoint("TOPRIGHT")
+
+ c.container = CreateFrame("Frame", nil, c)
+ c.container:SetPoint("TOPLEFT", c.header, "BOTTOMLEFT", CONTAINER_X, CONTAINER_Y)
+ c.container:SetWidth(kind == "bar" and BAR_W or (STRIDE * PITCH - PAD))
+
+ c.empty = c:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
+ c.empty:SetPoint("TOPLEFT", c.container, "TOPLEFT", 2, -2)
+ -- Bounded and left-aligned: the empty text is now a sentence rather than "(empty)", and an
+ -- unbounded string would run past the panel's right edge instead of wrapping.
+ c.empty:SetWidth(CAT_W - 30)
+ c.empty:SetJustifyH("LEFT")
+ c.empty:SetText("(empty)")
+ c.empty:Hide()
+
+ function c:Relayout()
+ local n = self._count
+ local contH
+ if self.kind == "bar" then
+ contH = (n > 0) and (n * BAR_PITCH - (BAR_PITCH - BAR_H)) or 0
+ else
+ local rows = math.ceil(n / STRIDE)
+ contH = (rows > 0) and (rows * PITCH - PAD) or 0
+ end
+ if n == 0 then contH = 14 end
+ self.container:SetHeight(math.max(1, contH))
+
+ if self._expanded then self.container:Show() else self.container:Hide() end
+ if n == 0 and self._expanded then self.empty:Show() else self.empty:Hide() end
+
+ for i, item in ipairs(self.items) do
+ if i <= n and self._expanded then
+ item:ClearAllPoints()
+ if self.kind == "bar" then
+ item:SetPoint("TOPLEFT", self.container, "TOPLEFT", 0, -(i - 1) * BAR_PITCH)
+ else
+ local col, row = (i - 1) % STRIDE, math.floor((i - 1) / STRIDE)
+ item:SetPoint("TOPLEFT", self.container, "TOPLEFT", col * PITCH, -row * PITCH)
+ end
+ item:Show()
+ else
+ item:Hide()
+ end
+ end
+
+ self.header:SetExpanded(self._expanded)
+ self:SetHeight(HEADER_H + (self._expanded and (math.abs(CONTAINER_Y) + self.container:GetHeight()) or 0))
+ end
+
+ function c:Toggle()
+ self._expanded = not self._expanded
+ self:Relayout()
+ if CDS.RestackCategories then CDS.RestackCategories() end
+ end
+
+ -- Fill from a MIXED list: a number is a spellID, a table is an equip entry (see SettingsAdapter's
+ -- equipEntry). Tiles are pooled — the pool only ever grows, and surplus tiles are hidden by
+ -- Relayout rather than destroyed.
+ function c:SetItems(ids)
+ self._count = #ids
+ for i, id in ipairs(ids) do
+ local item = self.items[i]
+ if not item then
+ item = self._factory(self.container)
+ self.items[i] = item
+ end
+ item._catID = self._catID
+ if type(id) == "table" then
+ -- Two table shapes now reach here: an aura row (SettingsAdapter's auraRow) and an equip
+ -- entry. They are dispatched on a marker field rather than by sniffing for `token`, because
+ -- an aura row legitimately has neither a token nor an itemID and would take the equip path.
+ if id.aura then item:SetAuraEntry(id) else item:SetEquipEntry(id) end
+ else
+ item:SetSpell(id)
+ end
+ end
+ self.header.Count:SetText(tostring(#ids))
+ self:Relayout()
+ end
+
+ return c
+end
+
+-- ── Panel wiring ────────────────────────────────────────────────────────────────────────────────
+
+local categories = {} -- catID -> frame
+
+-- Stack the visible sections top-down and size the scroll child to match, so the scrollbar knows
+-- how far it can go.
+function CDS.RestackCategories()
+ local panel = CDS.panel
+ if not panel then return end
+
+ local mode = CDS.GetDisplayMode() or "spells"
+ local y = 0
+ local prev
+ for _, catID in ipairs(Adapter.MODE_ORDER[mode] or {}) do
+ local c = categories[catID]
+ if c and c._active then
+ c:ClearAllPoints()
+ if prev then
+ c:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -CAT_GAP)
+ else
+ c:SetPoint("TOPLEFT", panel.content, "TOPLEFT", 0, 0)
+ end
+ c:Show()
+ y = y + c:GetHeight() + CAT_GAP
+ prev = c
+ end
+ end
+ panel.content:SetHeight(math.max(1, y))
+end
+
+function CDS.RefreshLayout()
+ local panel = CDS.panel
+ if not panel then return end
+
+ local _, class = UnitClass("player")
+ local mode = CDS.GetDisplayMode() or "spells"
+
+ -- The settings tab is not a grid of anything. Its page is a sibling scroll child owned by
+ -- SettingsOptions.lua, so re-reading it is a refresh, not a rebuild — and the panel's event handler
+ -- calls this on every SPELL_UPDATE_ICON, which would otherwise deactivate and re-source every
+ -- category behind a page the player is not looking at.
+ if mode == "settings" then
+ if CDS.RefreshSettingsPage then CDS.RefreshSettingsPage() end
+ return
+ end
+
+ -- Deactivate everything first, so a category that belongs to the other tab can't linger.
+ for _, c in pairs(categories) do c._active = false; c:Hide() end
+
+ for _, catID in ipairs(Adapter.MODE_ORDER[mode] or {}) do
+ local items = Adapter.GetItems(catID, class)
+ -- An EMPTY source pool is skipped outright rather than shown as "(empty)". A player with no
+ -- on-use trinket equipped should not be told about a Trinkets section at all — it would read as
+ -- a broken feature rather than an absent input. Stored categories still show when empty,
+ -- because there an empty list is a state the player put them in.
+ if not (Adapter.IsSourcePool and Adapter.IsSourcePool(catID) and #items == 0) then
+ local c = categories[catID]
+ if not c then
+ c = makeCategory(panel.content, Adapter.Kind(catID))
+ c._catID = catID
+ c.header.Text:SetText(Adapter.Label(catID))
+ if Adapter.EmptyText then c.empty:SetText(Adapter.EmptyText(catID)) end
+ categories[catID] = c
+ end
+ c._active = true
+ c:SetItems(items)
+ end
+ end
+
+ CDS.RestackCategories()
+ -- Through GetSearchText, never the raw edit box — its idle text is the "Search" placeholder.
+ if CDS.ApplyItemFilter and CDS.GetSearchText then CDS.ApplyItemFilter(CDS.GetSearchText()) end
+end
+
+-- Search DIMS non-matching tiles rather than reflowing the grid — retail's behaviour, and it keeps
+-- an item's position stable while you type.
+function CDS.ApplyItemFilter(text)
+ text = (text or ""):lower()
+ local blank = (text == "")
+ for _, c in pairs(categories) do
+ if c._active then
+ for i, item in ipairs(c.items) do
+ if i <= c._count then
+ local match = blank or ((item.spellName or ""):lower():find(text, 1, true) ~= nil)
+ item:SetAlpha(match and 1 or 0.25)
+ end
+ end
+ end
+ end
+end
+
+CDS._categories = categories -- test seam
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsControls.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsControls.lua
new file mode 100644
index 0000000..dacab59
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsControls.lua
@@ -0,0 +1,691 @@
+-- DragonUI_NewEra/modules/cooldownviewer/SettingsControls.lua — the control kit behind the /cdm
+-- Settings tab.
+--
+-- WHY A KIT, AND NOT DragonUI's PanelControls. The options-tab helpers (Controls:AddToggle /
+-- AddSlider / AddDropdown in DragonUI_Options/panel/controls.lua) are AceGUI widgets: they only lay
+-- themselves out inside an AceGUI container, and they call `parent:AddChild(...)`. Our panel body is
+-- a plain ScrollFrame over a plain Frame, so there is no container to hand them — and DragonUI is
+-- read-only to us (CONTRACTS §0), so teaching its controls to render into a raw frame is not on the
+-- table. This is the three widgets the settings page actually needs, over the client's own
+-- templates: UICheckButtonTemplate, OptionsSliderTemplate and a menu button through core/Menu.lua.
+--
+-- NO WowStyle1DropdownTemplate, for the same reason the footer has none (PORT_PLAN §G.11): it is
+-- retail-only. A dropdown here is a plain button labelled with the current value that opens a radio
+-- menu — the widget differs, the behaviour does not.
+--
+-- SHAPE. `Kit.New(parent, width)` returns a column with a y cursor. Every Add* appends a
+-- fixed-height row and returns it; `col:Relayout()` stacks the visible rows and sizes `parent` so
+-- the scrollbar learns the extent. Sections are collapsible, and a collapsed section's rows are
+-- HIDDEN rather than destroyed — the same pooling contract the category grids use, and what makes
+-- collapsing free of any rebuild.
+--
+-- EVERY CONTROL KEEPS A `refresh` CLOSURE that re-reads its own getter, and `col:Refresh()` runs all
+-- of them. That is what keeps the page honest when a setting changes from somewhere that is not this
+-- page: a layout apply, the master toggle in DragonUI's options, a reset. A page that only wrote
+-- would drift silently, and a stale checkbox is indistinguishable from a setting that did not take.
+--
+-- Taint: plain frames and SavedVariables writes. Nothing here is secure or protected.
+
+local NE = DragonUI_NewEra
+
+NE.cooldownviewersettings = NE.cooldownviewersettings or {}
+local CDS = NE.cooldownviewersettings
+
+local Kit = {}
+CDS.controls = Kit
+
+local ROW_INDENT = 6 -- rows inside a section sit in a little from the header
+local HEADER_H = 24
+local CHECK_H = 26
+local SLIDER_H = 46
+local DROPDOWN_H = 28
+local BUTTON_H = 30
+local SECTION_GAP = 10
+
+-- OptionsSliderTemplate finds its Low/High/Text FontStrings through $parent name lookups, so every
+-- slider needs a global name. UIPanelButtonTemplate does not, but naming both keeps /framestack
+-- readable when something is mis-anchored.
+local serial = 0
+local function nextName(kind)
+ serial = serial + 1
+ return "NE_CDMSetting" .. kind .. serial
+end
+
+local function tip(frame, title, text)
+ if not title then return end
+ frame:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText(title)
+ if text then GameTooltip:AddLine(text, 1, 1, 1, true) end
+ GameTooltip:Show()
+ end)
+ frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
+end
+
+local function labelFor(values, v)
+ for _, e in ipairs(values) do
+ if e[1] == v then return e[2] end
+ end
+ return tostring(v)
+end
+
+-- Slider values arrive continuous from a drag on this client — SetObeyStepOnDrag is retail-only, and
+-- SetValueStep alone only governs the arrow keys. So the step is applied here, on the way in, which
+-- is what Blizzard's own option sliders do.
+local function snap(v, step, minV)
+ if not step or step <= 0 then return v end
+ return minV + math.floor((v - minV) / step + 0.5) * step
+end
+
+-- ── Column ──────────────────────────────────────────────────────────────────────────────────────
+
+local Column = {}
+Column.__index = Column
+
+-- A THEME, not a second kit. The Settings tab is a scrolling page of sections; the edit-mode dialog
+-- is a fixed panel of 32px rows transcribed from retail's EditModeSystemSettingsDialog. Same widgets,
+-- different metrics and fonts — so the differences live in one table the caller passes, and every
+-- default here is exactly what the tab already had.
+local DEFAULT_THEME = {
+ labelFont = "GameFontHighlightSmall",
+ checkSize = 24,
+ checkH = CHECK_H,
+ sliderH = DROPDOWN_H,
+ dropH = DROPDOWN_H,
+ labelW = 96, -- the control column's left edge (compact slider + art dropdown)
+ controlW = nil, -- fixed control width; nil = fill the control column
+ dropdownArt = false, -- true = the modern textholder + arrow, instead of a red panel button
+ -- The BigRedThreeSlice is the addon's standard button (core/ButtonSkin.lua), so this is ON by
+ -- default and the flag exists to opt a row OUT, not in.
+ buttonArt = true,
+ buttonH = nil, -- row height for AddButton; nil = BUTTON_H
+ buttonArtH = nil, -- the button's own height inside that row; nil = 22
+}
+
+function Kit.New(parent, width, theme)
+ local t = {}
+ for k, v in pairs(DEFAULT_THEME) do t[k] = v end
+ for k, v in pairs(theme or {}) do t[k] = v end
+ local col = setmetatable({
+ frame = parent,
+ width = width or 330,
+ theme = t,
+ entries = {}, -- ordered rows: { frame =, h =, section =, gap =, indent = }
+ sections = {},
+ refreshers = {},
+ }, Column)
+ return col
+end
+
+function Column:_add(frame, h, opts)
+ opts = opts or {}
+ self.entries[#self.entries + 1] = {
+ frame = frame,
+ h = h,
+ -- A row belongs to whatever section was opened last. Rows added before the first AddSection
+ -- (the page's intro text) have no section and are therefore never collapsible.
+ section = opts.standalone and nil or self._section,
+ gap = opts.gap or 0,
+ indent = opts.indent or (self._section and ROW_INDENT or 0),
+ }
+ return frame
+end
+
+function Column:_row(kind, h, opts)
+ local f = CreateFrame(kind or "Frame", nil, self.frame)
+ f:SetHeight(h)
+ f:SetWidth(self.width - ((opts and opts.indent) or (self._section and ROW_INDENT or 0)))
+ self:_add(f, h, opts)
+ return f
+end
+
+function Column:Relayout()
+ local y = 0
+ for _, e in ipairs(self.entries) do
+ local visible = (not e.section) or e.section.expanded
+ if visible then
+ y = y + e.gap
+ e.frame:ClearAllPoints()
+ e.frame:SetPoint("TOPLEFT", self.frame, "TOPLEFT", e.indent, -y)
+ e.frame:Show()
+ y = y + e.h
+ else
+ e.frame:Hide()
+ end
+ end
+ -- The trailing pad stops the last row butting against the footer.
+ self.frame:SetHeight(math.max(1, y + 10))
+ self._height = y
+ return y
+end
+
+function Column:Refresh()
+ for _, fn in ipairs(self.refreshers) do fn() end
+end
+
+-- ── Section header ──────────────────────────────────────────────────────────────────────────────
+-- Same look as the category headers next door (a faint bar with the client's own +/- glyph), because
+-- the two tabs are the same window and a settings section that styled itself differently would read
+-- as belonging to a different addon.
+
+function Column:AddSection(title, expanded)
+ local section = { expanded = expanded and true or false, title = title }
+ self.sections[#self.sections + 1] = section
+
+ self._section = nil -- the header itself is never inside the section it opens
+ local h = self:_row("Button", HEADER_H, { gap = SECTION_GAP, indent = 0 })
+ self._section = section
+ section.header = h
+
+ local bg = h:CreateTexture(nil, "BACKGROUND")
+ bg:SetAllPoints()
+ bg:SetTexture(1, 1, 1, 0.06)
+
+ h.Toggle = h:CreateTexture(nil, "ARTWORK")
+ h.Toggle:SetSize(16, 16)
+ h.Toggle:SetPoint("LEFT", h, "LEFT", 4, 0)
+
+ h.Text = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ h.Text:SetPoint("LEFT", h.Toggle, "RIGHT", 4, 0)
+ h.Text:SetText(title)
+
+ local function paint()
+ h.Toggle:SetTexture(section.expanded and "Interface\\Buttons\\UI-MinusButton-Up"
+ or "Interface\\Buttons\\UI-PlusButton-Up")
+ end
+ paint()
+
+ h:SetScript("OnClick", function()
+ section.expanded = not section.expanded
+ paint()
+ self:Relayout()
+ end)
+
+ section.SetExpanded = function(_, on)
+ section.expanded = on and true or false
+ paint()
+ end
+ return section
+end
+
+-- Ends the current section, so a following row is top-level again.
+function Column:EndSection()
+ self._section = nil
+end
+
+-- ── Description text ────────────────────────────────────────────────────────────────────────────
+
+function Column:AddText(text, opts)
+ opts = opts or {}
+ local indent = opts.indent or (self._section and ROW_INDENT or 0)
+ local f = CreateFrame("Frame", nil, self.frame)
+ f:SetWidth(self.width - indent)
+
+ local fs = f:CreateFontString(nil, "ARTWORK", opts.font or "GameFontDisableSmall")
+ fs:SetPoint("TOPLEFT")
+ fs:SetWidth(self.width - indent - 4)
+ fs:SetJustifyH("LEFT")
+ fs:SetText(text)
+
+ -- GetStringHeight is only meaningful once the text and width are set; the floor covers the offline
+ -- harness, whose FontString stub cannot measure.
+ local h = math.max(14, (fs.GetStringHeight and fs:GetStringHeight() or 0) + 2)
+ f:SetHeight(h)
+ f.Text = fs
+ self:_add(f, h, { gap = opts.gap or 2, indent = indent })
+ return f
+end
+
+-- ── Checkbox ────────────────────────────────────────────────────────────────────────────────────
+-- o = { label, desc, get, set, onChanged }
+
+function Column:AddCheckbox(o)
+ local th = self.theme
+ local row = self:_row("Button", th.checkH)
+
+ local cb = CreateFrame("CheckButton", nextName("Check"), row, "UICheckButtonTemplate")
+ cb:SetSize(th.checkSize, th.checkSize)
+ cb:SetPoint("LEFT", row, "LEFT", 0, 0)
+
+ local label = row:CreateFontString(nil, "ARTWORK", th.labelFont)
+ label:SetPoint("LEFT", cb, "RIGHT", 2, 0)
+ label:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ label:SetJustifyH("LEFT")
+ label:SetText(o.label or "")
+
+ local function refresh()
+ cb:SetChecked(o.get and o.get() and true or false)
+ end
+
+ local function apply(on)
+ cb:SetChecked(on)
+ if o.set then o.set(on) end
+ if PlaySound then
+ PlaySound(on and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff")
+ end
+ if o.onChanged then o.onChanged(on) end
+ end
+
+ -- UICheckButtonTemplate has already flipped its own state by the time OnClick runs, so the box
+ -- reports the NEW value; the row has not, so it flips the current one. Both land in `apply`, which
+ -- is the only thing that writes.
+ cb:SetScript("OnClick", function(self) apply(self:GetChecked() and true or false) end)
+ row:SetScript("OnClick", function() apply(not (cb:GetChecked() and true or false)) end)
+
+ tip(cb, o.label, o.desc)
+ refresh()
+ self.refreshers[#self.refreshers + 1] = refresh
+ row.Check, row.Label, row.Refresh = cb, label, refresh
+ return row
+end
+
+-- ── Slider ──────────────────────────────────────────────────────────────────────────────────────
+-- o = { label, desc, min, max, step, get, set, format }
+
+function Column:AddSlider(o)
+ local row = self:_row("Frame", SLIDER_H)
+ local minV, maxV, step = o.min or 0, o.max or 100, o.step or 1
+
+ local label = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
+ label:SetPoint("TOPLEFT", row, "TOPLEFT", 2, -2)
+ label:SetJustifyH("LEFT")
+ label:SetText(o.label or "")
+
+ local value = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
+ value:SetPoint("TOPRIGHT", row, "TOPRIGHT", -4, -2)
+ value:SetJustifyH("RIGHT")
+
+ local name = nextName("Slider")
+ local sl = CreateFrame("Slider", name, row, "OptionsSliderTemplate")
+ sl:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -20)
+ sl:SetWidth(self.width - (self._section and ROW_INDENT or 0) - 24)
+ sl:SetMinMaxValues(minV, maxV)
+ sl:SetValueStep(step)
+ if sl.SetObeyStepOnDrag then sl:SetObeyStepOnDrag(true) end
+
+ -- The template's own three FontStrings: $parentText sits centred ABOVE the bar, which would
+ -- duplicate our label, so it is blanked. Low/High carry the range.
+ local function templateText(suffix, text)
+ local fs = _G[name .. suffix]
+ if fs and fs.SetText then fs:SetText(text) end
+ end
+ templateText("Text", "")
+ templateText("Low", tostring(minV))
+ templateText("High", tostring(maxV))
+
+ local function fmt(v)
+ if o.format then return o.format(v) end
+ return tostring(v)
+ end
+
+ local function refresh()
+ local cur = snap(o.get and o.get() or minV, step, minV)
+ sl._neSuppress = true
+ sl:SetValue(cur)
+ sl._neSuppress = false
+ sl._neVal = cur
+ value:SetText(fmt(cur))
+ end
+
+ sl:SetScript("OnValueChanged", function(self, raw)
+ if self._neSuppress then return end
+ local v = snap(raw or minV, step, minV)
+ if v < minV then v = minV elseif v > maxV then v = maxV end
+ -- Re-seat the thumb on the snapped value. Guarded, or this SetValue re-enters us.
+ if v ~= raw then
+ self._neSuppress = true
+ self:SetValue(v)
+ self._neSuppress = false
+ end
+ value:SetText(fmt(v))
+ -- Only WRITE on a real change. A drag fires this continuously, and every write re-runs the
+ -- viewer's RefreshLayout — which relays out every icon.
+ if self._neVal ~= v then
+ self._neVal = v
+ if o.set then o.set(v) end
+ if o.onChanged then o.onChanged(v) end
+ end
+ end)
+
+ -- The minimal bar: retail's MinimalSliderWithSteppers art, rebuilt piece by piece in
+ -- core/ScrollbarReskin.lua. Scripts are none of its business — it swaps textures and sizes the
+ -- frame — so it can sit here rather than dodging tip()'s SetScript below.
+ if NE.scrollbar and NE.scrollbar.SkinSlider then pcall(NE.scrollbar.SkinSlider, sl) end
+
+ tip(sl, o.label, o.desc)
+ refresh()
+ self.refreshers[#self.refreshers + 1] = refresh
+ row.Slider, row.Label, row.Value, row.Refresh = sl, label, value, refresh
+ return row
+end
+
+-- ── Compact slider ──────────────────────────────────────────────────────────────────────────────
+-- o = { label, desc, min, max, step, get, set, format, labelWidth }
+--
+-- One line: label, a nudge arrow, the bar, the other arrow, the value. The tab's tall two-line slider
+-- is right for a page you scroll; it is wrong for the edit-mode dialog, which sits ON the screen next
+-- to the frame it edits and has to stay small enough to see past.
+--
+-- THE ARROWS ARE NOT DECORATION. A drag on this client reports continuous values (SetObeyStepOnDrag is
+-- retail-only), so landing on an exact 65% by hand is luck. The arrows step by exactly `step`, which is
+-- the only precise way to set one of these — retail's edit-mode sliders have them for the same reason.
+
+local ARROW_W, VALUE_W, LABEL_W = 18, 44, 96
+
+function Column:AddCompactSlider(o)
+ local th = self.theme
+ local row = self:_row("Frame", th.sliderH)
+ local minV, maxV, step = o.min or 0, o.max or 100, o.step or 1
+ local labelW = o.labelWidth or th.labelW or LABEL_W
+ local rowW = self.width - (self._section and ROW_INDENT or 0)
+
+ local label = row:CreateFontString(nil, "ARTWORK", th.labelFont)
+ label:SetPoint("LEFT", row, "LEFT", 2, 0)
+ label:SetWidth(labelW)
+ label:SetJustifyH("LEFT")
+ label:SetText(o.label or "")
+
+ local value = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
+ value:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ value:SetWidth(VALUE_W)
+ value:SetJustifyH("RIGHT")
+
+ local name = nextName("CSlider")
+ local sl = CreateFrame("Slider", name, row, "OptionsSliderTemplate")
+ sl:SetPoint("LEFT", row, "LEFT", labelW + ARROW_W + 6, 0)
+ sl:SetWidth(math.max(40, rowW - labelW - VALUE_W - (ARROW_W * 2) - 14))
+ sl:SetMinMaxValues(minV, maxV)
+ sl:SetValueStep(step)
+ if sl.SetObeyStepOnDrag then sl:SetObeyStepOnDrag(true) end
+
+ -- All three of the template's own FontStrings are blanked here: Low/High would collide with the
+ -- label and the value at this width, and Text sits centred above the bar with nowhere to go.
+ for _, suffix in ipairs({ "Text", "Low", "High" }) do
+ local fs = _G[name .. suffix]
+ if fs and fs.SetText then fs:SetText("") end
+ end
+
+ local function fmt(v)
+ if o.format then return o.format(v) end
+ return tostring(v)
+ end
+
+ local function refresh()
+ local cur = snap(o.get and o.get() or minV, step, minV)
+ sl._neSuppress = true
+ sl:SetValue(cur)
+ sl._neSuppress = false
+ sl._neVal = cur
+ value:SetText(fmt(cur))
+ end
+
+ local function commit(v)
+ if v < minV then v = minV elseif v > maxV then v = maxV end
+ value:SetText(fmt(v))
+ if sl._neVal ~= v then
+ sl._neVal = v
+ if o.set then o.set(v) end
+ if o.onChanged then o.onChanged(v) end
+ end
+ end
+
+ sl:SetScript("OnValueChanged", function(self, raw)
+ if self._neSuppress then return end
+ local v = snap(raw or minV, step, minV)
+ if v < minV then v = minV elseif v > maxV then v = maxV end
+ if v ~= raw then
+ self._neSuppress = true
+ self:SetValue(v)
+ self._neSuppress = false
+ end
+ commit(v)
+ end)
+
+ local function arrow(point, xOff, dir, up, down, dis, atlas)
+ local b = CreateFrame("Button", nil, row)
+ b:SetSize(ARROW_W, ARROW_W)
+ b:SetPoint(point, row, point, xOff, 0)
+ b:SetNormalTexture(up)
+ b:SetPushedTexture(down)
+ b:SetDisabledTexture(dis)
+
+ -- The bar's own steppers, off the same sheet as the bar (NE.scrollbar.SkinSlider): retail's
+ -- MinimalSliderWithSteppers ships the chevrons alongside the track and the diamond, and a
+ -- spellbook page-turn glyph six pixels from that bar is the mismatch this pass is about.
+ --
+ -- Set on TOP of the page-turn paths rather than instead of them: SetNormalTexture is what creates
+ -- the texture object in the first place, and a miss here leaves that original art in place rather
+ -- than a blank button. The BUTTON stays 18x18 — the row's arithmetic is written against ARROW_W
+ -- and the hit area wants to be square — so only the glyph takes the atlas's own size.
+ if atlas and NE.tex and NE.tex.SetAtlas then
+ local n = b:GetNormalTexture()
+ if n and NE.tex.SetAtlas(n, atlas, true) then
+ local p, dt = b:GetPushedTexture(), b:GetDisabledTexture()
+ NE.tex.SetAtlas(p, atlas, true)
+ if NE.tex.SetAtlas(dt, atlas, true) and dt.SetDesaturated then dt:SetDesaturated(true) end
+ local h = b:CreateTexture(nil, "HIGHLIGHT")
+ NE.tex.SetAtlas(h, atlas, true)
+ h:SetBlendMode("ADD")
+ h:SetAlpha(0.35)
+ -- Pressed is the glyph nudged a pixel down-right, which is what the client's own buttons do
+ -- and what this sheet leaves us — it carries one chevron per direction, no state variants.
+ for _, t in ipairs({ n, p, dt, h }) do
+ if t then
+ t:ClearAllPoints()
+ t:SetPoint("CENTER", b, "CENTER", (t == p) and 1 or 0, (t == p) and -1 or 0)
+ end
+ end
+ end
+ end
+
+ b:SetScript("OnClick", function()
+ -- Drive the slider rather than the store: SetValue fires OnValueChanged, which is the one
+ -- place that snaps, clamps and writes. Two paths into one setting is how they drift.
+ local cur = sl._neVal or minV
+ sl:SetValue(cur + dir * step)
+ if PlaySound then PlaySound("igMainMenuOptionCheckBoxOn") end
+ end)
+ return b
+ end
+
+ row.Left = arrow("LEFT", labelW + 2, -1,
+ "Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up",
+ "Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Down",
+ "Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Disabled",
+ "minimal_sliderbar_button_left")
+ row.Right = arrow("RIGHT", -(VALUE_W + 4), 1,
+ "Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up",
+ "Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down",
+ "Interface\\Buttons\\UI-SpellbookIcon-NextPage-Disabled",
+ "minimal_sliderbar_button_right")
+
+ -- The minimal bar — see AddSlider.
+ if NE.scrollbar and NE.scrollbar.SkinSlider then pcall(NE.scrollbar.SkinSlider, sl) end
+
+ tip(sl, o.label, o.desc)
+ refresh()
+ self.refreshers[#self.refreshers + 1] = refresh
+ row.Slider, row.Label, row.Value, row.Refresh = sl, label, value, refresh
+ return row
+end
+
+-- ── Dropdown ────────────────────────────────────────────────────────────────────────────────────
+-- o = { label, desc, values = { {value, label}, ... }, get, set }
+--
+-- `values` is an ORDERED array, not the value->label map the options tab takes: that map is fed to
+-- AceGUI, which sorts it for you, and a radio menu has to decide its own order. "Always / In Combat
+-- / Hidden" reads as a progression; alphabetised it does not.
+
+-- The modern trigger, assembled by hand. 3.3.5a has no WowStyle1DropdownTemplate, and retail's own
+-- version paints the body with a STATIC atlas and puts the gradient on the ARROW — so the states
+-- below are arrow art, and the body has none. Built from a bare Button rather than reskinning
+-- UIPanelButtonTemplate: fighting a template's own art for a look this different is more code than
+-- the four textures it takes to draw it.
+-- The body is THREE textures, not one. See the atlas note in core/NineSliceLayouts.lua: the source is
+-- a 54x41 rounded rect with a 12px bevel, and stretching that to a 200x26 row smeared the corners
+-- sideways and squashed the bevel — which is exactly what "the dropdowns are formatted very weirdly"
+-- was looking at. The caps keep the corner aspect (their width follows the row height), and only the
+-- flat middle stretches, which is a plain vertical border run and so stretches invisibly.
+local BODY_CAP_W, BODY_CAP_H = 18, 41 -- native size of one cap in the atlas
+
+local function bodyPieces(btn, h)
+ local capW = BODY_CAP_W * (h / BODY_CAP_H)
+ btn._bodyLeft:SetWidth(capW)
+ btn._bodyRight:SetWidth(capW)
+end
+
+local function buildModernTrigger(row, name, w, h)
+ local btn = CreateFrame("Button", name, row)
+ btn:SetSize(w, h)
+
+ local left = btn:CreateTexture(nil, "BACKGROUND")
+ left:SetPoint("TOPLEFT")
+ left:SetPoint("BOTTOMLEFT")
+ NE.tex.SetAtlas(left, "common-dropdown-textholder-left", false)
+
+ local right = btn:CreateTexture(nil, "BACKGROUND")
+ right:SetPoint("TOPRIGHT")
+ right:SetPoint("BOTTOMRIGHT")
+ NE.tex.SetAtlas(right, "common-dropdown-textholder-right", false)
+
+ local body = btn:CreateTexture(nil, "BACKGROUND")
+ body:SetPoint("TOPLEFT", left, "TOPRIGHT")
+ body:SetPoint("BOTTOMRIGHT", right, "BOTTOMLEFT")
+ NE.tex.SetAtlas(body, "common-dropdown-textholder-center", false)
+
+ btn._bodyLeft, btn._bodyRight = left, right
+ bodyPieces(btn, h)
+ -- The caps are sized from the height, so they have to follow it. Dialog rows are fixed today, but a
+ -- theme that changed dropH would otherwise silently keep the old cap width.
+ btn:HookScript("OnSizeChanged", function(self) bodyPieces(self, self:GetHeight()) end)
+
+ -- Square, and inset far enough to sit INSIDE the field rather than straddling the right cap's
+ -- bevel. The atlas is 27x27; anything non-square here skews the chevron.
+ local arrow = btn:CreateTexture(nil, "ARTWORK")
+ arrow:SetSize(h - 6, h - 6)
+ arrow:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
+ NE.tex.SetAtlas(arrow, "common-dropdown-a-button", false)
+
+ local text = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+ text:SetPoint("LEFT", btn, "LEFT", 8, 0)
+ text:SetPoint("RIGHT", arrow, "LEFT", -2, 0)
+ text:SetJustifyH("LEFT")
+ btn.Text = text
+ -- A bare Button has no SetText/GetText of its own; the callers below (and every refresh) expect
+ -- the template's, so provide them rather than teaching each call site which kind it holds.
+ btn.SetText = function(self, s) self.Text:SetText(s) end
+ btn.GetText = function(self) return self.Text:GetText() end
+
+ btn._body, btn._arrow = body, arrow
+ btn.RefreshArt = function(self)
+ local suffix = ""
+ if self._neDown and self._neOver then suffix = "-pressedhover"
+ elseif self._neOver then suffix = "-hover"
+ elseif self._neDown then suffix = "-pressed" end
+ -- Recorded as well as applied: every state comes off ONE sheet, so the file path cannot say
+ -- which one is showing and a test would have nothing to read.
+ self._arrowAtlas = "common-dropdown-a-button" .. suffix
+ NE.tex.SetAtlas(self._arrow, self._arrowAtlas, false)
+ end
+ return btn
+end
+
+function Column:AddDropdown(o)
+ local th = self.theme
+ local row = self:_row("Frame", th.dropH)
+ local values = o.values or {}
+
+ local label = row:CreateFontString(nil, "ARTWORK", th.labelFont)
+ label:SetPoint("LEFT", row, "LEFT", 2, 0)
+ label:SetJustifyH("LEFT")
+ label:SetText(o.label or "")
+
+ local btn
+ if th.dropdownArt then
+ -- FILL THE CONTROL COLUMN. It used to be a fixed 200 anchored at labelW+6, which put a dropdown's
+ -- left edge 4px right of the compact slider's nudge arrow and its right edge 37px short of the
+ -- slider's value text — so in a stack of alternating rows nothing lined up with anything. The
+ -- column is defined once, by the slider: it starts where the slider's left arrow starts
+ -- (labelW + 2) and ends where the slider's value ends (the row, less 4).
+ local rowW = self.width - (self._section and ROW_INDENT or 0)
+ local w = th.controlW or o.width or (rowW - th.labelW - 6)
+ btn = buildModernTrigger(row, nextName("Drop"), w, th.dropH - 6)
+ btn:SetPoint("LEFT", row, "LEFT", th.labelW + 2, 0)
+ else
+ btn = CreateFrame("Button", nextName("Drop"), row, "UIPanelButtonTemplate")
+ btn:SetSize(o.width or 130, 22)
+ btn:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ end
+
+ local function refresh()
+ btn:SetText(labelFor(values, o.get and o.get()))
+ end
+
+ local function generator(_, root)
+ for _, entry in ipairs(values) do
+ local v, text = entry[1], entry[2]
+ root:CreateRadio(text,
+ function() return (o.get and o.get()) == v end,
+ function()
+ if o.set then o.set(v) end
+ refresh()
+ if o.onChanged then o.onChanged(v) end
+ end)
+ end
+ end
+ row.MenuGenerator = generator -- test seam: the tree is assertable without opening a menu
+
+ btn:SetScript("OnClick", function(self)
+ if NE.menu and NE.menu.ToggleAnchored then
+ NE.menu.ToggleAnchored(generator, self,
+ { point = "TOPRIGHT", relativePoint = "BOTTOMRIGHT", x = 0, y = -2 })
+ end
+ end)
+
+ tip(btn, o.label, o.desc)
+ -- AFTER tip, and hooked rather than set: tip owns OnEnter/OnLeave with SetScript, which would
+ -- discard a hook installed before it. The arrow's hover and pressed art is the only state this
+ -- trigger has, so losing it would leave a button that never acknowledges the mouse.
+ if btn.RefreshArt then
+ btn:HookScript("OnEnter", function(s) s._neOver = true; s:RefreshArt() end)
+ btn:HookScript("OnLeave", function(s) s._neOver = false; s:RefreshArt() end)
+ btn:HookScript("OnMouseDown", function(s) s._neDown = true; s:RefreshArt() end)
+ btn:HookScript("OnMouseUp", function(s) s._neDown = false; s:RefreshArt() end)
+ end
+ refresh()
+ self.refreshers[#self.refreshers + 1] = refresh
+ row.Button, row.Label, row.Refresh = btn, label, refresh
+ return row
+end
+
+-- ── Action button ───────────────────────────────────────────────────────────────────────────────
+-- o = { label, desc, onClick, width }
+
+function Column:AddButton(o)
+ local th = self.theme
+ local row = self:_row("Frame", th.buttonH or BUTTON_H)
+
+ local btn = CreateFrame("Button", nextName("Button"), row, "UIPanelButtonTemplate")
+ btn:SetSize(o.width or 200, th.buttonArtH or 22)
+ btn:SetPoint("LEFT", row, "LEFT", 2, 0)
+ btn:SetText(o.label or "")
+ btn:SetScript("OnClick", function()
+ if o.onClick then o.onClick() end
+ end)
+
+ -- The red 3-slice, where the theme asks for it. A button built by this kit sits next to buttons the
+ -- edit-mode dialog builds itself and skins, and one unskinned row among them is more obviously wrong
+ -- than none of them being skinned would be.
+ if th.buttonArt and NE.button and NE.button.Skin then pcall(NE.button.Skin, btn) end
+
+ tip(btn, o.label, o.desc)
+ row.Button = btn
+ return row
+end
+
+function Column:AddSpacer(h)
+ local f = CreateFrame("Frame", nil, self.frame)
+ f:SetSize(1, h or 8)
+ self:_add(f, h or 8, {})
+ return f
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsMenu.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsMenu.lua
new file mode 100644
index 0000000..cea107c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsMenu.lua
@@ -0,0 +1,356 @@
+-- DragonUI_NewEra/modules/cooldownviewer/SettingsMenu.lua — the right-click item menu and the
+-- panel's settings cog. Downport of the menu halves of NewEra/CooldownViewerSettings/Categories.lua
+-- (showItemMenu, applyAlertBadge) and Panel.lua (settingsMenuGenerator).
+--
+-- THIS IS THE PHASE THAT SWITCHES PHASE 4A ON. The alert engine and the ready-sound catalogue have
+-- shipped and been running since 4a, but nothing could assign them: every store was reachable only
+-- from Lua. This file is the assignment surface. Nothing below is new machinery — it is calls into
+-- M.alerts, M.SetReadySoundKit and CDS.adapter, arranged as a menu.
+--
+-- The Phase 4a ItemMenu.lua was deleted rather than debugged, on the owner's call, because the
+-- content belonged here from the start: upstream carries Move-to / Remove / Ready Sound / Alert in
+-- ONE generator on the settings item, not on the live HUD icon. That is also why the menu needs
+-- three levels (item -> Ready Sound -> category -> entry) and therefore core/Menu.lua's ClassicAPI
+-- backend, rather than the native two-level UIDropDownMenu.
+--
+-- ONE DELIBERATE DIVERGENCE FROM UPSTREAM: the FX enum. Upstream's fx values index
+-- NE.groupbuff.VISUAL_ALERT (1 = marching ants, 6 = flash), a Group Buff Filter enum this addon
+-- does not have. Ours is AL.FX — 1/2/3 over LibCustomGlow — so the FX submenu is generated FROM
+-- AL.FX rather than hardcoding two entries. Hardcoding upstream's 1/6 here would have silently
+-- written 6, which AL.FX has no renderer for.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+local CDS = NE.cooldownviewersettings
+local Adapter = CDS.adapter
+
+local REFRESH_WINDOWS = { 10, 20, 30, 40, 50 } -- percent; AL clamps to [10, 50]
+
+-- ── Alert badge ─────────────────────────────────────────────────────────────────────────────────
+-- A small marker in the corner of any tile that has an alert or a ready sound configured, so the
+-- grid shows its own state instead of making the player right-click every icon to find out.
+-- Upstream draws the `common-icon-visual` glyph. That atlas is not registered in this addon, so the
+-- badge is upstream's own fallback: a gold dot on a dark strip. Ask HasAtlas first rather than
+-- letting SetAtlas fail — a failed SetAtlas logs an ATLAS MISS, and this runs once per tile per
+-- rebuild, which would bury the log in a message the answer to which is always "no".
+
+local BADGE_ATLAS = "common-icon-visual"
+
+local function applyAlertBadge(item)
+ if not item.AlertBG then
+ item.AlertBG = item:CreateTexture(nil, "OVERLAY")
+ item.AlertBG:SetSize(14, 12)
+ item.AlertBG:SetPoint("BOTTOMRIGHT", item, "BOTTOMRIGHT", 0, 0)
+ item.AlertBG:SetTexture(0, 0, 0, 0.7)
+
+ item.AlertBadge = item:CreateTexture(nil, "OVERLAY")
+ item.AlertBadge:SetSize(6, 6)
+ item.AlertBadge:SetPoint("CENTER", item.AlertBG, "CENTER", 0, 0)
+ local haveGlyph = NE.tex and NE.tex.HasAtlas and NE.tex.HasAtlas(BADGE_ATLAS)
+ and NE.tex.SetAtlas(item.AlertBadge, BADGE_ATLAS, false)
+ if not haveGlyph then item.AlertBadge:SetTexture(1, 0.82, 0, 1) end
+ end
+
+ local configured = (M.GetReadySoundKit and M.GetReadySoundKit(item.spellID) ~= nil)
+ or (M.alerts and M.alerts.GetType and M.alerts.GetType(item.spellID) ~= nil)
+ if configured then
+ item.AlertBG:Show(); item.AlertBadge:Show()
+ else
+ item.AlertBG:Hide(); item.AlertBadge:Hide()
+ end
+end
+
+CDS._applyAlertBadge = applyAlertBadge
+
+-- Extra tooltip lines describing what the badge means for this specific tile. Called by
+-- SettingsCategories' OnEnter, after the spell tooltip has been laid down.
+function CDS._itemTooltipExtra(item, tooltip)
+ if not (item and item.spellID and tooltip) then return end
+ local AL = M.alerts
+ local t = AL and AL.GetType and AL.GetType(item.spellID)
+ if t then
+ local fxName
+ for _, fx in ipairs((AL and AL.FX) or {}) do
+ if fx.id == AL.GetFX(item.spellID) then fxName = fx.name end
+ end
+ tooltip:AddLine(("Alert: %s (%s)"):format(t, fxName or "?"), 0.4, 1, 0.4)
+ end
+ local kit = M.GetReadySoundKit and M.GetReadySoundKit(item.spellID)
+ if kit then
+ tooltip:AddLine("Ready sound: " .. (M.GetSoundKitName(kit) or tostring(kit)), 0.4, 0.8, 1)
+ end
+end
+
+-- ── The item menu ───────────────────────────────────────────────────────────────────────────────
+
+-- An equip row moves by TOKEN, a spell or aura by spellID. The two stores are genuinely different —
+-- a spell move rewrites an editable list, a trinket move writes one assignment value — so the
+-- branch is here rather than hidden inside Assign, where a caller could not tell which happened.
+local function addMoveEntries(root, item, class)
+ for _, target in ipairs(Adapter.GetValidTargets(item._catID)) do
+ root:CreateButton("Move to " .. Adapter.Label(target), function()
+ if item.token then
+ Adapter.AssignEquip(item.token, item._catID, target)
+ else
+ Adapter.Assign(item.spellID, item._catID, target, class)
+ end
+ CDS.RefreshLayout()
+ end)
+ end
+end
+
+-- Is this row an AURA row — Tracked Buffs, Tracked Bars, or Not Displayed on the Auras tab?
+--
+-- It decides which of the four triggers this file is allowed to offer. Two of them are driven by a
+-- COOLDOWN finishing (ItemMixin:ConsumeReadyTransition), and an aura item does not define it: there
+-- is no cooldown behind a tracked buff to finish. Offering them anyway is what produced the "the FX
+-- on tracked buffs doesn't work" report — an alert that stores, badges and previews, and then has no
+-- event that could ever fire it.
+local function isAuraRow(item)
+ local meta = item and item._catID and Adapter.Meta and Adapter.Meta(item._catID)
+ return (meta and meta.mode == "auras") and true or false
+end
+
+-- One submenu per sound category, a radio per sound, plus None. Selecting previews the cue —
+-- retail's "Play Sample" — because a sound you cannot hear before committing is not a choice.
+--
+-- Not offered on an aura row: the cue plays from FireReadyAlerts, on the cooldown -> ready edge.
+-- A stored kit is still clearable there (see the aura branch), because a setting you can reach only
+-- to regret is worse than one you were never shown.
+local function addSoundEntries(root, item)
+ if not (item.spellID and M.SOUND_DATA and M.SOUND_CATEGORY_ORDER and M.SetReadySoundKit) then return end
+
+ if isAuraRow(item) then
+ -- Only when something is actually stored — otherwise the row simply has no sound section. This
+ -- exists for layouts saved BEFORE the gate, where a kit could be assigned to a buff and then sat
+ -- there lighting the badge with nothing behind it and no per-item way to take it off.
+ if M.GetReadySoundKit(item.spellID) == nil then return end
+ root:CreateDivider()
+ root:CreateButton("Clear Ready Sound", function()
+ M.SetReadySoundKit(item.spellID, nil)
+ applyAlertBadge(item)
+ end):SetTooltip("Clear Ready Sound",
+ "A ready sound plays when a COOLDOWN finishes.|nA tracked buff has none, so this one can|nnever play. Clearing it also clears the badge.")
+ return
+ end
+
+ root:CreateDivider()
+ local soundRoot = root:CreateButton("Ready Sound")
+
+ soundRoot:CreateRadio("None",
+ function() return M.GetReadySoundKit(item.spellID) == nil end,
+ function()
+ M.SetReadySoundKit(item.spellID, nil)
+ applyAlertBadge(item)
+ end)
+
+ for _, cat in ipairs(M.SOUND_CATEGORY_ORDER) do
+ local catSub = soundRoot:CreateButton(cat)
+ for _, e in ipairs(M.SOUND_DATA[cat] or {}) do
+ catSub:CreateRadio(e.name,
+ function() return M.GetReadySoundKit(item.spellID) == e.kit end,
+ function()
+ M.SetReadySoundKit(item.spellID, e.kit)
+ if M.PlayReadySound then M.PlayReadySound(e.kit) end
+ applyAlertBadge(item)
+ end)
+ end
+ end
+end
+
+-- Does this spell have an aura of its own name up right now? The same lookup the Refresh alert
+-- makes at runtime (Alerts.lua's inRefreshWindow): player first, then target.
+--
+-- A "yes" here is proof Refresh will work. A "no" is NOT proof it will not — the aura may simply be
+-- inactive at the moment you opened the menu. So the tooltip states the REQUIREMENT and adds a live
+-- sighting when there is one, rather than greying the entry out on a guess. Knowing for certain
+-- would mean shipping a generated "applies an aura named after itself" table out of Spell.dbc; that
+-- is a real option, but it is a generator pass, not a menu tweak.
+local function auraSeen(name)
+ if not (name and NE.aura and NE.aura.FindByName) then return false end
+ for _, unit in ipairs({ "player", "target" }) do
+ local row = NE.aura.FindByName(unit, name)
+ if row and row.duration and row.duration > 0 then return true end
+ end
+ return false
+end
+
+-- Event (None / Available / Refresh / Usable), FX style, and — for Refresh — the window %.
+-- Choosing an event or an FX flashes a sample on the tile itself, the same "see it before you
+-- commit" contract as the sound preview.
+local function addAlertEntries(root, item)
+ local AL = M.alerts
+ if not (item.spellID and AL and AL.SetType) then return end
+ root:CreateDivider()
+ local alertRoot = root:CreateButton("Alert")
+
+ local function isType(t) return AL.GetType(item.spellID) == t end
+ -- Preview in the alert type's OWN colour. Each type has a distinct tint, so previewing everything
+ -- as "usable" showed a yellow flash for a choice that would glow green in play.
+ local function pick(t)
+ return function()
+ AL.SetType(item.spellID, t)
+ if t then AL.Preview(item, AL.GetFX(item.spellID), t) elseif AL.ClearFX then AL.ClearFX(item) end
+ applyAlertBadge(item)
+ end
+ end
+
+ alertRoot:CreateRadio("None", function() return AL.GetType(item.spellID) == nil end, pick(nil))
+
+ -- Available is the cooldown -> ready edge, so it is a SPELL trigger only. Its own tooltip used to
+ -- promise "Works for every spell", which is true and was being read on rows that hold no spell.
+ -- None stays above it, so a layout that stored `available` on a buff before this gate can still be
+ -- set back to None or moved onto Refresh.
+ if not isAuraRow(item) then
+ alertRoot:CreateRadio("Available", function() return isType("available") end, pick("available"))
+ :SetTooltip("Available", "Flashes once, the moment the cooldown finishes.|nWorks for every spell.")
+ end
+
+ -- Refresh is the one event that genuinely cannot fire for some spells, so it says what it needs.
+ local pct = math.floor((AL.GetWindow(item.spellID) or 0.3) * 100 + 0.5)
+ local refreshText
+ if isAuraRow(item) then
+ -- The caveat below is about a COOLDOWN that applies no aura, which cannot be the case here: the
+ -- row is the aura. This is the one trigger a tracked buff is built for, and saying so is the
+ -- point — it is also the one that was silently doing nothing until the ticker learned to look.
+ refreshText = ("Glows during the last %d%% of this buff's|nremaining time."):format(pct)
+ else
+ refreshText = ("Glows during the last %d%% of this spell's own|nbuff or debuff."):format(pct)
+ .. "|n|nA cooldown that applies no aura — Shadowfiend,|nPsychic Scream — can never trigger it."
+ end
+ refreshText = refreshText .. (auraSeen(item.spellName)
+ and "|n|n|cff40ff40Its aura is active now, so this will work.|r"
+ or "|n|n|cffffd200No aura of this name is up right now.|r")
+ alertRoot:CreateRadio("Refresh", function() return isType("refresh") end, pick("refresh"))
+ :SetTooltip("Refresh", refreshText)
+
+ -- ACTIVE, on aura rows only. The other three all ask questions about a COOLDOWN, and a proc with
+ -- no castable spell of its own name can answer "yes" to none of them. Offered first among the
+ -- aura-row triggers because for a tracked buff it is the obvious one.
+ if isAuraRow(item) then
+ alertRoot:CreateRadio("Active", function() return isType("active") end, pick("active"))
+ :SetTooltip("Active", "Glows for as long as this buff is on you.|n|nThe one that works for a"
+ .. " proc: it asks whether the|nbuff is up, not whether something is castable|nor off cooldown.")
+ end
+
+ -- USABLE asks the client whether a spell of this name can be cast. On an aura row that is a real
+ -- question for a self-buff you re-cast (Inner Fire, Fortitude) and a meaningless one for a proc —
+ -- IsUsableSpell does not know "Surge of Light", so the alert stores, badges, and never fires.
+ -- Offered only when the client can actually answer, which it can only do for a spell in the book.
+ --
+ -- Probed with GetSpellInfo rather than IsUsableSpell: the latter returns nil BOTH for a name it
+ -- does not know and for a known spell that is merely unaffordable right now, so reading it here
+ -- would hide the entry from anyone who opened the menu out of mana.
+ local usableCastable = (not isAuraRow(item)) or (item.spellName and GetSpellInfo(item.spellName) ~= nil)
+ if usableCastable then
+ local usableText = "Glows for as long as the spell is off cooldown|nand affordable."
+ local threshold = M.alertdata and M.alertdata.ExecuteThreshold
+ and M.alertdata.ExecuteThreshold(item.spellID, item._rankCDIDs)
+ if threshold then
+ usableText = usableText .. ("|n|nThis one also waits for a target below %d%% health."):format(threshold * 100)
+ end
+ if isAuraRow(item) then
+ usableText = usableText .. "|n|nOn a buff row this is about RE-CASTING it,|nnot about the buff being up — that is Active."
+ end
+ alertRoot:CreateRadio("Usable", function() return isType("usable") end, pick("usable"))
+ :SetTooltip("Usable", usableText)
+ end
+
+ alertRoot:CreateDivider()
+
+ -- Generated from AL.FX, not from upstream's hardcoded pair. See the header.
+ local fxSub = alertRoot:CreateButton("FX Style")
+ for _, fx in ipairs(AL.FX or {}) do
+ fxSub:CreateRadio(fx.name,
+ function() return AL.GetFX(item.spellID) == fx.id end,
+ function()
+ AL.SetFX(item.spellID, fx.id)
+ AL.Preview(item, fx.id, AL.GetType(item.spellID) or "available")
+ end)
+ end
+
+ local winSub = alertRoot:CreateButton("Refresh Window")
+ for _, pct in ipairs(REFRESH_WINDOWS) do
+ winSub:CreateRadio(pct .. "%",
+ function() return math.abs((AL.GetWindow(item.spellID) * 100) - pct) < 0.5 end,
+ function() AL.SetWindow(item.spellID, pct / 100) end)
+ end
+end
+
+-- The generator. Kept separate from the open call so a test can build the tree and drive it without
+-- any of UIDropDownMenu present.
+function CDS.ItemMenuGenerator(item, class)
+ return function(_, root)
+ root:CreateTitle(item.spellName or "")
+ addMoveEntries(root, item, class)
+
+ -- Remove only appears when the entry is genuinely deletable — a stored user aura. A spell is
+ -- never removed, only returned to the catalog, which "Move to Not Displayed" already does; a
+ -- trinket is discovered, so it leaves by being unequipped and never by a menu.
+ if not item.token and Adapter.IsRemovable and Adapter.IsRemovable(item.spellID, item._catID, class) then
+ root:CreateDivider()
+ root:CreateButton("|cffff5555Remove|r", function()
+ Adapter.Remove(item.spellID, item._catID, class)
+ CDS.RefreshLayout()
+ end)
+ end
+
+ addSoundEntries(root, item)
+ addAlertEntries(root, item)
+ end
+end
+
+-- Called by SettingsCategories for every tile click.
+function CDS.OnItemClick(item, button)
+ if button ~= "RightButton" then return end
+ if not (item and item._catID and (item.spellID or item.token)) then return end
+ if not (NE.menu and NE.menu.OpenContext) then return end
+ local _, class = UnitClass("player")
+ NE.menu.OpenContext(CDS.ItemMenuGenerator(item, class))
+end
+
+-- ── The settings cog ────────────────────────────────────────────────────────────────────────────
+
+function CDS.SettingsMenuGenerator(_, root)
+ root:CreateCheckbox("Show Unlearned",
+ function() return M.GetShowUnlearned and M.GetShowUnlearned() end,
+ function()
+ M.SetShowUnlearned(not M.GetShowUnlearned())
+ CDS.RefreshLayout()
+ end)
+
+ root:CreateDivider()
+ root:CreateButton("Reset Spell Lists", function() StaticPopup_Show("NE_CDM_RESET_TRACKING") end)
+ root:CreateButton("Clear All Alerts", function() StaticPopup_Show("NE_CDM_RESET_ALERTS") end)
+end
+
+-- Both resets are destructive and irreversible (there is no undo store until 4b-5), so both confirm.
+StaticPopupDialogs = StaticPopupDialogs or {}
+StaticPopupDialogs["NE_CDM_RESET_TRACKING"] = {
+ -- Names the scope in the confirm too. This is the last thing read before an irreversible action,
+ -- and it used to say nothing about WHOSE lists — while the code behind it cleared every class.
+ text = "Reset this class's Cooldown Manager spell and buff lists to their defaults?\n\nOther classes, alerts, sounds and frame positions are not affected.",
+ button1 = YES or "Yes",
+ button2 = NO or "No",
+ OnAccept = function()
+ if M.ResetTracking then M.ResetTracking() end
+ if CDS.RefreshLayout then CDS.RefreshLayout() end
+ end,
+ timeout = 0, whileDead = 1, hideOnEscape = 1,
+}
+StaticPopupDialogs["NE_CDM_RESET_ALERTS"] = {
+ text = "Clear every configured alert and ready sound?\n\nSpell lists and frame positions are not affected.",
+ button1 = YES or "Yes",
+ button2 = NO or "No",
+ OnAccept = function()
+ if M.ResetAlerts then M.ResetAlerts() end
+ if CDS.RefreshLayout then CDS.RefreshLayout() end
+ end,
+ timeout = 0, whileDead = 1, hideOnEscape = 1,
+}
+
+function CDS.ToggleSettingsMenu(cog)
+ if not (NE.menu and NE.menu.ToggleAnchored) then return end
+ NE.menu.ToggleAnchored(CDS.SettingsMenuGenerator, cog, { point = "TOPRIGHT", relativePoint = "BOTTOMRIGHT", x = 0, y = -2 })
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsOptions.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsOptions.lua
new file mode 100644
index 0000000..8673b2d
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsOptions.lua
@@ -0,0 +1,291 @@
+-- DragonUI_NewEra/modules/cooldownviewer/SettingsOptions.lua — the /cdm Settings tab.
+--
+-- Every Cooldown Manager setting lives here, which is the point: retail's Cooldown Manager settings
+-- sit IN the Cooldown Manager, and until now ours sat in DragonUI's options panel — a different
+-- window, reached by a different path (PORT_PLAN §G.10).
+--
+-- WHAT MOVED, AND WHAT DID NOT. The DragonUI options section keeps exactly two things: the master
+-- enable toggle (that panel is where a player goes to turn a module OFF, and it is read-only to us —
+-- CONTRACTS §0, we register a section, we do not own the window) and a button that opens this one.
+-- No stored VALUE is rendered in two windows: two views onto one store stay in sync only as long as
+-- both are rebuilt on show, and the first time one is not, the player is looking at a stale value with
+-- no way to tell. (Actions are a different matter — see the Reset section below.)
+--
+-- THE PER-VIEWER SETTINGS ARE NOT HERE ANY MORE. They were, and for a while they were in two places at
+-- once: EditorPanel.lua renders them as a dialog beside the frame in edit mode, which is retail's
+-- shape and the better one — you are looking at the frame while you change its layout, instead of at a
+-- list of numbers in a window that covers it. That duplication was held together by a refresh contract
+-- (owner steer: remove it). What remains here is everything the dialog does NOT carry, which is
+-- everything that is not per-viewer: buffed-spell glow, icon fit, talent specs, buff tracking, resets.
+--
+-- What is left in this tab's place is one section of buttons, one per viewer, each of which opens edit
+-- mode WITH that viewer selected and its dialog already up. The affordance is the same; the settings
+-- are one click further and beside the thing they change.
+--
+-- Frame POSITION was always the movers' (`/dui edit`) — §B1's decision, unchanged — so the viewers'
+-- settings and their position now live in the same place, which is what retail does.
+--
+-- THE PAGE IS NOT A CATEGORY. It is a second scroll child beside `panel.content`, swapped in by
+-- SetDisplayMode (§G.10's option (a)). The alternative — a "category" whose renderer draws controls —
+-- would put widget layout behind an adapter whose entire contract is "return a list of spellIDs",
+-- and every consumer of that list (ApplyItemFilter, the drag path, RestackCategories) would grow a
+-- not-a-grid branch.
+--
+-- BUILT LAZILY, on the first switch to this tab: ~60 frames that a player who never opens the tab
+-- should not pay for.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+NE.cooldownviewersettings = NE.cooldownviewersettings or {}
+local CDS = NE.cooldownviewersettings
+
+local PAGE_W = 330
+
+-- Ordered, unlike the options tab's value->label maps: those go to AceGUI, which sorts them, and a
+-- radio menu has to pick its own order. The per-viewer lists that used to sit here went with the
+-- controls that read them; EditorPanel.lua carries its own copies.
+local TRACK_DEST = { { "both", "Icons and bars" }, { "icon", "Icons only" }, { "bar", "Bars only" } }
+
+local function pct(v) return tostring(v) .. "%" end
+
+local function say(msg)
+ if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cff1784d1Cooldown Manager|r " .. msg) end
+end
+
+-- ── "Position and configure" ────────────────────────────────────────────────────────────────────
+-- The last unported piece of upstream's panel: its cog menu carried an "Edit Mode" entry that hid the
+-- window and toggled retail Edit Mode (Panel.lua:211). §G.4 left it as "drop it or route it at
+-- /dui edit" and never decided. Routed, and improved on: because DragonUI's editor can be told which
+-- frame to select, the affordance belongs per viewer rather than once globally.
+--
+-- It now opens the viewer's SETTINGS DIALOG too, not just the editor. Since the per-viewer controls
+-- left this tab, this button is the only route from here to them, and dropping the player into edit
+-- mode to go hunting for the right handle would be a worse tab than the one it replaced.
+--
+-- The panel closes only on SUCCESS. Hiding first and then failing (no editor, or in combat) would take
+-- the window away and leave the player with nothing to show why.
+local function positionViewer(category)
+ local frame = M.viewers and M.viewers[category]
+ if not frame then
+ say("that viewer doesn't exist yet — it builds at login.")
+ return
+ end
+ -- No master-enable check here on purpose, though a disabled Cooldown Manager registers no editor
+ -- handle (Register.lua's editorVisible) and this would land a settings dialog beside a frame that is
+ -- not on screen. It cannot be reached in that state: the window refuses to open while the module is
+ -- off, and M.SetEnabled closes one that is already up. A guard for it would be a comment describing
+ -- something that cannot happen, which is worse than no guard at all.
+ if not NE.OpenFrameEditor then
+ say("editor mode isn't available in this DragonUI build. Type /dui edit to position frames.")
+ return
+ end
+ local ok, why = NE.OpenFrameEditor(frame)
+ if not ok then
+ say(why or "editor mode couldn't be opened.")
+ return
+ end
+ -- Editor mode covers the screen and the viewer being positioned may sit under this window.
+ if CDS.HidePanel then CDS.HidePanel() end
+ -- Then its settings, through the same seam a click on the handle uses — so the frame is selected and
+ -- the dialog placed exactly as it would have been.
+ local anchor = frame.editorAnchor
+ if anchor and NE.OpenFrameEditorSettings then
+ NE.OpenFrameEditorSettings(anchor)
+ end
+end
+CDS.PositionViewer = positionViewer -- test seam
+
+-- ── Where the per-viewer settings went ──────────────────────────────────────────────────────────
+--
+-- One row per viewer, and no settings. Every control this section used to hold — Enabled, orientation,
+-- icon direction, visibility, icons per row, icon size, icon padding, opacity, show timer, show
+-- tooltips, hide when inactive, and the two bar-only ones — is in the edit-mode dialog, next to the
+-- frame it changes. Rendering them here as well is the exact duplication this file's header rules out.
+
+local function buildViewerLinks(col)
+ col:AddSection("Viewer layout", true)
+ col:AddText("Each viewer's own settings — size, spacing, orientation, visibility, what its icons "
+ .. "show — live on the frame, in edit mode, where you can see what you are changing. These open "
+ .. "edit mode with that viewer selected and its settings already up. Closes this window; not "
+ .. "available in combat.")
+
+ for _, spec in ipairs(M.VIEWER_SPECS or {}) do
+ col:AddButton({
+ label = spec.label,
+ desc = spec.desc,
+ width = 200,
+ onClick = function() positionViewer(spec.category) end,
+ })
+ end
+end
+
+-- ── The page ────────────────────────────────────────────────────────────────────────────────────
+
+local col -- built once, on first switch to the tab
+
+local function build(parent)
+ local Kit = CDS.controls
+ if not (Kit and Kit.New) then return nil end
+
+ local c = Kit.New(parent, PAGE_W)
+
+ c:AddText("Everything the Cooldown Manager can be told to do that is not about one viewer's layout. "
+ .. "Layout and position both live on the frame itself, in edit mode (/dui edit) — click a viewer "
+ .. "there for its own settings, or use the buttons just below to go straight to one.",
+ { font = "GameFontHighlightSmall" })
+
+ buildViewerLinks(c)
+
+ -- ── Buffed spells. Retail tints the swipe gold while a spell's own buff is on you; that setter is
+ -- WoD+, so ours haloes the icon instead (PORT_PLAN §H.2 8c) — which then frees the timer to show
+ -- whichever number is more useful, rather than being retail's only way to say "buffed".
+ c:AddSection("Buffed spells", false)
+ c:AddText("A spell can be on cooldown and buffing you at the same time. The glow says which icons "
+ .. "are buffed; the timer says how long.")
+ c:AddCheckbox({
+ label = "Glow while buffed",
+ desc = "Halo the icon gold while the spell's buff (or, for a shaman, its totem) is up.",
+ get = function() return M.IsBuffGlowEnabled() end,
+ set = function(v) M.SetBuffGlowEnabled(v) end,
+ })
+ c:AddCheckbox({
+ label = "Show the buff's time, not the cooldown",
+ desc = "Retail's behaviour: while buffed, the icon counts down the BUFF. Off, it counts down the "
+ .. "spell's cooldown and the glow alone marks it as buffed — which is clearer when the "
+ .. "two differ, as on Prayer of Mending.",
+ get = function() return M.BuffShowsAuraTime() end,
+ set = function(v) M.SetBuffShowsAuraTime(v) end,
+ })
+
+ -- ── Icon fit. Retail masks its icons, which both insets and rounds them; §C2 records that
+ -- MaskTexture has no polyfill on this client, so the inset is reproducible and the rounding is not.
+ c:AddSection("Icon fit", false)
+ c:AddText("The frame is a soft shadow that falls on the icon's outer edge, so it only shows where "
+ .. "there is icon underneath it. Strength draws it more than once to deepen it — that is also "
+ .. "what makes its rounded corners read, since the icons themselves cannot be rounded here. "
+ .. "Inset shrinks the icon, which slides the shadow off it, so raise that one sparingly.")
+ c:AddSlider({
+ label = "Frame strength", min = 1, max = M.FRAME_STRENGTH_MAX, step = 1,
+ get = function() return M.GetFrameStrength() end,
+ set = function(v) M.SetFrameStrength(v) end,
+ })
+ c:AddSlider({
+ label = "Icon inset", min = 0, max = M.ICON_INSET_EXTRA_MAX, step = 1, format = pct,
+ get = function() return M.GetIconInsetExtra() end,
+ set = function(v) M.SetIconInsetExtra(v) end,
+ })
+
+ -- ── Per-spec layout. Dual talent specialisation means one character genuinely wants two layouts.
+ c:AddSection("Talent specs", false)
+ c:AddText("Which spells and buffs you track is remembered separately for each talent spec, so a "
+ .. "Discipline layout and a Holy one do not overwrite each other. Where each viewer sits is "
+ .. "always remembered per character; the appearance settings are shared unless you say otherwise "
+ .. "below.")
+ c:AddCheckbox({
+ label = "Separate layout per spec",
+ desc = "Off, both specs share one set of lists. Turning it on copies the layout you have now "
+ .. "into the spec you are in.",
+ get = function() return M.IsPerSpecLayout() end,
+ set = function(v) M.SetPerSpecLayout(v) end,
+ })
+ c:AddCheckbox({
+ label = "Separate appearance per character",
+ -- Says what happens on BOTH edges, because the reversibility is the reason it is safe to try:
+ -- a character that has changed nothing keeps reading the shared values, so nothing moves when
+ -- this is ticked, and unticking it simply stops consulting the per-character values rather than
+ -- discarding them.
+ desc = "Off, orientation, icons per row, size, padding and opacity are one setup for every "
+ .. "character. On, each character can differ — until you change something here it still "
+ .. "follows the shared setup, so nothing moves when you tick this, and unticking it "
+ .. "gives the shared setup back without losing what you changed.",
+ get = function() return M.IsPerCharacterFrames() end,
+ set = function(v) M.SetPerCharacterFrames(v) end,
+ })
+ c:AddCheckbox({
+ label = "Layouts include appearance",
+ -- Governs APPLYING, not saving. A share string always carries appearance, so this is the
+ -- recipient's decision rather than the author's — and it is off by default because an imported
+ -- layout that silently resizes and reorients four viewers is how "load a layout" stops being an
+ -- action anyone trusts.
+ desc = "Off, loading or importing a layout changes only what you track — lists, tracked buffs, "
+ .. "trinkets, alerts and sounds. On, it also applies the orientation, icons per row, "
+ .. "size, padding and opacity the layout was saved with.|n|nLayouts always SAVE "
+ .. "appearance either way, so this only decides what happens when one is applied. Revert "
+ .. "always puts appearance back, whatever this says.",
+ get = function() return M.LayoutsIncludeAppearance() end,
+ set = function(v) M.SetLayoutsIncludeAppearance(v) end,
+ })
+
+ -- ── Buff tracking. The buff viewers have no curated list: any player buff shorter than the
+ -- auto-track window shows automatically, which is what makes trinket, potion and proc buffs work
+ -- without enumerating them in advance.
+ c:AddSection("Buff tracking", false)
+ c:AddText("Buffs you have not seen before are recorded and listed under Not Displayed on the "
+ .. "Tracked Buffs tab, where you can assign the ones you want. Nothing appears on screen until "
+ .. "you do.")
+ c:AddCheckbox({
+ label = ("Auto-track buffs under %ds"):format(M.BUFF_TRACK_MAX_DURATION or 120),
+ desc = "Show every short buff the moment it lands, without assigning it first. Convenient on a "
+ .. "character you are still setting up; in a raid it fills the viewers with other "
+ .. "people's cooldowns, food and flasks.",
+ get = function() return M.IsAutoTrackBuffs() end,
+ set = function(v) M.SetAutoTrackBuffs(v) end,
+ onChanged = function() if M.RefreshActiveViewer then M.RefreshActiveViewer() end end,
+ })
+ c:AddDropdown({
+ label = "Show them as", values = TRACK_DEST, width = 140,
+ desc = "Which of the two buff viewers auto-tracked buffs land in.",
+ get = function() return M.AutoTrackDest() end,
+ set = function(v) M.SetAutoTrackDest(v) end,
+ onChanged = function() if M.RefreshActiveViewer then M.RefreshActiveViewer() end end,
+ })
+
+ -- ── Resets. These are also in the cog menu, and that is deliberate: they are ACTIONS routed to the
+ -- same confirm popups (SettingsMenu.lua), not two renderings of one stored value, so there is no
+ -- state to fall out of sync. The cog is at hand while working the lists; a player looking for a
+ -- reset looks under Settings. What the §G.10 decision removes is a second view of the same VALUES in
+ -- a second window — not a second way to press a button.
+ c:AddSection("Reset", false)
+ c:AddText("Both of these are immediate and cannot be undone from here — Revert only covers layout "
+ .. "changes.")
+ c:AddButton({
+ label = "Reset spell and buff lists",
+ -- Says CLASS, not "character". The store is one shared DragonUI profile keyed by class, so this
+ -- is the honest scope: another class is untouched, and another character of the SAME class reads
+ -- the same lists and will see them reset too. Claiming "per-character" is what the old copy did,
+ -- while the code behind it reset every class at once.
+ desc = "Restore the curated defaults and the auto-track window for THIS CLASS, clearing its "
+ .. "spell lists, aura assignments and trinket placement. Other classes, alerts, sounds "
+ .. "and positions are not affected.",
+ onClick = function() StaticPopup_Show("NE_CDM_RESET_TRACKING") end,
+ })
+ c:AddButton({
+ label = "Clear all alerts and sounds",
+ desc = "Remove every per-spell alert and ready sound. Spell lists and positions are not affected.",
+ onClick = function() StaticPopup_Show("NE_CDM_RESET_ALERTS") end,
+ })
+ c:EndSection()
+
+ c:Relayout()
+ return c
+end
+
+-- Called by SetDisplayMode. Returns the column, or nil if the kit is missing (in which case the tab
+-- shows an empty page rather than erroring on the click).
+function CDS.EnsureSettingsPage()
+ if col then return col end
+ local panel = CDS.panel
+ if not (panel and panel.settingsContent) then return nil end
+ col = build(panel.settingsContent)
+ CDS.settingsColumn = col -- test seam
+ return col
+end
+
+-- Re-read every control. Needed because this page is not the only writer: a layout apply, a reset,
+-- or the master toggle in DragonUI's options can all move a value underneath it.
+function CDS.RefreshSettingsPage()
+ if not col then return end
+ col:Refresh()
+ col:Relayout()
+end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsPanel.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsPanel.lua
new file mode 100644
index 0000000..07ca4e4
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsPanel.lua
@@ -0,0 +1,525 @@
+-- DragonUI_NewEra/modules/cooldownviewer/SettingsPanel.lua — the /cdm settings window.
+--
+-- Phase 4b-1: the SHELL only. Chrome, the Spells/Auras side tabs, the scroll body, the search box,
+-- and the open/close plumbing. The category grids (4b-2) and the item context menu that actually
+-- assigns things (4b-3) build on top; `CDS.RefreshLayout` is the stub they replace.
+--
+-- Downport of NewEra/CooldownViewerSettings/Panel.lua (612 lines). See PORT_PLAN §G for the full
+-- scope. What changed and why:
+--
+-- * THREE side tabs, but not upstream's three. Upstream's Group Buffs tab hosts NE.groupbuff.filter,
+-- a module this addon does not have (12 references); it is dropped whole, along with
+-- CDS.UpdateGroupBuffsTabState. In its place is a SETTINGS tab (PORT_PLAN §G.10, §G.12) holding
+-- every viewer setting, which retail keeps in its Edit Mode dialog and we used to keep in
+-- DragonUI's options panel.
+--
+-- * NO strata-raising hack. Upstream fights a real problem — Era opens context menus at
+-- FULLSCREEN_DIALOG, the same strata as its panel, so menus rendered BEHIND it and had to be
+-- raised to TOOLTIP inside securecallfunction. 3.3.5a's UIDropDownMenu opens its list frames at
+-- DIALOG and we sit at MEDIUM, so menus clear us naturally. The whole raiseMenuAbovePanel /
+-- securecallfunction contract is unnecessary here and is not ported.
+--
+-- * NO WowStyle1DropdownTemplate. That template is retail-only. The footer layout dropdown is
+-- Phase 4b-5, and the Auras tab's auto-track dropdown already has a working equivalent in the
+-- options tab, so neither is rebuilt here.
+--
+-- * The settings cog landed in 4b-3, once core/Menu.lua existed to give it a menu; Revert became
+-- real in 4b-5, when SettingsPresets.lua brought the snapshot/restore pair it needs.
+--
+-- Taint: a plain display window. No secure templates, no protected frames, SavedVariables reads
+-- only. Nothing here can taint the combat path.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+NE.cooldownviewersettings = NE.cooldownviewersettings or {}
+local CDS = NE.cooldownviewersettings
+
+local PANEL_NAME = "NE_CooldownViewerSettings"
+local PANEL_W, PANEL_H = 399, 609
+local PANEL_SCALE = 1.3
+
+-- Height of the button strip below the Inset, measured from the frame's bottom edge.
+-- ButtonFrameTemplate anchors its Inset at BOTTOMRIGHT y=26; this is that +10, on the owner's
+-- request, because the layout and Revert buttons sat tight against the Inset's bottom border.
+-- Both the Inset and the scroll body key off this, so the strip has ONE height rather than two
+-- numbers that have to be remembered together.
+local FOOTER_H = 36
+local SCROLL_BOTTOM_PAD = 8 -- the scroll body's own clearance inside the Inset
+
+-- Which side tab a viewer category belongs to. Essential/Utility are spellbook-driven; the buff
+-- viewers are aura-driven. Nothing maps to "settings" — that tab is not a list of anything.
+local CATEGORY_TO_MODE = {
+ essential = "spells",
+ utility = "spells",
+ buffIcon = "auras",
+ buffBar = "auras",
+}
+
+local panel -- lazily built on first open
+local currentMode -- "spells" | "auras" | "settings"
+
+-- ── Build ───────────────────────────────────────────────────────────────────────────────────────
+
+local function build()
+ if panel then return panel end
+
+ local f = CreateFrame("Frame", PANEL_NAME, UIParent, "ButtonFrameTemplate")
+ -- The red 3-slice is the addon's standard button; Watch keeps this window's panel buttons
+ -- skinned as its panes are built (core/ButtonSkin.lua). Opt out per button with _neNoSkin.
+ if NE.buttonskin and NE.buttonskin.Watch then pcall(NE.buttonskin.Watch, f) end
+ f:SetSize(PANEL_W, PANEL_H)
+ f:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 16, -116)
+
+ -- MEDIUM, matching our other standalone windows. Upstream needs DIALOG + SetToplevel to clear a
+ -- retail Edit Mode overlay that does not exist here; sitting lower keeps dropdown lists (DIALOG
+ -- on this client) above us without any per-menu strata juggling.
+ f:SetFrameStrata("MEDIUM")
+ f:EnableMouse(true)
+ f:SetMovable(true)
+ f:SetClampedToScreen(true)
+ f:RegisterForDrag("LeftButton")
+ f:SetScript("OnDragStart", f.StartMoving)
+ f:SetScript("OnDragStop", f.StopMovingOrSizing)
+ f:Hide()
+
+ -- Re-pull icons when the client reports them changed, and when an item's data finally arrives —
+ -- an equip row's icon is nil until the server delivers it.
+ f:RegisterEvent("SPELL_UPDATE_ICON")
+ f:RegisterEvent("GET_ITEM_INFO_RECEIVED")
+ -- A trinket swap changes the discovery set, so the Trinkets section has to re-source. Registered
+ -- unfiltered: RegisterUnitEvent is our own compat shim and this frame has no other unit events.
+ f:RegisterEvent("UNIT_INVENTORY_CHANGED")
+ -- Phase 7d: the aura catalog is spec-gated, so a respec or a dual-spec swap changes which rows
+ -- exist. The gate's cache expires on a short TTL and would recover on its own, but only at the
+ -- next refresh — which for an open window could be never.
+ f:RegisterEvent("CHARACTER_POINTS_CHANGED")
+ f:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
+ f:SetScript("OnEvent", function(self, event)
+ if event == "CHARACTER_POINTS_CHANGED" or event == "ACTIVE_TALENT_GROUP_CHANGED" then
+ if M.InvalidateTalentCache then M.InvalidateTalentCache() end
+ -- The learn gate reads the curated set, and which of those spells the player KNOWS has just
+ -- changed with the spec.
+ if M.InvalidateCuratedCache then M.InvalidateCuratedCache() end
+ end
+ if self:IsShown() then CDS.RefreshLayout() end
+ end)
+
+ -- …and again once the spellbook table has actually caught up. Refreshing on the talent event alone
+ -- is too early: core/SpellRanks.lua rebuilds on a deferred timer, so this window re-rendered from
+ -- the OLD book and then had no reason to look again. That is the reported "switched from Holy to
+ -- Disc and can still see Circle of Healing — it only disappears when I try to move it": moving a
+ -- tile refreshes the layout, by which time the table is right. The viewers already subscribe here
+ -- (Register.lua); the panel never did.
+ if NE.spellbook and NE.spellbook.OnRebuilt then
+ NE.spellbook.OnRebuilt(function()
+ -- Redundant today — Register.lua's subscription runs first and already clears this — and kept
+ -- anyway, because "first" is only true while these two register in their current order. A
+ -- refresh that reads a stale cache is the failure mode this whole callback exists to fix.
+ if M.InvalidateCuratedCache then M.InvalidateCuratedCache() end
+ if f:IsShown() then CDS.RefreshLayout() end
+ end)
+ end
+
+ -- On OnHide, not in HidePanel: the close button and ESC both call Hide() directly. A drag in
+ -- flight owns a cursor icon parented to UIParent and a dimmed, locked source tile, and closing
+ -- the window out from under it would strand both.
+ f:HookScript("OnHide", function()
+ if CDS.CancelDrag then CDS.CancelDrag() end
+ -- The undo is a SESSION step, and its scope is one visit to this window. Keeping it across a
+ -- close would offer to revert a layout change the player made and then spent an hour building on.
+ if CDS.ClearUndo then CDS.ClearUndo() end
+ end)
+
+ -- Shared modern chrome: hides the classic ButtonFrameTemplate border, applies our nineslice,
+ -- retextures the streaks and sets the title styling. Same path modules/collections/Window.lua uses.
+ local PC = NE.panelchrome
+ if PC and PC.Apply then
+ PC.Apply(f, { layout = "PortraitFrameTemplate", title = "Cooldown Manager", noPortrait = true })
+ end
+
+ -- THE BODY: plain, full-brightness UI-Background-Rock — the same stone every other from-scratch
+ -- standalone window in this addon shows (Guild, LFG, Professions, AuctionHouse, Social,
+ -- Collections, Encounter Journal).
+ --
+ -- PC.ApplyModernChrome tints its own f.Bg to 0.32 grey. That tint is a DOWNPORT fix for a
+ -- different frame's first-paint colour flash, and on a window this size it reads as a dark wash
+ -- rather than stone. Collections strips it for exactly this reason (modules/collections/Window.lua
+ -- "bgTint"); this window is the last one that still wore it.
+ if f.Bg and f.Bg.SetVertexColor then f.Bg:SetVertexColor(1, 1, 1) end
+
+ -- The INSET is the recessed content area, and it reads near-black over the body stone — the same
+ -- fill and the same colour as every other inset in this addon (Collections' buildInset, LFG's
+ -- category rail).
+ --
+ -- THE SUBLEVEL IS THE POINT. This window's previous Inset fill was created at BACKGROUND subLevel
+ -- -5 while PanelChrome's f.Bg sits at subLevel 0 and spans the entire frame, so it drew UNDERNEATH
+ -- the body stone and was never once visible — the window looked like whatever f.Bg was tinted to,
+ -- and the fill might as well not have existed. It has to sit ABOVE f.Bg.
+ --
+ -- (ClassicAPI's PortraitFrameTemplate declares its own $parentBg with NO parentKey, so PanelChrome
+ -- builds f.Bg fresh at the default subLevel instead of re-texturing the template's at -6. That is
+ -- what put the two on the wrong sides of each other.)
+ if f.Inset then
+ -- Lift the Inset's bottom edge to make the button strip FOOTER_H tall. Only the BOTTOMRIGHT
+ -- point is re-set: SetPoint replaces an existing anchor of the same name, so the template's
+ -- TOPLEFT stays exactly where it was and this cannot drift from it.
+ f.Inset:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -6, FOOTER_H)
+ if f.Inset.Bg then f.Inset.Bg:Hide() end -- the template's own old-ClassicAPI marble
+ local ibg = f:CreateTexture(nil, "BACKGROUND", nil, 1)
+ ibg:SetPoint("TOPLEFT", f.Inset, "TOPLEFT", 0, 0)
+ ibg:SetPoint("BOTTOMRIGHT", f.Inset, "BOTTOMRIGHT", 0, 0)
+ ibg:SetTexture(0.05, 0.05, 0.06, 0.92)
+ f.InsetBg = ibg
+ -- Guards it against HideClassicChrome's BACKGROUND walk, which hides everything but f.Bg. Chrome
+ -- is applied above this today; the Keep is what makes re-applying it survivable.
+ if PC and PC.Keep then PC.Keep(f, ibg) end
+ end
+ if PC and PC.ModernizeCloseButton then PC.ModernizeCloseButton(f.CloseButton) end
+
+ -- Portrait: retail shows the spec icon. No specs on 3.3.5a, so use the class icon — the same
+ -- fallback the character panel's spec portrait uses, and a solid circle that fills the cutout.
+ if f.portrait and NE.portrait and NE.portrait.ApplyCutout then
+ local _, class = UnitClass("player")
+ local coords = class and CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[class]
+ f.portrait:SetTexture("Interface\\TargetingFrame\\UI-Classes-Circles")
+ if coords then f.portrait:SetTexCoord(unpack(coords)) end
+ NE.portrait.ApplyCutout(f.portrait, f)
+ end
+
+ -- 1.3 = the owner's +30%. Applied as a SCALE, not by growing PANEL_W/H: the grid geometry
+ -- (38px tiles on a 46px pitch, 7 to a row, a 344-wide category) is upstream's probe-confirmed
+ -- layout, and a bigger frame around unchanged tiles would just add margin.
+ --
+ -- That 1.3 now lives in core/Scale.lua as BASE_SCALE["cooldownmanager"], and this window is a
+ -- normal entry in the options tab's Window Scaling list (mode: ui / none / custom). Its default
+ -- mode is "none", which IS PinPixelPerfect(f, 1.3) — identical to the line this replaced, and
+ -- still re-pinned on any UI-scale change. PANEL_SCALE stays as the no-NE.scale fallback.
+ if NE.scale and NE.scale.Apply then
+ if NE.scale.SetFrame then NE.scale.SetFrame("cooldownmanager", f) end
+ NE.scale.Apply("cooldownmanager")
+ elseif NE.FrameUtil and NE.FrameUtil.PinPixelPerfect then
+ NE.FrameUtil.PinPixelPerfect(f, PANEL_SCALE)
+ end
+
+ if NE.FrameUtil then
+ if NE.FrameUtil.EscClose then NE.FrameUtil.EscClose(PANEL_NAME) end
+ if NE.FrameUtil.WirePanelSounds then
+ -- Retail plays the class-talent open/close kits; those are retail sound IDs, so the helper's
+ -- vanilla character-pane fallback is what actually sounds here.
+ NE.FrameUtil.WirePanelSounds(f, nil, nil,
+ SOUNDKIT and SOUNDKIT.IG_CHARACTER_INFO_OPEN,
+ SOUNDKIT and SOUNDKIT.IG_CHARACTER_INFO_CLOSE)
+ end
+ end
+
+ -- Side tabs, hung off the right edge. Anchors mirror retail's QuestMapFrame: first
+ -- TOPLEFT -> panel TOPRIGHT(+1,-28), next TOP -> prev BOTTOM(0,-3). Retail reuses one glyph for
+ -- both active and inactive states, so only activeAtlas is passed.
+ f.spellsTab = NE.tabs.MakeSideTab(f, {
+ activeAtlas = "icon_cooldownmanager", tooltip = "Spells", iconSize = 32,
+ onClick = function() CDS.SetDisplayMode("spells") end,
+ })
+ f.spellsTab.displayMode = "spells"
+ f.spellsTab:SetPoint("TOPLEFT", f, "TOPRIGHT", 1, -28)
+
+ f.aurasTab = NE.tabs.MakeSideTab(f, {
+ activeAtlas = "icon_trackedbuffs", tooltip = "Tracked Buffs", iconSize = 32,
+ onClick = function() CDS.SetDisplayMode("auras") end,
+ })
+ f.aurasTab.displayMode = "auras"
+ f.aurasTab:SetPoint("TOP", f.spellsTab, "BOTTOM", 0, -3)
+
+ -- The settings tab. Its glyph is the client's own questlog cog rather than a third CDM-sheet icon:
+ -- that sheet's spare glyph is icon_buffreorder, which means "reorder group buffs" and would be a
+ -- lie here. Reusing the cog also ties the tab to the cog beside the search box, which is what a
+ -- player already reads as "options in this window". At 18px it is a hair over its native 15x16, so
+ -- it stays crisp.
+ f.settingsTab = NE.tabs.MakeSideTab(f, {
+ activeAtlas = "questlog-icon-setting", tooltip = "Settings", iconSize = 18,
+ onClick = function() CDS.SetDisplayMode("settings") end,
+ })
+ f.settingsTab.displayMode = "settings"
+ f.settingsTab:SetPoint("TOP", f.aurasTab, "BOTTOM", 0, -3)
+
+ f.tabButtons = { f.spellsTab, f.aurasTab, f.settingsTab }
+
+ -- Search box. Filtering DIMS non-matching items in place rather than reflowing the grid, which is
+ -- what retail does; Categories.lua owns the per-item overlay in 4b-2.
+ f.search = CreateFrame("EditBox", PANEL_NAME .. "Search", f, "SearchBoxTemplate")
+ f.search:SetSize(290, 30)
+ f.search:SetPoint("TOPLEFT", 72, -30)
+ f.search:SetAutoFocus(false)
+ f.search:HookScript("OnTextChanged", function()
+ if CDS.ApplyItemFilter then CDS.ApplyItemFilter(CDS.GetSearchText()) end
+ end)
+
+ -- Settings cog, immediately right of the search box (retail anchors its SettingsDropdown
+ -- LEFT -> SearchBox.RIGHT +5). Same 16x18 native-atlas recipe as the spellbook and professions
+ -- cogs; the menu itself is SettingsMenu.lua's, opened through NE.menu so it toggles closed on a
+ -- second click.
+ local cog = CreateFrame("Button", nil, f)
+ cog:SetSize(16, 18)
+ cog:SetPoint("LEFT", f.search, "RIGHT", 5, 0)
+ cog.Icon = cog:CreateTexture(nil, "ARTWORK")
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(cog.Icon, "questlog-icon-setting", true)) then
+ cog.Icon:SetTexture("Interface\\Buttons\\UI-OptionsButton")
+ cog.Icon:SetSize(16, 16)
+ end
+ cog.Icon:SetPoint("CENTER")
+ cog.Hi = cog:CreateTexture(nil, "HIGHLIGHT")
+ if not (NE.tex and NE.tex.SetAtlas and NE.tex.SetAtlas(cog.Hi, "questlog-icon-setting", true)) then
+ cog.Hi:SetTexture("Interface\\Buttons\\UI-OptionsButton")
+ cog.Hi:SetSize(16, 16)
+ end
+ cog.Hi:SetPoint("CENTER")
+ cog.Hi:SetBlendMode("ADD")
+ cog.Hi:SetAlpha(0.4)
+ cog:SetScript("OnClick", function(self)
+ if CDS.ToggleSettingsMenu then CDS.ToggleSettingsMenu(self) end
+ end)
+ cog:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText("Options")
+ GameTooltip:Show()
+ end)
+ cog:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ f.settingsCog = cog
+
+ -- Scrollable body. A plain UIPanelScrollFrameTemplate — upstream uses the same, and there is no
+ -- WowScrollBox anywhere in the source to have to replace.
+ f.scroll = CreateFrame("ScrollFrame", PANEL_NAME .. "Scroll", f, "UIPanelScrollFrameTemplate")
+ f.scroll:SetPoint("TOPLEFT", 17, -72)
+ f.scroll:SetPoint("BOTTOMRIGHT", -30, FOOTER_H + SCROLL_BOTTOM_PAD) -- clears the footer row below
+ f.content = CreateFrame("Frame", nil, f.scroll)
+ f.content:SetSize(330, 1)
+ f.scroll:SetScrollChild(f.content)
+ -- The MODERN minimal scrollbar — the hand-built track+thumb every other list in this addon uses,
+ -- not NE.scrollbar.Reskin.
+ --
+ -- Reskin is the in-place re-skin of the stock UIPanelScrollBar Slider, and it is the older path
+ -- for a documented reason: it "was not rendering — the user still saw the default Blizzard bar"
+ -- (core/ScrollbarReskin.lua's own header, and the reason BuildCustom exists at all). On this frame
+ -- it could not have rendered: Reskin reaches for `scroll.ScrollBar`, and 3.3.5a's
+ -- UIPanelScrollFrameTemplate declares its slider as `$parentScrollBar` with no parentKey, so that
+ -- field is nil and Reskin returned at its second line without touching anything.
+ --
+ -- BuildCustomPixel is the variant for a real ScrollFrame — it drives SetVerticalScroll directly
+ -- and sizes the thumb from visible:total, rather than BuildCustom's row-step heuristic for
+ -- FauxScrollFrames. x = -6 puts the bar in the gutter this panel already reserves between the
+ -- scroll body's right edge and the Inset border.
+ if NE.scrollbar and NE.scrollbar.BuildCustomPixel then
+ pcall(NE.scrollbar.BuildCustomPixel, f.scroll, { x = -6 })
+ end
+
+ -- A SECOND scroll child for the settings tab, swapped in by SetDisplayMode. The grids own
+ -- `f.content` end to end — RefreshLayout rebuilds it from the adapter on every call — so a page of
+ -- controls needs its own child rather than a corner of theirs (PORT_PLAN §G.10 option (a)). Empty
+ -- until the tab is first opened; SettingsOptions.lua fills it then.
+ f.settingsContent = CreateFrame("Frame", nil, f.scroll)
+ f.settingsContent:SetSize(330, 1)
+ f.settingsContent:Hide()
+
+ -- Footer: the layout picker and Revert. Retail's is a WowStyle1DropdownTemplate, which does not
+ -- exist here — but the widget was never the point. This is a plain button that opens the same menu
+ -- tree through core/Menu.lua, with the selected layout's name as its label so the footer still
+ -- answers "which layout am I on" at a glance.
+ f.layoutButton = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
+ f.layoutButton:SetSize(180, 22)
+ f.layoutButton:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 18, 6)
+ f.layoutButton:SetScript("OnClick", function(self)
+ if CDS.ToggleLayoutMenu then CDS.ToggleLayoutMenu(self) end
+ end)
+ f.layoutButton:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText("Layouts")
+ GameTooltip:AddLine("Save, load, import and export the whole|nCooldown Manager setup for this class.",
+ 1, 1, 1)
+ GameTooltip:Show()
+ end)
+ f.layoutButton:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ -- Revert was deferred in 4b-1 because it needs a snapshot/restore pair, which arrived with
+ -- SettingsPresets.lua. Disabled until there is something to revert, so it never reads as broken.
+ f.revertButton = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
+ f.revertButton:SetSize(90, 22)
+ f.revertButton:SetPoint("LEFT", f.layoutButton, "RIGHT", 6, 0)
+ f.revertButton:SetText("Revert")
+ f.revertButton:SetScript("OnClick", function()
+ if CDS.Revert then CDS.Revert() end
+ end)
+ -- A disabled Button eats OnEnter, so the greyed Revert had no way to say WHY it was grey — and a
+ -- button that is permanently dark and silent reads as broken rather than as inapplicable. Reported
+ -- as exactly that: "the revert button does nothing". The tooltip is the only thing that can answer
+ -- "what would this even undo?", so it has to survive the state it is most needed in.
+ f.revertButton:SetMotionScriptsWhileDisabled(true)
+ f.revertButton:SetScript("OnEnter", function(self)
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetText("Revert")
+ GameTooltip:AddLine("Undoes the last layout change — applying a layout,|nimporting one, or the "
+ .. "starter reset.|n|nOne step, and only for this session.", 1, 1, 1)
+ if not (CDS.CanRevert and CDS.CanRevert()) then
+ GameTooltip:AddLine(" ")
+ GameTooltip:AddLine("Nothing to undo. It covers LAYOUTS, not the settings|non these tabs — a "
+ .. "viewer's own size and position revert|nfrom its edit-mode panel instead.", 1, 0.3, 0.3)
+ end
+ GameTooltip:Show()
+ end)
+ f.revertButton:SetScript("OnLeave", function() GameTooltip:Hide() end)
+
+ panel = f
+ CDS.panel = f
+ return f
+end
+
+CDS.Build = build
+
+-- ── Display mode ────────────────────────────────────────────────────────────────────────────────
+
+function CDS.SetDisplayMode(mode)
+ build()
+ if mode ~= "spells" and mode ~= "auras" and mode ~= "settings" then mode = "spells" end
+ currentMode = mode
+ CDS.displayMode = mode -- read by the category grids in 4b-2
+
+ for _, tab in ipairs(panel.tabButtons) do
+ tab:SetSelectedState(tab.displayMode == mode)
+ end
+
+ local settings = (mode == "settings")
+
+ -- The search box and the cog are both about the GRIDS: one dims non-matching tiles, the other
+ -- holds the Show Unlearned filter. Neither has any meaning over a page of sliders, and a search
+ -- box that silently does nothing is worse than an absent one.
+ if settings then panel.search:Hide() else panel.search:Show() end
+ if settings then panel.settingsCog:Hide() else panel.settingsCog:Show() end
+
+ -- Same for the footer: a layout captures spell lists, tracked auras, trinket placement, alerts and
+ -- sounds — not viewer geometry. Leaving "Layout: X" under a page of icon-size sliders would imply
+ -- it saves them.
+ if settings then panel.layoutButton:Hide() else panel.layoutButton:Show() end
+ if settings then panel.revertButton:Hide() else panel.revertButton:Show() end
+
+ if settings then
+ -- Built on first use; a player who never opens this tab never pays for its ~60 frames.
+ if CDS.EnsureSettingsPage then CDS.EnsureSettingsPage() end
+ panel.content:Hide()
+ panel.settingsContent:Show()
+ panel.scroll:SetScrollChild(panel.settingsContent)
+ if CDS.RefreshSettingsPage then CDS.RefreshSettingsPage() end
+ else
+ panel.settingsContent:Hide()
+ panel.content:Show()
+ panel.scroll:SetScrollChild(panel.content)
+ CDS.RefreshLayout()
+ end
+
+ -- After the refresh, so the new child has its final height: the ScrollFrame recomputes its range
+ -- from the child, and the scrollbar is stale until it does. The offset is one number on the SCROLL
+ -- frame, not per-child, so a swap from halfway down a long page would otherwise land the short one
+ -- scrolled past its own end.
+ panel.scroll:UpdateScrollChildRect()
+ panel.scroll:SetVerticalScroll(0)
+end
+
+function CDS.GetDisplayMode() return currentMode end
+
+-- ── Footer state ────────────────────────────────────────────────────────────────────────────────
+-- Both are called from SettingsPresets after any change, and are safe before the panel exists —
+-- Presets loads after this file but a slash command can reach either first.
+
+function CDS.RefreshLayoutDropdown()
+ if not (panel and panel.layoutButton) then return end
+ local cur = CDS.presets and CDS.presets.Current and CDS.presets.Current()
+ panel.layoutButton:SetText(cur or "Layout: Starter")
+end
+
+function CDS.RefreshRevertState()
+ if not (panel and panel.revertButton) then return end
+ if CDS.CanRevert and CDS.CanRevert() then
+ panel.revertButton:Enable()
+ else
+ panel.revertButton:Disable()
+ end
+end
+
+-- ── Search text ─────────────────────────────────────────────────────────────────────────────────
+-- ClassicAPI's SearchBoxTemplate has NO Instructions FontString: its placeholder IS the edit box's
+-- text (SearchBoxTemplate_OnLoad calls SetText(SEARCH), and OnEditFocusLost puts it back). So an
+-- untouched search box reads back "Search", and handing that to the filter dimmed every tile in the
+-- panel to 25% — which is what "all the icons are greyed out" turned out to be. Every read of the
+-- box goes through here.
+function CDS.GetSearchText()
+ if not (panel and panel.search) then return "" end
+ local t = panel.search:GetText() or ""
+ if t == (SEARCH or "Search") then return "" end
+ return t
+end
+
+-- Replaced by the real category-grid builder in SettingsCategories.lua, which loads after this file.
+-- Kept as a no-op so a load failure there costs the grids, not every SetDisplayMode call. (It used to
+-- paint a "coming in the next step" placeholder; that placeholder outlived 4b-2 by four phases.)
+function CDS.RefreshLayout() end
+
+-- ── Public entry points ─────────────────────────────────────────────────────────────────────────
+
+-- THE WINDOW IS PART OF THE MODULE, so an off Cooldown Manager has no window (owner's decision). Every
+-- route in — /cdm, the DragonUI options button, OpenTo from anywhere — funnels through ShowPanel, so
+-- this is the one gate, and it sits BEFORE build(): the panel is several hundred frames built lazily on
+-- first open, and a player who never turns the module on should never pay for them.
+--
+-- It also answers the question that off-by-default would otherwise leave open — where a player goes to
+-- switch it on. Not here: the window would be a dead end that configures viewers it cannot show. The
+-- switch is in DragonUI's options, which is where every other module's is, so the message names it.
+local function windowAvailable()
+ if M.IsEnabled() then return true end
+ if DEFAULT_CHAT_FRAME then
+ DEFAULT_CHAT_FRAME:AddMessage("|cff1784d1Cooldown Manager|r is turned off. Enable it in DragonUI's "
+ .. "options, under New Era > Cooldown Manager.")
+ end
+ return false
+end
+
+-- Always through SetDisplayMode, never a bare RefreshLayout: which body is on screen, which chrome is
+-- shown, and which refresher runs all depend on the mode, and re-opening on the settings tab has to
+-- re-read the store rather than rebuild the grids.
+function CDS.ShowPanel()
+ if not windowAvailable() then return end
+ build()
+ CDS.SetDisplayMode(currentMode or "spells")
+ if CDS.RefreshLayoutDropdown then CDS.RefreshLayoutDropdown() end
+ if CDS.RefreshRevertState then CDS.RefreshRevertState() end
+ panel:Show()
+end
+
+function CDS.HidePanel()
+ if panel then panel:Hide() end
+end
+
+-- Closing is never gated. The window can be up when the module goes off — open /cdm, then untick the
+-- toggle in DragonUI's options — and /cdm has to stay the way to shut it. (M.SetEnabled closes it for
+-- exactly that reason; this is the case where the player gets there first.)
+function CDS.TogglePanel()
+ if panel and panel:IsShown() then panel:Hide(); return end
+ CDS.ShowPanel()
+end
+
+-- Open with a viewer category pre-selecting the right tab. `category` may also be a display mode, so a
+-- caller can open straight onto "settings".
+--
+-- Gated before build() for the same reason ShowPanel is — this is a public entry point in its own right
+-- (M.OpenSettingsPanel), and reaching build() through it would defeat the point of gating the other.
+function CDS.OpenTo(category)
+ if not windowAvailable() then return end
+ build()
+ currentMode = CATEGORY_TO_MODE[category]
+ or ((category == "spells" or category == "auras" or category == "settings") and category)
+ or "spells"
+ CDS.ShowPanel()
+end
+
+-- One entry point for slash commands and any future keybind.
+M.OpenSettingsPanel = function(category) CDS.OpenTo(category) end
+
+SLASH_NECDMSETTINGS1 = "/cdm"
+SlashCmdList["NECDMSETTINGS"] = function() CDS.TogglePanel() end
diff --git a/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsPresets.lua b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsPresets.lua
new file mode 100644
index 0000000..8befc5c
--- /dev/null
+++ b/src/Data/ruRU/patch-ruRU-5/Interface/AddOns/DragonUI_NewEra/modules/cooldownviewer/SettingsPresets.lua
@@ -0,0 +1,679 @@
+-- DragonUI_NewEra/modules/cooldownviewer/SettingsPresets.lua — named layouts, import/export, and
+-- the session Undo the panel has been deferring since 4b-1. Phase 4b-5.
+--
+-- Downport of NewEra/CooldownViewerSettings/Presets.lua (378 lines), which is itself a
+-- reimplementation of retail's Cooldown Manager layout dropdown.
+--
+-- A "layout" is a snapshot of everything the /cdm panel can edit:
+--
+-- customLists[cat][CLASS] the editable Essential/Utility spell lists (per class)
+-- trackedAura[CLASS] the unified tracked-aura pool (per class)
+-- equipAssign[token] trinket placement (account-wide keys)
+-- alerts / sounds the per-cooldown alert and ready-sound maps (by spellID)
+--
+-- THE SNAPSHOT PAIR CAME WITH IT. Upstream's Panel.lua owns snapshotState/restoreState and exposes
+-- them as CDS.SnapshotState / CDS.RestoreState / CDS.DeepCopy; our 4b-1 panel deferred the Revert
+-- button precisely because that pair did not exist yet. It lives here now, so Revert is real — a
+-- layout apply and an undo are the same operation with a different source snapshot, and building
+-- two mechanisms for that would have been the mistake.
+--
+-- WHAT THE DOWNPORT CHANGED
+--
+-- * NO WowStyle1DropdownTemplate. Upstream's footer dropdown is a retail template. The footer
+-- control here is a plain button that opens CDS.BuildLayoutMenu through core/Menu.lua, which is
+-- the same menu tree either way — only the widget differs.
+--
+-- * IMPORT IS CLASS-CHECKED, which upstream does not do. A snapshot's spell lists are keyed by
+-- class, so restoring a Priest layout on a Mage writes into the Priest's slot and appears to do
+-- nothing at all. That is the worst possible failure mode for a paste — silence. Import now
+-- refuses with a message naming the class it was built for.
+--
+-- * Nothing else. The codec is upstream's and is already 3.3.5a-clean: string.byte/char, math.floor
+-- and string.find are all vanilla Lua 5.1, and there is no bit library anywhere in it.
+--
+-- SAFETY. Import NEVER executes pasted input. The parser is a hand-rolled typed reader — no
+-- loadstring, no setfenv — and every parse is wrapped in pcall, so a malformed or hostile paste
+-- produces an error message rather than an error. Plain SavedVariables reads/writes and StaticPopups
+-- throughout: no secure templates, nothing that can taint the combat path.
+
+local NE = DragonUI_NewEra
+local M = NE.cooldownviewer
+
+local CDS = NE.cooldownviewersettings
+
+local Presets = {}
+CDS.presets = Presets
+
+local MAGIC = "NECDM1"
+
+-- ── Store ───────────────────────────────────────────────────────────────────────────────────────
+
+local function cdLeaf(create)
+ local cfg = NE.Config and NE.Config()
+ if not cfg then return nil end
+ if not cfg.cooldownviewer then
+ if not create then return nil end
+ cfg.cooldownviewer = {}
+ end
+ return cfg.cooldownviewer
+end
+
+local function store()
+ local cd = cdLeaf(true)
+ if not cd then return nil end
+ cd.layouts = cd.layouts or {}
+ return cd.layouts
+end
+
+local function getCurrent()
+ local cd = cdLeaf(false)
+ return cd and cd.currentLayout or nil
+end
+
+local function setCurrent(name)
+ local cd = cdLeaf(true)
+ if cd then cd.currentLayout = name end
+end
+
+-- ── Snapshot / restore ──────────────────────────────────────────────────────────────────────────
+
+function CDS.DeepCopy(v)
+ if type(v) ~= "table" then return v end
+ local out = {}
+ for k, val in pairs(v) do out[k] = CDS.DeepCopy(val) end
+ return out
+end
+
+-- Appearance, captured RESOLVED rather than as a copy of a stored table.
+--
+-- What the exporting character actually sees is the account-wide table with their own per-character
+-- overrides on top (§H.3.8), so a raw copy of either half alone would export something nobody is
+-- looking at. Resolving through GetOpt also keeps the per-character bucket out of share strings
+-- entirely — its keys are a character name and a realm, and they mean nothing on another account.
+--
+-- Every frame gets every key, including the two bar-only ones. They resolve to their defaults on an
+-- icon viewer and nothing reads them there, and a loop with no exceptions in it is worth more than
+-- the few dozen bytes a per-frame key list would save.
+local function snapshotFrames()
+ local out = {}
+ for _, frameID in pairs(M.FRAME_ID or {}) do
+ local t = {}
+ for key in pairs(M.DEFAULTS or {}) do t[key] = M.GetOpt(frameID, key) end
+ out[frameID] = t
+ end
+ return out
+end
+
+-- The five editable leaves plus appearance, plus the class the snapshot was taken on. `class` is not
+-- decoration: it is what makes an imported string verifiable, and it is the key the spell lists are
+-- stored under.
+function CDS.SnapshotState()
+ local cd = cdLeaf(true)
+ if not cd then return nil end
+ local _, class = UnitClass("player")
+ -- The three LAYOUT leaves come from the active talent group's bucket, not the flat store, or a
+ -- saved layout would capture whatever the pre-per-spec migration happened to leave behind. Alerts
+ -- and sounds are per-spell preferences rather than layout and stay shared across specs.
+ local lay = M._layoutBucket and M._layoutBucket(true) or cd
+ return {
+ class = class,
+ customLists = CDS.DeepCopy(lay.customLists) or {},
+ trackedAura = CDS.DeepCopy(lay.trackedAura) or {},
+ equipAssign = CDS.DeepCopy(lay.equipAssign) or {},
+ alerts = CDS.DeepCopy(cd.alerts) or {},
+ sounds = CDS.DeepCopy(cd.sounds) or {},
+ -- Always captured, never conditionally applied. See M.LayoutsIncludeAppearance.
+ frames = snapshotFrames(),
+ }
+end
+
+-- Write a snapshot back and re-render. Whole-leaf assignment, not a merge: a layout is a complete
+-- state, so a spell the snapshot does not mention must end up unmentioned rather than surviving from
+-- whatever was there before.
+--
+-- InvalidateCuratedCache matters here. GetActiveSpellList caches the resolved curated list, and
+-- replacing customLists underneath it would otherwise leave the viewers rendering the previous
+-- layout until something else happened to dirty the cache.
+-- `opts.appearance` — true to force, false to force skip, nil to follow the player's setting. Revert
+-- passes true: an undo's contract is to put things back exactly as they were, including a change the
+-- setting happened to allow at the time and would not allow now.
+function CDS.RestoreState(snap, opts)
+ if type(snap) ~= "table" then return false end
+ local cd = cdLeaf(true)
+ if not cd then return false end
+
+ local wantAppearance = (opts and opts.appearance)
+ if wantAppearance == nil then
+ wantAppearance = M.LayoutsIncludeAppearance and M.LayoutsIncludeAppearance()
+ end
+ if wantAppearance and type(snap.frames) == "table" then
+ -- Through SetOpt, not by assigning the table: SetOpt is what knows whether this character writes
+ -- to its own bucket or to the shared one (§H.3.8), and it re-applies each viewer as it goes.
+ for frameID, vals in pairs(snap.frames) do
+ if type(vals) == "table" then
+ for key, value in pairs(vals) do M.SetOpt(frameID, key, value) end
+ end
+ end
+ end
+
+ -- Applied to the ACTIVE talent group only. A layout is something you chose for how you are playing
+ -- right now; writing it into both groups would undo the other spec's setup as a side effect of
+ -- loading one.
+ local lay = M._layoutBucket and M._layoutBucket(true) or cd
+ lay.customLists = CDS.DeepCopy(snap.customLists) or {}
+ lay.trackedAura = CDS.DeepCopy(snap.trackedAura) or {}
+ lay.equipAssign = CDS.DeepCopy(snap.equipAssign) or {}
+ cd.alerts = CDS.DeepCopy(snap.alerts) or {}
+ cd.sounds = CDS.DeepCopy(snap.sounds) or {}
+
+ if M.InvalidateCuratedCache then M.InvalidateCuratedCache() end
+ if M.RefreshActiveViewer then M.RefreshActiveViewer() end
+ if CDS.RefreshLayout then CDS.RefreshLayout() end
+ return true
+end
+
+-- ── Session undo ────────────────────────────────────────────────────────────────────────────────
+-- ONE step, and it is deliberately one: the panel's edits are individually reversible by hand (move
+-- it back, pick None), so the thing a player actually needs is "undo the layout I just applied", not
+-- a history. The snapshot is taken before any apply/starter/import, and cleared when the panel
+-- closes so a stale revert from an hour ago can never fire.
+
+-- The selected layout NAME is tracked beside the snapshot rather than inside it. It is session
+-- bookkeeping, not editable state, and putting it in the snapshot would bake it into every export
+-- string — so a shared layout would arrive carrying the name of the layout its author had selected.
+local undoSnap, undoCurrent
+
+local function markUndo()
+ undoSnap, undoCurrent = CDS.SnapshotState(), getCurrent()
+ if CDS.RefreshRevertState then CDS.RefreshRevertState() end
+end
+
+function CDS.CanRevert() return undoSnap ~= nil end
+
+function CDS.Revert()
+ if not undoSnap then return false end
+ local snap, cur = undoSnap, undoCurrent
+ undoSnap, undoCurrent = nil, nil
+ -- Unconditionally, including appearance. Revert undoes what the apply DID, and the setting may
+ -- have been flipped in between — leaving a resized viewer behind would make undo partial.
+ CDS.RestoreState(snap, { appearance = true })
+ setCurrent(cur)
+ if CDS.RefreshLayoutDropdown then CDS.RefreshLayoutDropdown() end
+ if CDS.RefreshRevertState then CDS.RefreshRevertState() end
+ return true
+end
+
+function CDS.ClearUndo()
+ undoSnap, undoCurrent = nil, nil
+ if CDS.RefreshRevertState then CDS.RefreshRevertState() end
+end
+
+-- ── Layouts ─────────────────────────────────────────────────────────────────────────────────────
+
+function Presets.Capture() return CDS.SnapshotState() end
+
+function Presets.Apply(name)
+ local st = store()
+ local snap = st and st[name]
+ if not snap then return false end
+ markUndo()
+ CDS.RestoreState(snap)
+ setCurrent(name)
+ if CDS.RefreshLayoutDropdown then CDS.RefreshLayoutDropdown() end
+ return true
+end
+
+-- New and Copy both snapshot the LIVE state, and that is correct rather than lazy: selecting a
+-- layout applies it, so the live state always equals the selected layout's saved data.
+function Presets.SaveAs(name)
+ if not name or name == "" then return false end
+ local st = store()
+ local snap = Presets.Capture()
+ if not (st and snap) then return false end
+ st[name] = snap
+ setCurrent(name)
+ if CDS.RefreshLayoutDropdown then CDS.RefreshLayoutDropdown() end
+ return true
+end
+
+function Presets.Rename(oldName, newName)
+ if not (oldName and newName) or newName == "" or newName == oldName then return false end
+ local st = store()
+ if not (st and st[oldName]) then return false end
+ st[newName] = st[oldName]
+ st[oldName] = nil
+ if getCurrent() == oldName then setCurrent(newName) end
+ if CDS.RefreshLayoutDropdown then CDS.RefreshLayoutDropdown() end
+ return true
+end
+
+function Presets.Delete(name)
+ local st = store()
+ if not (st and name and st[name]) then return false end
+ st[name] = nil
+ if getCurrent() == name then setCurrent(nil) end
+ if CDS.RefreshLayoutDropdown then CDS.RefreshLayoutDropdown() end
+ return true
+end
+
+function Presets.Names()
+ local st = store() or {}
+ local names = {}
+ for k in pairs(st) do names[#names + 1] = k end
+ table.sort(names)
+ return names
+end
+
+function Presets.Current() return getCurrent() end
+
+-- Everything back to the curated defaults, and off any named layout — you are now on the unsaved
+-- starter, which is exactly retail's built-in starter layout.
+function Presets.UseStarter()
+ markUndo()
+ local _, class = UnitClass("player")
+ if M.ResetCustomList then
+ M.ResetCustomList("essential", class)
+ M.ResetCustomList("utility", class)
+ end
+ if M.ResetTrackedAura then M.ResetTrackedAura(class) end
+ local cd = cdLeaf(true)
+ local lay = M._layoutBucket and M._layoutBucket(true)
+ if lay then
+ lay.equipAssign = {} -- every trinket back to the source pool: the starter state is opt-in
+ end
+ if cd then
+ cd.alerts = {}
+ cd.sounds = {}
+ end
+ setCurrent(nil)
+ if M.InvalidateCuratedCache then M.InvalidateCuratedCache() end
+ if M.RefreshActiveViewer then M.RefreshActiveViewer() end
+ if CDS.RefreshLayout then CDS.RefreshLayout() end
+ if CDS.RefreshLayoutDropdown then CDS.RefreshLayoutDropdown() end
+ return true
+end
+
+-- ── Per-spec starter ────────────────────────────────────────────────────────────────────────────
+--
+-- UseStarter above resets to the CLASS list — every curated spell the class has. That list is a union
+-- across the class's specs, because the curated tables cannot express anything narrower (the runtime
+-- gate is "have you learned it", and every druid has learned Wrath), so a Feral druid lands on
+-- Balance's nukes as well as their own. This narrows it to one tree.
+--
+-- DISABLED, NOT DELETED. The starter writes a custom list holding EVERY spell in the class's Essential
+-- list, with the spec's own turned on and the rest turned off — so off-spec spells land under Not
+-- Displayed in the picker, one drag from coming back. Deleting them would make the starter a thing you
+-- cannot undo except by resetting the whole layout.
+--
+-- ESSENTIAL ONLY. Utility is defensives, interrupts and escapes that a character of any spec might
+-- press; the bloat this exists to answer is all in Essential.
+function Presets.UseSpecStarter(tab)
+ tab = tonumber(tab)
+ local _, class = UnitClass("player")
+ local byTab = class and (M.STARTER_BY_CLASS or {})[class]
+ local want = byTab and tab and byTab[tab]
+ if not want then return false end
+
+ markUndo()
+
+ local keep = {}
+ for _, id in ipairs(want) do keep[id] = true end
+
+ -- Built from the CURATED list, not from the starter, so a spell the starter does not name is still
+ -- listed (disabled) rather than missing. GetEditableList seeds one from the curated defaults when
+ -- the character has no custom list yet, which is exactly the shape needed here.
+ local list = M.GetEditableList and M.GetEditableList("essential", class)
+ if not list then return false end
+ for _, entry in ipairs(list) do
+ entry.enabled = keep[entry.spellID] and true or false
+ end
+ if M.SetCustomList then M.SetCustomList("essential", class, list) end
+
+ -- Off any named layout: what is on screen is no longer what that layout said.
+ setCurrent(nil)
+ if M.InvalidateCuratedCache then M.InvalidateCuratedCache() end
+ if M.RefreshActiveViewer then M.RefreshActiveViewer() end
+ if CDS.RefreshLayout then CDS.RefreshLayout() end
+ if CDS.RefreshLayoutDropdown then CDS.RefreshLayoutDropdown() end
+ return true
+end
+
+-- ONCE PER BUCKET, and that is the whole safety argument. A layout bucket is created the first time a
+-- character plays a talent group, and this marks it the moment it is seeded — so the auto path can
+-- never run twice over the same bucket and can never overwrite curation. Re-deriving on every respec
+-- was considered and rejected by the owner for exactly that reason.
+--
+-- Skipped entirely when DetectSpec returns nil (no points, or a tie): there is nothing to seed FROM,
+-- and a level-5 character simply meets the full class list until they spend a point. The next call
+-- after that point lands, seeds properly, because the marker is only set when a starter is applied.
+function Presets.SeedStarterIfFresh()
+ local lay = M._layoutBucket and M._layoutBucket(true)
+ if not lay or lay.starterSeeded then return false end
+ local tab = M.DetectSpec and M.DetectSpec()
+ if not tab then return false end
+ lay.starterSeeded = tab
+ local ok = Presets.UseSpecStarter(tab)
+ if ok and NE.Log then
+ NE.Log("CDM", "seeded the " .. tostring((M.SpecNames and M.SpecNames()[tab]) or tab)
+ .. " starter layout for a new spec")
+ end
+ return ok
+end
+
+-- ── Share-string codec ──────────────────────────────────────────────────────────────────────────
+-- Base64 over a hand-rolled typed serializer. Base64 because a share string has to survive being
+-- pasted through a chat window and a forum post; typed-serializer because the alternative — emitting
+-- Lua and reading it back with loadstring — means executing a stranger's paste.
+--
+-- Tags: z=nil, T=true, F=false, n;, s:, t;…
+-- Length-prefixed strings, so a spell name containing any of the tag characters is still safe.
+
+local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+local B64DEC = {}
+for i = 1, #B64 do B64DEC[B64:sub(i, i)] = i - 1 end
+
+local function base64Encode(data)
+ local out, i, n = {}, 1, #data
+ while i <= n do
+ local b1 = data:byte(i)
+ local b2 = data:byte(i + 1)
+ local b3 = data:byte(i + 2)
+ local n1 = math.floor(b1 / 4)
+ local n2 = (b1 % 4) * 16 + math.floor((b2 or 0) / 16)
+ local n3 = ((b2 or 0) % 16) * 4 + math.floor((b3 or 0) / 64)
+ local n4 = (b3 or 0) % 64
+ out[#out + 1] = B64:sub(n1 + 1, n1 + 1)
+ out[#out + 1] = B64:sub(n2 + 1, n2 + 1)
+ out[#out + 1] = b2 and B64:sub(n3 + 1, n3 + 1) or "="
+ out[#out + 1] = b3 and B64:sub(n4 + 1, n4 + 1) or "="
+ i = i + 3
+ end
+ return table.concat(out)
+end
+
+local function base64Decode(s)
+ s = s:gsub("[^%w%+/=]", "")
+ local out, i, n = {}, 1, #s
+ while i <= n do
+ local c1 = B64DEC[s:sub(i, i)]
+ local c2 = B64DEC[s:sub(i + 1, i + 1)]
+ local c3c = s:sub(i + 2, i + 2); local c3 = B64DEC[c3c]
+ local c4c = s:sub(i + 3, i + 3); local c4 = B64DEC[c4c]
+ if not (c1 and c2) then break end
+ out[#out + 1] = string.char(c1 * 4 + math.floor(c2 / 16))
+ if c3c ~= "=" and c3 then out[#out + 1] = string.char((c2 % 16) * 16 + math.floor(c3 / 4)) end
+ if c4c ~= "=" and c4 then out[#out + 1] = string.char((c3 % 4) * 64 + c4) end
+ i = i + 4
+ end
+ return table.concat(out)
+end
+
+local function serialize(v, out)
+ local t = type(v)
+ if v == nil then out[#out + 1] = "z"
+ elseif t == "boolean" then out[#out + 1] = v and "T" or "F"
+ elseif t == "number" then out[#out + 1] = "n" .. tostring(v) .. ";"
+ elseif t == "string" then out[#out + 1] = "s" .. #v .. ":" .. v
+ elseif t == "table" then
+ local count = 0
+ for _ in pairs(v) do count = count + 1 end
+ out[#out + 1] = "t" .. count .. ";"
+ for k, val in pairs(v) do serialize(k, out); serialize(val, out) end
+ else
+ out[#out + 1] = "z" -- functions/userdata never appear in a snapshot
+ end
+end
+
+-- Raises on malformed input, which is what decode's pcall is for.
+local function parseValue(s, i)
+ local tag = s:sub(i, i)
+ i = i + 1
+ if tag == "z" then return nil, i
+ elseif tag == "T" then return true, i
+ elseif tag == "F" then return false, i
+ elseif tag == "n" then
+ local j = s:find(";", i, true); if not j then error("num") end
+ local num = tonumber(s:sub(i, j - 1)); if not num then error("num") end
+ return num, j + 1
+ elseif tag == "s" then
+ local j = s:find(":", i, true); if not j then error("str") end
+ local len = tonumber(s:sub(i, j - 1)); if not len then error("str") end
+ local start = j + 1
+ if start + len - 1 > #s then error("str") end
+ return s:sub(start, start + len - 1), start + len
+ elseif tag == "t" then
+ local j = s:find(";", i, true); if not j then error("tbl") end
+ local count = tonumber(s:sub(i, j - 1)); if not count then error("tbl") end
+ -- No bound on `count`, deliberately. A declared pair count is attacker-controlled, so the
+ -- obvious move is to cap it — but the loop is already self-limiting: every iteration must
+ -- consume at least one byte of payload or raise, so a header claiming a billion pairs fails on
+ -- the first one rather than spinning. A cap here would be a guard no input can reach, and the
+ -- test below is what establishes that rather than asserting the cap exists.
+ i = j + 1
+ local out = {}
+ for _ = 1, count do
+ local k; k, i = parseValue(s, i)
+ local v; v, i = parseValue(s, i)
+ if k ~= nil then out[k] = v end
+ end
+ return out, i
+ end
+ error("tag")
+end
+
+function Presets.Encode(snap)
+ local out = {}
+ serialize(snap, out)
+ return MAGIC .. base64Encode(table.concat(out))
+end
+
+-- Returns snapshot, or nil + a reason fit to show the player. Never errors.
+function Presets.Decode(str)
+ if type(str) ~= "string" then return nil, "nothing pasted" end
+ str = str:gsub("%s", "")
+ if str == "" then return nil, "nothing pasted" end
+ if str:sub(1, #MAGIC) ~= MAGIC then return nil, "not a Cooldown Manager layout string" end
+ local payload = base64Decode(str:sub(#MAGIC + 1))
+ local ok, val = pcall(function() return (parseValue(payload, 1)) end)
+ if not ok or type(val) ~= "table" then return nil, "corrupt layout string" end
+ if type(val.class) ~= "string" then return nil, "not a Cooldown Manager layout" end
+ -- The class gate upstream lacks. The spell lists are keyed by class, so applying another class's
+ -- layout writes into that class's slot and changes nothing visible — a silent no-op is a worse
+ -- outcome than a refusal, because the player has no way to tell it from a broken feature.
+ local _, class = UnitClass("player")
+ if class and val.class ~= class then
+ return nil, ("that layout is for a %s"):format(val.class)
+ end
+ return val
+end
+
+-- ── Import / export ─────────────────────────────────────────────────────────────────────────────
+
+function Presets.Export(name)
+ local st = store()
+ local snap = (name and st and st[name]) or Presets.Capture()
+ if not snap then return end
+ StaticPopup_Show("NE_CDM_LAYOUT_EXPORT", nil, nil, { str = Presets.Encode(snap) })
+end
+
+function Presets.Import(text)
+ local snap, err = Presets.Decode(text)
+ if not snap then
+ local msg = "Cooldown Manager layout: " .. (err or "invalid string")
+ if UIErrorsFrame then UIErrorsFrame:AddMessage(msg, 1, 0.3, 0.3) end
+ if NE.Log then NE.Log(msg) end
+ return false
+ end
+ CDS.PromptName("Imported", function(name)
+ if not name or name == "" then return end
+ local st = store()
+ if not st then return end
+ st[name] = snap
+ Presets.Apply(name)
+ end)
+ return true
+end
+
+-- ── StaticPopups ────────────────────────────────────────────────────────────────────────────────
+-- 3.3.5a's StaticPopup names its edit box