--[[ FoxbaneAssist — клиентская часть серверного помощника боя (модуль mod-assisted-combat). Сервер сам решает, что кастовать, и присылает addon-сообщения с префиксом FBAC: N|| — рекомендованная способность (spellId = 0 значит «сбросить») S|<0|1>|<штраф ГКД> — помощник включён и для класса есть правила E|<код> — применить нечего, код причины (power, cd, range, los, target, none) R|:<изучено>:<уровень>|... — состав ротации по приоритету для списка в книге Подсказка живёт на панели команд: на кнопке способности «Помощник» подменяется иконка, а сама рекомендованная способность, если она стоит на панели, подсвечивается рамкой. Состав ротации показывает кнопка в правом верхнем углу книги заклинаний. Трогаем только текстуры: защищённые кнопки и их атрибуты в бою не меняются, иначе клиент ругается «ошибка интерфейсной операции, вызванная модификацией». ]] local VERSION = "2.0" local PREFIX = "FBAC" local ASSIST_SPELL_ID = 930001 local NO_ICON = "Interface\\Icons\\INV_Misc_QuestionMark" local ICON_INTERVAL = 0.15 local SCAN_INTERVAL = 3.0 local DB_VERSION = 5 local BAR_PREFIXES = { "ActionButton", "BonusActionButton", "MultiBarBottomLeftButton", "MultiBarBottomRightButton", "MultiBarRightButton", "MultiBarLeftButton", } local ERROR_TEXT = { power = "не хватает ресурса", cd = "всё на восстановлении", range = "неподходящая дистанция", los = "цель не видна", target = "нет цели", item = "нет нужного оружия", unknown = "нужные способности ещё не изучены", none = "пока нечего применить", } local defaults = { version = DB_VERSION, glow = true, introShown = false, rotations = {}, -- персонаж -> последняя присланная сервером ротация } local db local UpdateBookButton local button -- невидимая защищённая кнопка: нужна только для горячей клавиши local bookButton local currentSpellId = 0 local currentSpellName local serverEnabled = true local gcdPenalty = 25 local rotation = {} local warnedMissingSpell = false local pendingSecure = false local iconLeft, scanLeft = 0, 0 local lastErrorCode, lastErrorAt = nil, 0 local buttons = {} -- все кнопки панелей local assistButtons = {} -- те, на которых стоит помощник local glowTargets = {} -- те, где стоит рекомендованная способность local slotSpell = {} -- кнопка -> имя способности на ней local overridden = {} local glowing = {} local scanTip BINDING_HEADER_FOXBANEASSIST = "Помощник боя" _G["BINDING_NAME_CLICK FoxbaneAssistButton:LeftButton"] = "Применить совет помощника" local function Print(msg) if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffПомощник:|r " .. msg) end end --- Что лежит на панелях ------------------------------------------------------ local function CollectButtons() for _, prefix in ipairs(BAR_PREFIXES) do for index = 1, 12 do local btn = _G[prefix .. index] if btn and btn.GetName then buttons[#buttons + 1] = btn end end end end -- Номер слота лежит в разных местах: зависит от страницы панели и от того, -- кто создал кнопку, поэтому перебираем все известные источники. local function ButtonSlot(btn) local slot = btn.action if not slot and ActionButton_CalculateAction then slot = ActionButton_CalculateAction(btn) end if not slot and btn.GetAttribute then slot = btn:GetAttribute("action") end return tonumber(slot) end -- GetActionInfo на 3.3.5 отдаёт полезный spell id не для всякого действия -- (макросы, кастомные спеллы), поэтому имя дочитываем из подсказки слота. local function SlotActionName(slot) if not scanTip then scanTip = CreateFrame("GameTooltip", "FoxbaneAssistScanTip", nil, "GameTooltipTemplate") end scanTip:SetOwner(UIParent, "ANCHOR_NONE") scanTip:ClearLines() scanTip:SetAction(slot) local line = _G["FoxbaneAssistScanTipTextLeft1"] return line and line:GetText() end local function ButtonGlow(btn) if not btn.fbGlow then local texture = btn:CreateTexture(nil, "OVERLAY") texture:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") texture:SetBlendMode("ADD") texture:SetPoint("TOPLEFT", btn, "TOPLEFT", -14, 14) texture:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 14, -14) texture:Hide() btn.fbGlow = texture end return btn.fbGlow end local function RestoreIcon(btn) overridden[btn] = nil local slot = ButtonSlot(btn) local texture = _G[btn:GetName() .. "Icon"] if texture and slot then texture:SetTexture(GetActionTexture(slot)) end end local function IsAssistButton(btn) for _, other in ipairs(assistButtons) do if other == btn then return true end end return false end --- Отрисовка ---------------------------------------------------------------- local function ApplyGlow() for btn in pairs(glowing) do glowing[btn] = nil if btn.fbGlow then btn.fbGlow:Hide() end end for _, btn in ipairs(glowTargets) do glowing[btn] = true ButtonGlow(btn):Show() end end local function UpdateGlowTargets() for index = #glowTargets, 1, -1 do glowTargets[index] = nil end if db.glow and currentSpellName then for _, btn in ipairs(buttons) do if slotSpell[btn] == currentSpellName and not IsAssistButton(btn) then glowTargets[#glowTargets + 1] = btn end end end ApplyGlow() end -- Blizzard перерисовывает иконки панелей на множестве событий, поэтому подмену -- приходится возобновлять; это дёшево, кнопок помощника обычно одна. local function ApplyIcons() local texture = currentSpellId > 0 and select(3, GetSpellInfo(currentSpellId)) or nil for _, btn in ipairs(assistButtons) do local slotIcon = _G[btn:GetName() .. "Icon"] if slotIcon then if texture then slotIcon:SetTexture(texture) overridden[btn] = true elseif overridden[btn] then RestoreIcon(btn) end end end end local function Rescan() for index = #assistButtons, 1, -1 do assistButtons[index] = nil end local assistName = GetSpellInfo(ASSIST_SPELL_ID) for _, btn in ipairs(buttons) do local slot = ButtonSlot(btn) local name if slot and HasAction(slot) then local actionType, id = GetActionInfo(slot) if actionType == "spell" and id and id > 0 then name = GetSpellInfo(id) end if not name then name = SlotActionName(slot) end end slotSpell[btn] = name if assistName and name == assistName then assistButtons[#assistButtons + 1] = btn elseif overridden[btn] then RestoreIcon(btn) end end UpdateGlowTargets() ApplyIcons() end local function SetRecommendation(spellId) if spellId == currentSpellId then return end currentSpellId = spellId currentSpellName = spellId > 0 and GetSpellInfo(spellId) or nil ApplyIcons() UpdateGlowTargets() UpdateBookButton() end local function ShowError(code) local text = ERROR_TEXT[code] if not text then return end -- Помощник опрашивается часто, и одна и та же причина не должна забивать экран. local now = GetTime() if code == lastErrorCode and (now - lastErrorAt) < 1.0 then return end lastErrorCode, lastErrorAt = code, now if UIErrorsFrame then UIErrorsFrame:AddMessage("Помощник: " .. text, 1, 0.3, 0.3, 1) end end --- Кнопка для горячей клавиши ------------------------------------------------ -- Имя спелла берётся из клиентского Spell.dbc; без записи в патче кнопка кастовать не сможет. local function RefreshSpellAttribute() local name = GetSpellInfo(ASSIST_SPELL_ID) if name then if InCombatLockdown() then pendingSecure = true else button:SetAttribute("spell", name) if bookButton then bookButton:SetAttribute("spell", name) end end return true end if not warnedMissingSpell then warnedMissingSpell = true Print("|cffff5555спелл помощника не найден в клиенте.|r Обнови клиентский патч через лаунчер.") end return false end -- Видимой кнопки посреди экрана нет: помощник живёт на панели команд и в книге. -- Эта кнопка нужна только затем, чтобы к ней цеплялась горячая клавиша из Bindings.xml, -- поэтому она размером в пиксель, прозрачная и не ловит мышь. local function BuildButton() button = CreateFrame("Button", "FoxbaneAssistButton", UIParent, "SecureActionButtonTemplate") button:SetAttribute("type", "spell") button:RegisterForClicks("AnyUp") button:SetWidth(1) button:SetHeight(1) button:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0) button:SetAlpha(0) button:EnableMouse(false) end --- Кнопка в книге заклинаний ------------------------------------------------- local function RotationTooltip(owner) GameTooltip:SetOwner(owner, "ANCHOR_LEFT") GameTooltip:AddLine("Однокнопочный помощник") GameTooltip:AddLine("Применяет способности во время боя, чередуя их согласно заданным приоритетам.", 1, 0.6, 0.2, true) GameTooltip:AddLine(string.format( "Общее время восстановления способностей, применяемых помощником, увеличивается на %d%%.", gcdPenalty), 1, 0.6, 0.2, true) GameTooltip:AddLine(" ") if not serverEnabled then GameTooltip:AddLine("Для твоего класса помощник выключен.", 1, 0.4, 0.4) elseif #rotation == 0 then GameTooltip:AddLine("Список появится через несколько секунд после входа в мир.", 0.7, 0.7, 0.7) else for index = 1, #rotation do local entry = rotation[index] local name, _, tex = GetSpellInfo(entry.id) if name then -- Иконку вставляем прямо в строку: отдельных колонок в подсказке нет. local line = string.format("|T%s:18:18:0:0|t %s", tex or NO_ICON, name) if not entry.known then GameTooltip:AddLine(line .. string.format(" (с %d уровня)", entry.level), 0.5, 0.5, 0.5) elseif entry.id == currentSpellId then GameTooltip:AddLine(line, 0.4, 1, 0.4) else GameTooltip:AddLine(line, 1, 1, 1) end end end end GameTooltip:AddLine(" ") GameTooltip:AddLine("Перетащи на панель команд, чтобы применять помощника.", 0.6, 0.6, 0.6) GameTooltip:Show() end -- Ротацию держим в SavedVariables: она приходит с сервера не мгновенно, -- а список способностей класса должен быть виден сразу после входа. local function RotationKey() return (UnitName("player") or "?") .. "-" .. (GetRealmName() or "?") end local function LoadRotation() local saved = db.rotations and db.rotations[RotationKey()] if not saved then return end for index = 1, #saved do local entry = saved[index] rotation[index] = { id = entry.id, known = entry.known, level = entry.level } end end local function SaveRotation() db.rotations = db.rotations or {} local copy = {} for index = 1, #rotation do local entry = rotation[index] copy[index] = { id = entry.id, known = entry.known, level = entry.level } end db.rotations[RotationKey()] = copy end -- PickupSpell в 3.3.5 работает по номеру строки в книге, а не по id спелла, -- поэтому ищем помощника перебором книги по имени. local function AssistBookIndex() local target = GetSpellInfo(ASSIST_SPELL_ID) if not target then return nil end local index = 1 while true do local name = GetSpellName(index, BOOKTYPE_SPELL) if not name then return nil end if name == target then return index end index = index + 1 end end local function PickupAssistSpell() if InCombatLockdown() then return end local index = AssistBookIndex() if index then pcall(PickupSpell, index, BOOKTYPE_SPELL) end if not GetCursorInfo() then Print("не удалось взять способность: её нет в книге заклинаний. Обнови клиент лаунчером.") end end local function BuildBookButton() if bookButton or not SpellBookFrame then return end bookButton = CreateFrame("Button", "FoxbaneAssistBookButton", SpellBookFrame, "SecureActionButtonTemplate") bookButton:SetAttribute("type", "spell") bookButton:RegisterForClicks("AnyUp") bookButton:RegisterForDrag("LeftButton") bookButton:SetWidth(34) bookButton:SetHeight(34) bookButton:SetPoint("TOPRIGHT", SpellBookFrame, "TOPRIGHT", -46, -38) local tex = bookButton:CreateTexture(nil, "ARTWORK") tex:SetAllPoints(bookButton) tex:SetTexCoord(0.07, 0.93, 0.07, 0.93) tex:SetTexture(select(3, GetSpellInfo(ASSIST_SPELL_ID)) or NO_ICON) bookButton.icon = tex local ring = bookButton:CreateTexture(nil, "OVERLAY") ring:SetTexture("Interface\\Buttons\\UI-Quickslot2") ring:SetPoint("TOPLEFT", bookButton, "TOPLEFT", -13, 13) ring:SetPoint("BOTTOMRIGHT", bookButton, "BOTTOMRIGHT", 13, -13) bookButton:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD") bookButton:SetScript("OnEnter", function(self) RotationTooltip(self) end) bookButton:SetScript("OnLeave", function() GameTooltip:Hide() end) bookButton:SetScript("OnDragStart", PickupAssistSpell) end function UpdateBookButton() if not bookButton then return end bookButton.icon:SetTexture(select(3, GetSpellInfo(ASSIST_SPELL_ID)) or NO_ICON) if GameTooltip:IsOwned(bookButton) then RotationTooltip(bookButton) end end --- Сообщения сервера -------------------------------------------------------- local function OnAddonMessage(prefix, message, _, sender) if prefix ~= PREFIX or not message then return end if sender and sender ~= UnitName("player") then return end local spellId = message:match("^N|(%d+)|") if spellId then SetRecommendation(tonumber(spellId) or 0) return end local code = message:match("^E|(%a+)") if code then ShowError(code) return end local list = message:match("^R(.*)$") if list then for index = #rotation, 1, -1 do rotation[index] = nil end for id, known, level in list:gmatch("(%d+):(%d+):(%d+)") do rotation[#rotation + 1] = { id = tonumber(id), known = known == "1", level = tonumber(level), } end SaveRotation() UpdateBookButton() return end local status, penalty = message:match("^S|(%d+)|?(%d*)") if status then serverEnabled = (status == "1") gcdPenalty = tonumber(penalty) or gcdPenalty UpdateBookButton() end end local function OnTick(_, elapsed) iconLeft = iconLeft - elapsed if iconLeft <= 0 then iconLeft = ICON_INTERVAL ApplyIcons() end scanLeft = scanLeft - elapsed if scanLeft <= 0 then scanLeft = SCAN_INTERVAL Rescan() end end --- Команды ------------------------------------------------------------------ local function Help() Print("версия " .. VERSION .. ". Перетащи «Помощника» из книги заклинаний на панель команд — на его кнопке будет меняться иконка того, что советует сервер.") Print(" /fbassist glow on|off — подсветка рекомендованной способности на панели") Print(" /fbassist debug — что аддон видит на панелях") Print("Кнопка помощника — в правом верхнем углу книги заклинаний: наведи, чтобы увидеть ротацию, перетащи на панель команд.") end local function DebugDump() Print("версия " .. VERSION .. ", рекомендация: " .. (currentSpellName or "нет") .. " (id " .. currentSpellId .. ")") Print("спелл помощника в клиенте: " .. (GetSpellInfo(ASSIST_SPELL_ID) or "|cffff5555НЕ НАЙДЕН|r")) Print("кнопок помощника на панелях: " .. #assistButtons) local shown = 0 for _, btn in ipairs(buttons) do local name = slotSpell[btn] if name then shown = shown + 1 if shown <= 24 then Print(string.format(" %s слот=%s: %s%s", btn:GetName(), tostring(ButtonSlot(btn)), name, IsAssistButton(btn) and " |cff66ff66<= ПОМОЩНИК|r" or "")) end end end Print("занятых кнопок: " .. shown .. " из " .. #buttons) end SLASH_FOXBANEASSIST1 = "/fbassist" SLASH_FOXBANEASSIST2 = "/fba" SlashCmdList["FOXBANEASSIST"] = function(input) local cmd, arg = string.match(input or "", "^(%S*)%s*(.-)$") cmd = string.lower(cmd or "") arg = string.lower(arg or "") if cmd == "glow" then db.glow = (arg ~= "off") UpdateGlowTargets() Print("подсветка на панели: " .. (db.glow and "вкл" or "выкл") .. ".") elseif cmd == "debug" then DebugDump() else Help() end end --- События ------------------------------------------------------------------ local events = CreateFrame("Frame") events:RegisterEvent("ADDON_LOADED") events:RegisterEvent("PLAYER_LOGIN") events:RegisterEvent("PLAYER_ENTERING_WORLD") events:RegisterEvent("PLAYER_REGEN_ENABLED") events:RegisterEvent("SPELLS_CHANGED") events:RegisterEvent("LEARNED_SPELL_IN_TAB") events:RegisterEvent("ACTIONBAR_SLOT_CHANGED") events:RegisterEvent("ACTIONBAR_PAGE_CHANGED") events:RegisterEvent("UPDATE_BONUS_ACTIONBAR") events:RegisterEvent("CHAT_MSG_ADDON") events:SetScript("OnEvent", function(_, event, a1, a2, a3, a4) if event == "CHAT_MSG_ADDON" then OnAddonMessage(a1, a2, a3, a4) return end if event == "ADDON_LOADED" then if a1 ~= "FoxbaneAssist" then return end FoxbaneAssistDB = FoxbaneAssistDB or {} db = FoxbaneAssistDB for key, value in pairs(defaults) do if db[key] == nil then db[key] = value end end -- Плавающая кнопка убрана совсем: помощник берётся из книги на панель команд. if db.version ~= DB_VERSION then db.version = DB_VERSION db.point, db.relPoint, db.x, db.y = nil, nil, nil, nil db.size, db.locked, db.buttonMode, db.showName = nil, nil, nil, nil end CollectButtons() LoadRotation() BuildButton() BuildBookButton() events:SetScript("OnUpdate", OnTick) return end if not button then return end if event == "PLAYER_REGEN_ENABLED" and pendingSecure then pendingSecure = false RefreshSpellAttribute() end if event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" or event == "SPELLS_CHANGED" or event == "LEARNED_SPELL_IN_TAB" then RefreshSpellAttribute() end Rescan() if event == "PLAYER_LOGIN" and not db.introShown then db.introShown = true Help() end end)