From c4911ef9fe0474dfb78466836d555701f73bb739 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Sat, 4 Apr 2026 23:38:34 +0200 Subject: [PATCH 01/42] 1.5.1 --- CHANGELOG.md | 17 + api/bag-ammo-labels.lua | 463 ++++++++ api/options-expammo.lua | 27 + api/options-smartpet.lua | 321 ++++++ api/options-zbuttons.lua | 32 + data/ds-items.lua | 27 + data/ds-zones-fallback.lua | 7 + data/ds-zones-wma.lua | 3 + init/api.xml | 1 + modules/expammo/engine.lua | 81 ++ modules/smartpet/engine.lua | 1293 ++++++++++++++++++++++ modules/smartpet/loader.xml | 6 + modules/zhunter/ZSpellButtonTemplate.lua | 2 +- modules/zhunter/zButtonAmmo.lua | 9 +- 14 files changed, 2283 insertions(+), 6 deletions(-) create mode 100644 api/bag-ammo-labels.lua create mode 100644 api/options-smartpet.lua create mode 100644 modules/smartpet/engine.lua create mode 100644 modules/smartpet/loader.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a70cdb..229d6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to MetaHunt will be documented in this file. +## [1.5.1] - 2026-04-01 + + +### Added + +- **zAmmo — bag ammo labels**: Quality-colored short-name labels overlaid on ammo items in bags and bank, with optional heat-colored DPS damage indicator. Grey below 7.5 DPS, red-to-green gradient from 7.5 to 20.5. Three new checkboxes in Options → zButtons → zAmmo → Bags section: bag labels, bank labels, and damage display. Works with both default bags and pfUI bags. + +- **MM Widget — immunity detection & rotation toggles**: The MM Widget keybind now detects when the current target is immune to a damage school (Fire / Nature / Arcane) and automatically skips the useless shot, casting Aimed or Steady Shot instead. Immunity is detected in two ways: reactively via combat log "immune" messages, and proactively via hardcoded creature-type rules (e.g. Elementals are always flagged Nature-immune on target). Immune cells show a red border. Three new checkboxes in Options → ExpAmmo → Rotation let you permanently disable individual shots (Multi-Shot, Serpent Sting, Arcane Shot) — unchecked shots are skipped during their proc window. + + +### Fixed + +- **Zone mapping — 22 beasts with raw zone IDs**: Fixed 8 unmapped zone IDs (Blackrock Depths, Molten Core, Wailing Caverns, Maraudon, Sunnyglade, Icepoint Rock, Winter Veil Vale, Gilneas City, Karazhan) that caused 22 beasts to display as "Zone 617" instead of their proper zone name in the Hunter Book. + +- **MM Widget — IsStateBlocked nil error**: Moved `MTH_EA_IsStateBlocked` definition before its first use, fixing a nil function call on load. + + ## [1.5.0] - 2026-03-31 diff --git a/api/bag-ammo-labels.lua b/api/bag-ammo-labels.lua new file mode 100644 index 0000000..eb7b53a --- /dev/null +++ b/api/bag-ammo-labels.lua @@ -0,0 +1,463 @@ +-- ============================================================ +-- Bag Ammo Labels — standalone overlay for bag & bank ammo slots +-- Uses globals from zButtonAmmo.lua: +-- zButtonAmmo_ShortArrowLabels, zButtonAmmo_ShortBulletLabels, +-- zButtonAmmo_GetQualityColor, zButtonAmmo_GetColorGradient, +-- zButtonAmmo_GetKnownAmmoTypes +-- Uses DPS data from MTH_DS_AmmoItems[id].dps +-- ============================================================ + +-- Saved variable cache (per-character, stored in "general" module) +local cache = {} -- bagAmmoLabels, bankAmmoLabels, bagAmmoDamage + +local function GetStore() + return MTH and MTH.GetModuleCharSavedVariables and MTH:GetModuleCharSavedVariables("general") +end + +local function GetCached(key, default) + if cache[key] == nil then + local store = GetStore() + if type(store) == "table" and store[key] ~= nil then + cache[key] = store[key] and true or false + else + cache[key] = default or false + end + end + return cache[key] +end + +local function SetCached(key, enabled) + cache[key] = enabled and true or false + local store = GetStore() + if type(store) == "table" then + store[key] = cache[key] + end +end + +-- Public getters/setters +function MTH_BagAmmoLabels_GetEnabled() + return GetCached("bagAmmoLabels") +end +function MTH_BagAmmoLabels_SetEnabled(enabled) + SetCached("bagAmmoLabels", enabled) + MTH_BagAmmoLabels_RefreshAll() +end + +function MTH_BankAmmoLabels_GetEnabled() + return GetCached("bankAmmoLabels") +end +function MTH_BankAmmoLabels_SetEnabled(enabled) + SetCached("bankAmmoLabels", enabled) + MTH_BagAmmoLabels_RefreshAllBank() +end + +function MTH_BagAmmoDamage_GetEnabled() + return GetCached("bagAmmoDamage") +end +function MTH_BagAmmoDamage_SetEnabled(enabled) + SetCached("bagAmmoDamage", enabled) + MTH_BagAmmoLabels_RefreshAll() + MTH_BagAmmoLabels_RefreshAllBank() +end + +-- Heat color: red (7.5 DPS) → yellow → green (20.5 DPS); below 7.5 = grey +local DPS_MIN = 7.5 +local DPS_MAX = 20.5 + +local function GetDpsHeatColor(dps) + if not dps then return 1, 1, 1 end + if dps < DPS_MIN then return 0.6, 0.6, 0.6 end + local perc = (dps - DPS_MIN) / (DPS_MAX - DPS_MIN) + return zButtonAmmo_GetColorGradient(perc) +end + +-- Look up DPS from MTH_DS_AmmoItems by numeric item ID +local function GetAmmoDps(numericId) + if not MTH_DS_AmmoItems then return nil end + local entry = MTH_DS_AmmoItems[numericId] + return entry and entry.dps +end + +-- Detect whether pfUI bags are active +local function HasPfUI() + return pfUI and pfUI.bags and pfUI.bag and true or false +end + +-- Is this bagID a bank bag? +local BANK_BAGS = { [-1] = true, [5] = true, [6] = true, [7] = true, [8] = true, [9] = true, [10] = true, [11] = true } + +local function IsBankBag(bagID) + return BANK_BAGS[bagID] or false +end + +-- Apply or clear labels on a single bag-slot button +local function UpdateButton(button, bagID, slot, showName, showDmg) + if not button then return end + + -- Lazy-create name label (TOPLEFT) + if not button._mthAmmoLabel then + button._mthAmmoLabel = button:CreateFontString(nil, "OVERLAY") + button._mthAmmoLabel:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1) + button._mthAmmoLabel:SetFont("Fonts\\ARIALN.ttf", 9, "OUTLINE") + button._mthAmmoLabel:Hide() + end + -- Lazy-create damage label (CENTER, large + bold) + if not button._mthAmmoDmgLabel then + button._mthAmmoDmgLabel = button:CreateFontString(nil, "OVERLAY") + button._mthAmmoDmgLabel:SetPoint("CENTER", button, "CENTER", 0, 0) + button._mthAmmoDmgLabel:SetFont("Fonts\\ARIALN.ttf", 10, "THICKOUTLINE") + button._mthAmmoDmgLabel:Hide() + end + + local nameLabel = button._mthAmmoLabel + local dmgLabel = button._mthAmmoDmgLabel + + if not showName and not showDmg then + nameLabel:Hide() + dmgLabel:Hide() + return + end + + local link = GetContainerItemLink(bagID, slot) + if not link then + nameLabel:Hide() + dmgLabel:Hide() + return + end + + local _, _, id = string.find(link, "Hitem:(%d+)") + if not id then + nameLabel:Hide() + dmgLabel:Hide() + return + end + + local numericId = tonumber(id) + local itemName, _, itemQuality, _, _, _, _, _, itemSlot = GetItemInfo(id) + if not itemName then + nameLabel:Hide() + dmgLabel:Hide() + return + end + + local isAmmo = (itemSlot == "INVTYPE_AMMO") + if not isAmmo then + local knownAmmo = zButtonAmmo_GetKnownAmmoTypes and zButtonAmmo_GetKnownAmmoTypes() + isAmmo = knownAmmo and knownAmmo[itemName] + end + if not isAmmo then + nameLabel:Hide() + dmgLabel:Hide() + return + end + + -- Name label + if showName then + local shortText = zButtonAmmo_ShortArrowLabels[itemName] + or zButtonAmmo_ShortBulletLabels[itemName] + if shortText then + local cr, cg, cb = zButtonAmmo_GetQualityColor(itemQuality) + nameLabel:SetTextColor(cr, cg, cb, 1) + nameLabel:SetText(shortText) + nameLabel:Show() + else + nameLabel:Hide() + end + else + nameLabel:Hide() + end + + -- Damage label + if showDmg then + local dps = GetAmmoDps(numericId) + if dps then + local cr, cg, cb = GetDpsHeatColor(dps) + dmgLabel:SetTextColor(cr, cg, cb, 1) + dmgLabel:SetText(dps) + dmgLabel:Show() + else + dmgLabel:Hide() + end + else + dmgLabel:Hide() + end +end + +-- Clear both labels on a button +local function ClearButton(button) + if not button then return end + if button._mthAmmoLabel then button._mthAmmoLabel:Hide() end + if button._mthAmmoDmgLabel then button._mthAmmoDmgLabel:Hide() end +end + +-- Forward declaration +local RefreshAllVisibleFrames + +-- Refresh labels on a single bag (works for any bagID) +local function RefreshBag(bagID, showName, showDmg) + local numSlots = GetContainerNumSlots(bagID) + if numSlots == 0 then return end + + -- pfUI path + if HasPfUI() then + local pfBag = pfUI.bags[bagID] + if not pfBag or not pfBag.slots then return end + for slot = 1, numSlots do + local slotData = pfBag.slots[slot] + local button = slotData and slotData.frame + if button then + if showName or showDmg then + UpdateButton(button, bagID, slot, showName, showDmg) + else + ClearButton(button) + end + end + end + return + end + + -- Default UI path + + -- Main bank (bagID -1): buttons are BankFrameItem1..28 + if bagID == -1 then + for slot = 1, numSlots do + local button = getglobal("BankFrameItem" .. slot) + if button then + if showName or showDmg then + UpdateButton(button, -1, slot, showName, showDmg) + else + ClearButton(button) + end + end + end + return + end + + -- All other bags (0-4 and 5-11): search all ContainerFrames + for ci = 1, 13 do + local frame = getglobal("ContainerFrame" .. ci) + if frame and frame:IsVisible() and frame:GetID() == bagID then + local frameName = frame:GetName() + -- Visual button i = API slot (numSlots - i + 1) + for i = 1, numSlots do + local button = getglobal(frameName .. "Item" .. i) + local apiSlot = numSlots - i + 1 + if button then + if showName or showDmg then + UpdateButton(button, bagID, apiSlot, showName, showDmg) + else + ClearButton(button) + end + end + end + -- Clear stale labels on any buttons beyond numSlots + for slot = numSlots + 1, 24 do + local button = getglobal(frameName .. "Item" .. slot) + if not button then break end + ClearButton(button) + end + return + end + end +end + +-- Public: refresh all inventory bags (0-4) +function MTH_BagAmmoLabels_RefreshAll() + RefreshAllVisibleFrames() +end + +-- Public: refresh all bank bags (-1, 5-11) +function MTH_BagAmmoLabels_RefreshAllBank() + RefreshAllVisibleFrames() +end + +-- Refresh all visible default UI ContainerFrames (frame-centric: no bagID lookup) +RefreshAllVisibleFrames = function() + local bagShowName = MTH_BagAmmoLabels_GetEnabled() + local bankShowName = MTH_BankAmmoLabels_GetEnabled() + local showDmg = MTH_BagAmmoDamage_GetEnabled() + + if HasPfUI() then + -- pfUI: refresh via pfUI bag structure + for bagID = 0, 4 do + RefreshBag(bagID, bagShowName, showDmg) + end + local bankBags = { -1, 5, 6, 7, 8, 9, 10, 11 } + for _, bagID in ipairs(bankBags) do + RefreshBag(bagID, bankShowName, showDmg) + end + return + end + + -- Default UI: iterate all visible ContainerFrames + for ci = 1, 13 do + local cf = getglobal("ContainerFrame" .. ci) + if cf and cf:IsVisible() then + local bagID = cf:GetID() + local showName = IsBankBag(bagID) and bankShowName or bagShowName + local numSlots = GetContainerNumSlots(bagID) + local fname = cf:GetName() + -- Visual button i = API slot (numSlots - i + 1) + for i = 1, numSlots do + local button = getglobal(fname .. "Item" .. i) + local apiSlot = numSlots - i + 1 + if button then + if showName or showDmg then + UpdateButton(button, bagID, apiSlot, showName, showDmg) + else + ClearButton(button) + end + end + end + for slot = numSlots + 1, 24 do + local button = getglobal(fname .. "Item" .. slot) + if not button then break end + ClearButton(button) + end + end + end + + -- Bank bag -1: BankFrameItem buttons + if bankShowName or showDmg then + local numSlots = GetContainerNumSlots(-1) + for slot = 1, numSlots do + local button = getglobal("BankFrameItem" .. slot) + if button then + UpdateButton(button, -1, slot, bankShowName, showDmg) + end + end + end +end + +-- Event-driven refresh: coalesce with a short delay +do + local frame = CreateFrame("Frame", "MTH_BagAmmoLabelsFrame", UIParent) + local needsRefresh = false + local elapsed = 0 + + frame:RegisterEvent("BAG_UPDATE") + frame:RegisterEvent("PLAYERBANKSLOTS_CHANGED") + frame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED") + frame:RegisterEvent("BANKFRAME_OPENED") + frame:RegisterEvent("PLAYER_ENTERING_WORLD") + + frame:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + -- After login/reload, delay 1s for item cache to populate + elapsed = -0.95 + needsRefresh = true + frame:Show() + return + end + + if event == "BANKFRAME_OPENED" then + if MTH_BankAmmoLabels_GetEnabled() or MTH_BagAmmoDamage_GetEnabled() then + needsRefresh = true + if not frame:IsShown() then + elapsed = 0 + frame:Show() + end + end + return + end + + if event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" then + if MTH_BankAmmoLabels_GetEnabled() or MTH_BagAmmoDamage_GetEnabled() then + needsRefresh = true + if not frame:IsShown() then + elapsed = 0 + frame:Show() + end + end + return + end + + -- BAG_UPDATE + local bagID = arg1 + if not bagID then return end + if IsBankBag(bagID) then + if not MTH_BankAmmoLabels_GetEnabled() and not MTH_BagAmmoDamage_GetEnabled() then return end + else + if not MTH_BagAmmoLabels_GetEnabled() and not MTH_BagAmmoDamage_GetEnabled() then return end + end + needsRefresh = true + if not frame:IsShown() then + elapsed = 0 + frame:Show() + end + end) + + frame:SetScript("OnUpdate", function() + elapsed = elapsed + (arg1 or 0.016) + if elapsed < 0.05 then return end + needsRefresh = false + frame:Hide() + RefreshAllVisibleFrames() + end) + frame:Hide() + + -- Hook ContainerFrame OnShow: trigger a refresh when a bag becomes visible + if not HasPfUI() then + for ci = 1, 13 do + local cf = getglobal("ContainerFrame" .. ci) + if cf then + local origOnShow = cf:GetScript("OnShow") + cf:SetScript("OnShow", function() + if origOnShow then origOnShow() end + needsRefresh = true + if not frame:IsShown() then + elapsed = 0 + frame:Show() + end + end) + end + end + end + + -- Post-hook ContainerFrame_Update for default UI (synchronous update) + if type(ContainerFrame_Update) == "function" then + local origCFU = ContainerFrame_Update + ContainerFrame_Update = function(cf) + origCFU(cf) + if cf and cf:IsVisible() then + local bagID = cf:GetID() + local showDmg = MTH_BagAmmoDamage_GetEnabled() + local showName + if IsBankBag(bagID) then + showName = MTH_BankAmmoLabels_GetEnabled() + else + showName = MTH_BagAmmoLabels_GetEnabled() + end + if showName or showDmg then + local numSlots = GetContainerNumSlots(bagID) + local fname = cf:GetName() + -- Visual button i = API slot (numSlots - i + 1) + for i = 1, numSlots do + local button = getglobal(fname .. "Item" .. i) + local apiSlot = numSlots - i + 1 + if button then + UpdateButton(button, bagID, apiSlot, showName, showDmg) + end + end + for slot = numSlots + 1, 24 do + local button = getglobal(fname .. "Item" .. slot) + if not button then break end + ClearButton(button) + end + end + else + -- Frame hidden: clear all labels + if cf then + local fname = cf:GetName() + if fname then + for slot = 1, 24 do + local button = getglobal(fname .. "Item" .. slot) + if not button then break end + ClearButton(button) + end + end + end + end + end + end +end diff --git a/api/options-expammo.lua b/api/options-expammo.lua index 780ba1a..69b0651 100644 --- a/api/options-expammo.lua +++ b/api/options-expammo.lua @@ -355,6 +355,33 @@ local function MTH_EA_OPT_BuildUI(container) if quiverTip then quiverTip:SetWidth(290) end table.insert(rightGroup, quiverTip) + -- ── Rotation ───────────────────────────────────────────── + table.insert(rightGroup, MTH_EA_OPT_Header(container, "Rotation", -400, OPT_R)) + + local rotTip = MTH_EA_OPT_Tip(container, + "Uncheck a shot to permanently skip it during its proc window.\n The keybind will cast Steady/Aimed instead.", + -418, OPT_R, 0.65, 0.65, 0.65) + if rotTip then rotTip:SetWidth(290) end + table.insert(rightGroup, rotTip) + + local multiCB = MTH_EA_OPT_CB(container, "MTH_ExpAmmoBlockMultiCB", + "Multi-Shot (Explosive)", + -452, OPT_R, cfg.blockExplosive ~= true, + function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockExplosive(not v) end end) + table.insert(rightGroup, multiCB) + + local serpentCB = MTH_EA_OPT_CB(container, "MTH_ExpAmmoBlockSerpentCB", + "Serpent Sting (Poisonous)", + -476, OPT_R, cfg.blockPoisonous ~= true, + function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockPoisonous(not v) end end) + table.insert(rightGroup, serpentCB) + + local arcaneCB = MTH_EA_OPT_CB(container, "MTH_ExpAmmoBlockArcaneCB", + "Arcane Shot (Enchanted)", + -500, OPT_R, cfg.blockEnchanted ~= true, + function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockEnchanted(not v) end end) + table.insert(rightGroup, arcaneCB) + -- Apply initial enabled state MTH_EA_OPT_SetGroupEnabled(rightGroup, showHint) end diff --git a/api/options-smartpet.lua b/api/options-smartpet.lua new file mode 100644 index 0000000..bfb9b51 --- /dev/null +++ b/api/options-smartpet.lua @@ -0,0 +1,321 @@ +------------------------------------------------------ +-- MetaHunt: SmartPet Options Panel +-- TWoW 1.18.1 +------------------------------------------------------ + +local function MTH_SP_OPT_Ready() + return type(MTH_GetFrame) == "function" + and type(MTH_CreateCheckbox) == "function" + and type(MTH_CreateSlider) == "function" +end + +local function MTH_SP_OPT_GetCfg() + if not MTH_SmartPet or not MTH_SmartPet.GetConfig then return {} end + return MTH_SmartPet.GetConfig() +end + +local MTH_SP_OPT_built = false + +-- Layout constants +local OPT_L = 16 -- left column X +local OPT_R = 290 -- right column X +local OPT_CW = 250 -- column width (for tip text wrapping) + +-- Spacing constants +local GAP_SECTION = 34 -- between sections +local GAP_CB = 24 -- checkbox height (icon + text) +local GAP_TIP = 16 -- small tip text height +local GAP_BTN = 28 -- button height + margin +local GAP_SLIDER = 52 -- slider total height (label + track + value) +local GAP_CB_SLIDER = 40 -- checkbox to slider (checkbox + slider label above track) + +-- Helper: checkbox with click handler +local function SP_CB(container, name, label, yOff, xOff, isChecked, onClick) + local cb = MTH_CreateCheckbox(container, name, label, yOff, xOff or OPT_L) + if cb then + cb:SetChecked(isChecked and 1 or 0) + cb:SetScript("OnClick", function() + local v = this:GetChecked() + onClick(v == 1 or v == true) + end) + end + return cb +end + +-- Helper: section header label +local function SP_Header(container, text, yOff, xOff) + local fs = container:CreateFontString(nil, "ARTWORK", "GameFontNormal") + fs:SetPoint("TOPLEFT", container, "TOPLEFT", xOff or OPT_L, yOff) + fs:SetText("|cffff9900" .. text .. "|r") + return fs +end + +-- Helper: small descriptive text +local function SP_Tip(container, text, yOff, xOff) + local fs = container:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + fs:SetPoint("TOPLEFT", container, "TOPLEFT", xOff or OPT_L, yOff) + fs:SetWidth(OPT_CW) + fs:SetJustifyH("LEFT") + fs:SetTextColor(0.4, 0.6, 1.0) + fs:SetText(text) + return fs +end + +-- Taunt Mode cycle +local TAUNT_MODES = { "auto", "growl", "cower", "off" } +local TAUNT_LABELS = { + auto = "Auto (solo=Growl, tank=Cower)", + growl = "Always Growl", + cower = "Always Cower", + off = "Off (manual)", +} + +local function SP_NextTauntMode(current) + for i = 1, 4 do + if TAUNT_MODES[i] == current then + return TAUNT_MODES[math.mod(i, 4) + 1] + end + end + return "auto" +end + +-- Channel cycle +local CHANNELS = { "SAY", "PARTY", "RAID", "GUILD" } + +local function SP_NextChannel(current) + for i = 1, 4 do + if CHANNELS[i] == current then + return CHANNELS[math.mod(i, 4) + 1] + end + end + return "SAY" +end + +-- Build the UI +local function MTH_SP_OPT_BuildUI(container) + if MTH_SP_OPT_built then return end + MTH_SP_OPT_built = true + + local cfg = MTH_SP_OPT_GetCfg() + local api = MTH_SmartPet or {} + + --------------------------------------------------------------------------- + -- Title (large, like ExpAmmo) + --------------------------------------------------------------------------- + local title = container:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge") + title:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L, -16) + title:SetText("Smart Pet - Pet Combat Management") + + --------------------------------------------------------------------------- + -- Module enable checkbox + --------------------------------------------------------------------------- + local moduleEnabled = true + if MTH and MTH.IsModuleEnabled then + moduleEnabled = MTH:IsModuleEnabled("smartpet", false) and true or false + end + SP_CB(container, "MTH_SP_OPT_ModuleEnable", "Enable Smart Pet module", + -46, OPT_L, moduleEnabled, function(v) + if MTH and MTH.SetModuleEnabled then MTH:SetModuleEnabled("smartpet", v) end + end) + + -- Columns start below the module checkbox + local COL_TOP = -80 + + -- Vertical divider + local divider = container:CreateTexture(nil, "BACKGROUND") + divider:SetTexture(0.3, 0.3, 0.3, 0.6) + divider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R - 14, COL_TOP) + divider:SetPoint("BOTTOMLEFT", container, "TOPLEFT", OPT_R - 14, -530) + divider:SetWidth(1) + + --------------------------------------------------------------------------- + -- LEFT COLUMN — Focus Management + --------------------------------------------------------------------------- + local y = COL_TOP + + SP_Header(container, "Focus Management", y) + y = y - 22 + + -- Smart Focus + SP_CB(container, "MTH_SP_OPT_SmartFocus", "Enable Smart Focus", + y, OPT_L, cfg.smartFocus, function(v) + if api.SetSmartFocus then api.SetSmartFocus(v) end + end) + y = y - GAP_CB + SP_Tip(container, "Budget pet focus across DPS abilities.", y) + y = y - GAP_TIP + SP_Tip(container, "|cff80ff80High focus|r — all DPS abilities ON.", y) + y = y - GAP_TIP + SP_Tip(container, "|cffffcc00Mid focus|r — expensive ability first, others wait.", y) + y = y - GAP_TIP + SP_Tip(container, "|cffff8080Low focus|r — cheapest ability always ON (never idle).", y) + y = y - GAP_TIP + SP_Tip(container, "Taunt cost is always reserved.", y) + y = y - GAP_SECTION + + --------------------------------------------------------------------------- + -- LEFT COLUMN — Taunt Management + --------------------------------------------------------------------------- + SP_Header(container, "Taunt Management", y) + y = y - GAP_TIP + SP_Tip(container, "Controls Growl/Cower autocast based on party.", y) + y = y - 22 + + local tauntBtn = CreateFrame("Button", "MTH_SP_OPT_TauntBtn", container, "UIPanelButtonTemplate") + tauntBtn:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L, y) + tauntBtn:SetWidth(250) + tauntBtn:SetHeight(22) + tauntBtn:SetText("Mode: " .. (TAUNT_LABELS[cfg.tauntMode] or "Auto")) + tauntBtn:SetScript("OnClick", function() + local c = MTH_SP_OPT_GetCfg() + local newMode = SP_NextTauntMode(c.tauntMode) + if api.SetTauntMode then api.SetTauntMode(newMode) end + this:SetText("Mode: " .. (TAUNT_LABELS[newMode] or newMode)) + end) + y = y - GAP_BTN + + SP_CB(container, "MTH_SP_OPT_PvpDetaunt", "PVP Auto-Detaunt", + y, OPT_L, cfg.pvpDetaunt, function(v) + if api.SetPvpDetaunt then api.SetPvpDetaunt(v) end + end) + y = y - GAP_SECTION + + --------------------------------------------------------------------------- + -- LEFT COLUMN — CC-Break Prevention + --------------------------------------------------------------------------- + SP_Header(container, "CC-Break Prevention", y) + y = y - 22 + + SP_CB(container, "MTH_SP_OPT_CcBreak", "Check for breakable CC", + y, OPT_L, cfg.ccBreakCheck, function(v) + if api.SetCcBreakCheck then api.SetCcBreakCheck(v) end + end) + y = y - GAP_CB + + local ccLabels = { block = "Block attack", warn = "Warn only" } + local ccModeBtn = CreateFrame("Button", "MTH_SP_OPT_CcModeBtn", container, "UIPanelButtonTemplate") + ccModeBtn:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L + 20, y) + ccModeBtn:SetWidth(180) + ccModeBtn:SetHeight(20) + ccModeBtn:SetText("CC Mode: " .. (ccLabels[cfg.ccBreakMode] or "Block")) + ccModeBtn:SetScript("OnClick", function() + local c = MTH_SP_OPT_GetCfg() + local newMode = (c.ccBreakMode == "block") and "warn" or "block" + if api.SetCcBreakMode then api.SetCcBreakMode(newMode) end + this:SetText("CC Mode: " .. (ccLabels[newMode] or newMode)) + end) + y = y - GAP_SECTION + + --------------------------------------------------------------------------- + -- RIGHT COLUMN — General + --------------------------------------------------------------------------- + local ry = COL_TOP + + -- The Button + SP_Header(container, "The Button", ry, OPT_R) + ry = ry - 22 + + SP_CB(container, "MTH_SP_OPT_TheButton", "Enable The Button keybind", + ry, OPT_R, cfg.theButton, function(v) + if api.SetTheButton then api.SetTheButton(v) end + end) + ry = ry - GAP_CB + SP_Tip(container, "Enemy=attack, friendly=assist, none=recall.", ry, OPT_R) + ry = ry - GAP_SECTION + + -- Rush on Attack + SP_CB(container, "MTH_SP_OPT_Rush", "Rush on initial attack", + ry, OPT_R, cfg.rushOnAttack, function(v) + if api.SetRushOnAttack then api.SetRushOnAttack(v) end + end) + ry = ry - GAP_CB + SP_Tip(container, "Charge / Dash / Dive when pet is sent to attack.", ry, OPT_R) + ry = ry - GAP_SECTION + + -- NoChase + SP_CB(container, "MTH_SP_OPT_NoChase", "NoChase (recall on mob flee)", + ry, OPT_R, cfg.noChase, function(v) + if api.SetNoChase then api.SetNoChase(v) end + end) + ry = ry - GAP_CB + SP_Tip(container, "Recall pet when target attempts to flee.", ry, OPT_R) + ry = ry - GAP_SECTION + + -- AutoCower + SP_Header(container, "AutoCower", ry, OPT_R) + ry = ry - 22 + + SP_CB(container, "MTH_SP_OPT_AutoCower", "Enable AutoCower on low HP", + ry, OPT_R, cfg.autoCower, function(v) + if api.SetAutoCower then api.SetAutoCower(v) end + end) + ry = ry - GAP_CB_SLIDER + + local acSlider = MTH_CreateSlider(container, "MTH_SP_OPT_AutoCowerSlider", + "HP Threshold (%)", 5, 80, 5, ry) + if acSlider then + acSlider:ClearAllPoints() + acSlider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, ry) + acSlider:SetWidth(200) + acSlider:SetValue(cfg.autoCowerPct or 30) + local valText = getglobal("MTH_SP_OPT_AutoCowerSliderValue") + if valText then valText:SetText(tostring(cfg.autoCowerPct or 30)) end + acSlider.onChange = function(val) + if api.SetAutoCowerPct then api.SetAutoCowerPct(val) end + end + end + ry = ry - GAP_SLIDER + + -- AutoWarn + SP_Header(container, "AutoWarn", ry, OPT_R) + ry = ry - 22 + + SP_CB(container, "MTH_SP_OPT_AutoWarn", "Warn when pet HP is low", + ry, OPT_R, cfg.autoWarn, function(v) + if api.SetAutoWarn then api.SetAutoWarn(v) end + end) + ry = ry - GAP_CB_SLIDER + + local awSlider = MTH_CreateSlider(container, "MTH_SP_OPT_AutoWarnSlider", + "HP Threshold (%)", 5, 60, 5, ry) + if awSlider then + awSlider:ClearAllPoints() + awSlider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, ry) + awSlider:SetWidth(200) + awSlider:SetValue(cfg.autoWarnPct or 20) + local valText = getglobal("MTH_SP_OPT_AutoWarnSliderValue") + if valText then valText:SetText(tostring(cfg.autoWarnPct or 20)) end + awSlider.onChange = function(val) + if api.SetAutoWarnPct then api.SetAutoWarnPct(val) end + end + end + ry = ry - GAP_SLIDER + + local chanBtn = CreateFrame("Button", "MTH_SP_OPT_ChanBtn", container, "UIPanelButtonTemplate") + chanBtn:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, ry) + chanBtn:SetWidth(180) + chanBtn:SetHeight(22) + chanBtn:SetText("Channel: " .. (cfg.autoWarnChannel or "SAY")) + chanBtn:SetScript("OnClick", function() + local c = MTH_SP_OPT_GetCfg() + local newChan = SP_NextChannel(c.autoWarnChannel or "SAY") + if api.SetAutoWarnChannel then api.SetAutoWarnChannel(newChan) end + this:SetText("Channel: " .. newChan) + end) + ry = ry - GAP_CB + SP_Tip(container, "Chat channel for low-HP warnings.", ry, OPT_R) +end + +-- Public setup function (called by options-shell.lua) +function MTH_SetupSmartPetOptions() + if not MTH_SP_OPT_Ready() then return end + local container = MTH_GetFrame("MetaHuntOptionsSmartPet") + if not container then return end + local ok, err = pcall(MTH_SP_OPT_BuildUI, container) + if not ok then + if MTH and MTH.Print then + MTH:Print("[SmartPet Options] " .. tostring(err), "error") + end + MTH_SP_OPT_built = false + end +end diff --git a/api/options-zbuttons.lua b/api/options-zbuttons.lua index e005654..60930ec 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -1705,4 +1705,36 @@ function MTH_SetupGeneralOptions() ensureHelpText(tooltipsSection, "MetaHuntGeneralTooltipsFoodHelp", foodHelp, -298) end + local bagsSection = ensureSection("MetaHuntGeneralBagsBox", "Bag Ammo Labels", topY - 372, 168, "right") + if bagsSection then + local bagLabels = ensureCheckbox(bagsSection, "MetaHuntGeneralBagLabelsToggle", "Add text to ammunitions in bags", -10, + MTH_BagAmmoLabels_GetEnabled and MTH_BagAmmoLabels_GetEnabled() or false) + if bagLabels then + bagLabels:SetScript("OnClick", function() + local enabled = MTH_ZB_IsChecked(this) + if MTH_BagAmmoLabels_SetEnabled then MTH_BagAmmoLabels_SetEnabled(enabled) end + end) + end + + local bankLabels = ensureCheckbox(bagsSection, "MetaHuntGeneralBankLabelsToggle", "Add text to ammunitions in bank", -36, + MTH_BankAmmoLabels_GetEnabled and MTH_BankAmmoLabels_GetEnabled() or false) + if bankLabels then + bankLabels:SetScript("OnClick", function() + local enabled = MTH_ZB_IsChecked(this) + if MTH_BankAmmoLabels_SetEnabled then MTH_BankAmmoLabels_SetEnabled(enabled) end + end) + end + + local dmgLabels = ensureCheckbox(bagsSection, "MetaHuntGeneralBagDmgToggle", "Show ammo damage (heat-coloured)", -62, + MTH_BagAmmoDamage_GetEnabled and MTH_BagAmmoDamage_GetEnabled() or false) + if dmgLabels then + dmgLabels:SetScript("OnClick", function() + local enabled = MTH_ZB_IsChecked(this) + if MTH_BagAmmoDamage_SetEnabled then MTH_BagAmmoDamage_SetEnabled(enabled) end + end) + end + + ensureHelpText(bagsSection, "MetaHuntGeneralBagDmgHelp", "Damage is heat-coloured from red (7.5) to green (20.5).", -88) + end + end diff --git a/data/ds-items.lua b/data/ds-items.lua index dfff698..2d81053 100644 --- a/data/ds-items.lua +++ b/data/ds-items.lua @@ -10,6 +10,7 @@ MTH_DS_AmmoItems = { ["level"] = 5, ["reqlevel"] = 1, ["icon"] = "INV_Ammo_Arrow_02", + ["dps"] = 1.5, ["subtype"] = "arrow", ["vendors"] = { [150] = 0, @@ -118,6 +119,7 @@ MTH_DS_AmmoItems = { ["level"] = 15, ["reqlevel"] = 10, ["icon"] = "INV_Ammo_Arrow_02", + ["dps"] = 3.5, ["subtype"] = "arrow", ["vendors"] = { [150] = 0, @@ -275,6 +277,7 @@ MTH_DS_AmmoItems = { ["level"] = 5, ["reqlevel"] = 1, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 2, ["subtype"] = "bullet", ["vendors"] = { [151] = 0, @@ -359,6 +362,7 @@ MTH_DS_AmmoItems = { ["level"] = 15, ["reqlevel"] = 10, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 5.5, ["subtype"] = "bullet", ["vendors"] = { [227] = 0, @@ -494,6 +498,7 @@ MTH_DS_AmmoItems = { ["level"] = 30, ["reqlevel"] = 25, ["icon"] = "INV_Ammo_Arrow_02", + ["dps"] = 5.5, ["subtype"] = "arrow", ["vendors"] = { [227] = 0, @@ -621,6 +626,7 @@ MTH_DS_AmmoItems = { ["level"] = 30, ["reqlevel"] = 25, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 7.5, ["subtype"] = "bullet", ["vendors"] = { [227] = 0, @@ -736,18 +742,21 @@ MTH_DS_AmmoItems = { ["name"] = "Feathered Arrow", ["level"] = 35, ["icon"] = "INV_Ammo_Arrow_02", + ["dps"] = 7.5, ["subtype"] = "arrow", }, [3465] = { ["name"] = "Exploding Shot", ["level"] = 36, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 7.5, ["subtype"] = "bullet", }, [4960] = { ["name"] = "Flash Pellet", ["level"] = 7, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 3.5, ["subtype"] = "bullet", }, [5568] = { @@ -755,6 +764,7 @@ MTH_DS_AmmoItems = { ["level"] = 18, ["reqlevel"] = 13, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 3.5, ["subtype"] = "bullet", ["drops"] = { [2156] = 30.97, @@ -781,6 +791,7 @@ MTH_DS_AmmoItems = { ["level"] = 10, ["reqlevel"] = 5, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 2, ["subtype"] = "bullet", }, [8068] = { @@ -788,6 +799,7 @@ MTH_DS_AmmoItems = { ["level"] = 20, ["reqlevel"] = 15, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 5.5, ["subtype"] = "bullet", }, [8069] = { @@ -795,6 +807,7 @@ MTH_DS_AmmoItems = { ["level"] = 35, ["reqlevel"] = 30, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 7.5, ["subtype"] = "bullet", }, [9399] = { @@ -802,6 +815,7 @@ MTH_DS_AmmoItems = { ["level"] = 40, ["reqlevel"] = 35, ["icon"] = "INV_Ammo_Arrow_01", + ["dps"] = 7.5, ["subtype"] = "arrow", ["drops"] = { [6906] = 100, @@ -812,6 +826,7 @@ MTH_DS_AmmoItems = { ["level"] = 42, ["reqlevel"] = 37, ["icon"] = "INV_Ammo_Bullet_01", + ["dps"] = 12.5, ["subtype"] = "bullet", }, [10513] = { @@ -819,6 +834,7 @@ MTH_DS_AmmoItems = { ["level"] = 49, ["reqlevel"] = 44, ["icon"] = "INV_Ammo_Bullet_01", + ["dps"] = 12.5, ["subtype"] = "bullet", }, [10579] = { @@ -826,6 +842,7 @@ MTH_DS_AmmoItems = { ["level"] = 42, ["reqlevel"] = 37, ["icon"] = "INV_Ammo_Arrow_02", + ["dps"] = 12.5, ["subtype"] = "arrow", }, [11284] = { @@ -833,6 +850,7 @@ MTH_DS_AmmoItems = { ["level"] = 45, ["reqlevel"] = 40, ["icon"] = "INV_Ammo_Bullet_01", + ["dps"] = 10.5, ["subtype"] = "bullet", ["vendors"] = { [734] = 0, @@ -934,6 +952,7 @@ MTH_DS_AmmoItems = { ["level"] = 45, ["reqlevel"] = 40, ["icon"] = "INV_Weapon_ShortBlade_25", + ["dps"] = 10.5, ["subtype"] = "arrow", ["vendors"] = { [228] = 0, @@ -1043,6 +1062,7 @@ MTH_DS_AmmoItems = { ["level"] = 52, ["reqlevel"] = 47, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 10.5, ["subtype"] = "bullet", ["drops"] = { [9025] = 16, @@ -1053,6 +1073,7 @@ MTH_DS_AmmoItems = { ["level"] = 59, ["reqlevel"] = 54, ["icon"] = "Ability_Hunter_CriticalShot", + ["dps"] = 20, ["subtype"] = "arrow", ["drops"] = { [9236] = 100, @@ -1063,6 +1084,7 @@ MTH_DS_AmmoItems = { ["level"] = 61, ["reqlevel"] = 56, ["icon"] = "INV_Ammo_Bullet_02", + ["dps"] = 20.5, ["subtype"] = "bullet", ["drops"] = { [10997] = 100, @@ -1073,6 +1095,7 @@ MTH_DS_AmmoItems = { ["level"] = 57, ["reqlevel"] = 52, ["icon"] = "INV_Ammo_Bullet_03", + ["dps"] = 17.5, ["subtype"] = "bullet", ["vendors"] = { [17078] = 0, @@ -1083,6 +1106,7 @@ MTH_DS_AmmoItems = { ["level"] = 57, ["reqlevel"] = 52, ["icon"] = "INV_Ammo_Arrow_02", + ["dps"] = 17.5, ["subtype"] = "arrow", ["vendors"] = { [17078] = 0, @@ -1093,6 +1117,7 @@ MTH_DS_AmmoItems = { ["level"] = 1, ["reqlevel"] = 1, ["icon"] = "INV_Ammo_Arrow_02", + ["dps"] = 1.5, ["subtype"] = "arrow", }, [19316] = { @@ -1100,6 +1125,7 @@ MTH_DS_AmmoItems = { ["level"] = 54, ["reqlevel"] = 51, ["icon"] = "Spell_Frost_IceShard", + ["dps"] = 14.5, ["subtype"] = "arrow", ["vendors"] = { [13216] = 0, @@ -1113,6 +1139,7 @@ MTH_DS_AmmoItems = { ["level"] = 54, ["reqlevel"] = 51, ["icon"] = "Spell_Frost_FrostBlast", + ["dps"] = 14.5, ["subtype"] = "bullet", ["vendors"] = { [13216] = 0, diff --git a/data/ds-zones-fallback.lua b/data/ds-zones-fallback.lua index aae0d9d..8bf751b 100644 --- a/data/ds-zones-fallback.lua +++ b/data/ds-zones-fallback.lua @@ -54,20 +54,27 @@ MTH_DS_ZoneNamesFallback = { [1519] = "Stormwind City", [1537] = "Ironforge", [1583] = "Blackrock Spire", + [1584] = "Blackrock Depths", [1941] = "Caverns of Time", [1977] = "Zul'Gurub", [2040] = "Alah'Thalas", [2100] = "Maraudon", + [2717] = "Molten Core", [2597] = "Alterac Valley", [3456] = "Naxxramas", [3457] = "Tower of Karazhan", [4012] = "Scarlet Enclave", + [5023] = "Sunnyglade", + [5024] = "Icepoint Rock", [5077] = "Crescent Grove", [5121] = "Tel'Abim", + [5130] = "Winter Veil Vale", [5135] = "Scarlet Monastery Library", [5179] = "Gilneas", + [5208] = "Gilneas City", [5225] = "Thalassian Highlands", [5536] = "Blackstone Island", + [5723] = "Karazhan", [5561] = "Balor", [5581] = "Northwind", [5601] = "Dragonmaw Retreat", diff --git a/data/ds-zones-wma.lua b/data/ds-zones-wma.lua index 7f0ebb6..4524f5e 100644 --- a/data/ds-zones-wma.lua +++ b/data/ds-zones-wma.lua @@ -216,12 +216,15 @@ MTH_MAP_WMA_TO_PFQ = { [607] = 1477, -- TheTempleOfAtalHakkar [609] = 719, -- BlackFathomDeeps [611] = 717, -- TheStockade + [615] = 1584, -- BlackrockDepths (interior) [617] = 2717, -- MoltenCore [619] = 1977, -- ZulGurub [623] = 1584, -- BlackrockDepths [625] = 3429, -- RuinsofAhnQiraj [627] = 2159, -- OnyxiasLair [629] = 1583, -- BlackrockSpire + [631] = 718, -- WailingCaverns (interior) + [633] = 2100, -- Maraudon (interior) [635] = 2677, -- BlackwingLair [637] = 5138, -- TheDeadmines [639] = 722, -- RazorfenDowns diff --git a/init/api.xml b/init/api.xml index 2ea8c68..9bc6d81 100644 --- a/init/api.xml +++ b/init/api.xml @@ -21,6 +21,7 @@ + diff --git a/modules/expammo/engine.lua b/modules/expammo/engine.lua index 421ad9c..777cf60 100644 --- a/modules/expammo/engine.lua +++ b/modules/expammo/engine.lua @@ -91,6 +91,13 @@ local CELL_DEF = { icon = "Interface\\Icons\\Ability_TheBlackArrow" }, } +-- ── Creature-type immunity rules ────────────────────────────── +-- Maps UnitCreatureType("target") → set of ammo states the target resists. +-- Used by PLAYER_TARGET_CHANGED to proactively flag blocked states. +local CREATURE_TYPE_IMMUNITIES = { + ["Elemental"] = { [POISONOUS] = true }, -- Elementals are Nature-immune +} + -- ── Talent gate ────────────────────────────────────────────── -- The module only shows if the player has the talent. -- Checked on login and whenever MTH_HT_OnScanComplete fires. @@ -304,11 +311,28 @@ local function MTH_EA_EnsureConfigDefaults() if cfg.bigCD == nil then cfg.bigCD = false end if cfg.hideOOC == nil then cfg.hideOOC = false end if cfg.useQuiverNoClip == nil then cfg.useQuiverNoClip = false end + if cfg.blockExplosive == nil then cfg.blockExplosive = false end + if cfg.blockPoisonous == nil then cfg.blockPoisonous = false end + if cfg.blockEnchanted == nil then cfg.blockEnchanted = false end end -- Combat state flag (avoids needing InCombatLockdown, which doesn't exist in 1.12) local inCombat = false +-- ── Runtime immunity / block tracking ───────────────────────── +-- Volatile: refreshed on PLAYER_TARGET_CHANGED and combat-log "immune" messages. +local MTH_EA_BlockedStates = {} -- [state] = true if creature-type or reactive immunity + +function MTH_EA_IsStateBlocked(state) + if not state or state == IDLE then return false end + local cfg = MTH_EA_GetConfig() + if state == EXPLOSIVE and cfg.blockExplosive then return true end + if state == POISONOUS and cfg.blockPoisonous then return true end + if state == ENCHANTED and cfg.blockEnchanted then return true end + if MTH_EA_BlockedStates[state] then return true end + return false +end + -- Throttle for PLAYER_AURAS_CHANGED: even with the fast texture path this event -- fires 10-50+/sec in combat, so cap processing at ~6/sec. local MTH_EA_AurasChangedLastTime = 0 @@ -493,6 +517,13 @@ end -- Action button: what spell to show and what spell to cast right now. -- Returns displaySpell, icon, r, g, b, cooldownRemaining, castSpell local function MTH_EA_GetActionSpell() + -- If current ammo state is blocked (creature immune or rotation toggle), + -- skip the consume-spell and fall through to Aimed/Steady instead. + if rt.state ~= IDLE and MTH_EA_IsStateBlocked(rt.state) then + local cd = MTH_EA_GetSpellCD(SPELL_AIMED) + return SPELL_AIMED, ICON_AIMED, 1.00, 0.85, 0.00, cd, (cd > 0 and SPELL_STEADY or SPELL_AIMED) + end + if rt.state == EXPLOSIVE then local cd = MTH_EA_GetSpellCD(SPELL_MULTI) return SPELL_MULTI, ICON_MULTI, 1.00, 0.60, 0.05, cd, (cd > 0 and SPELL_STEADY or SPELL_MULTI) @@ -654,6 +685,11 @@ MTH_EA_UpdateUI = function() end cdTexts[i]:SetText("") end + + -- Red border override for blocked (immune / rotation-disabled) states + if MTH_EA_IsStateBlocked(def.state) then + c:SetBackdropBorderColor(1, 0, 0, 0.85) + end end -- ── TOP cell: Lock and Load ─────────────────────────────── @@ -1158,6 +1194,8 @@ frame:RegisterEvent("SPELLCAST_STOP") frame:RegisterEvent("SPELLCAST_FAILED") frame:RegisterEvent("SPELLCAST_INTERRUPTED") frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED") -- invalidate Aimed Shot slot cache +frame:RegisterEvent("PLAYER_TARGET_CHANGED") -- proactive creature-type immunity +frame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") -- reactive "immune" detection frame:SetScript("OnUpdate", function() if not MTH_EA_IsEnabled() then return end @@ -1304,6 +1342,34 @@ frame:SetScript("OnEvent", function() return end + if event == "PLAYER_TARGET_CHANGED" then + MTH_EA_BlockedStates = {} + if UnitExists("target") then + local creatureType = UnitCreatureType("target") + if creatureType and CREATURE_TYPE_IMMUNITIES[creatureType] then + for st, _ in pairs(CREATURE_TYPE_IMMUNITIES[creatureType]) do + MTH_EA_BlockedStates[st] = true + end + end + end + MTH_EA_UpdateUI() + return + end + + if event == "CHAT_MSG_SPELL_SELF_DAMAGE" then + local msg = arg1 or "" + if string.find(msg, "immune", 1, true) then + for state, spell in pairs(CONSUME_SPELL) do + if string.find(msg, spell, 1, true) then + MTH_EA_BlockedStates[state] = true + MTH_EA_UpdateUI() + break + end + end + end + return + end + if event == "PLAYER_REGEN_DISABLED" then -- Entering combat inCombat = true @@ -1537,5 +1603,20 @@ MTH_ExpAmmo = { local cfg = MTH_EA_GetConfig() cfg.useQuiverNoClip = v and true or false end, + SetBlockExplosive = function(v) + local cfg = MTH_EA_GetConfig() + cfg.blockExplosive = v and true or false + MTH_EA_UpdateUI() + end, + SetBlockPoisonous = function(v) + local cfg = MTH_EA_GetConfig() + cfg.blockPoisonous = v and true or false + MTH_EA_UpdateUI() + end, + SetBlockEnchanted = function(v) + local cfg = MTH_EA_GetConfig() + cfg.blockEnchanted = v and true or false + MTH_EA_UpdateUI() + end, ToggleAnchor = MTH_EA_ToggleAnchor, } diff --git a/modules/smartpet/engine.lua b/modules/smartpet/engine.lua new file mode 100644 index 0000000..5517f6d --- /dev/null +++ b/modules/smartpet/engine.lua @@ -0,0 +1,1293 @@ +------------------------------------------------------ +-- MetaHunt: SmartPet — Pet Combat Management +-- TWoW 1.18.1 +------------------------------------------------------ +-- +-- Integrated pet combat manager inspired by SmartPet v2.5.1. +-- Handles taunt management, focus budgeting, CC-break +-- prevention, PVP auto-detaunt, The Button keybind, +-- NoChase, Dash/Dive on attack, auto-cower, and +-- low-health chat warnings. +------------------------------------------------------ + +-- ── Localised ability names ────────────────────────────────── +-- Resolved from MTH_LocaleData.spells via MTH:LocalizeSpell(). +-- PET_ACTION_FOLLOW / PET_ACTION_ATTACK are built-in WoW tokens, +-- not localised spell names. +local L_GROWL, L_COWER, L_CLAW, L_BITE, L_DASH, L_DIVE +local L_LIGHTNING_BREATH, L_SCREECH, L_SCORPID_POISON +local L_CHARGE, L_DEATH_ROLL, L_FURIOUS_HOWL, L_SAVAGE_REND +local L_THUNDERSTOMP, L_POISON_SPIT, L_POLLEN_BURST +local L_PROWL, L_SHELL_SHIELD, L_GRACE, L_BUBBLE_BARRIER +local L_WEB, L_ROAR_OF_FORTITUDE, L_PACKLEADER, L_STRIDER_PRESENCE +local L_FOLLOW = "PET_ACTION_FOLLOW" +local L_ATTACK = "PET_ACTION_ATTACK" +local L_FLEE = "attempts to run away in fear" + +-- ── CC debuff token list (English keys into spell locale) ──── +local CC_DEBUFF_TOKENS = { + "Gouge", "Sap", "Charm", "Seduction", "Sheep", "Polymorph", + "Tame Beast", "Scare Beast", "Sleep", "Hibernate", "Fear", + "Mind Control", "Blind", "Scatter Shot", "Enslave Demon", + "Shackle Undead", "Reckless Charge", "Freezing Trap Effect", + "Intimidating Shout", "Repentance", "Wyvern Sting", +} + +-- Populated once MTH:LocalizeSpell is available (VARIABLES_LOADED) +local CC_DEBUFF_SET = {} + +-- Maps localised ability names to rt.slot keys. +local ABILITY_MAP = {} +local function BuildAbilityMap() + ABILITY_MAP = { + [L_GROWL] = "growl", + [L_COWER] = "cower", + [L_CLAW] = "claw", + [L_BITE] = "bite", + [L_FOLLOW] = "follow", + [L_ATTACK] = "attack", + [L_DASH] = "dash", + [L_DIVE] = "dive", + } + -- Conditionally add family-specific abilities (nil-safe: L_ vars may be nil before locale resolution) + if L_LIGHTNING_BREATH then ABILITY_MAP[L_LIGHTNING_BREATH] = "lightningBreath" end + if L_SCREECH then ABILITY_MAP[L_SCREECH] = "screech" end + if L_SCORPID_POISON then ABILITY_MAP[L_SCORPID_POISON] = "scorpidPoison" end + if L_CHARGE then ABILITY_MAP[L_CHARGE] = "charge" end + if L_DEATH_ROLL then ABILITY_MAP[L_DEATH_ROLL] = "deathRoll" end + if L_FURIOUS_HOWL then ABILITY_MAP[L_FURIOUS_HOWL] = "furiousHowl" end + if L_SAVAGE_REND then ABILITY_MAP[L_SAVAGE_REND] = "savageRend" end + if L_THUNDERSTOMP then ABILITY_MAP[L_THUNDERSTOMP] = "thunderstomp" end + if L_POISON_SPIT then ABILITY_MAP[L_POISON_SPIT] = "poisonSpit" end + if L_POLLEN_BURST then ABILITY_MAP[L_POLLEN_BURST] = "pollenBurst" end + if L_PROWL then ABILITY_MAP[L_PROWL] = "prowl" end + if L_SHELL_SHIELD then ABILITY_MAP[L_SHELL_SHIELD] = "shellShield" end + if L_GRACE then ABILITY_MAP[L_GRACE] = "grace" end + if L_BUBBLE_BARRIER then ABILITY_MAP[L_BUBBLE_BARRIER] = "bubbleBarrier" end + if L_WEB then ABILITY_MAP[L_WEB] = "web" end + if L_ROAR_OF_FORTITUDE then ABILITY_MAP[L_ROAR_OF_FORTITUDE] = "roarOfFortitude" end + if L_PACKLEADER then ABILITY_MAP[L_PACKLEADER] = "packleader" end + if L_STRIDER_PRESENCE then ABILITY_MAP[L_STRIDER_PRESENCE] = "striderPresence" end +end + +-- Called at VARIABLES_LOADED when the locale system is ready +local function ResolveLocaleStrings() + if not (MTH and MTH.LocalizeSpell) then return end + + L_GROWL = MTH:LocalizeSpell("Growl") + L_COWER = MTH:LocalizeSpell("Cower") + L_CLAW = MTH:LocalizeSpell("Claw") + L_BITE = MTH:LocalizeSpell("Bite") + L_DASH = MTH:LocalizeSpell("Dash") + L_DIVE = MTH:LocalizeSpell("Dive") + + -- Family-specific abilities + L_LIGHTNING_BREATH = MTH:LocalizeSpell("Lightning Breath") + L_SCREECH = MTH:LocalizeSpell("Screech") + L_SCORPID_POISON = MTH:LocalizeSpell("Scorpid Poison") + L_CHARGE = MTH:LocalizeSpell("Charge") + L_DEATH_ROLL = MTH:LocalizeSpell("Death Roll") + L_FURIOUS_HOWL = MTH:LocalizeSpell("Furious Howl") + L_SAVAGE_REND = MTH:LocalizeSpell("Savage Rend") + L_THUNDERSTOMP = MTH:LocalizeSpell("Thunderstomp") + L_POISON_SPIT = MTH:LocalizeSpell("Poison Spit") + L_POLLEN_BURST = MTH:LocalizeSpell("Pollen Burst") + L_PROWL = MTH:LocalizeSpell("Prowl") + L_SHELL_SHIELD = MTH:LocalizeSpell("Shell Shield") + L_GRACE = MTH:LocalizeSpell("Grace") + L_BUBBLE_BARRIER = MTH:LocalizeSpell("Bubble Barrier") + L_WEB = MTH:LocalizeSpell("Web") + L_ROAR_OF_FORTITUDE = MTH:LocalizeSpell("Roar of Fortitude") + L_PACKLEADER = MTH:LocalizeSpell("Packleader") + L_STRIDER_PRESENCE = MTH:LocalizeSpell("Strider Presence") + + -- Flee pattern from spell locale (special key) + local spellLocales = MTH_LocaleData and MTH_LocaleData.spells + if spellLocales then + local locale = MTH.currentLocale or "enUS" + local aliases = { enGB = "enUS", esMX = "esES", zhTW = "zhCN" } + if aliases[locale] then locale = aliases[locale] end + local map = spellLocales[locale] or spellLocales.enUS + if map and map["SP_FLEE_PATTERN"] then + L_FLEE = map["SP_FLEE_PATTERN"] + end + end + + -- Rebuild CC debuff set from localised names + CC_DEBUFF_SET = {} + for _, token in ipairs(CC_DEBUFF_TOKENS) do + local localized = MTH:LocalizeSpell(token) + CC_DEBUFF_SET[localized] = true + end + + -- Rebuild ability map with resolved names + BuildAbilityMap() +end + +-- ── Constants ──────────────────────────────────────────────── +local FOCUS_REGEN_INTERVAL = 5 -- pet focus ticks roughly every 5s + +-- Pet action bar slot count (WoW 1.12) +local NUM_PET_SLOTS = NUM_PET_ACTION_SLOTS or 10 + +-- Ability metadata: focus cost and behaviour type. +-- type: "taunt", "autocastDPS" (spammable, no meaningful CD), +-- "semiAutocast" (short CD, still autocasts frequently), +-- "cdDPS" (significant cooldown), "mobility", "defensive", +-- "utility", "partyBuff" +local ABILITY_INFO = { + growl = { cost = 15, type = "taunt" }, + cower = { cost = 15, type = "taunt" }, + bite = { cost = 35, type = "autocastDPS" }, + claw = { cost = 25, type = "autocastDPS" }, + lightningBreath = { cost = 50, type = "autocastDPS" }, + scorpidPoison = { cost = 25, type = "semiAutocast" }, + screech = { cost = 20, type = "semiAutocast" }, + charge = { cost = 35, type = "cdDPS" }, + deathRoll = { cost = 50, type = "cdDPS" }, + furiousHowl = { cost = 50, type = "cdDPS" }, + savageRend = { cost = 25, type = "cdDPS" }, + thunderstomp = { cost = 60, type = "cdDPS" }, + poisonSpit = { cost = 35, type = "cdDPS" }, + pollenBurst = { cost = 40, type = "cdDPS" }, + dash = { cost = 20, type = "mobility" }, + dive = { cost = 20, type = "mobility" }, + prowl = { cost = 40, type = "utility" }, + shellShield = { cost = 10, type = "defensive" }, + grace = { cost = 10, type = "defensive" }, + bubbleBarrier = { cost = 20, type = "defensive" }, + web = { cost = 0, type = "utility" }, + roarOfFortitude = { cost = 50, type = "partyBuff" }, + packleader = { cost = 50, type = "partyBuff" }, + striderPresence = { cost = 50, type = "partyBuff" }, +} + +-- Ability keys whose autocast is managed by the focus budget system. +local MANAGED_DPS_TYPES = { autocastDPS = true, semiAutocast = true, cdDPS = true } + +-- ── Config defaults (persisted per-character) ──────────────── +local CONFIG_DEFAULTS = { + tauntMode = "auto", -- "auto" | "growl" | "cower" | "off" + pvpDetaunt = true, + ccBreakCheck = true, + ccBreakMode = "block", -- "block" | "warn" + theButton = true, + smartFocus = false, + autoCower = false, + autoCowerPct = 30, + noChase = false, + rushOnAttack = true, + autoWarn = false, + autoWarnPct = 20, + autoWarnChannel = "SAY", +} + +-- ── Runtime state (not persisted) ──────────────────────────── +local rt = { + inCombat = false, + inPVP = false, + -- Backed-up pre-PVP autocast states + pvpBackup = nil, -- { growl=bool, cower=bool, autoCower=bool } + -- Pet action bar slot indices (-1 = not found) + slot = { + attack = -1, + follow = -1, + growl = -1, + cower = -1, + bite = -1, + claw = -1, + dash = -1, + dive = -1, + lightningBreath = -1, + screech = -1, + scorpidPoison = -1, + charge = -1, + deathRoll = -1, + furiousHowl = -1, + savageRend = -1, + thunderstomp = -1, + poisonSpit = -1, + pollenBurst = -1, + prowl = -1, + shellShield = -1, + grace = -1, + bubbleBarrier = -1, + web = -1, + roarOfFortitude = -1, + packleader = -1, + striderPresence = -1, + }, + -- Pet spellbook indices for rush abilities (used for CastSpell) + spellbook = { + dash = -1, + dive = -1, + charge = -1, + }, + -- Focus tracking + focus1 = 100, + focus2 = 100, + lastFocusTick = 0, + nextFocusTick = 0, + -- Smart Focus: remember which DPS autocasts were ON at combat start + preCombatAutocast = {}, -- { [slotKey] = bool } + -- AutoCower: track if we swapped to cower + autoCowered = false, + -- AutoWarn throttle + lastWarnTime = 0, + lastWarnPct = 0, + -- NoChase recall flag + recallPending = false, + -- TheButton context + theButtonRecall = false, + -- Party has a tank (cached) + partyHasTank = false, +} + +-- ── Hidden tooltip for debuff scanning (legacy path) ───────── +local debuffTip = nil +local function EnsureDebuffTip() + if debuffTip then return debuffTip end + debuffTip = CreateFrame("GameTooltip", "MTH_SP_DebuffTip", UIParent, "GameTooltipTemplate") + debuffTip:SetOwner(UIParent, "ANCHOR_NONE") + return debuffTip +end + +-- ── Config helpers ─────────────────────────────────────────── +local function GetCfg() + if type(MTH_SavedVariables) ~= "table" then return CONFIG_DEFAULTS end + if type(MTH_SavedVariables.modules) ~= "table" then + MTH_SavedVariables.modules = {} + end + local cfg = MTH_SavedVariables.modules["smartpet"] + if type(cfg) ~= "table" then + cfg = {} + MTH_SavedVariables.modules["smartpet"] = cfg + end + -- Fill defaults for missing keys + for k, v in pairs(CONFIG_DEFAULTS) do + if cfg[k] == nil then + cfg[k] = v + end + end + return cfg +end + +local function IsEnabled() + if not (MTH and MTH.IsModuleEnabled) then return false end + return MTH:IsModuleEnabled("smartpet", false) +end + +-- ── Pet action bar helpers ─────────────────────────────────── +local function GetAutocast(slotIndex) + if slotIndex < 1 then return false end + local name, subtext, texture, isToken, isActive, autoCastAllowed, autoCastEnabled = GetPetActionInfo(slotIndex) + return autoCastEnabled and true or false +end + +local function SetAutocast(slotIndex, enabled) + if slotIndex < 1 then return end + local current = GetAutocast(slotIndex) + if (current and not enabled) or (not current and enabled) then + TogglePetAutocast(slotIndex) + end +end + +-- ── Pet action bar scanning ────────────────────────────────── + +local function ScanPetActionBar() + for key in pairs(rt.slot) do + rt.slot[key] = -1 + end + for i = 1, NUM_PET_SLOTS do + local name = GetPetActionInfo(i) + if name then + local key = ABILITY_MAP[name] + if key then + rt.slot[key] = i + end + end + end +end + +-- Scan pet spellbook for rush ability indices (for CastSpell) +local function ScanPetSpellbook() + rt.spellbook.dash = -1 + rt.spellbook.dive = -1 + rt.spellbook.charge = -1 + for i = 1, 20 do + local spellName = GetSpellName(i, BOOKTYPE_PET) + if not spellName then break end + if spellName == L_DASH then + rt.spellbook.dash = i + elseif spellName == L_DIVE then + rt.spellbook.dive = i + elseif spellName == L_CHARGE then + rt.spellbook.charge = i + end + end +end + +-- ── Party/tank detection ───────────────────────────────────── +local TANK_CLASSES = { WARRIOR = true, PALADIN = true } +-- Druid tanks detected by class + bear form buff (approximate) + +local function ScanPartyForTank() + local members = GetNumPartyMembers and GetNumPartyMembers() or 0 + local raid = GetNumRaidMembers and GetNumRaidMembers() or 0 + + if members == 0 and raid == 0 then + rt.partyHasTank = false + return + end + + -- Raid scan + if raid > 0 then + for i = 1, raid do + local unit = "raid" .. i + if UnitExists(unit) and not UnitIsUnit(unit, "player") then + local _, cls = UnitClass(unit) + if cls and TANK_CLASSES[cls] then + rt.partyHasTank = true + return + end + if cls == "DRUID" then + -- Check for bear form via power type (rage = 1) + if UnitPowerType and UnitPowerType(unit) == 1 then + rt.partyHasTank = true + return + end + end + end + end + rt.partyHasTank = false + return + end + + -- Party scan + for i = 1, members do + local unit = "party" .. i + if UnitExists(unit) then + local _, cls = UnitClass(unit) + if cls and TANK_CLASSES[cls] then + rt.partyHasTank = true + return + end + if cls == "DRUID" then + if UnitPowerType and UnitPowerType(unit) == 1 then + rt.partyHasTank = true + return + end + end + end + end + rt.partyHasTank = false +end + +-- ── Taunt Management ───────────────────────────────────────── +local function ApplyTauntMode() + if not IsEnabled() then return end + local cfg = GetCfg() + local mode = cfg.tauntMode + + if mode == "off" then return end + if mode == "growl" then + SetAutocast(rt.slot.growl, true) + SetAutocast(rt.slot.cower, false) + return + end + if mode == "cower" then + SetAutocast(rt.slot.growl, false) + SetAutocast(rt.slot.cower, true) + return + end + -- auto: solo → growl, party with tank → cower + if rt.partyHasTank then + SetAutocast(rt.slot.growl, false) + if rt.slot.cower > 0 then + SetAutocast(rt.slot.cower, true) + end + else + SetAutocast(rt.slot.growl, true) + SetAutocast(rt.slot.cower, false) + end +end + +-- ── PVP Auto-Detaunt ───────────────────────────────────────── +local function StartPVP() + if rt.inPVP then return end + local cfg = GetCfg() + if not cfg.pvpDetaunt then return end + + rt.inPVP = true + rt.pvpBackup = { + growl = GetAutocast(rt.slot.growl), + cower = GetAutocast(rt.slot.cower), + autoCower = cfg.autoCower, + } + SetAutocast(rt.slot.growl, false) + SetAutocast(rt.slot.cower, false) +end + +local function EndPVP() + if not rt.inPVP then return end + rt.inPVP = false + if rt.pvpBackup then + SetAutocast(rt.slot.growl, rt.pvpBackup.growl) + SetAutocast(rt.slot.cower, rt.pvpBackup.cower) + -- Restore autoCower setting — it doesn't get toggled on the bar, just the config flag + rt.pvpBackup = nil + end +end + +-- ── CC-Break Prevention ────────────────────────────────────── +-- Returns true if the target has a breakable CC debuff. +local function TargetHasBreakableCC() + local cfg = GetCfg() + if not cfg.ccBreakCheck then return false end + + -- NamPower path: GetUnitField aura array + if type(GetUnitField) == "function" then + local auras = GetUnitField("target", "aura") + if type(auras) == "table" then + for _, aura in ipairs(auras) do + if type(aura) == "table" then + local auraName = aura.name or (aura.spellId and GetSpellRecField and GetSpellRecField(aura.spellId, "name")) + if auraName and CC_DEBUFF_SET[auraName] then + return true + end + end + end + return false + end + end + + -- Legacy path: hidden tooltip scan + local tip = EnsureDebuffTip() + tip:ClearLines() + for j = 1, 16 do + if not UnitDebuff("target", j) then break end + tip:SetUnitDebuff("target", j) + local nameRegion = getglobal("MTH_SP_DebuffTipTextLeft1") + if nameRegion then + local debuffName = nameRegion:GetText() + if debuffName and CC_DEBUFF_SET[debuffName] then + return true + end + end + end + return false +end + +-- ── Rush on attack (Dash / Dive / Charge) ──────────────────── +local function CastChargeAbility() + local cfg = GetCfg() + if not cfg.rushOnAttack then return end + if rt.inCombat then return end -- only on initial engage + + -- Try Charge first (Boar — rush + damage), then Dash, then Dive + local idx = -1 + if rt.spellbook.charge > 0 then + idx = rt.spellbook.charge + elseif rt.spellbook.dash > 0 then + idx = rt.spellbook.dash + elseif rt.spellbook.dive > 0 then + idx = rt.spellbook.dive + end + if idx < 1 then return end + + local start, duration = GetSpellCooldown(idx, BOOKTYPE_PET) + if start == 0 and duration == 0 then + CastSpell(idx, BOOKTYPE_PET) + end +end + +-- ── Smart Focus ────────────────────────────────────────────── +-- Estimate how many focus-regen ticks will occur during an ability's cooldown. +local function EstimatedFocusRegen(slotIndex) + if slotIndex < 1 then return 0 end + local start, duration = GetPetActionCooldown(slotIndex) + if not start or start == 0 then return 0 end + local remaining = (start + duration) - GetTime() + if remaining <= 0 then return 0 end + -- Each tick gives ~FOCUS_REGEN_INTERVAL worth of focus ticks at ~24 focus per 5s cycle + -- SmartPet used FocusManager * 4.8; we use a simpler remaining/5 * 24 estimate + local ticks = remaining / FOCUS_REGEN_INTERVAL + return ticks * 24 +end + +local function HandleFocusEvent() + if not IsEnabled() then return end + local cfg = GetCfg() + if not cfg.smartFocus and cfg.tauntMode == "off" then return end + if not rt.inCombat then return end + if rt.inPVP then return end + + -- Track focus regen tick timing + rt.focus2 = rt.focus1 + rt.focus1 = UnitMana("pet") or 0 + if rt.focus1 > rt.focus2 then + rt.lastFocusTick = GetTime() + rt.nextFocusTick = rt.lastFocusTick + 4.5 + end + + local currentFocus = rt.focus1 + + -- Determine taunt cost (must always be reservable) + local tauntCost = 0 + if cfg.tauntMode ~= "off" then + if GetAutocast(rt.slot.cower) then + tauntCost = ABILITY_INFO.cower.cost + elseif rt.slot.growl > 0 then + tauntCost = ABILITY_INFO.growl.cost + end + end + + -- Collect managed DPS abilities (on bar AND pre-combat autocast was ON) + local managed = {} -- { {key, cost, slot}, ... } + local managedN = 0 + for key, wasOn in pairs(rt.preCombatAutocast) do + if wasOn and rt.slot[key] > 0 and ABILITY_INFO[key] then + managedN = managedN + 1 + managed[managedN] = { key = key, cost = ABILITY_INFO[key].cost, slot = rt.slot[key] } + end + end + + if managedN == 0 then return end + + -- Sort by cost descending (most expensive = highest priority to keep active) + table.sort(managed, function(a, b) return a.cost > b.cost end) + + if cfg.smartFocus and managedN >= 2 then + -- Smart Focus: reserve taunt cost once, then budget the rest across DPS. + -- Always keep the cheapest ability ON so the pet never sits idle; + -- the worst case is growl is delayed by one regen tick (~5s). + local budget = currentFocus - tauntCost + if budget < 0 then budget = 0 end + if MTH and MTH.Print then + MTH:Print("[SP] focus=" .. currentFocus .. " reserve=" .. tauntCost .. " budget=" .. budget .. " managed=" .. managedN, "debug") + end + local anyEnabled = false + for i = 1, managedN do + local m = managed[i] + if budget >= m.cost then + SetAutocast(m.slot, true) + anyEnabled = true + if MTH and MTH.Print then + MTH:Print("[SP] " .. m.key .. " ON (cost=" .. m.cost .. " left=" .. (budget - m.cost) .. ")", "debug") + end + budget = budget - m.cost + elseif i == managedN and not anyEnabled then + -- Cheapest ability: keep ON so pet isn't idle + SetAutocast(m.slot, true) + if MTH and MTH.Print then + MTH:Print("[SP] " .. m.key .. " ON* (cheapest, waiting for regen)", "debug") + end + else + SetAutocast(m.slot, false) + if MTH and MTH.Print then + MTH:Print("[SP] " .. m.key .. " OFF (cost=" .. m.cost .. " budget=" .. budget .. ")", "debug") + end + end + end + return + end + + -- Basic taunt-priority: disable any DPS ability that would starve the taunt + if cfg.tauntMode ~= "off" and tauntCost > 0 then + for i = 1, managedN do + local m = managed[i] + local regen = EstimatedFocusRegen(m.slot) + if (currentFocus + regen) - m.cost < tauntCost then + SetAutocast(m.slot, false) + if MTH and MTH.Print then + MTH:Print("[SP] tauntGuard " .. m.key .. " OFF (focus=" .. currentFocus .. " regen=" .. math.floor(regen) .. ")", "debug") + end + else + SetAutocast(m.slot, true) + end + end + end +end + +-- ── AutoCower on low HP ────────────────────────────────────── +local function HandlePetHealth() + if not IsEnabled() then return end + if not rt.inCombat then return end + local cfg = GetCfg() + + local hp = UnitHealth("pet") or 0 + local hpMax = UnitHealthMax("pet") or 1 + if hpMax == 0 then hpMax = 1 end + local pct = (100 * hp) / hpMax + + -- AutoCower + if cfg.autoCower and rt.slot.cower > 0 then + if pct < cfg.autoCowerPct then + if not rt.autoCowered then + rt.autoCowered = true + SetAutocast(rt.slot.growl, false) + SetAutocast(rt.slot.cower, true) + end + else + if rt.autoCowered then + rt.autoCowered = false + ApplyTauntMode() + end + end + end + + -- AutoWarn + if cfg.autoWarn then + local warnInterval + if pct >= 60 then + warnInterval = 10 + elseif pct >= 50 then + warnInterval = 8 + elseif pct >= 30 then + warnInterval = 6 + else + warnInterval = 4 + end + if pct < cfg.autoWarnPct + and (GetTime() - rt.lastWarnTime) > warnInterval + and rt.lastWarnPct > pct then + local petName = UnitName("pet") or "Pet" + local msg = petName .. " needs healing! (" .. string.format("%d", pct) .. "% Health)" + local chan = string.upper(cfg.autoWarnChannel or "SAY") + -- Validate channel + if chan == "PARTY" then + if not (UnitInParty and UnitInParty("player")) then chan = "SAY" end + elseif chan == "RAID" then + if not (GetNumRaidMembers and GetNumRaidMembers() > 0) then chan = "SAY" end + elseif chan == "GUILD" then + if not (IsInGuild and IsInGuild()) then chan = "SAY" end + elseif chan ~= "SAY" then + chan = "SAY" + end + SendChatMessage(msg, chan) + rt.lastWarnTime = GetTime() + end + rt.lastWarnPct = pct + end +end + +-- ── NoChase ────────────────────────────────────────────────── +local function HandleMonsterEmote(emoteText, sourceName) + if not IsEnabled() then return end + local cfg = GetCfg() + if not cfg.noChase then return end + + local petTarget = UnitName("pettarget") + if sourceName and petTarget and sourceName == petTarget + and strfind(emoteText or "", L_FLEE) then + PetFollow() + rt.theButtonRecall = true + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Recall Pet", 0.0, 0.0, 1.0, 1.0, UIERRORS_HOLD_TIME) + end + end +end + +-- ── Pet Damage Meter ───────────────────────────────────────── +local meter = { + active = false, + recording = false, -- true while actively collecting data + startTime = 0, + duration = 0, -- 0 = unlimited, >0 = auto-stop after N seconds + totalDmg = 0, + totalHits = 0, + totalCrits = 0, + totalMisses = 0, + abilities = {}, -- [name] = { dmg=N, hits=N, crits=N, misses=N } + petName = "", + autocastSnapshot = "", -- "Bite=ON Claw=ON Growl=OFF ..." +} + +local MeterPrintSummary -- forward declaration +local MeterStop -- forward declaration + +local function MeterReset() + meter.startTime = GetTime() + meter.recording = true + meter.totalDmg = 0 + meter.totalHits = 0 + meter.totalCrits = 0 + meter.totalMisses = 0 + meter.abilities = {} + -- Snapshot pet name + meter.petName = UnitName("pet") or "Unknown" + -- Ensure slot map is current before reading autocast states + ScanPetActionBar() + -- Snapshot all known ability autocast states + local parts = {} + local partN = 0 + for key, info in pairs(ABILITY_INFO) do + if rt.slot[key] and rt.slot[key] > 0 then + local on = GetAutocast(rt.slot[key]) + partN = partN + 1 + parts[partN] = key .. "=" .. (on and "ON" or "OFF") + end + end + table.sort(parts) + meter.autocastSnapshot = table.concat(parts, " ") +end + +MeterStop = function() + meter.recording = false + meter.active = false + if meter.totalDmg > 0 then MeterPrintSummary() end + MTH:Print("|cffff9900[SP Meter]|r Stopped.") +end + +local function MeterGetAbility(name) + if not meter.abilities[name] then + meter.abilities[name] = { dmg = 0, hits = 0, crits = 0, misses = 0 } + end + return meter.abilities[name] +end + +local function MeterParseCombatMsg(msg) + if not meter.recording or not msg then return end + + local ability, dmg = nil, nil + + -- hit: "'s hits for ." + _, _, ability, dmg = strfind(msg, "'s ([%a ]-) hits .+ for (%d+)") + if ability and dmg then + dmg = tonumber(dmg) or 0 + local a = MeterGetAbility(ability) + a.hits = a.hits + 1 + a.dmg = a.dmg + dmg + meter.totalHits = meter.totalHits + 1 + meter.totalDmg = meter.totalDmg + dmg + return + end + + -- crit: "'s crits for ." + _, _, ability, dmg = strfind(msg, "'s ([%a ]-) crits .+ for (%d+)") + if ability and dmg then + dmg = tonumber(dmg) or 0 + local a = MeterGetAbility(ability) + a.crits = a.crits + 1 + a.dmg = a.dmg + dmg + meter.totalCrits = meter.totalCrits + 1 + meter.totalDmg = meter.totalDmg + dmg + return + end + + -- miss/resist: "'s misses" / "'s was resisted" + _, _, ability = strfind(msg, "'s ([%a ]-) misses") + if not ability then + _, _, ability = strfind(msg, "'s ([%a ]-) was ") + end + if ability then + local a = MeterGetAbility(ability) + a.misses = a.misses + 1 + meter.totalMisses = meter.totalMisses + 1 + return + end +end + +local function MeterParsePetMelee(msg) + if not meter.recording or not msg then return end + + local dmg = nil + + -- hit: " hits for ." + _, _, dmg = strfind(msg, " hits .+ for (%d+)") + if dmg then + dmg = tonumber(dmg) or 0 + local a = MeterGetAbility("Melee") + a.hits = a.hits + 1 + a.dmg = a.dmg + dmg + meter.totalHits = meter.totalHits + 1 + meter.totalDmg = meter.totalDmg + dmg + return + end + + -- crit: " crits for ." + _, _, dmg = strfind(msg, " crits .+ for (%d+)") + if dmg then + dmg = tonumber(dmg) or 0 + local a = MeterGetAbility("Melee") + a.crits = a.crits + 1 + a.dmg = a.dmg + dmg + meter.totalCrits = meter.totalCrits + 1 + meter.totalDmg = meter.totalDmg + dmg + return + end + + -- miss: " misses ." + if strfind(msg, " misses ") then + local a = MeterGetAbility("Melee") + a.misses = a.misses + 1 + meter.totalMisses = meter.totalMisses + 1 + return + end +end + +MeterPrintSummary = function() + if not MTH or not MTH.Print then return end + local elapsed = GetTime() - meter.startTime + if elapsed < 1 then elapsed = 1 end + local dps = meter.totalDmg / elapsed + local smartLabel = GetCfg().smartFocus and "ON" or "OFF" + local totalSwings = meter.totalHits + meter.totalCrits + meter.totalMisses + + MTH:Print(string.format( + "[SP Meter] Pet=%s SmartFocus=%s Duration=%.1fs TotalDmg=%d DPS=%.1f Swings=%d (H:%d C:%d M:%d)", + meter.petName, smartLabel, elapsed, meter.totalDmg, dps, totalSwings, + meter.totalHits, meter.totalCrits, meter.totalMisses), "debug") + + MTH:Print("[SP Meter] Autocasts: " .. meter.autocastSnapshot, "debug") + + -- Per-ability breakdown, sorted by damage + local sorted = {} + local sortN = 0 + for name, data in pairs(meter.abilities) do + sortN = sortN + 1 + sorted[sortN] = { name = name, dmg = data.dmg, hits = data.hits, crits = data.crits, misses = data.misses } + end + table.sort(sorted, function(a, b) return a.dmg > b.dmg end) + for i = 1, sortN do + local e = sorted[i] + local uses = e.hits + e.crits + e.misses + local pct = (meter.totalDmg > 0) and (100 * e.dmg / meter.totalDmg) or 0 + MTH:Print(string.format( + " %s: %d dmg (%.0f%%) %d uses (H:%d C:%d M:%d)", + e.name, e.dmg, pct, uses, e.hits, e.crits, e.misses), "debug") + end +end + +-- ── Combat lifecycle ───────────────────────────────────────── +local function OnCombatStart() + if not IsEnabled() then return end + ScanPetActionBar() + rt.inCombat = true + rt.theButtonRecall = false + rt.autoCowered = false + rt.lastWarnTime = 0 + rt.lastWarnPct = (100 * (UnitHealth("pet") or 0)) / math.max(UnitHealthMax("pet") or 1, 1) + + -- Reset meter on new fight (only for untimed meters; timed meters keep recording across fights) + if meter.active and meter.duration == 0 then + MeterReset() + end + + -- Record pre-combat autocast states for all managed DPS abilities + rt.preCombatAutocast = {} + for key, info in pairs(ABILITY_INFO) do + if MANAGED_DPS_TYPES[info.type] and rt.slot[key] and rt.slot[key] > 0 then + rt.preCombatAutocast[key] = GetAutocast(rt.slot[key]) + if MTH and MTH.Print then + MTH:Print("[SP] snapshot " .. key .. "=" .. (GetAutocast(rt.slot[key]) and "ON" or "OFF") .. " slot=" .. rt.slot[key] .. " cost=" .. ABILITY_INFO[key].cost, "debug") + end + end + end + + -- Smart Focus initial state: keep only the most expensive ability enabled + local cfg = GetCfg() + if cfg.smartFocus then + local managed = {} + local managedN = 0 + for key, wasOn in pairs(rt.preCombatAutocast) do + if wasOn and ABILITY_INFO[key] then + managedN = managedN + 1 + managed[managedN] = { key = key, cost = ABILITY_INFO[key].cost, slot = rt.slot[key] } + end + end + if MTH and MTH.Print then + MTH:Print("[SP] combatStart smartFocus=ON managedON=" .. managedN, "debug") + end + if managedN >= 2 then + table.sort(managed, function(a, b) return a.cost > b.cost end) + for i = 1, managedN do + local state = (i == 1) and "ON" or "OFF" + if MTH and MTH.Print then + MTH:Print("[SP] init " .. managed[i].key .. "=" .. state .. " (cost=" .. managed[i].cost .. ")", "debug") + end + SetAutocast(managed[i].slot, i == 1) + end + end + else + if MTH and MTH.Print then + MTH:Print("[SP] combatStart smartFocus=OFF", "debug") + end + end +end + +local function OnCombatEnd() + -- Print meter summary on combat end (only for untimed meters) + if meter.active and meter.duration == 0 and meter.totalDmg > 0 then + MeterPrintSummary() + end + + -- Restore all managed DPS autocasts to pre-combat state + if rt.preCombatAutocast then + for key, wasOn in pairs(rt.preCombatAutocast) do + if rt.slot[key] and rt.slot[key] > 0 then + SetAutocast(rt.slot[key], wasOn) + if MTH and MTH.Print then + MTH:Print("[SP] restore " .. key .. "=" .. (wasOn and "ON" or "OFF"), "debug") + end + end + end + end + + if rt.inPVP then EndPVP() end + + rt.inCombat = false + rt.autoCowered = false + rt.theButtonRecall = false + + -- Re-apply taunt mode (restores growl/cower to config state) + ApplyTauntMode() +end + +-- ── PetAttack wrapper (used by The Button and hook) ────────── +local MTH_SP_OriginalPetAttack = nil + +local function SmartPetAttack() + if not IsEnabled() then + if MTH_SP_OriginalPetAttack then MTH_SP_OriginalPetAttack() end + return + end + + local cfg = GetCfg() + + -- CC-Break check + if cfg.ccBreakCheck and UnitExists("target") then + if TargetHasBreakableCC() then + if cfg.ccBreakMode == "block" then + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Breakable debuff found — pet attack blocked", 1.0, 0.3, 0.0, 1.0, UIERRORS_HOLD_TIME) + end + return + else + if UIErrorsFrame then + UIErrorsFrame:AddMessage("Warning: target has breakable CC", 1.0, 1.0, 0.0, 1.0, UIERRORS_HOLD_TIME) + end + end + end + end + + -- PVP check + if UnitExists("target") and UnitIsPlayer("target") and UnitCanAttack("player", "target") then + StartPVP() + end + + -- Dash/Dive on attack + CastChargeAbility() + + -- Issue the actual PetAttack + if MTH_SP_OriginalPetAttack then MTH_SP_OriginalPetAttack() end +end + +-- ── The Button ─────────────────────────────────────────────── +-- Global function called from Bindings.xml +function MTH_SmartPet_TheButton() + if not IsEnabled() then return end + local cfg = GetCfg() + if not cfg.theButton then return end + + -- NoChase recall pending + if rt.theButtonRecall then + PetFollow() + rt.theButtonRecall = false + return + end + + -- No pet alive + if not UnitExists("pet") or UnitIsDead("pet") then return end + + -- No target or dead target → recall + if not UnitExists("target") or UnitIsDead("target") then + PetFollow() + return + end + + -- Friendly target → assist (attack their target) + if UnitIsPlayer("target") and UnitCanCooperate("player", "target") then + if UnitExists("targettarget") and not UnitIsDead("targettarget") + and UnitCanAttack("player", "targettarget") then + AssistUnit("target") + SmartPetAttack() + end + return + end + + -- Enemy target → attack + if UnitCanAttack("player", "target") and not UnitIsDead("target") then + SmartPetAttack() + return + end + + -- Fallback: recall + PetFollow() +end + +-- ── Taunt toggle (keybind) ─────────────────────────────────── +function MTH_SmartPet_TauntToggle() + if not IsEnabled() then return end + local growlOn = GetAutocast(rt.slot.growl) + local cowerOn = GetAutocast(rt.slot.cower) + + if growlOn then + -- Switch to cower + SetAutocast(rt.slot.growl, false) + if rt.slot.cower > 0 then SetAutocast(rt.slot.cower, true) end + elseif cowerOn then + -- Switch to growl + SetAutocast(rt.slot.cower, false) + if rt.slot.growl > 0 then SetAutocast(rt.slot.growl, true) end + else + -- Neither on: enable growl + if rt.slot.growl > 0 then SetAutocast(rt.slot.growl, true) end + end +end + +-- ── Event frame ────────────────────────────────────────────── +local frame = CreateFrame("Frame", "MTH_SmartPetFrame") +frame:Hide() + +-- Meter auto-stop ticker (runs only while meter is active with a duration) +local meterTickFrame = CreateFrame("Frame") +meterTickFrame:Hide() +meterTickFrame:SetScript("OnUpdate", function() + if not meter.active or not meter.recording then + meterTickFrame:Hide() + return + end + if meter.duration > 0 and (GetTime() - meter.startTime) >= meter.duration then + MeterStop() + meterTickFrame:Hide() + end +end) + +frame:RegisterEvent("VARIABLES_LOADED") +frame:RegisterEvent("PET_BAR_UPDATE") +frame:RegisterEvent("PET_UI_UPDATE") +frame:RegisterEvent("PET_ATTACK_START") +frame:RegisterEvent("PET_ATTACK_STOP") +frame:RegisterEvent("PLAYER_DEAD") +frame:RegisterEvent("UNIT_HEALTH") +frame:RegisterEvent("UNIT_FOCUS") +frame:RegisterEvent("PARTY_MEMBERS_CHANGED") +frame:RegisterEvent("RAID_ROSTER_UPDATE") +frame:RegisterEvent("CHAT_MSG_MONSTER_EMOTE") +frame:RegisterEvent("PLAYER_ENTERING_WORLD") +frame:RegisterEvent("CHAT_MSG_SPELL_PET_DAMAGE") +frame:RegisterEvent("CHAT_MSG_COMBAT_PET_HITS") +frame:RegisterEvent("CHAT_MSG_COMBAT_PET_MISSES") + +frame:SetScript("OnEvent", function() + -- Meter events bypass the module-enabled check + if event == "CHAT_MSG_SPELL_PET_DAMAGE" then + if meter.recording then + MTH:Print("[SP Meter] " .. event .. ": " .. (arg1 or "nil"), "debug") + MeterParseCombatMsg(arg1) + end + return + end + if event == "CHAT_MSG_COMBAT_PET_HITS" or event == "CHAT_MSG_COMBAT_PET_MISSES" then + if meter.recording then + MTH:Print("[SP Meter] " .. event .. ": " .. (arg1 or "nil"), "debug") + MeterParsePetMelee(arg1) + end + return + end + + if not IsEnabled() then + -- Even with module off, handle meter start/stop (untimed only) + if meter.active and meter.duration == 0 then + if event == "PET_ATTACK_START" then + MeterReset() + elseif event == "PET_ATTACK_STOP" or event == "PLAYER_DEAD" then + if meter.totalDmg > 0 then MeterPrintSummary() end + end + end + return + end + + if event == "VARIABLES_LOADED" or event == "PLAYER_ENTERING_WORLD" then + ResolveLocaleStrings() + ScanPetActionBar() + ScanPetSpellbook() + ScanPartyForTank() + -- Hook PetAttack once + if not MTH_SP_OriginalPetAttack and type(PetAttack) == "function" then + MTH_SP_OriginalPetAttack = PetAttack + PetAttack = SmartPetAttack + end + ApplyTauntMode() + return + end + + if event == "PET_BAR_UPDATE" or event == "PET_UI_UPDATE" then + ScanPetActionBar() + ScanPetSpellbook() + return + end + + if event == "PET_ATTACK_START" then + OnCombatStart() + return + end + + if event == "PET_ATTACK_STOP" or event == "PLAYER_DEAD" then + OnCombatEnd() + return + end + + if event == "UNIT_FOCUS" and arg1 == "pet" then + HandleFocusEvent() + return + end + + if event == "UNIT_HEALTH" and arg1 == "pet" then + HandlePetHealth() + return + end + + if event == "PARTY_MEMBERS_CHANGED" or event == "RAID_ROSTER_UPDATE" then + ScanPartyForTank() + if not rt.inCombat then + ApplyTauntMode() + end + return + end + + if event == "CHAT_MSG_MONSTER_EMOTE" then + HandleMonsterEmote(arg1, arg2) + return + end +end) + +-- ── Public API (for options panel and external access) ──────── +MTH_SmartPet = { + GetConfig = GetCfg, + + SetTauntMode = function(mode) + local cfg = GetCfg() + if mode == "auto" or mode == "growl" or mode == "cower" or mode == "off" then + cfg.tauntMode = mode + if not rt.inCombat then ApplyTauntMode() end + end + end, + + SetPvpDetaunt = function(v) GetCfg().pvpDetaunt = v and true or false end, + + SetCcBreakCheck = function(v) GetCfg().ccBreakCheck = v and true or false end, + SetCcBreakMode = function(mode) + if mode == "block" or mode == "warn" then + GetCfg().ccBreakMode = mode + end + end, + + SetTheButton = function(v) GetCfg().theButton = v and true or false end, + SetSmartFocus = function(v) GetCfg().smartFocus = v and true or false end, + + SetAutoCower = function(v) GetCfg().autoCower = v and true or false end, + SetAutoCowerPct = function(v) + v = tonumber(v) + if v and v >= 1 and v <= 99 then GetCfg().autoCowerPct = v end + end, + + SetNoChase = function(v) GetCfg().noChase = v and true or false end, + SetRushOnAttack = function(v) GetCfg().rushOnAttack = v and true or false end, + + SetAutoWarn = function(v) GetCfg().autoWarn = v and true or false end, + SetAutoWarnPct = function(v) + v = tonumber(v) + if v and v >= 1 and v <= 99 then GetCfg().autoWarnPct = v end + end, + SetAutoWarnChannel = function(chan) + chan = string.upper(tostring(chan or "SAY")) + if chan == "SAY" or chan == "PARTY" or chan == "RAID" or chan == "GUILD" then + GetCfg().autoWarnChannel = chan + end + end, + + -- Force a rescan (useful after pet swap) + Rescan = function() + ScanPetActionBar() + ScanPetSpellbook() + ScanPartyForTank() + if not rt.inCombat then ApplyTauntMode() end + end, + + -- Damage meter (duration=0 for unlimited, >0 for timed auto-stop) + StartMeter = function(duration) + meter.active = true + meter.duration = duration or 0 + MeterReset() + if duration and duration > 0 then + meterTickFrame:Show() + MTH:Print(string.format("|cffff9900[SP Meter]|r ON — recording for %ds.", duration)) + else + MTH:Print("|cffff9900[SP Meter]|r ON — /sp meter to stop.") + end + end, + + StopMeter = function() + if meter.active then + MeterStop() + meterTickFrame:Hide() + else + MTH:Print("|cffff9900[SP Meter]|r Not running.") + end + end, + + -- Reprint last summary + PrintMeter = function() + if meter.totalDmg > 0 then + MeterPrintSummary() + else + MTH:Print("|cffff9900[SP Meter]|r No data recorded.") + end + end, +} + +-- ── Register with MTH framework so IsModuleEnabled works ───── +local MTH_SmartPetModule = { + name = "smartpet", + enabled = false, + events = {}, +} + +function MTH_SmartPetModule:init() + self.initialized = true +end + +function MTH_SmartPetModule:setEnabled(enabled) + -- No-op: SmartPet checks IsEnabled() on every event +end + +if MTH and MTH.RegisterModule then + MTH:RegisterModule("smartpet", MTH_SmartPetModule) +end + +-- ── Slash command ──────────────────────────────────────────── +SLASH_MTHSmartPet1 = "/sp" +SlashCmdList["MTHSmartPet"] = function(msg) + msg = string.lower(msg or "") + if msg == "meter" then + if meter.active then + MTH_SmartPet.StopMeter() + else + MTH_SmartPet.StartMeter(0) + end + elseif msg == "meter print" then + MTH_SmartPet.PrintMeter() + else + -- "/sp meter 60" — timed start + local _, _, secs = strfind(msg, "^meter (%d+)$") + if secs then + MTH_SmartPet.StartMeter(tonumber(secs)) + else + MTH:Print("|cffff9900[SmartPet]|r /sp meter — toggle damage meter") + MTH:Print("|cffff9900[SmartPet]|r /sp meter 60 — record for 60s then stop") + MTH:Print("|cffff9900[SmartPet]|r /sp meter print — reprint last fight") + end + end +end diff --git a/modules/smartpet/loader.xml b/modules/smartpet/loader.xml new file mode 100644 index 0000000..6d68df7 --- /dev/null +++ b/modules/smartpet/loader.xml @@ -0,0 +1,6 @@ + + + + diff --git a/modules/zhunter/ZSpellButtonTemplate.lua b/modules/zhunter/ZSpellButtonTemplate.lua index f804208..e5b5ca0 100644 --- a/modules/zhunter/ZSpellButtonTemplate.lua +++ b/modules/zhunter/ZSpellButtonTemplate.lua @@ -219,7 +219,7 @@ function ZSpellButton_SetSize(parent, size, setChildren) local cdscale = 0.75 / 36 if setChildren then local button - for i=1, parent.count do + for i=1, (parent.count or 0) do button = getglobal(parent.name..i) if button then button:SetHeight(size) diff --git a/modules/zhunter/zButtonAmmo.lua b/modules/zhunter/zButtonAmmo.lua index 2629c9c..4786c7f 100644 --- a/modules/zhunter/zButtonAmmo.lua +++ b/modules/zhunter/zButtonAmmo.lua @@ -38,7 +38,6 @@ local zButtonAmmo_LastFullScanAt = 0 local zButtonAmmo_MinUnknownBagScanInterval = 5.0 local zButtonAmmo_EmptyScanStreak = 0 local zButtonAmmo_LastEquippedAmmoSeen = nil -local zButtonAmmo_GetKnownAmmoTypes local function zButtonAmmo_BagHasAmmoNow(bagId) local bag = tonumber(bagId) @@ -117,7 +116,7 @@ zButtonAmmo_GetKnownAmmoTypes = function() return knownAmmo end -local zButtonAmmo_ShortArrowLabels = { +zButtonAmmo_ShortArrowLabels = { [ARROWS_ROUGH] = "Rough", [ARROWS_SHARP] = "Sharp", [ARROWS_RAZOR] = "Razor", @@ -131,7 +130,7 @@ local zButtonAmmo_ShortArrowLabels = { [ARROWS_DOOMSHOT] = "Doom", } -local zButtonAmmo_ShortBulletLabels = { +zButtonAmmo_ShortBulletLabels = { [BULLETS_LIGHT] = "Light", [BULLETS_CRAFTLIGHT] = "C.Light", [BULLETS_FLASH] = "Flash", @@ -151,7 +150,7 @@ local zButtonAmmo_ShortBulletLabels = { [BULLETS_ICETHREADED] = "Ice", } -local function zButtonAmmo_GetColorGradient(perc) +function zButtonAmmo_GetColorGradient(perc) perc = perc > 1 and 1 or perc perc = perc < 0 and 0 or perc local r1, g1, b1, r2, g2, b2 @@ -188,7 +187,7 @@ local function zButtonAmmo_EnsureShortLabelText(button) return button and button.ammoShortLabelText end -local function zButtonAmmo_GetQualityColor(quality) +function zButtonAmmo_GetQualityColor(quality) local tableRef = MTH_ITEM_QUALITY_COLORS or ITEM_QUALITY_COLORS if tableRef then local q = tonumber(quality) From 6c007f2c1744facc3caab80e1d2edd74974c9b12 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Sat, 4 Apr 2026 23:43:46 +0200 Subject: [PATCH 02/42] 1.5.1 --- CHANGELOG.md | 8 +++++--- MetaHunt.toc | 2 +- api/constants.lua | 2 +- api/core-framework.lua | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 229d6a6..02d11a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,16 @@ All notable changes to MetaHunt will be documented in this file. ### Added -- **zAmmo — bag ammo labels**: Quality-colored short-name labels overlaid on ammo items in bags and bank, with optional heat-colored DPS damage indicator. Grey below 7.5 DPS, red-to-green gradient from 7.5 to 20.5. Three new checkboxes in Options → zButtons → zAmmo → Bags section: bag labels, bank labels, and damage display. Works with both default bags and pfUI bags. +- **zAmmo — bag ammo labels**: Quality-colored short-name labels overlaid on ammo items in bags and bank, with optional heat-colored DPS damage indicator. Three new checkboxes General options: bag labels, bank labels, and damage display. Works with both default bags and pfUI bags. -- **MM Widget — immunity detection & rotation toggles**: The MM Widget keybind now detects when the current target is immune to a damage school (Fire / Nature / Arcane) and automatically skips the useless shot, casting Aimed or Steady Shot instead. Immunity is detected in two ways: reactively via combat log "immune" messages, and proactively via hardcoded creature-type rules (e.g. Elementals are always flagged Nature-immune on target). Immune cells show a red border. Three new checkboxes in Options → ExpAmmo → Rotation let you permanently disable individual shots (Multi-Shot, Serpent Sting, Arcane Shot) — unchecked shots are skipped during their proc window. +- **MM Widget — immunity detection**: The MM Widget keybind now detects when the current target is immune to a damage school (Fire / Nature / Arcane) and automatically skips the useless shot, casting Aimed or Steady Shot instead. Immunity is detected in two ways: reactively via combat log "immune" messages, and proactively via hardcoded creature-type rules (e.g. Elementals are always flagged Nature-immune on target). Immune cells show a red border. + +- **MM Widget — rotation toggles**: Three new checkboxes in Options → ExpAmmo → Rotation let you permanently disable individual shots (Multi-Shot, Serpent Sting, Arcane Shot) — unchecked shots are skipped during their proc window. ### Fixed -- **Zone mapping — 22 beasts with raw zone IDs**: Fixed 8 unmapped zone IDs (Blackrock Depths, Molten Core, Wailing Caverns, Maraudon, Sunnyglade, Icepoint Rock, Winter Veil Vale, Gilneas City, Karazhan) that caused 22 beasts to display as "Zone 617" instead of their proper zone name in the Hunter Book. +- **Zone mapping — 22 beasts with raw zone IDs**: Fixed 8 unmapped zone IDs (Blackrock Depths, Molten Core, Wailing Caverns, Maraudon, Sunnyglade, Icepoint Rock, Winter Veil Vale, Gilneas City, Karazhan) that caused 22 beasts to display as "Zone xxx" instead of their proper zone name in the Hunter Book. - **MM Widget — IsStateBlocked nil error**: Moved `MTH_EA_IsStateBlocked` definition before its first use, fixing a nil function call on load. diff --git a/MetaHunt.toc b/MetaHunt.toc index 64dc1a1..d5f77d0 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -1,5 +1,5 @@ ## Interface: 11200 -## Version: 1.5.0 +## Version: 1.5.1 ## Title: MetaHunt - |cff00ff00Hunter ## Author: Metasploit and his Copilot ;) ## Notes: Unified addon suite for huntards making old addons compatible with TurtleWoW, and adding an arsenal of never seen Hunter's utilities. diff --git a/api/constants.lua b/api/constants.lua index 9cf8621..0aaa945 100644 --- a/api/constants.lua +++ b/api/constants.lua @@ -1,5 +1,5 @@ MTH_CONST = MTH_CONST or {} -MTH_CONST.version = MTH_CONST.version or "1.5.0" +MTH_CONST.version = MTH_CONST.version or "1.5.1" MTH_CONST.WEAPON_TYPES = { BOWS = "Bows", diff --git a/api/core-framework.lua b/api/core-framework.lua index 9f90965..049fa2b 100644 --- a/api/core-framework.lua +++ b/api/core-framework.lua @@ -1,5 +1,5 @@ MTH = MTH or { - version = "1.5.0", + version = "1.5.1", name = "MetaHunt", modules = {}, config = {}, From 04e04a4853cd7eed256381a759444ada3f2b0c69 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Wed, 8 Apr 2026 09:36:17 +0200 Subject: [PATCH 03/42] 1.5.2 --- CHANGELOG.md | 12 ++++++++++++ modules/zhunter/Localization.lua | 1 + modules/zhunter/zButtonCraft.lua | 2 +- modules/zhunter/zButtonTrack.lua | 11 ++++++----- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d11a6..4a9b9a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to MetaHunt will be documented in this file. +## [1.5.2] - [Unreleased] + + +### Added + +- **zTrack — Find Fish**: Added new spell Find Fish to the zTrack list of trackings. + +### Fixed + +- **zCraft — Smelting**: Smeling was previously not appearing in zCraft because incorrectly hardcoded as "Mining", this is fixed. + + ## [1.5.1] - 2026-04-01 diff --git a/modules/zhunter/Localization.lua b/modules/zhunter/Localization.lua index 46f1d15..309c357 100644 --- a/modules/zhunter/Localization.lua +++ b/modules/zhunter/Localization.lua @@ -87,6 +87,7 @@ ZHUNTER_TRACK_MINERALS = "Find Minerals" ZHUNTER_TRACK_HERBS = "Find Herbs" ZHUNTER_TRACK_TREASURE = "Find Treasure" ZHUNTER_TRACK_TREES = "Find Trees" +ZHUNTER_TRACK_FISH = "Find Fish" ZHUNTER_TRAP_FREEZING = "Freezing Trap" ZHUNTER_TRAP_FROST = "Frost Trap" diff --git a/modules/zhunter/zButtonCraft.lua b/modules/zhunter/zButtonCraft.lua index 4d84cb3..1eec705 100644 --- a/modules/zhunter/zButtonCraft.lua +++ b/modules/zhunter/zButtonCraft.lua @@ -39,7 +39,7 @@ local ZCRAFT_PROFESSIONS = { { spellName = "Leatherworking", display = "Leatherworking" }, { spellName = "Tailoring", display = "Tailoring" }, { spellName = "Jewelcrafting", display = "Jewelcrafting" }, - { spellName = "Mining", display = "Smelting" }, + { spellName = "Smelting", display = "Smelting" }, { spellName = "Cooking", display = "Cooking" }, { spellName = "First Aid", display = "First Aid" }, { spellName = "Herbalism", display = "Herbalism" }, diff --git a/modules/zhunter/zButtonTrack.lua b/modules/zhunter/zButtonTrack.lua index cc33a61..bbd9809 100644 --- a/modules/zhunter/zButtonTrack.lua +++ b/modules/zhunter/zButtonTrack.lua @@ -5,7 +5,7 @@ end local root = zButtonTrack_GetRoot() if not root["zButtonTrack"] then root["zButtonTrack"] = {} - root["zButtonTrack"]["spells"] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + root["zButtonTrack"]["spells"] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} root["zButtonTrack"]["rows"] = 1 root["zButtonTrack"]["horizontal"] = nil root["zButtonTrack"]["vertical"] = nil @@ -33,7 +33,8 @@ ZHunterMod_Track_Spells = { ZHUNTER_TRACK_MINERALS, ZHUNTER_TRACK_HERBS, ZHUNTER_TRACK_TREASURE, - ZHUNTER_TRACK_TREES + ZHUNTER_TRACK_TREES, + ZHUNTER_TRACK_FISH } local ZHUNTER_TRACK_MAX = table.getn(ZHunterMod_Track_Spells) @@ -352,7 +353,7 @@ function zButtonTrack_CreateButtons() for i=1, table.getn(ZHunterMod_Track_Spells) do if not tonumber(saved["spells"][i]) then info = ZHunterMod_Track_Spells - saved["spells"] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} + saved["spells"] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} break end local spellIndex = saved["spells"][i] @@ -406,7 +407,7 @@ function zButtonTrack_Reset() local currentRoot = zButtonTrack_GetRoot() currentRoot["zButtonTrack"] = {} local saved = zButtonTrack_GetSaved() - saved["spells"] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} + saved["spells"] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} saved["rows"] = 1 saved["horizontal"] = nil saved["vertical"] = nil @@ -420,7 +421,7 @@ function zButtonTrack_Reset() saved["children"] = {} saved["children"]["size"] = 36 saved["children"]["hideonclick"] = 1 - saved["visible"] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + saved["visible"] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} zButtonTrack_EnsureConfig() end From 38e080261c453938c5b32deb69dbeb9a8c93ef37 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Sun, 6 Sep 2026 21:59:29 +0200 Subject: [PATCH 04/42] Fix rank-0 pet spell icons (Bubble Barrier, Charge); finalize 1.5.2 changelog; ignore Copilot data --- .gitignore | 5 +++++ CHANGELOG.md | 4 +++- data/ds-pet-spells.lua | 8 ++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 07377a9..0f5ed70 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,8 @@ LOCALIZATION*.md # Dev tooling — Python generators, data processing scripts, scratch files # Not part of the addon, never ship this .tools/ + +# GitHub Copilot — never sync Copilot data to GitHub +.copilot/ +.github/copilot-instructions.md +**/copilot-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9b9a9..d0d585f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to MetaHunt will be documented in this file. -## [1.5.2] - [Unreleased] +## [1.5.2] - 2026-04-08 ### Added @@ -12,6 +12,8 @@ All notable changes to MetaHunt will be documented in this file. ### Fixed +- **ds-pet-spells — wrong rank 0 icons**: Rank 0 (triggered/sub-spell) entries for Bubble Barrier and Charge had incorrect icons (`Spell_Frost_FrostNova` and `Ability_Warrior_Charge` respectively) instead of matching their ranked ability icons (`spell_bubl1` and `Ability_Hunter_Pet_Boar`). Fixed in both `allSpells` and `byAbility` sections. + - **zCraft — Smelting**: Smeling was previously not appearing in zCraft because incorrectly hardcoded as "Mining", this is fixed. diff --git a/data/ds-pet-spells.lua b/data/ds-pet-spells.lua index 3660b74..718fcba 100644 --- a/data/ds-pet-spells.lua +++ b/data/ds-pet-spells.lua @@ -198,7 +198,7 @@ MTH_DS_PetSpells = { ["effects"] = { "(2) School Damage (Frost) Value: 15 Radius: 10 yards", }, - ["icon"] = "Spell_Frost_FrostNova", + ["icon"] = "spell_bubl1", ["id"] = 36527, ["name"] = "Bubble Barrier", ["range"] = "0 yards (Self Only)", @@ -299,7 +299,7 @@ MTH_DS_PetSpells = { "(96) Charge", "(58) Weapon Damage + Value: 75", }, - ["icon"] = "Ability_Warrior_Charge", + ["icon"] = "Ability_Hunter_Pet_Boar", ["id"] = 22120, ["name"] = "Charge", ["range"] = "8-25 yards (Charge)", @@ -2013,7 +2013,7 @@ MTH_DS_PetSpells = { ["effects"] = { "(2) School Damage (Frost) Value: 15 Radius: 10 yards", }, - ["icon"] = "Spell_Frost_FrostNova", + ["icon"] = "spell_bubl1", ["id"] = 36527, ["name"] = "Bubble Barrier", ["range"] = "0 yards (Self Only)", @@ -2121,7 +2121,7 @@ MTH_DS_PetSpells = { "(96) Charge", "(58) Weapon Damage + Value: 75", }, - ["icon"] = "Ability_Warrior_Charge", + ["icon"] = "Ability_Hunter_Pet_Boar", ["id"] = 22120, ["name"] = "Charge", ["range"] = "8-25 yards (Charge)", From c55cd66ffc05e5657d85ebd7524d3f040740b4cb Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Tue, 8 Sep 2026 01:25:51 +0200 Subject: [PATCH 05/42] Fix realm-gate disable for book/options, pet-rank learning, unique beasts - Hunter's Book and options window now show a full 'DISABLED.' overlay on realm-gated realms instead of rendering content - Pet training: new rank of an already-known ability is now recognised, announced, and reflected in tooltips - Restore curated 'unique appearance' beast allowlist (34 beasts) in data/init.lua - CHANGELOG updated --- CHANGELOG.md | 17 +- MetaHunt.lua | 2 +- MetaHunt.toc | 2 +- README.MD | 11 +- api/core-framework.lua | 69 +++- api/core-scan-beasttraining.lua | 35 +- api/hunterbook.lua | 98 +++-- api/options-credits.lua | 21 +- api/options-feedomatic.lua | 8 +- api/options-shell.lua | 47 +++ data/ds-bags.lua | 46 +-- data/ds-items-origins.lua | 648 +++++++++++++++--------------- data/ds-pet-spells-trainer.lua | 16 +- data/ds-pet-spells.lua | 400 +++++++++--------- data/ds-racial.lua | 84 ++-- data/init.lua | 93 ++++- locales/ui/enUS.lua | 6 +- modules/feedomatic/FeedOMatic.lua | 72 +++- modules/tooltips/module.lua | 19 +- 19 files changed, 996 insertions(+), 698 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d585f..e0daa37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ All notable changes to MetaHunt will be documented in this file. -## [1.5.2] - 2026-04-08 +## [1.5.2] - 2026-09-08 +Res on Octowow server. ### Added @@ -12,9 +13,21 @@ All notable changes to MetaHunt will be documented in this file. ### Fixed +- **FeedOMatic — "Set Key" button error**: Clicking **Set Key** in Options → Feed-o-Matic threw an error and wouldn't let you bind a feed key. It now works again. + +- **FeedOMatic — pet left hungry when bags were full**: When your bags were full, FeedOMatic could grab a random tiny stack (like a single meat your pet won't eat) to free a bag slot, so your pet stayed hungry. It now always picks food your pet can actually eat. + +- **Tooltips — "Not learned yet" hint missing on pet abilities**: When you tamed a new beast, hovering one of its abilities on your pet bar no longer showed the red "Not learned yet, keep going !" reminder, even for abilities you hadn't learned. The hint is back and now correctly shows for any ability rank your hunter hasn't learned yet. + +- **Pet training — new rank of a known ability not registered**: When you trained a higher rank of an ability you already had (e.g. learning Charge Rank 2 after Rank 1), MetaHunt treated it as already known — so the "Pet ability learned and recorded" message never appeared and the pet-bar tooltip kept saying "Not learned yet" for the new rank. Learning a new rank is now recognised on its own, announces correctly, and the tooltip immediately reflects the rank you just learned. + +- **Hunter's Book — missing "Unique Appearance" beasts**: The unique-appearance flag had gone missing from the beast database, so the "Unique" badge and filter in the Hunter's Book came up empty. Restored a curated list of the genuinely one-of-a-kind beasts a collector hunts for (Humar the Pridelord, Darksaber, Shar'lan, Frostsaber Pride Watcher, Mazzranache, Moonfeather, Azurebeak, Old Cliff Jumper, Rotting Agam'ar, Razzashi Serpent, Magmalash Scorpid, Spirit Fox and more), so the Unique badge and filter work again without wrongly flagging common colour-variant mobs. + - **ds-pet-spells — wrong rank 0 icons**: Rank 0 (triggered/sub-spell) entries for Bubble Barrier and Charge had incorrect icons (`Spell_Frost_FrostNova` and `Ability_Warrior_Charge` respectively) instead of matching their ranked ability icons (`spell_bubl1` and `Ability_Hunter_Pet_Boar`). Fixed in both `allSpells` and `byAbility` sections. -- **zCraft — Smelting**: Smeling was previously not appearing in zCraft because incorrectly hardcoded as "Mining", this is fixed. +- **zCraft — Smelting**: Smelting was previously not appearing in zCraft because incorrectly hardcoded as "Mining", this is fixed. + +- **Realm Blocking**: The Addon will auto-disable itself on all Mafia's realms. ## [1.5.1] - 2026-04-01 diff --git a/MetaHunt.lua b/MetaHunt.lua index 3d5045e..54afb42 100644 --- a/MetaHunt.lua +++ b/MetaHunt.lua @@ -4,7 +4,7 @@ -- Modern modular architecture with legacy addon support -- -- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --- TARGET RUNTIME: LUA 5.0 (WoW 1.12 / Turtle-WoW) +-- TARGET RUNTIME: LUA 5.0 (WoW 1.12 / Twow 1.18) -- - Max 32 upvalues per function -- - No # operator (use table.getn) -- - No string.gmatch (use string.gfind) diff --git a/MetaHunt.toc b/MetaHunt.toc index d5f77d0..d553386 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -2,7 +2,7 @@ ## Version: 1.5.1 ## Title: MetaHunt - |cff00ff00Hunter ## Author: Metasploit and his Copilot ;) -## Notes: Unified addon suite for huntards making old addons compatible with TurtleWoW, and adding an arsenal of never seen Hunter's utilities. +## Notes: Unified addon suite for huntards making old addons compatible with Twow, and adding an arsenal of never seen Hunter's utilities. ## SavedVariables: MTH_SavedVariables, FOM_Config, FOM_FoodQuality, FOM_AddedFoods, FOM_RemovedFoods, FOM_Cooking, FOM_QuestFood, FOM_LocaleInfo ## SavedVariablesPerCharacter: ZHunterMod_Saved, MTH_CharSavedVariables diff --git a/README.MD b/README.MD index 536ea2a..f6f7024 100644 --- a/README.MD +++ b/README.MD @@ -1,6 +1,6 @@ # MetaHunt -MetaHunt is a Turtle WoW Hunter toolkit. +MetaHunt is a Twow Hunter toolkit. It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken on Twow. It provides a bunch of useful tools for all huntards, wheter they are still levelling, lone-wolf, or HL raider. @@ -8,11 +8,11 @@ It provides a bunch of useful tools for all huntards, wheter they are still leve - Altough Nampower isn't mandatory for most of the features, it is a must-have for some. - If you were using old versions of Feed-O-Matic, ICU, zHunterMod and HunterHelper, you dont need those with MetaHunt and you should disable them to avoid conflicts. - MetaHunt is fully compatible with Quiver and is not a replacement for it. - +- Zero support for Capycraft and Ravencraft mafia. ## Twow Data -MetaHunt ships with large Turtle WOW datastores : +MetaHunt ships with large Twow datastores : - 100% accurate beasts' data, their locations, and the abilities they can learn you - All Pet families, their abilities and their diet @@ -21,6 +21,9 @@ MetaHunt ships with large Turtle WOW datastores : - All Stable, Hunter and Pet Masters - All Ammo vendors +You can browse all the beasts in game within the Book of Huntards, but also on the github site : https://DuvelCorp.github.io/MetaHunt-Web/ + + ## Smart Ammo - Auto swap your main ammo to junk ammo for shots that doesn't scale on damage, and re-equip you main ammo right away. Don't waste your Doomshots on nothing anymore, save money ! @@ -50,7 +53,7 @@ Old zHunterMod addon on steroids. Fully compatible with Twow, and enhanced with ## Feed-O-Matic -Now compatible with Turtle, feed your pet with one key bind, it will always pickup the best food for your pet. +Now compatible with Twow, feed your pet with one key bind, it will always pickup the best food for your pet. ## Chronometer diff --git a/api/core-framework.lua b/api/core-framework.lua index 049fa2b..f7e47c3 100644 --- a/api/core-framework.lua +++ b/api/core-framework.lua @@ -7,7 +7,18 @@ MTH = MTH or { } local MTH_CHAT_PREFIX = "|cFFFFFFFF[|r|cFFD14A4AMeta|r|cFFABD473Hunt|r|cFFFFFFFF]|r " -local MTH_NON_HUNTER_BLOCK_MESSAGE = "You are not a hunter and you are not allowed here. Disable the add-on for this character." +local MTH_NON_HUNTER_BLOCK_MESSAGE = "You are not a hunter. Disable the add-on for this character." +local MTH_REALM_BLOCK_MESSAGE = "Zero support for Ravencraft/Capybara mafia's realms. Consider joining Octowow." +local MTH_BLOCKED_REALMS = { + ["Medivh"] = true, + ["\230\176\184\230\173\140\232\141\146\233\135\142"] = true, + ["\211\192\184\232\187\196\210\176"] = true, + ["\229\159\186\232\181\171\231\186\179\230\150\175"] = true, + ["\187\249\186\213\196\201\203\185"] = true, + ["\231\190\164\230\152\159\231\155\134\229\156\176"] = true, + ["\200\186\208\199\197\232\181\216"] = true, +} + local MTH_MESSAGE_DEFAULTS = { initModulesLoaded = false, initSarcasticWelcome = true, @@ -189,6 +200,57 @@ function MTH:ApplyClassGate(_source) return blocked end +function MTH:GetCurrentRealmName() + local realm = nil + if type(GetRealmName) == "function" then + realm = GetRealmName() + end + if (realm == nil or realm == "") and type(GetCVar) == "function" then + realm = GetCVar("realmName") + end + return realm +end + +function MTH:IsRealmGateBlocked() + return self and self._realmGateBlocked and true or false +end + +function MTH:CheckRealmGate() + -- Realm name is only reliable once logged in; an empty result never blocks. + local realm = self:GetCurrentRealmName() + if type(realm) == "string" and realm ~= "" then + self._currentRealm = realm + if MTH_BLOCKED_REALMS[realm] then + self._realmGateBlocked = true + end + end + return self:IsRealmGateBlocked() +end + +function MTH:AnnounceRealmGateBlocked() + if self._realmGateAnnounced then + return + end + local sink = MTH_TryOutputClassGateMessage(MTH_REALM_BLOCK_MESSAGE) + if sink == "chat" or sink == "uierrors" or sink == "print" then + self._realmGateAnnounced = true + end +end + +function MTH:ApplyRealmGate(_source) + if self:IsHardBlocked() then + return true + end + if self:CheckRealmGate() then + self:AnnounceRealmGateBlocked() + if self.ShutdownForNonHunter then + self:ShutdownForNonHunter(_source or "realm-gate") + end + return true + end + return false +end + MTH:ApplyClassGate("startup") if not MTH._classGateLifecycleFrame then @@ -202,6 +264,11 @@ if not MTH._classGateLifecycleFrame then MTH._classGateAnnounced = nil MTH._classGateAnnouncePending = nil end + if MTH and MTH.ApplyRealmGate and MTH:ApplyRealmGate(event) then + this:UnregisterAllEvents() + this:SetScript("OnEvent", nil) + return + end if MTH and MTH.ApplyClassGate and MTH:ApplyClassGate(event) and MTH._classGateAnnouncedChat then this:UnregisterAllEvents() this:SetScript("OnEvent", nil) diff --git a/api/core-scan-beasttraining.lua b/api/core-scan-beasttraining.lua index acda272..4dd0316 100644 --- a/api/core-scan-beasttraining.lua +++ b/api/core-scan-beasttraining.lua @@ -335,6 +335,39 @@ local function MTH_PT_IsHunterTokenKnown(token) return false end +-- Rank-strict variant of MTH_PT_IsHunterTokenKnown: only the EXACT token counts, with +-- no fall back to the base ability. Knowing Charge (Rank 1) must NOT make Charge (Rank 2) +-- look already-learned, otherwise learning a new rank is never recorded or announced. +local function MTH_PT_IsExactTokenKnown(token) + if token == nil or token == "" then + return false + end + + local store = MTH_PT_GetStore() + if store.hunterKnownMap and store.hunterKnownMap[token] == true then + return true + end + if store.spellMap and store.spellMap[token] and store.spellMap[token].isKnown == true then + return true + end + return false +end + +-- Authoritative, rank-strict check for whether the HUNTER has learned a pet ability at a +-- given rank. Consults both hunterKnownMap and spellMap, so callers (e.g. the tooltip hint) +-- stay correct even when a full Beast Training rescan has not yet rebuilt spellMap rows. +function MTH_IsPetAbilityKnownByHunter(abilityName, rankNumber) + local ability = MTH_PT_NormalizeName(abilityName) + if ability == "" then + return false + end + local token = MTH_PT_MakeTokenFromAbilityRank(ability, rankNumber) + if token == "" then + return false + end + return MTH_PT_IsExactTokenKnown(token) +end + local function MTH_PT_ProcessLearnSystemMessage(rawMessage) local message = tostring(rawMessage or "") if message == "" then return false end @@ -368,7 +401,7 @@ local function MTH_PT_ProcessLearnSystemMessage(rawMessage) local token = MTH_PT_MakeTokenFromAbilityRank(abilityName, rankNumber) if token == "" then return false end - local alreadyKnown = MTH_PT_IsHunterTokenKnown(token) + local alreadyKnown = MTH_PT_IsExactTokenKnown(token) local changed = false if not alreadyKnown then changed = MTH_PT_MarkHunterKnownToken(token) diff --git a/api/hunterbook.lua b/api/hunterbook.lua index 1e69c65..2f1808a 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -20,34 +20,34 @@ local MTH_BOOK_GetZoneName local MTH_BOOK_GetPlayerLevelValue local MTH_BOOK_IsSpellInLevelScope local MTH_BOOK_GetScopedRankSummary -local MTH_BOOK_BEAST_DATABASE_URL = "https://database.turtlecraft.gg/?npc=%d" -local MTH_BOOK_NPC_DATABASE_URL = "https://database.turtlecraft.gg/?npc=%d" -local MTH_BOOK_ITEM_DATABASE_URL = "https://database.turtlecraft.gg/?item=%d" +local MTH_BOOK_BEAST_DATABASE_URL = "https://octowow.st/db/?npc=%d" +local MTH_BOOK_NPC_DATABASE_URL = "https://octowow.st/db/?npc=%d" +local MTH_BOOK_ITEM_DATABASE_URL = "https://octowow.st/db/?item=%d" local MTH_BOOK_STABLE_CURRENT_ID_CACHE = nil local MTH_BOOK_FALLBACK_BAG_ITEMS = { - [2101] = { name = "Light Quiver", subtype = "quiver", slots = 6, reqlevel = nil, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=2101" }, - [2102] = { name = "Small Ammo Pouch", subtype = "ammo pouch", slots = 6, reqlevel = nil, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=2102" }, - [2662] = { name = "Ribbly's Quiver", subtype = "quiver", slots = 16, reqlevel = 50, quality = 2, sourceUrl = "https://database.turtlecraft.gg/?item=2662" }, - [2663] = { name = "Ribbly's Bandolier", subtype = "ammo pouch", slots = 16, reqlevel = 50, quality = 2, sourceUrl = "https://database.turtlecraft.gg/?item=2663" }, - [3573] = { name = "Hunting Quiver", subtype = "quiver", slots = 10, reqlevel = nil, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=3573" }, - [3574] = { name = "Hunting Ammo Sack", subtype = "ammo pouch", slots = 10, reqlevel = nil, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=3574" }, - [3604] = { name = "Bandolier of the Night Watch", subtype = "ammo pouch", slots = 12, reqlevel = nil, quality = 2, sourceUrl = "https://database.turtlecraft.gg/?item=3604" }, - [3605] = { name = "Quiver of the Night Watch", subtype = "quiver", slots = 12, reqlevel = nil, quality = 2, sourceUrl = "https://database.turtlecraft.gg/?item=3605" }, - [5439] = { name = "Small Quiver", subtype = "quiver", slots = 8, reqlevel = nil, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=5439" }, - [5441] = { name = "Small Shot Pouch", subtype = "ammo pouch", slots = 8, reqlevel = nil, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=5441" }, - [7278] = { name = "Light Leather Quiver", subtype = "quiver", slots = 8, reqlevel = nil, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=7278" }, - [7279] = { name = "Small Leather Ammo Pouch", subtype = "ammo pouch", slots = 8, reqlevel = nil, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=7279" }, - [7371] = { name = "Heavy Quiver", subtype = "quiver", slots = 14, reqlevel = 30, quality = 2, sourceUrl = "https://database.turtlecraft.gg/?item=7371" }, - [7372] = { name = "Heavy Leather Ammo Pouch", subtype = "ammo pouch", slots = 14, reqlevel = 30, quality = 2, sourceUrl = "https://database.turtlecraft.gg/?item=7372" }, - [8217] = { name = "Quickdraw Quiver", subtype = "quiver", slots = 16, reqlevel = 40, quality = 2, sourceUrl = "https://database.turtlecraft.gg/?item=8217" }, - [8218] = { name = "Thick Leather Ammo Pouch", subtype = "ammo pouch", slots = 16, reqlevel = 40, quality = 2, sourceUrl = "https://database.turtlecraft.gg/?item=8218" }, - [11362] = { name = "Medium Quiver", subtype = "quiver", slots = 10, reqlevel = 10, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=11362" }, - [11363] = { name = "Medium Shot Pouch", subtype = "ammo pouch", slots = 10, reqlevel = 10, quality = 1, sourceUrl = "https://database.turtlecraft.gg/?item=11363" }, - [18714] = { name = "Ancient Sinew Wrapped Lamina", subtype = "quiver", slots = 18, reqlevel = 60, quality = 4, sourceUrl = "https://database.turtlecraft.gg/?item=18714" }, - [19319] = { name = "Harpy Hide Quiver", subtype = "quiver", slots = 16, reqlevel = 55, quality = 3, sourceUrl = "https://database.turtlecraft.gg/?item=19319" }, - [19320] = { name = "Gnoll Skin Bandolier", subtype = "ammo pouch", slots = 16, reqlevel = 55, quality = 3, sourceUrl = "https://database.turtlecraft.gg/?item=19320" }, - [61549] = { name = "Swiftfeather Quiver", subtype = "quiver", slots = 16, reqlevel = 57, quality = 3, sourceUrl = "https://database.turtlecraft.gg/?item=61549" }, + [2101] = { name = "Light Quiver", subtype = "quiver", slots = 6, reqlevel = nil, quality = 1, sourceUrl = "https://octowow.st/db/?item=2101" }, + [2102] = { name = "Small Ammo Pouch", subtype = "ammo pouch", slots = 6, reqlevel = nil, quality = 1, sourceUrl = "https://octowow.st/db/?item=2102" }, + [2662] = { name = "Ribbly's Quiver", subtype = "quiver", slots = 16, reqlevel = 50, quality = 2, sourceUrl = "https://octowow.st/db/?item=2662" }, + [2663] = { name = "Ribbly's Bandolier", subtype = "ammo pouch", slots = 16, reqlevel = 50, quality = 2, sourceUrl = "https://octowow.st/db/?item=2663" }, + [3573] = { name = "Hunting Quiver", subtype = "quiver", slots = 10, reqlevel = nil, quality = 1, sourceUrl = "https://octowow.st/db/?item=3573" }, + [3574] = { name = "Hunting Ammo Sack", subtype = "ammo pouch", slots = 10, reqlevel = nil, quality = 1, sourceUrl = "https://octowow.st/db/?item=3574" }, + [3604] = { name = "Bandolier of the Night Watch", subtype = "ammo pouch", slots = 12, reqlevel = nil, quality = 2, sourceUrl = "https://octowow.st/db/?item=3604" }, + [3605] = { name = "Quiver of the Night Watch", subtype = "quiver", slots = 12, reqlevel = nil, quality = 2, sourceUrl = "https://octowow.st/db/?item=3605" }, + [5439] = { name = "Small Quiver", subtype = "quiver", slots = 8, reqlevel = nil, quality = 1, sourceUrl = "https://octowow.st/db/?item=5439" }, + [5441] = { name = "Small Shot Pouch", subtype = "ammo pouch", slots = 8, reqlevel = nil, quality = 1, sourceUrl = "https://octowow.st/db/?item=5441" }, + [7278] = { name = "Light Leather Quiver", subtype = "quiver", slots = 8, reqlevel = nil, quality = 1, sourceUrl = "https://octowow.st/db/?item=7278" }, + [7279] = { name = "Small Leather Ammo Pouch", subtype = "ammo pouch", slots = 8, reqlevel = nil, quality = 1, sourceUrl = "https://octowow.st/db/?item=7279" }, + [7371] = { name = "Heavy Quiver", subtype = "quiver", slots = 14, reqlevel = 30, quality = 2, sourceUrl = "https://octowow.st/db/?item=7371" }, + [7372] = { name = "Heavy Leather Ammo Pouch", subtype = "ammo pouch", slots = 14, reqlevel = 30, quality = 2, sourceUrl = "https://octowow.st/db/?item=7372" }, + [8217] = { name = "Quickdraw Quiver", subtype = "quiver", slots = 16, reqlevel = 40, quality = 2, sourceUrl = "https://octowow.st/db/?item=8217" }, + [8218] = { name = "Thick Leather Ammo Pouch", subtype = "ammo pouch", slots = 16, reqlevel = 40, quality = 2, sourceUrl = "https://octowow.st/db/?item=8218" }, + [11362] = { name = "Medium Quiver", subtype = "quiver", slots = 10, reqlevel = 10, quality = 1, sourceUrl = "https://octowow.st/db/?item=11362" }, + [11363] = { name = "Medium Shot Pouch", subtype = "ammo pouch", slots = 10, reqlevel = 10, quality = 1, sourceUrl = "https://octowow.st/db/?item=11363" }, + [18714] = { name = "Ancient Sinew Wrapped Lamina", subtype = "quiver", slots = 18, reqlevel = 60, quality = 4, sourceUrl = "https://octowow.st/db/?item=18714" }, + [19319] = { name = "Harpy Hide Quiver", subtype = "quiver", slots = 16, reqlevel = 55, quality = 3, sourceUrl = "https://octowow.st/db/?item=19319" }, + [19320] = { name = "Gnoll Skin Bandolier", subtype = "ammo pouch", slots = 16, reqlevel = 55, quality = 3, sourceUrl = "https://octowow.st/db/?item=19320" }, + [61549] = { name = "Swiftfeather Quiver", subtype = "quiver", slots = 16, reqlevel = 57, quality = 3, sourceUrl = "https://octowow.st/db/?item=61549" }, } local function MTH_BOOK_FamiliesTrace(message) @@ -5661,11 +5661,58 @@ _G.MTH_BOOK_GetScopedRankSummary = MTH_BOOK_GetScopedRankSummary _G.MTH_BOOK_UpdateResults = MTH_BOOK_UpdateResults _G.MTH_BOOK_RefreshFilter = MTH_BOOK_RefreshFilter +-- On a blocked realm the whole book must be inert. Rather than rendering any pages we +-- drop an opaque "DISABLED." panel over the entire window (with its own close button so +-- the frame can still be dismissed). Returns true when the realm gate is active. +local function MTH_BOOK_UpdateRealmGateOverlay(frame) + if not frame then return false end + local blocked = (MTH and MTH.IsRealmGateBlocked and MTH:IsRealmGateBlocked()) and true or false + + local overlay = frame.realmGateOverlay + if not overlay then + if not blocked then return false end + + overlay = CreateFrame("Frame", "MTH_BOOK_RealmGateOverlay", frame) + overlay:SetAllPoints(frame) + overlay:EnableMouse(true) + overlay:EnableMouseWheel(true) + overlay:SetFrameStrata("FULLSCREEN_DIALOG") + if overlay.SetFrameLevel and frame.GetFrameLevel then + overlay:SetFrameLevel(frame:GetFrameLevel() + 50) + end + + local bg = overlay:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(overlay) + bg:SetTexture("Interface\\Buttons\\WHITE8X8") + if bg.SetVertexColor then bg:SetVertexColor(0, 0, 0, 1) end + + local label = overlay:CreateFontString(nil, "OVERLAY") + label:SetPoint("CENTER", overlay, "CENTER", 0, 0) + if label.SetFont then label:SetFont("Fonts\\FRIZQT__.TTF", 48, "OUTLINE") end + label:SetText("DISABLED.") + label:SetTextColor(1.0, 0.1, 0.1) + + local close = CreateFrame("Button", nil, overlay, "UIPanelCloseButton") + close:SetPoint("TOPRIGHT", overlay, "TOPRIGHT", -4, -4) + close:SetScript("OnClick", function() frame:Hide() end) + + frame.realmGateOverlay = overlay + end + + if blocked then + overlay:Show() + else + overlay:Hide() + end + return blocked +end + function MTH_OpenHunterBook() if not MTH_HB_RequireOpenDeps() then return end local frame = MTH_BOOK_EnsureWindow() if not frame then return end frame:Show() + if MTH_BOOK_UpdateRealmGateOverlay(frame) then return end MTH_BOOK_RefreshFilter() end @@ -5677,6 +5724,7 @@ function MTH_ToggleHunterBook() frame:Hide() else frame:Show() + if MTH_BOOK_UpdateRealmGateOverlay(frame) then return end MTH_BOOK_RefreshFilter() end end diff --git a/api/options-credits.lua b/api/options-credits.lua index 79d59f4..194b303 100644 --- a/api/options-credits.lua +++ b/api/options-credits.lua @@ -142,7 +142,7 @@ function MTH_SetupCreditsOptions() -4) cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_ABOUT_TOOLTIPS", "- Tooltips module is based on Hunter Helper, another vanilla addon of Fizzwidget. I kept the general idea, but there's not much original code left from it in Metahunt as I refactored everything so it plugs on MetaHunt controlled event handlers, can work with Turtle wow data, and is now usable on other things than Beasts."), + MTH_CR_L("CREDITS_ABOUT_TOOLTIPS", "- Tooltips module is based on Hunter Helper, another vanilla addon of Fizzwidget. I kept the general idea, but there's not much original code left from it in Metahunt as I refactored everything so it plugs on MetaHunt controlled event handlers, can work with Twow data, and is now usable on other things than Beasts."), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateLink(content, "https://legacy-wow.com/vanilla-addons/fizzwidget-hunter-helper/", @@ -179,34 +179,21 @@ function MTH_SetupCreditsOptions() cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_ABOUT_DATA_TITLE", "About data sources"), "GameFontNormal", -14, 1.00, 0.82, 0.00) cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_ABOUT_DATA_INTRO", "Metahunt ships with its own Turtle-WoW datastores, limited to hunter stuff only, and most of this data is up-to-date (Feb 2026).\n\n" + MTH_CR_L("CREDITS_ABOUT_DATA_INTRO", "Metahunt ships with its own Twow datastores, limited to hunter stuff only, and most of this data is up-to-date (Feb 2026).\n\n" .. "Sources used to build the data include:"), "GameFontNormalSmall", -10) - cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_DATA_PFQUEST", "- pfQuest for Beasts and Ammo vendors NPCs and their spawn points (and beasts respawn times if any). Thank you again Shagu."), - "GameFontNormalSmall", -10) - - cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_DATA_SHEET", "- This invaluable and very accurate spreadsheet for beasts having pet abilities. A big thanks to all twow players that crafted this! Now you dont need that sheet anymore, you have Metahunt! And the Great Book of Huntards is awesome and accurate also because of YOU."), - "GameFontNormalSmall", -10) - cursor = MTH_CreditsCreateLink(content, - "https://docs.google.com/spreadsheets/d/1u33knqlYXvY-jR6pvTd58gyDg8OYEuwYsSoEnIjeA8k/edit?gid=4136205#gid=4136205", - "https://docs.google.com/spreadsheets/d/1u33knqlYXvY-jR6pvTd58gyDg8OYEuwYsSoEnIjeA8k/edit?gid=4136205#gid=4136205", - cursor, - -4) - cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_DATA_OTHER", "- Atlas twow for ranged weapon sources\n\n" .. "- Twow wiki for pet diet (which was incomplete, missing Serpents and Foxes)"), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_CLOSING", "With that being said, go back hunting fellas!"), + MTH_CR_L("CREDITS_CLOSING", "With that being said, go back hunting fellas! Preferably on Octowow."), "GameFontNormalSmall", -10, 1.00, 0.82, 0.00) cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_SIGNATURE", "\n" - .. "Metasploit , of Nordaanar."), + .. "Metasploit , of Nordaanar, now N'Zoth"), "GameFontNormalSmall", -10) end diff --git a/api/options-feedomatic.lua b/api/options-feedomatic.lua index 4d8dc3f..6c0c79c 100644 --- a/api/options-feedomatic.lua +++ b/api/options-feedomatic.lua @@ -114,6 +114,10 @@ local function MTH_FOM_BuildBindingChord(key) key) end +-- Forward declaration: defined below but referenced by the capture-frame +-- closures above it, so it must be an in-scope local upvalue (not a global). +local MTH_FOM_SaveBinding + local function MTH_FOM_MakeCaptureFrame() local f = CreateFrame("Frame", "MTH_FOMBindCapture", UIParent) f:SetFrameStrata("FULLSCREEN_DIALOG") @@ -168,7 +172,7 @@ local function MTH_FOM_StartBindingCapture() end end -local function MTH_FOM_SaveBinding(key) +function MTH_FOM_SaveBinding(key) if not SetBinding or not SaveBindings or not GetBindingKey then if MTH and MTH.Print then MTH:Print(MTH_FOM_L("FOM_ERROR_KEYBIND_API_UNAVAILABLE", "Keybinding APIs unavailable.")) @@ -258,7 +262,7 @@ function MTH_SetupFeedOMaticOptions() statusNotice:SetJustifyH("LEFT") statusNotice:SetJustifyV("TOP") statusNotice:SetTextColor(0.35, 0.65, 1) - statusNotice:SetText(MTH_FOM_L("FOM_STATUS_NOTICE", "Feed-O-Matic, created by the great Fizzwidget, is not yet entirely ready for TurtleWoW because I am missing \na reliable list of all foods, mostly impossible to fetch from the DB.\n It still works much better in this version, but all stuff related to Food buff and Cooking isnt fully functional.")) + statusNotice:SetText(MTH_FOM_L("FOM_STATUS_NOTICE", "Feed-O-Matic, created by the great Fizzwidget, is not yet entirely ready for Twow because I am missing \na reliable list of all foods, mostly impossible to fetch from the DB.\n It still works much better in this version, but all stuff related to Food buff and Cooking isnt fully functional.")) local moduleCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsFeedOMaticEnabled", MTH_FOM_L("FOM_ENABLE_MODULE", "Enable FeedOMatic module"), -32 + yAdjust) if moduleCheck then diff --git a/api/options-shell.lua b/api/options-shell.lua index e40931f..87bbeb7 100644 --- a/api/options-shell.lua +++ b/api/options-shell.lua @@ -271,6 +271,52 @@ function MTH_ResetAndSelectOptionsTab(tabKey) MTH_SelectOptionsTab(tabKey) end +-- On a blocked realm the options window must be inert too. Instead of drawing any tabs we +-- drop an opaque "DISABLED." panel over the whole window (with its own close button so it can +-- still be dismissed). Returns true when the realm gate is active. +local function MTH_OPTIONS_UpdateRealmGateOverlay(frame) + if not frame then return false end + local blocked = (MTH and MTH.IsRealmGateBlocked and MTH:IsRealmGateBlocked()) and true or false + + local overlay = frame.realmGateOverlay + if not overlay then + if not blocked then return false end + + overlay = CreateFrame("Frame", "MetaHuntOptionsRealmGateOverlay", frame) + overlay:SetAllPoints(frame) + overlay:EnableMouse(true) + overlay:EnableMouseWheel(true) + overlay:SetFrameStrata("FULLSCREEN_DIALOG") + if overlay.SetFrameLevel and frame.GetFrameLevel then + overlay:SetFrameLevel(frame:GetFrameLevel() + 50) + end + + local bg = overlay:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(overlay) + bg:SetTexture("Interface\\Buttons\\WHITE8X8") + if bg.SetVertexColor then bg:SetVertexColor(0, 0, 0, 1) end + + local label = overlay:CreateFontString(nil, "OVERLAY") + label:SetPoint("CENTER", overlay, "CENTER", 0, 0) + if label.SetFont then label:SetFont("Fonts\\FRIZQT__.TTF", 48, "OUTLINE") end + label:SetText("DISABLED.") + label:SetTextColor(1.0, 0.1, 0.1) + + local close = CreateFrame("Button", nil, overlay, "UIPanelCloseButton") + close:SetPoint("TOPRIGHT", overlay, "TOPRIGHT", -4, -4) + close:SetScript("OnClick", function() frame:Hide() end) + + frame.realmGateOverlay = overlay + end + + if blocked then + overlay:Show() + else + overlay:Hide() + end + return blocked +end + function MTH_OpenOptions(tabKey) if not MTH_OPTIONS_SHELL_READY then return end local optionsFrame = MTH_EnsureOptionsFrame() @@ -278,5 +324,6 @@ function MTH_OpenOptions(tabKey) return end optionsFrame:Show() + if MTH_OPTIONS_UpdateRealmGateOverlay(optionsFrame) then return end MTH_SelectOptionsTab(tabKey or "General") end diff --git a/data/ds-bags.lua b/data/ds-bags.lua index 978d216..4ff7836 100644 --- a/data/ds-bags.lua +++ b/data/ds-bags.lua @@ -1,5 +1,5 @@ -- MetaHunt Bags Database (Quivers + Ammo Pouches) --- Source: https://database.turtlecraft.gg/?items=11 +-- Source: https://octowow.st/db/?items=11 -- Auto-generated extraction with source recursion. if not MTH_DS then MTH_DS = {} end @@ -14,7 +14,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Light Quiver\n6 Slot Bag\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2101", + ["sourceUrl"] = "https://octowow.st/db/?item=2101", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -89,7 +89,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Small Ammo Pouch\n6 Slot Bag\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2102", + ["sourceUrl"] = "https://octowow.st/db/?item=2102", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -144,7 +144,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Ribbly's Quiver\nBinds when picked up\nUnique\n16 Slot Bag\nRequires Level 50\nEquip: Increases ranged attack speed by 14%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2662", + ["sourceUrl"] = "https://octowow.st/db/?item=2662", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -171,7 +171,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Ribbly's Bandolier\nBinds when picked up\nUnique\n16 Slot Bag\nRequires Level 50\nEquip: Increases ranged attack speed by 14%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2663", + ["sourceUrl"] = "https://octowow.st/db/?item=2663", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -197,7 +197,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Hunting Quiver\nBinds when picked up\nUnique\n10 Slot Bag\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3573", + ["sourceUrl"] = "https://octowow.st/db/?item=3573", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -216,7 +216,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Hunting Ammo Sack\nBinds when picked up\n10 Slot Bag\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3574", + ["sourceUrl"] = "https://octowow.st/db/?item=3574", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -235,7 +235,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Bandolier of the Night Watch\nBinds when picked up\nUnique\n12 Slot Bag\nEquip: Increases ranged attack speed by 11%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3604", + ["sourceUrl"] = "https://octowow.st/db/?item=3604", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -254,7 +254,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Quiver of the Night Watch\nBinds when picked up\nUnique\n12 Slot Bag\nEquip: Increases ranged attack speed by 11%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3605", + ["sourceUrl"] = "https://octowow.st/db/?item=3605", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -273,7 +273,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Small Quiver\n8 Slot Bag\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5439", + ["sourceUrl"] = "https://octowow.st/db/?item=5439", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -621,7 +621,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Small Shot Pouch\n8 Slot Bag\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5441", + ["sourceUrl"] = "https://octowow.st/db/?item=5441", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -773,7 +773,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Light Leather Quiver\n8 Slot Bag\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=7278", + ["sourceUrl"] = "https://octowow.st/db/?item=7278", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -796,7 +796,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Small Leather Ammo Pouch\n8 Slot Bag\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=7279", + ["sourceUrl"] = "https://octowow.st/db/?item=7279", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -820,7 +820,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Heavy Quiver\n14 Slot Bag\nRequires Level 30\nEquip: Increases ranged attack speed by 12%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=7371", + ["sourceUrl"] = "https://octowow.st/db/?item=7371", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -871,7 +871,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Heavy Leather Ammo Pouch\n14 Slot Bag\nRequires Level 30\nEquip: Increases ranged attack speed by 12%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=7372", + ["sourceUrl"] = "https://octowow.st/db/?item=7372", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -895,7 +895,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Quickdraw Quiver\n16 Slot Bag\nRequires Level 40\nEquip: Increases ranged attack speed by 13%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8217", + ["sourceUrl"] = "https://octowow.st/db/?item=8217", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -919,7 +919,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Thick Leather Ammo Pouch\n16 Slot Bag\nRequires Level 40\nEquip: Increases ranged attack speed by 13%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8218", + ["sourceUrl"] = "https://octowow.st/db/?item=8218", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -943,7 +943,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Medium Quiver\n10 Slot Bag\nRequires Level 10\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11362", + ["sourceUrl"] = "https://octowow.st/db/?item=11362", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -1327,7 +1327,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Medium Shot Pouch\n10 Slot Bag\nRequires Level 10\nEquip: Increases ranged attack speed by 10%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11363", + ["sourceUrl"] = "https://octowow.st/db/?item=11363", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -1368,7 +1368,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Ancient Sinew Wrapped Lamina\nBinds when picked up\nUnique\n18 Slot Bag\nClasses: Hunter\nRequires Level 60\nEquip: Increases ranged attack speed by 15%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18714", + ["sourceUrl"] = "https://octowow.st/db/?item=18714", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -1388,7 +1388,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Harpy Hide Quiver\nBinds when picked up\nUnique\n16 Slot Bag\nRequires Level 55\nEquip: Increases ranged attack speed by 15%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19319", + ["sourceUrl"] = "https://octowow.st/db/?item=19319", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -1440,7 +1440,7 @@ MTH_DS_BagItems = { ["subtype"] = "ammo pouch", ["description"] = "", ["tooltip"] = "Gnoll Skin Bandolier\nBinds when picked up\nUnique\n16 Slot Bag\nRequires Level 55\nEquip: Increases ranged attack speed by 15%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19320", + ["sourceUrl"] = "https://octowow.st/db/?item=19320", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -1491,7 +1491,7 @@ MTH_DS_BagItems = { ["subtype"] = "quiver", ["description"] = "", ["tooltip"] = "Swiftfeather Quiver\nBinds when equipped\nUnique\n16 Slot Bag\nRequires Level 57\nEquip: Increases ranged attack speed by 14%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61549", + ["sourceUrl"] = "https://octowow.st/db/?item=61549", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", diff --git a/data/ds-items-origins.lua b/data/ds-items-origins.lua index 3f22a34..acf4fd9 100644 --- a/data/ds-items-origins.lua +++ b/data/ds-items-origins.lua @@ -15,7 +15,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Double-barreled Shotgun\nBinds when equippedRangedGun21 - 40 DamageSpeed 2.30(13.3 damage per second)\nDurability 70 / 70\nRequires Level 22", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2098", + ["sourceUrl"] = "https://octowow.st/db/?item=2098", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 408, @@ -120,7 +120,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Dwarven Hand Cannon\nBinds when equippedRangedGun66 - 124 DamageSpeed 2.90(32.8 damage per second)\nDurability 90 / 90\nRequires Level 53\nEquip: Chance to strike your ranged target with a Flaming Cannonball for 33 to 50 Fire damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2099", + ["sourceUrl"] = "https://octowow.st/db/?item=2099", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 489, @@ -135,7 +135,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Precisely Calibrated Boomstick\nBinds when equippedRangedGun38 - 45 DamageSpeed 1.50(27.7 damage per second)\n+14 Agility\nDurability 90 / 90\nRequires Level 43", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2100", + ["sourceUrl"] = "https://octowow.st/db/?item=2100", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 302, @@ -150,7 +150,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Worn ShortbowRangedBow2 - 5 DamageSpeed 2.30(1.5 damage per second)\nDurability 20 / 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2504", + ["sourceUrl"] = "https://octowow.st/db/?item=2504", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -197,7 +197,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Polished ShortbowRangedBow2 - 5 DamageSpeed 2.00(1.8 damage per second)\nDurability 20 / 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2505", + ["sourceUrl"] = "https://octowow.st/db/?item=2505", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -244,7 +244,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Hornwood Recurve BowRangedBow3 - 7 DamageSpeed 2.10(2.4 damage per second)\nDurability 30 / 30\nRequires Level 3", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2506", + ["sourceUrl"] = "https://octowow.st/db/?item=2506", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -340,7 +340,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Laminated Recurve BowRangedBow10 - 20 DamageSpeed 2.60(5.8 damage per second)\nDurability 40 / 40\nRequires Level 11", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2507", + ["sourceUrl"] = "https://octowow.st/db/?item=2507", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -506,7 +506,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Old BlunderbussRangedGun2 - 5 DamageSpeed 2.30(1.5 damage per second)\nDurability 20 / 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2508", + ["sourceUrl"] = "https://octowow.st/db/?item=2508", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -532,7 +532,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Ornate BlunderbussRangedGun4 - 9 DamageSpeed 2.20(3.0 damage per second)\nDurability 30 / 30\nRequires Level 4", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2509", + ["sourceUrl"] = "https://octowow.st/db/?item=2509", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -670,7 +670,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Solid BlunderbussRangedGun3 - 6 DamageSpeed 2.20(2.0 damage per second)\nDurability 20 / 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2510", + ["sourceUrl"] = "https://octowow.st/db/?item=2510", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -731,7 +731,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Hunter's BoomstickRangedGun7 - 14 DamageSpeed 2.10(5.0 damage per second)\nDurability 40 / 40\nRequires Level 9", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2511", + ["sourceUrl"] = "https://octowow.st/db/?item=2511", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -848,7 +848,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Rough ArrowProjectileArrowAdds 1.5 damage per second", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2512", + ["sourceUrl"] = "https://octowow.st/db/?item=2512", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -1517,7 +1517,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Sharp ArrowProjectileArrowAdds 3.5 damage per second\nRequires Level 10", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2515", + ["sourceUrl"] = "https://octowow.st/db/?item=2515", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -2527,7 +2527,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Light ShotProjectileBulletAdds 1.5 damage per second", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2516", + ["sourceUrl"] = "https://octowow.st/db/?item=2516", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -3049,7 +3049,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Heavy ShotProjectileBulletAdds 3.5 damage per second\nRequires Level 10", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2519", + ["sourceUrl"] = "https://octowow.st/db/?item=2519", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -3926,7 +3926,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, ShortRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2550", + ["sourceUrl"] = "https://octowow.st/db/?item=2550", ["sources"] = { }, }, @@ -3939,7 +3939,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Monster - CrossbowRangedCrossbow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2551", + ["sourceUrl"] = "https://octowow.st/db/?item=2551", ["sources"] = { }, }, @@ -3952,7 +3952,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - GunRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2552", + ["sourceUrl"] = "https://octowow.st/db/?item=2552", ["sources"] = { }, }, @@ -3965,7 +3965,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Cracked ShortbowRangedBow3 - 6 DamageSpeed 2.30(2.0 damage per second)\nDurability 30 / 30\nRequires Level 3", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2773", + ["sourceUrl"] = "https://octowow.st/db/?item=2773", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 293, @@ -4006,7 +4006,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Rust-covered BlunderbussRangedGun3 - 6 DamageSpeed 2.40(1.9 damage per second)\nDurability 30 / 30\nRequires Level 2", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2774", + ["sourceUrl"] = "https://octowow.st/db/?item=2774", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 293, @@ -4043,7 +4043,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Feeble ShortbowRangedBow4 - 8 DamageSpeed 1.80(3.3 damage per second)\nDurability 35 / 35\nRequires Level 8", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2777", + ["sourceUrl"] = "https://octowow.st/db/?item=2777", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 264, @@ -4080,7 +4080,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Cheap BlunderbussRangedGun5 - 9 DamageSpeed 2.20(3.2 damage per second)\nDurability 35 / 35\nRequires Level 8", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2778", + ["sourceUrl"] = "https://octowow.st/db/?item=2778", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 264, @@ -4117,7 +4117,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Light Hunting BowRangedBow5 - 10 DamageSpeed 1.70(4.4 damage per second)\nDurability 45 / 45\nRequires Level 14", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2780", + ["sourceUrl"] = "https://octowow.st/db/?item=2780", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 345, @@ -4178,7 +4178,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Dirty BlunderbussRangedGun6 - 12 DamageSpeed 2.20(4.1 damage per second)\nDurability 45 / 45\nRequires Level 13", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2781", + ["sourceUrl"] = "https://octowow.st/db/?item=2781", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 345, @@ -4235,7 +4235,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Mishandled Recurve BowRangedBow9 - 18 DamageSpeed 2.40(5.6 damage per second)\nDurability 55 / 55\nRequires Level 19", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2782", + ["sourceUrl"] = "https://octowow.st/db/?item=2782", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 336, @@ -4344,7 +4344,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Shoddy BlunderbussRangedGun7 - 14 DamageSpeed 2.10(5.0 damage per second)\nDurability 50 / 50\nRequires Level 17", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2783", + ["sourceUrl"] = "https://octowow.st/db/?item=2783", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 336, @@ -4453,7 +4453,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Stiff Recurve BowRangedBow13 - 25 DamageSpeed 2.80(6.8 damage per second)\nDurability 60 / 60\nRequires Level 23", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2785", + ["sourceUrl"] = "https://octowow.st/db/?item=2785", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 954, @@ -4554,7 +4554,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Oiled BlunderbussRangedGun9 - 19 DamageSpeed 2.00(7.0 damage per second)\nDurability 65 / 65\nRequires Level 24", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2786", + ["sourceUrl"] = "https://octowow.st/db/?item=2786", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 954, @@ -4655,7 +4655,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Hurricane\nBinds when equippedRangedBow34 - 63 DamageSpeed 1.60(30.3 damage per second)\nDurability 90 / 90\nRequires Level 48\nEquip: Chance to strike your target with a Frost Arrow for 31 to 46 Frost damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2824", + ["sourceUrl"] = "https://octowow.st/db/?item=2824", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 428, @@ -4670,7 +4670,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Bow of Searing Arrows\nBinds when equippedRangedBow47 - 88 DamageSpeed 2.70(25.0 damage per second)\nDurability 90 / 90\nRequires Level 37\nEquip: Chance to strike your ranged target with a Searing Arrow for 18 to 27 Fire damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2825", + ["sourceUrl"] = "https://octowow.st/db/?item=2825", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 257, @@ -4684,7 +4684,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Daryl's Hunting Bow\nBinds when picked upRangedBow9 - 18 DamageSpeed 2.30(5.9 damage per second)\nDurability 40 / 40", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2903", + ["sourceUrl"] = "https://octowow.st/db/?item=2903", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -4702,7 +4702,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Daryl's Hunting Rifle\nBinds when picked upRangedGun11 - 21 DamageSpeed 2.50(6.4 damage per second)\nDurability 40 / 40", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=2904", + ["sourceUrl"] = "https://octowow.st/db/?item=2904", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -4721,7 +4721,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Ranger Bow\nBinds when equippedRangedBow23 - 45 DamageSpeed 2.70(12.6 damage per second)\n+1 Agility\nDurability 65 / 65\nRequires Level 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3021", + ["sourceUrl"] = "https://octowow.st/db/?item=3021", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 457, @@ -4818,7 +4818,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Large Bore BlunderbussRangedGun13 - 24 DamageSpeed 2.50(7.4 damage per second)\nDurability 50 / 50\nRequires Level 16", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3023", + ["sourceUrl"] = "https://octowow.st/db/?item=3023", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -4956,7 +4956,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "BKP 2700 \"Enforcer\"RangedGun18 - 34 DamageSpeed 2.70(9.6 damage per second)\nDurability 60 / 60\nRequires Level 21", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3024", + ["sourceUrl"] = "https://octowow.st/db/?item=3024", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -5087,7 +5087,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "BKP 42 \"Ultra\"RangedGun20 - 38 DamageSpeed 2.10(13.8 damage per second)\nDurability 65 / 65\nRequires Level 31", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3025", + ["sourceUrl"] = "https://octowow.st/db/?item=3025", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -5134,7 +5134,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Reinforced BowRangedBow11 - 22 DamageSpeed 2.20(7.5 damage per second)\nDurability 50 / 50\nRequires Level 16", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3026", + ["sourceUrl"] = "https://octowow.st/db/?item=3026", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -5349,7 +5349,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Heavy Recurve BowRangedBow15 - 29 DamageSpeed 2.40(9.2 damage per second)\nDurability 55 / 55\nRequires Level 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3027", + ["sourceUrl"] = "https://octowow.st/db/?item=3027", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -5494,7 +5494,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "LongbowRangedBow21 - 39 DamageSpeed 2.30(13.0 damage per second)\nDurability 65 / 65\nRequires Level 29", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3028", + ["sourceUrl"] = "https://octowow.st/db/?item=3028", ["sources"] = { }, }, @@ -5507,7 +5507,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Razor ArrowProjectileArrowAdds 7.5 damage per second\nRequires Level 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3030", + ["sourceUrl"] = "https://octowow.st/db/?item=3030", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -6328,7 +6328,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Solid ShotProjectileBulletAdds 7.5 damage per second\nRequires Level 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3033", + ["sourceUrl"] = "https://octowow.st/db/?item=3033", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -7079,7 +7079,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Heavy Shortbow\nBinds when equippedRangedBow10 - 20 DamageSpeed 2.50(6.0 damage per second)\nDurability 40 / 40\nRequires Level 10", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3036", + ["sourceUrl"] = "https://octowow.st/db/?item=3036", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 488, @@ -7116,7 +7116,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Whipwood Recurve Bow\nBinds when equippedRangedBow17 - 32 DamageSpeed 1.80(13.6 damage per second)\nDurability 65 / 65\nRequires Level 29", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3037", + ["sourceUrl"] = "https://octowow.st/db/?item=3037", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 622, @@ -7197,7 +7197,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Short Ash Bow\nBinds when equippedRangedBow12 - 23 DamageSpeed 1.90(9.2 damage per second)\nDurability 55 / 55\nRequires Level 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3039", + ["sourceUrl"] = "https://octowow.st/db/?item=3039", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 531, @@ -7294,7 +7294,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Hunter's Muzzle Loader\nBinds when equippedRangedGun9 - 18 DamageSpeed 1.80(7.5 damage per second)\nDurability 45 / 45\nRequires Level 14", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3040", + ["sourceUrl"] = "https://octowow.st/db/?item=3040", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 564, @@ -7343,7 +7343,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "\"Mage-Eye\" Blunderbuss\nBinds when equippedRangedGun24 - 46 DamageSpeed 2.80(12.5 damage per second)\nDurability 65 / 65\nRequires Level 26", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3041", + ["sourceUrl"] = "https://octowow.st/db/?item=3041", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 699, @@ -7424,7 +7424,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "BKP \"Sparrow\" Smallbore\nBinds when equippedRangedGun15 - 30 DamageSpeed 1.70(13.2 damage per second)\nDurability 65 / 65\nRequires Level 28", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3042", + ["sourceUrl"] = "https://octowow.st/db/?item=3042", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 548, @@ -7505,7 +7505,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Naga Heartpiercer\nBinds when picked upRangedBow13 - 25 DamageSpeed 1.80(10.6 damage per second)\nDurability 60 / 60\nRequires Level 21", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3078", + ["sourceUrl"] = "https://octowow.st/db/?item=3078", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -7530,7 +7530,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Skorn's Rifle\nBinds when picked upRangedGun6 - 13 DamageSpeed 1.90(5.0 damage per second)\nDurability 35 / 35", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3079", + ["sourceUrl"] = "https://octowow.st/db/?item=3079", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -7549,7 +7549,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Sniper Rifle\nBinds when equippedRangedGun56 - 65 DamageSpeed 3.00(20.2 damage per second)\nDurability 65 / 65\nRequires Level 39\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3430", + ["sourceUrl"] = "https://octowow.st/db/?item=3430", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 636, @@ -7605,7 +7605,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Feathered ArrowProjectileArrowAdds 9.5 damage per second", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3464", + ["sourceUrl"] = "https://octowow.st/db/?item=3464", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -7623,7 +7623,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Exploding ShotProjectileBulletAdds 9.5 damage per second", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3465", + ["sourceUrl"] = "https://octowow.st/db/?item=3465", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -7641,7 +7641,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Raptor's End\nBinds when picked upRangedBow24 - 46 DamageSpeed 2.90(12.1 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3493", + ["sourceUrl"] = "https://octowow.st/db/?item=3493", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -7659,7 +7659,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "Dwarves aren't known for their subtlety.", ["tooltip"] = "Dwarven Fishing Pole\nBinds when picked upRangedGun9 - 19 DamageSpeed 1.90(7.4 damage per second)\nDurability 45 / 45\n\"Dwarves aren't known for their subtlety.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3567", + ["sourceUrl"] = "https://octowow.st/db/?item=3567", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -7677,7 +7677,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Bow of Plunder\nBinds when picked upRangedBow20 - 39 DamageSpeed 2.60(11.3 damage per second)\nDurability 60 / 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3742", + ["sourceUrl"] = "https://octowow.st/db/?item=3742", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -7696,7 +7696,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Taut Compound BowRangedBow13 - 25 DamageSpeed 2.50(7.6 damage per second)\nDurability 65 / 65\nRequires Level 26", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3778", + ["sourceUrl"] = "https://octowow.st/db/?item=3778", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 954, @@ -7785,7 +7785,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Long-barreled MusketRangedGun14 - 27 DamageSpeed 2.60(7.9 damage per second)\nDurability 65 / 65\nRequires Level 28", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=3780", + ["sourceUrl"] = "https://octowow.st/db/?item=3780", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 706, @@ -7874,7 +7874,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Balanced Long BowRangedBow15 - 29 DamageSpeed 1.80(12.2 damage per second)\nDurability 65 / 65\nRequires Level 40", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4025", + ["sourceUrl"] = "https://octowow.st/db/?item=4025", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 1421, @@ -7939,7 +7939,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Sentinel MusketRangedGun22 - 43 DamageSpeed 2.80(11.6 damage per second)\nDurability 65 / 65\nRequires Level 38", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4026", + ["sourceUrl"] = "https://octowow.st/db/?item=4026", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 1421, @@ -7999,7 +7999,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Flash Rifle\nBinds when picked upRangedGun19 - 36 DamageSpeed 1.80(15.3 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4086", + ["sourceUrl"] = "https://octowow.st/db/?item=4086", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8018,7 +8018,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Trueshot Bow\nBinds when equippedRangedBow24 - 45 DamageSpeed 1.90(18.2 damage per second)\nDurability 65 / 65\nRequires Level 36\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4087", + ["sourceUrl"] = "https://octowow.st/db/?item=4087", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 609, @@ -8075,7 +8075,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Ricochet Blunderbuss\nBinds when equippedRangedGun36 - 67 DamageSpeed 2.30(22.4 damage per second)\nDurability 65 / 65\nRequires Level 43\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4089", + ["sourceUrl"] = "https://octowow.st/db/?item=4089", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 705, @@ -8111,7 +8111,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Master Hunter's Bow\nBinds when picked upRangedBow35 - 65 DamageSpeed 2.40(20.8 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4110", + ["sourceUrl"] = "https://octowow.st/db/?item=4110", ["sources"] = { }, }, @@ -8123,7 +8123,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Master Hunter's Rifle\nBinds when picked upRangedGun37 - 70 DamageSpeed 2.60(20.6 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4111", + ["sourceUrl"] = "https://octowow.st/db/?item=4111", ["sources"] = { }, }, @@ -8135,7 +8135,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Shrapnel Blaster\nBinds when picked upRangedGun23 - 43 DamageSpeed 1.90(17.4 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4127", + ["sourceUrl"] = "https://octowow.st/db/?item=4127", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8154,7 +8154,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Rough Boomstick\nBinds when equippedRangedGun6 - 13 DamageSpeed 2.30(4.1 damage per second)\nDurability 35 / 35\nRequires Level 5", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4362", + ["sourceUrl"] = "https://octowow.st/db/?item=4362", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -8177,7 +8177,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Deadly Blunderbuss\nBinds when equippedRangedGun15 - 28 DamageSpeed 2.60(8.3 damage per second)\nDurability 50 / 50\nRequires Level 16", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4369", + ["sourceUrl"] = "https://octowow.st/db/?item=4369", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -8200,7 +8200,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Lovingly Crafted Boomstick\nBinds when equippedRangedGun12 - 23 DamageSpeed 1.80(9.7 damage per second)\nDurability 55 / 55\nRequires Level 19", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4372", + ["sourceUrl"] = "https://octowow.st/db/?item=4372", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -8223,7 +8223,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Silver-plated Shotgun\nBinds when equippedRangedGun19 - 37 DamageSpeed 2.70(10.4 damage per second)\nDurability 60 / 60\nRequires Level 21", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4379", + ["sourceUrl"] = "https://octowow.st/db/?item=4379", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -8246,7 +8246,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Moonsight Rifle\nBinds when equippedRangedGun14 - 26 DamageSpeed 1.70(11.8 damage per second)\nDurability 65 / 65\nRequires Level 24", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4383", + ["sourceUrl"] = "https://octowow.st/db/?item=4383", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -8269,7 +8269,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Ravenwood Bow\nBinds when equippedRangedBow17 - 32 DamageSpeed 1.90(12.9 damage per second)\nDurability 65 / 65\nRequires Level 27", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4474", + ["sourceUrl"] = "https://octowow.st/db/?item=4474", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -8295,7 +8295,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Light Bow\nBinds when equippedRangedBow9 - 18 DamageSpeed 1.70(7.9 damage per second)\nDurability 50 / 50\nRequires Level 16", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4576", + ["sourceUrl"] = "https://octowow.st/db/?item=4576", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 512, @@ -8404,7 +8404,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Compact Shotgun\nBinds when equippedRangedGun7 - 14 DamageSpeed 2.00(5.3 damage per second)\nDurability 35 / 35\nRequires Level 8", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4577", + ["sourceUrl"] = "https://octowow.st/db/?item=4577", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 544, @@ -8441,7 +8441,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Blackwood Recurve Bow\nBinds when equippedRangedBow7 - 14 DamageSpeed 2.70(3.9 damage per second)\nDurability 30 / 30\nRequires Level 4", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4763", + ["sourceUrl"] = "https://octowow.st/db/?item=4763", ["sources"] = { }, }, @@ -8453,7 +8453,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Hickory Shortbow\nBinds when picked upRangedBow5 - 10 DamageSpeed 2.00(3.8 damage per second)\nDurability 35 / 35", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4931", + ["sourceUrl"] = "https://octowow.st/db/?item=4931", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8471,7 +8471,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Flash PelletProjectileBulletAdds 2.0 damage per second", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=4960", + ["sourceUrl"] = "https://octowow.st/db/?item=4960", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8490,7 +8490,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, BlackRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5258", + ["sourceUrl"] = "https://octowow.st/db/?item=5258", ["sources"] = { }, }, @@ -8503,7 +8503,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, RedRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5259", + ["sourceUrl"] = "https://octowow.st/db/?item=5259", ["sources"] = { }, }, @@ -8516,7 +8516,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, BrownRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5260", + ["sourceUrl"] = "https://octowow.st/db/?item=5260", ["sources"] = { }, }, @@ -8529,7 +8529,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, GrayRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5261", + ["sourceUrl"] = "https://octowow.st/db/?item=5261", ["sources"] = { }, }, @@ -8542,7 +8542,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, Dark BrownRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5262", + ["sourceUrl"] = "https://octowow.st/db/?item=5262", ["sources"] = { }, }, @@ -8554,7 +8554,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Privateer Musket\nBinds when picked upRangedGun12 - 24 DamageSpeed 2.30(7.8 damage per second)\nDurability 50 / 50", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5309", + ["sourceUrl"] = "https://octowow.st/db/?item=5309", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8572,7 +8572,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Orcish Battle Bow\nBinds when picked upRangedBow7 - 14 DamageSpeed 1.90(5.5 damage per second)\nDurability 40 / 40", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5346", + ["sourceUrl"] = "https://octowow.st/db/?item=5346", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8591,7 +8591,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Smooth PebbleProjectileBulletAdds 4.0 damage per second\nRequires Level 13", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5568", + ["sourceUrl"] = "https://octowow.st/db/?item=5568", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -8751,7 +8751,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Ashwood Bow\nBinds when picked upRangedBow5 - 11 DamageSpeed 2.10(3.8 damage per second)\nDurability 35 / 35", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5596", + ["sourceUrl"] = "https://octowow.st/db/?item=5596", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8770,7 +8770,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Centaur Longbow\nBinds when equippedRangedBow9 - 18 DamageSpeed 2.20(6.1 damage per second)\nDurability 40 / 40\nRequires Level 11", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5748", + ["sourceUrl"] = "https://octowow.st/db/?item=5748", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -8802,7 +8802,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Lunaris Bow\nBinds when picked upRangedBow23 - 43 DamageSpeed 2.70(12.2 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=5817", + ["sourceUrl"] = "https://octowow.st/db/?item=5817", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8821,7 +8821,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Steelarrow Crossbow\nBinds when equippedRangedCrossbow29 - 45 DamageSpeed 3.40(10.9 damage per second)\nDurability 60 / 60\nRequires Level 22", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=6315", + ["sourceUrl"] = "https://octowow.st/db/?item=6315", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -8847,7 +8847,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Venomstrike\nBinds when picked upRangedBow16 - 30 DamageSpeed 2.40(9.6 damage per second)\nDurability 65 / 65\nRequires Level 19\nEquip: Chance to strike your ranged target with a Venom Shot for 31 to 46 Nature damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=6469", + ["sourceUrl"] = "https://octowow.st/db/?item=6469", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -8874,7 +8874,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Nightstalker Bow\nBinds when picked upRangedBow19 - 36 DamageSpeed 1.70(16.2 damage per second)\n+3 Agility\nDurability 75 / 75\nRequires Level 27", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=6696", + ["sourceUrl"] = "https://octowow.st/db/?item=6696", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -8899,7 +8899,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Cliffrunner's Aim\nBinds when picked upRangedBow19 - 36 DamageSpeed 2.30(12.0 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=6739", + ["sourceUrl"] = "https://octowow.st/db/?item=6739", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8917,7 +8917,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Blasting Hackbut\nBinds when picked upRangedGun30 - 56 DamageSpeed 2.80(15.4 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=6798", + ["sourceUrl"] = "https://octowow.st/db/?item=6798", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -8936,7 +8936,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Chesterfall Musket\nBinds when equippedRangedGun26 - 50 DamageSpeed 2.30(16.5 damage per second)\nDurability 75 / 75\nRequires Level 28", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=7729", + ["sourceUrl"] = "https://octowow.st/db/?item=7729", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -9192,7 +9192,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Crafted Light ShotProjectileBulletAdds 2.0 damage per second\nRequires Level 5", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8067", + ["sourceUrl"] = "https://octowow.st/db/?item=8067", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -9215,7 +9215,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Crafted Heavy ShotProjectileBulletAdds 4.5 damage per second\nRequires Level 15", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8068", + ["sourceUrl"] = "https://octowow.st/db/?item=8068", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -9238,7 +9238,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Crafted Solid ShotProjectileBulletAdds 8.5 damage per second\nRequires Level 30", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8069", + ["sourceUrl"] = "https://octowow.st/db/?item=8069", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -9261,7 +9261,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Cadet's BowRangedBow3 - 6 DamageSpeed 2.00(2.3 damage per second)\nDurability 25 / 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8179", + ["sourceUrl"] = "https://octowow.st/db/?item=8179", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 124, @@ -9298,7 +9298,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Hunting Bow\nBinds when equippedRangedBow8 - 16 DamageSpeed 2.60(4.6 damage per second)\nDurability 35 / 35\nRequires Level 6", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8180", + ["sourceUrl"] = "https://octowow.st/db/?item=8180", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 485, @@ -9335,7 +9335,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Hunting RifleRangedGun5 - 11 DamageSpeed 2.70(3.0 damage per second)\nDurability 30 / 30\nRequires Level 4", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8181", + ["sourceUrl"] = "https://octowow.st/db/?item=8181", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 298, @@ -9372,7 +9372,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Pellet RifleRangedGun4 - 9 DamageSpeed 2.60(2.5 damage per second)\nDurability 30 / 30\nRequires Level 2", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8182", + ["sourceUrl"] = "https://octowow.st/db/?item=8182", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 186, @@ -9409,7 +9409,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Precision Bow\nBinds when equippedRangedBow20 - 37 DamageSpeed 2.60(11.0 damage per second)\nDurability 60 / 60\nRequires Level 22", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8183", + ["sourceUrl"] = "https://octowow.st/db/?item=8183", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 556, @@ -9502,7 +9502,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Explosive Shotgun\nBinds when equippedRangedGun22 - 42 DamageSpeed 2.10(15.2 damage per second)\nDurability 65 / 65\nRequires Level 32", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=8188", + ["sourceUrl"] = "https://octowow.st/db/?item=8188", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 715, @@ -9555,7 +9555,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Precision ArrowProjectileArrowAdds 11.5 damage per second\nRequires Level 35", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=9399", + ["sourceUrl"] = "https://octowow.st/db/?item=9399", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -9581,7 +9581,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Baelog's Shortbow\nBinds when picked upRangedBow29 - 55 DamageSpeed 2.30(18.3 damage per second)\n+3 Agility\nDurability 65 / 65\nRequires Level 36", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=9400", + ["sourceUrl"] = "https://octowow.st/db/?item=9400", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -9607,7 +9607,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Galgann's Fireblaster\nBinds when picked upRangedGun44 - 84 DamageSpeed 2.60(24.6 damage per second)\nDurability 75 / 75\nRequires Level 42\nEquip: Chance to strike your ranged target with a Fire Blast for 12 to 19 Fire damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=9412", + ["sourceUrl"] = "https://octowow.st/db/?item=9412", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -9633,7 +9633,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Shadowforge Bushmaster\nBinds when equippedRangedGun46 - 86 DamageSpeed 2.90(22.8 damage per second)\n+7 Shadow Resistance\nDurability 75 / 75\nRequires Level 38", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=9422", + ["sourceUrl"] = "https://octowow.st/db/?item=9422", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -9728,7 +9728,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monolithic Bow\nBinds when equippedRangedBow41 - 77 DamageSpeed 2.70(21.9 damage per second)\n+6 Strength\n+3 Agility\nDurability 75 / 75\nRequires Level 36", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=9426", + ["sourceUrl"] = "https://octowow.st/db/?item=9426", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -9846,7 +9846,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Glass Shooter\nBinds when picked upRangedGun36 - 68 DamageSpeed 2.90(17.9 damage per second)\nDurability 75 / 75\nRequires Level 30", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=9456", + ["sourceUrl"] = "https://octowow.st/db/?item=9456", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -9872,7 +9872,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Hi-tech Supergun\nBinds when equippedRangedGun23 - 43 DamageSpeed 2.30(14.3 damage per second)\nDurability 75 / 75\nRequires Level 24", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=9487", + ["sourceUrl"] = "https://octowow.st/db/?item=9487", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -10009,7 +10009,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Mithril Blunderbuss\nBinds when equippedRangedGun36 - 68 DamageSpeed 2.90(17.9 damage per second)\n+5 Agility\nDurability 65 / 65\nRequires Level 36", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=10508", + ["sourceUrl"] = "https://octowow.st/db/?item=10508", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -10032,7 +10032,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Mithril Heavy-bore Rifle\nBinds when equippedRangedGun41 - 76 DamageSpeed 2.90(20.2 damage per second)\nDurability 65 / 65\nRequires Level 39\nEquip: +14 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=10510", + ["sourceUrl"] = "https://octowow.st/db/?item=10510", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -10055,7 +10055,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Hi-Impact Mithril SlugsProjectileBulletAdds 12.5 damage per second\nRequires Level 37", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=10512", + ["sourceUrl"] = "https://octowow.st/db/?item=10512", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -10078,7 +10078,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Mithril Gyro-ShotProjectileBulletAdds 15.0 damage per second\nRequires Level 44", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=10513", + ["sourceUrl"] = "https://octowow.st/db/?item=10513", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -10101,7 +10101,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Quillshooter\nBinds when equippedRangedBow31 - 58 DamageSpeed 2.80(15.9 damage per second)\nDurability 75 / 75\nRequires Level 33\nEquip: Chance to strike your ranged target with a Quill Shot for 66 to 99 Nature damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=10567", + ["sourceUrl"] = "https://octowow.st/db/?item=10567", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -10231,7 +10231,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Explosive ArrowProjectileArrowAdds 14.5 damage per second\nRequires Level 37", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=10579", + ["sourceUrl"] = "https://octowow.st/db/?item=10579", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -10254,7 +10254,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Stinging Bow\nBinds when equippedRangedBow36 - 67 DamageSpeed 2.10(24.5 damage per second)\n+3 Strength\nDurability 75 / 75\nRequires Level 42\nEquip: +14 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=10624", + ["sourceUrl"] = "https://octowow.st/db/?item=10624", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -10434,7 +10434,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Big Sniper GunRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11021", + ["sourceUrl"] = "https://octowow.st/db/?item=11021", ["sources"] = { }, }, @@ -10447,7 +10447,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Accurate SlugsProjectileBulletAdds 13.0 damage per second\nRequires Level 40", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11284", + ["sourceUrl"] = "https://octowow.st/db/?item=11284", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -11092,7 +11092,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Jagged ArrowProjectileArrowAdds 13.0 damage per second\nRequires Level 40", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11285", + ["sourceUrl"] = "https://octowow.st/db/?item=11285", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -11786,7 +11786,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Fine Shortbow\nBinds when equippedRangedBow7 - 14 DamageSpeed 1.70(6.2 damage per second)\nDurability 40 / 40\nRequires Level 11", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11303", + ["sourceUrl"] = "https://octowow.st/db/?item=11303", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -11861,7 +11861,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Fine Longbow\nBinds when equippedRangedBow14 - 26 DamageSpeed 2.70(7.4 damage per second)\nDurability 45 / 45\nRequires Level 14", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11304", + ["sourceUrl"] = "https://octowow.st/db/?item=11304", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -11943,7 +11943,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Dense Shortbow\nBinds when equippedRangedBow19 - 35 DamageSpeed 1.90(14.2 damage per second)\nDurability 65 / 65\nRequires Level 30", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11305", + ["sourceUrl"] = "https://octowow.st/db/?item=11305", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -12011,7 +12011,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Sturdy Recurve\nBinds when equippedRangedBow20 - 37 DamageSpeed 2.20(13.0 damage per second)\nDurability 65 / 65\nRequires Level 27", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11306", + ["sourceUrl"] = "https://octowow.st/db/?item=11306", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -12072,7 +12072,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Massive Longbow\nBinds when equippedRangedBow43 - 80 DamageSpeed 2.80(22.0 damage per second)\nDurability 65 / 65\nRequires Level 42", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11307", + ["sourceUrl"] = "https://octowow.st/db/?item=11307", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -12119,7 +12119,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Sylvan Shortbow\nBinds when equippedRangedBow32 - 59 DamageSpeed 2.00(22.8 damage per second)\nDurability 65 / 65\nRequires Level 44", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11308", + ["sourceUrl"] = "https://octowow.st/db/?item=11308", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -12166,7 +12166,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Houndmaster's Bow\nBinds when picked upRangedBow34 - 64 DamageSpeed 1.80(27.2 damage per second)\n+3 Agility\nDurability 75 / 75\nRequires Level 48\nEquip: Attack Power increased by 24 when fighting Beasts.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11628", + ["sourceUrl"] = "https://octowow.st/db/?item=11628", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -12192,7 +12192,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Houndmaster's Rifle\nBinds when picked upRangedGun44 - 82 DamageSpeed 2.30(27.4 damage per second)\n+3 Agility\nDurability 75 / 75\nRequires Level 48\nEquip: Attack Power increased by 24 when fighting Beasts.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11629", + ["sourceUrl"] = "https://octowow.st/db/?item=11629", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -12218,7 +12218,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Rockshard PelletsProjectileBulletAdds 18.0 damage per second\nRequires Level 47", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=11630", + ["sourceUrl"] = "https://octowow.st/db/?item=11630", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -12243,7 +12243,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Anvilmar Musket\nBinds when picked upRangedGun4 - 8 DamageSpeed 2.70(2.2 damage per second)\nDurability 25 / 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=12446", + ["sourceUrl"] = "https://octowow.st/db/?item=12446", ["sources"] = { }, }, @@ -12255,7 +12255,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Thistlewood Bow\nBinds when picked upRangedBow3 - 7 DamageSpeed 2.30(2.2 damage per second)\nDurability 25 / 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=12447", + ["sourceUrl"] = "https://octowow.st/db/?item=12447", ["sources"] = { }, }, @@ -12267,7 +12267,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Light Hunting Rifle\nBinds when picked upRangedGun2 - 6 DamageSpeed 1.90(2.1 damage per second)\nDurability 25 / 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=12448", + ["sourceUrl"] = "https://octowow.st/db/?item=12448", ["sources"] = { }, }, @@ -12279,7 +12279,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Primitive Bow\nBinds when picked upRangedBow4 - 8 DamageSpeed 2.70(2.2 damage per second)\nDurability 25 / 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=12449", + ["sourceUrl"] = "https://octowow.st/db/?item=12449", ["sources"] = { }, }, @@ -12292,7 +12292,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Gun, Silver MusketRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=12523", + ["sourceUrl"] = "https://octowow.st/db/?item=12523", ["sources"] = { }, }, @@ -12305,7 +12305,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Blackcrow\nBinds when picked upRangedCrossbow77 - 117 DamageSpeed 3.20(30.3 damage per second)\n+3 Agility\nDurability 75 / 75\nRequires Level 54\nEquip: Improves your chance to hit by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=12651", + ["sourceUrl"] = "https://octowow.st/db/?item=12651", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -12331,7 +12331,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Riphook\nBinds when picked upRangedBow46 - 87 DamageSpeed 2.20(30.2 damage per second)\nDurability 75 / 75\nRequires Level 54\nEquip: +22 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=12653", + ["sourceUrl"] = "https://octowow.st/db/?item=12653", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -12357,7 +12357,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "DoomshotProjectileArrowAdds 20.0 damage per second\nRequires Level 54", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=12654", + ["sourceUrl"] = "https://octowow.st/db/?item=12654", ["sources"] = { }, }, @@ -12370,7 +12370,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Harpyclaw Short Bow\nBinds when equippedRangedBow20 - 38 DamageSpeed 1.80(16.1 damage per second)\nDurability 75 / 75\nRequires Level 27", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13019", + ["sourceUrl"] = "https://octowow.st/db/?item=13019", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 442, @@ -12451,7 +12451,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Skystriker Bow\nBinds when equippedRangedBow30 - 57 DamageSpeed 2.10(20.7 damage per second)\nDurability 75 / 75\nRequires Level 34", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13020", + ["sourceUrl"] = "https://octowow.st/db/?item=13020", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 379, @@ -12504,7 +12504,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Needle Threader\nBinds when equippedRangedBow34 - 64 DamageSpeed 2.00(24.5 damage per second)\n+4 Spirit\n+6 Stamina\nDurability 75 / 75\nRequires Level 42", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13021", + ["sourceUrl"] = "https://octowow.st/db/?item=13021", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 407, @@ -12537,7 +12537,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Gryphonwing Long Bow\nBinds when equippedRangedBow53 - 100 DamageSpeed 2.70(28.3 damage per second)\n+8 Agility\n+4 Stamina\nDurability 75 / 75\nRequires Level 50", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13022", + ["sourceUrl"] = "https://octowow.st/db/?item=13022", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 651, @@ -12590,7 +12590,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Eaglehorn Long Bow\nBinds when equippedRangedBow40 - 76 DamageSpeed 1.80(32.2 damage per second)\n+10 Agility\n+4 Stamina\nDurability 75 / 75\nRequires Level 58", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13023", + ["sourceUrl"] = "https://octowow.st/db/?item=13023", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 316, @@ -12605,7 +12605,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Crystalpine Stinger\nBinds when equippedRangedCrossbow35 - 54 DamageSpeed 2.80(15.9 damage per second)\nDurability 75 / 75\nRequires Level 27", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13037", + ["sourceUrl"] = "https://octowow.st/db/?item=13037", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 442, @@ -12690,7 +12690,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Swiftwind\nBinds when equippedRangedCrossbow34 - 51 DamageSpeed 2.00(21.3 damage per second)\n+7 Agility\nDurability 75 / 75\nRequires Level 35", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13038", + ["sourceUrl"] = "https://octowow.st/db/?item=13038", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 350, @@ -12747,7 +12747,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Skull Splitting Crossbow\nBinds when equippedRangedCrossbow52 - 79 DamageSpeed 2.60(25.2 damage per second)\n+3 Strength\nDurability 75 / 75\nRequires Level 43\nEquip: +14 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13039", + ["sourceUrl"] = "https://octowow.st/db/?item=13039", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 397, @@ -12780,7 +12780,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Heartseeking Crossbow\nBinds when equippedRangedCrossbow71 - 108 DamageSpeed 3.10(28.9 damage per second)\n+9 Agility\n+4 Stamina\nDurability 75 / 75\nRequires Level 51\nEquip: Chance to strike your ranged target with a Shadowbolt for 13 to 20 Shadow damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13040", + ["sourceUrl"] = "https://octowow.st/db/?item=13040", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 706, @@ -12813,7 +12813,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Lil Timmy's Peashooter\nBinds when equippedRangedGun20 - 37 DamageSpeed 2.60(11.0 damage per second)\nDurability 60 / 60\nRequires Level 16", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13136", + ["sourceUrl"] = "https://octowow.st/db/?item=13136", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 243, @@ -12910,7 +12910,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Ironweaver\nBinds when equippedRangedGun31 - 59 DamageSpeed 2.60(17.3 damage per second)\nDurability 75 / 75\nRequires Level 29", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13137", + ["sourceUrl"] = "https://octowow.st/db/?item=13137", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 464, @@ -12991,7 +12991,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "The Silencer\nBinds when equippedRangedGun43 - 82 DamageSpeed 2.80(22.3 damage per second)\nDurability 75 / 75\nRequires Level 37\nEquip: +14 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13138", + ["sourceUrl"] = "https://octowow.st/db/?item=13138", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 353, @@ -13044,7 +13044,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Guttbuster\nBinds when equippedRangedGun49 - 92 DamageSpeed 2.70(26.1 damage per second)\n+8 Agility\n+3 Strength\nDurability 75 / 75\nRequires Level 45", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13139", + ["sourceUrl"] = "https://octowow.st/db/?item=13139", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 497, @@ -13081,7 +13081,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Shell Launcher Shotgun\nBinds when equippedRangedGun48 - 89 DamageSpeed 2.30(29.8 damage per second)\nDurability 75 / 75\nRequires Level 53\nEquip: Chance to strike your ranged target with a Flaming Shell for 18 to 27 Fire damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13146", + ["sourceUrl"] = "https://octowow.st/db/?item=13146", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 684, @@ -13114,7 +13114,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, WhiteRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13147", + ["sourceUrl"] = "https://octowow.st/db/?item=13147", ["sources"] = { }, }, @@ -13127,7 +13127,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Voone's Twitchbow\nBinds when picked upRangedBow31 - 58 DamageSpeed 1.60(27.8 damage per second)\nDurability 65 / 65\nRequires Level 55", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13175", + ["sourceUrl"] = "https://octowow.st/db/?item=13175", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -13153,7 +13153,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Burstshot Harquebus\nBinds when picked upRangedGun52 - 98 DamageSpeed 2.60(28.8 damage per second)\n+8 Stamina\nDurability 75 / 75\nRequires Level 51\nEquip: +10 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13248", + ["sourceUrl"] = "https://octowow.st/db/?item=13248", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -13180,7 +13180,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Miniature Cannon BallsProjectileBulletAdds 20.5 damage per second\nRequires Level 56", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13377", + ["sourceUrl"] = "https://octowow.st/db/?item=13377", ["sources"] = { }, }, @@ -13193,7 +13193,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Willey's Portable Howitzer\nBinds when picked upRangedGun63 - 118 DamageSpeed 2.90(31.2 damage per second)\n+9 Stamina\nDurability 75 / 75\nRequires Level 56\nEquip: +8 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13380", + ["sourceUrl"] = "https://octowow.st/db/?item=13380", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -13225,7 +13225,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Farmer Dalson's Shotgun\nBinds when picked upRangedGun34 - 64 DamageSpeed 1.90(25.8 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13474", + ["sourceUrl"] = "https://octowow.st/db/?item=13474", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -13244,7 +13244,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Recurve Long BowRangedBow26 - 50 DamageSpeed 2.50(15.2 damage per second)\nDurability 65 / 65\nRequires Level 45", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13824", + ["sourceUrl"] = "https://octowow.st/db/?item=13824", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 1422, @@ -13285,7 +13285,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Primed MusketRangedGun20 - 37 DamageSpeed 1.80(15.8 damage per second)\nDurability 65 / 65\nRequires Level 52", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13825", + ["sourceUrl"] = "https://octowow.st/db/?item=13825", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 1053, @@ -13322,7 +13322,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Gun, Tauren Blade SilverRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13923", + ["sourceUrl"] = "https://octowow.st/db/?item=13923", ["sources"] = { }, }, @@ -13335,7 +13335,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Gun, Tauren Scope Blade Feathered Silver DeluxeRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=13924", + ["sourceUrl"] = "https://octowow.st/db/?item=13924", ["sources"] = { }, }, @@ -13348,7 +13348,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, C01/B02 WhiteRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=14105", + ["sourceUrl"] = "https://octowow.st/db/?item=14105", ["sources"] = { }, }, @@ -13361,7 +13361,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, C02/B02 BlackRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=14118", + ["sourceUrl"] = "https://octowow.st/db/?item=14118", ["sources"] = { }, }, @@ -13373,7 +13373,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Durability BowRangedBow1 - 3 DamageSpeed 1.80(1.1 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=14394", + ["sourceUrl"] = "https://octowow.st/db/?item=14394", ["sources"] = { }, }, @@ -13386,7 +13386,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Gun, Tauren Feathers SilverRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=14642", + ["sourceUrl"] = "https://octowow.st/db/?item=14642", ["sources"] = { }, }, @@ -13398,7 +13398,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Owlsight Rifle\nBinds when picked upRangedGun14 - 27 DamageSpeed 2.60(7.9 damage per second)\nDurability 50 / 50", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15205", + ["sourceUrl"] = "https://octowow.st/db/?item=15205", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -13417,7 +13417,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Long Battle Bow\nBinds when equippedRangedBow18 - 34 DamageSpeed 2.20(11.8 damage per second)\nDurability 65 / 65\nRequires Level 24", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15284", + ["sourceUrl"] = "https://octowow.st/db/?item=15284", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 597, @@ -13510,7 +13510,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Archer's Longbow\nBinds when equippedRangedBow23 - 44 DamageSpeed 2.60(12.9 damage per second)\nDurability 65 / 65\nRequires Level 27", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15285", + ["sourceUrl"] = "https://octowow.st/db/?item=15285", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 549, @@ -13591,7 +13591,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Long Redwood Bow\nBinds when equippedRangedBow28 - 52 DamageSpeed 2.80(14.3 damage per second)\nDurability 65 / 65\nRequires Level 30", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15286", + ["sourceUrl"] = "https://octowow.st/db/?item=15286", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 621, @@ -13672,7 +13672,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Crusader Bow\nBinds when equippedRangedBow32 - 60 DamageSpeed 2.20(20.9 damage per second)\nDurability 65 / 65\nRequires Level 40\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15287", + ["sourceUrl"] = "https://octowow.st/db/?item=15287", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 636, @@ -13741,7 +13741,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Blasthorn Bow\nBinds when equippedRangedBow51 - 96 DamageSpeed 2.60(28.3 damage per second)\nDurability 65 / 65\nRequires Level 56\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15288", + ["sourceUrl"] = "https://octowow.st/db/?item=15288", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 533, @@ -13756,7 +13756,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Archstrike Bow\nBinds when equippedRangedBow48 - 91 DamageSpeed 2.30(30.2 damage per second)\nDurability 65 / 65\nRequires Level 60\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15289", + ["sourceUrl"] = "https://octowow.st/db/?item=15289", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 232, @@ -13771,7 +13771,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Harpy Needler\nBinds when equippedRangedBow44 - 84 DamageSpeed 2.70(23.7 damage per second)\nDurability 65 / 65\nRequires Level 46\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15291", + ["sourceUrl"] = "https://octowow.st/db/?item=15291", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 695, @@ -13828,7 +13828,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Siege Bow\nBinds when equippedRangedBow48 - 90 DamageSpeed 2.80(24.6 damage per second)\nDurability 65 / 65\nRequires Level 48\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15294", + ["sourceUrl"] = "https://octowow.st/db/?item=15294", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 748, @@ -13881,7 +13881,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Quillfire Bow\nBinds when equippedRangedBow41 - 77 DamageSpeed 2.30(25.7 damage per second)\nDurability 65 / 65\nRequires Level 50\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15295", + ["sourceUrl"] = "https://octowow.st/db/?item=15295", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 930, @@ -13934,7 +13934,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Hawkeye Bow\nBinds when equippedRangedBow35 - 65 DamageSpeed 1.70(29.4 damage per second)\nDurability 65 / 65\nRequires Level 58\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15296", + ["sourceUrl"] = "https://octowow.st/db/?item=15296", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 601, @@ -13949,7 +13949,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Smoothbore Gun\nBinds when equippedRangedGun29 - 54 DamageSpeed 2.50(16.6 damage per second)\nDurability 65 / 65\nRequires Level 34", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15322", + ["sourceUrl"] = "https://octowow.st/db/?item=15322", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 573, @@ -14002,7 +14002,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Percussion Shotgun\nBinds when equippedRangedGun37 - 70 DamageSpeed 2.30(23.3 damage per second)\nDurability 65 / 65\nRequires Level 45\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15323", + ["sourceUrl"] = "https://octowow.st/db/?item=15323", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 695, @@ -14043,7 +14043,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Burnside Rifle\nBinds when equippedRangedGun45 - 85 DamageSpeed 2.50(26.0 damage per second)\nDurability 65 / 65\nRequires Level 51\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15324", + ["sourceUrl"] = "https://octowow.st/db/?item=15324", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 909, @@ -14100,7 +14100,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Sharpshooter Harquebus\nBinds when equippedRangedGun43 - 80 DamageSpeed 2.20(28.0 damage per second)\nDurability 65 / 65\nRequires Level 55\nRandom Bonuses", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15325", + ["sourceUrl"] = "https://octowow.st/db/?item=15325", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 547, @@ -14153,7 +14153,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Gun, ShotgunRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15460", + ["sourceUrl"] = "https://octowow.st/db/?item=15460", ["sources"] = { }, }, @@ -14165,7 +14165,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Sidegunner Shottie\nBinds when picked upRangedGun32 - 61 DamageSpeed 2.90(16.0 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15691", + ["sourceUrl"] = "https://octowow.st/db/?item=15691", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14184,7 +14184,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Light CrossbowRangedCrossbow6 - 7 DamageSpeed 2.50(2.6 damage per second)\nDurability 30 / 30\nRequires Level 3", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15807", + ["sourceUrl"] = "https://octowow.st/db/?item=15807", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -14231,7 +14231,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Fine Light CrossbowRangedCrossbow20 - 20 DamageSpeed 2.70(7.4 damage per second)\nDurability 50 / 50\nRequires Level 16", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15808", + ["sourceUrl"] = "https://octowow.st/db/?item=15808", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -14271,7 +14271,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Heavy CrossbowRangedCrossbow36 - 37 DamageSpeed 2.80(13.0 damage per second)\nDurability 65 / 65\nRequires Level 29", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15809", + ["sourceUrl"] = "https://octowow.st/db/?item=15809", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -14311,7 +14311,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Thorium Rifle\nBinds when equippedRangedGun42 - 79 DamageSpeed 2.50(24.2 damage per second)\nDurability 65 / 65\nRequires Level 47\nEquip: +17 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15995", + ["sourceUrl"] = "https://octowow.st/db/?item=15995", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -14334,7 +14334,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Thorium ShellsProjectileBulletAdds 17.5 damage per second\nRequires Level 52", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=15997", + ["sourceUrl"] = "https://octowow.st/db/?item=15997", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -14357,7 +14357,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Dark Iron Rifle\nBinds when equippedRangedGun70 - 106 DamageSpeed 3.10(28.4 damage per second)\nDurability 75 / 75\nRequires Level 50\nEquip: Chance to strike your ranged target with Shadow Shot for 18 to 27 Shadow damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=16004", + ["sourceUrl"] = "https://octowow.st/db/?item=16004", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -14380,7 +14380,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Flawless Arcanite Rifle\nBinds when equippedRangedGun68 - 126 DamageSpeed 3.10(31.3 damage per second)\nDurability 75 / 75\nRequires Level 56\nEquip: Increased Guns +4.\nEquip: +10 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=16007", + ["sourceUrl"] = "https://octowow.st/db/?item=16007", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -14402,7 +14402,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Thornflinger\nBinds when picked upRangedBow52 - 97 DamageSpeed 2.80(26.6 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=16622", + ["sourceUrl"] = "https://octowow.st/db/?item=16622", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14420,7 +14420,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Smokey's Explosive Launcher\nBinds when picked upRangedGun52 - 98 DamageSpeed 2.70(27.8 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=16992", + ["sourceUrl"] = "https://octowow.st/db/?item=16992", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14438,7 +14438,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Gorewood Bow\nBinds when picked upRangedBow55 - 104 DamageSpeed 2.50(31.8 damage per second)\n+9 Stamina\n+3 Agility\n+2 Strength\nDurability 75 / 75", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=16996", + ["sourceUrl"] = "https://octowow.st/db/?item=16996", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14456,7 +14456,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Nail Spitter\nBinds when picked upRangedGun19 - 37 DamageSpeed 1.90(14.7 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=17042", + ["sourceUrl"] = "https://octowow.st/db/?item=17042", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14475,7 +14475,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Striker's Mark\nBinds when picked upRangedBow69 - 129 DamageSpeed 2.50(39.6 damage per second)\nDurability 90 / 90\nRequires Level 60\nEquip: +22 Attack Power.\nEquip: Improves your chance to hit by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=17069", + ["sourceUrl"] = "https://octowow.st/db/?item=17069", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14501,7 +14501,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Blastershot Launcher\nBinds when picked upRangedGun73 - 136 DamageSpeed 2.60(40.2 damage per second)\n+6 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: Improves your chance to get a critical strike by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=17072", + ["sourceUrl"] = "https://octowow.st/db/?item=17072", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14526,7 +14526,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Master Hunter's Bow\nBinds when picked upRangedBow32 - 61 DamageSpeed 2.40(19.4 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=17686", + ["sourceUrl"] = "https://octowow.st/db/?item=17686", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14544,7 +14544,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Master Hunter's Rifle\nBinds when picked upRangedGun35 - 66 DamageSpeed 2.60(19.4 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=17687", + ["sourceUrl"] = "https://octowow.st/db/?item=17687", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14563,7 +14563,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Megashot Rifle\nBinds when picked upRangedGun32 - 61 DamageSpeed 1.70(27.4 damage per second)\n+5 Arcane Resistance\nDurability 75 / 75\nRequires Level 48\nEquip: +19 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=17717", + ["sourceUrl"] = "https://octowow.st/db/?item=17717", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14588,7 +14588,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Verdant Keeper's Aim\nBinds when picked upRangedBow53 - 100 DamageSpeed 2.80(27.3 damage per second)\nDurability 75 / 75\nEquip: Chance to strike your ranged target with Keeper's Sting for 15 to 22 Nature damage.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=17753", + ["sourceUrl"] = "https://octowow.st/db/?item=17753", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14607,7 +14607,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Thorium Headed ArrowProjectileArrowAdds 17.5 damage per second\nRequires Level 52", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18042", + ["sourceUrl"] = "https://octowow.st/db/?item=18042", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -14626,7 +14626,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Core Marksman Rifle\nBinds when equippedRangedGun91 - 149 DamageSpeed 3.20(37.5 damage per second)\nDurability 90 / 90\nRequires Level 60\nEquip: +24 ranged Attack Power.\nEquip: Improves your chance to hit by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18282", + ["sourceUrl"] = "https://octowow.st/db/?item=18282", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -14649,7 +14649,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Satyr's Bow\nBinds when picked upRangedBow50 - 93 DamageSpeed 2.40(29.8 damage per second)\n+3 Agility\nDurability 75 / 75\nRequires Level 53\nEquip: Improves your chance to hit by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18323", + ["sourceUrl"] = "https://octowow.st/db/?item=18323", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14675,7 +14675,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Stoneshatter\nBinds when picked upRangedCrossbow73 - 111 DamageSpeed 2.90(31.7 damage per second)\nDurability 75 / 75\nRequires Level 57\nEquip: Increased Crossbows +4.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18388", + ["sourceUrl"] = "https://octowow.st/db/?item=18388", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14708,7 +14708,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Unsophisticated Hand Cannon\nBinds when picked upRangedGun48 - 91 DamageSpeed 2.50(27.8 damage per second)\n+8 Strength\nDurability 65 / 65\nRequires Level 55", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18460", + ["sourceUrl"] = "https://octowow.st/db/?item=18460", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14748,7 +14748,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Ogre Toothpick Shooter\nBinds when picked upRangedBow61 - 62 DamageSpeed 2.20(28.0 damage per second)\n+6 Agility\n+5 Stamina\nDurability 65 / 65\nRequires Level 55", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18482", + ["sourceUrl"] = "https://octowow.st/db/?item=18482", ["sources"] = { }, }, @@ -14761,7 +14761,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Ancient Bone Bow\nBinds when picked upRangedBow61 - 114 DamageSpeed 2.80(31.3 damage per second)\n+11 Agility\nDurability 75 / 75\nRequires Level 56", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18680", + ["sourceUrl"] = "https://octowow.st/db/?item=18680", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14823,7 +14823,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Rhok'delar, Longbow of the Ancient Keepers\nBinds when picked up\nUniqueRangedBow89 - 166 DamageSpeed 2.90(44.0 damage per second)\nDurability 90 / 90\nClasses: Hunter\nRequires Level 60\nEquip: Improves your chance to get a critical strike by 1%.\nEquip: +17 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18713", + ["sourceUrl"] = "https://octowow.st/db/?item=18713", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -14850,7 +14850,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Screeching Bow\nBinds when picked upRangedBow70 - 71 DamageSpeed 2.30(30.7 damage per second)\n+3 Stamina\n+10 Shadow Resistance\nDurability 75 / 75\nRequires Level 55", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18729", + ["sourceUrl"] = "https://octowow.st/db/?item=18729", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14876,7 +14876,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Carapace Spine Crossbow\nBinds when picked upRangedCrossbow82 - 124 DamageSpeed 3.30(31.2 damage per second)\n+9 Stamina\n+4 Agility\nDurability 75 / 75\nRequires Level 56", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18738", + ["sourceUrl"] = "https://octowow.st/db/?item=18738", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14902,7 +14902,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Xorothian Firestick\nBinds when picked upRangedGun57 - 108 DamageSpeed 2.60(31.7 damage per second)\n+6 Stamina\n+4 Agility\n+6 Shadow Resistance\nDurability 75 / 75\nRequires Level 57", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18755", + ["sourceUrl"] = "https://octowow.st/db/?item=18755", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -14928,7 +14928,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Grand Marshal's Bullseye\nBinds when picked upRangedBow74 - 109 DamageSpeed 1.80(50.8 damage per second)\n+9 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +41 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18833", + ["sourceUrl"] = "https://octowow.st/db/?item=18833", ["sources"] = { }, }, @@ -14941,7 +14941,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "High Warlord's Recurve\nBinds when picked upRangedBow74 - 109 DamageSpeed 1.80(50.8 damage per second)\n+9 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +41 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18835", + ["sourceUrl"] = "https://octowow.st/db/?item=18835", ["sources"] = { }, }, @@ -14954,7 +14954,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Grand Marshal's Repeater\nBinds when picked upRangedCrossbow120 - 175 DamageSpeed 2.90(50.9 damage per second)\n+9 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +41 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18836", + ["sourceUrl"] = "https://octowow.st/db/?item=18836", ["sources"] = { }, }, @@ -14967,7 +14967,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "High Warlord's Crossbow\nBinds when picked upRangedCrossbow120 - 175 DamageSpeed 2.90(50.9 damage per second)\n+9 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +41 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18837", + ["sourceUrl"] = "https://octowow.st/db/?item=18837", ["sources"] = { }, }, @@ -14980,7 +14980,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Grand Marshal's Hand Cannon\nBinds when picked upRangedGun120 - 175 DamageSpeed 2.90(50.9 damage per second)\n+9 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +41 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18855", + ["sourceUrl"] = "https://octowow.st/db/?item=18855", ["sources"] = { }, }, @@ -14993,7 +14993,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "High Warlord's Street Sweeper\nBinds when picked upRangedGun120 - 175 DamageSpeed 2.90(50.9 damage per second)\n+9 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +41 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=18860", + ["sourceUrl"] = "https://octowow.st/db/?item=18860", ["sources"] = { }, }, @@ -15006,7 +15006,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Monster - Fire ArrowProjectileArrowAdds 1.0 damage per second", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19082", + ["sourceUrl"] = "https://octowow.st/db/?item=19082", ["sources"] = { }, }, @@ -15018,7 +15018,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Bloodseeker\nBinds when picked upRangedCrossbow85 - 128 DamageSpeed 3.30(32.3 damage per second)\n+7 Agility\n+8 Strength\nDurability 75 / 75", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19107", + ["sourceUrl"] = "https://octowow.st/db/?item=19107", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -15036,7 +15036,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Highland Bow\nBinds when picked upRangedBow41 - 77 DamageSpeed 2.50(23.6 damage per second)\n+5 Agility\n+4 Stamina\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19114", + ["sourceUrl"] = "https://octowow.st/db/?item=19114", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -15055,7 +15055,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Ice Threaded Arrow\nBinds when picked upProjectileArrowAdds 16.5 damage per second\nRequires Level 51", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19316", + ["sourceUrl"] = "https://octowow.st/db/?item=19316", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15106,7 +15106,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "projectile", ["description"] = "", ["tooltip"] = "Ice Threaded Bullet\nBinds when picked upProjectileBulletAdds 16.5 damage per second\nRequires Level 51", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19317", + ["sourceUrl"] = "https://octowow.st/db/?item=19317", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15157,7 +15157,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Heartstriker\nBinds when picked upRangedBow80 - 149 DamageSpeed 2.60(44.0 damage per second)\n+9 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +24 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19350", + ["sourceUrl"] = "https://octowow.st/db/?item=19350", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15183,7 +15183,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Ashjre'thul, Crossbow of Smiting\nBinds when picked upRangedCrossbow124 - 186 DamageSpeed 3.40(45.6 damage per second)\n+7 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +36 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19361", + ["sourceUrl"] = "https://octowow.st/db/?item=19361", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15209,7 +15209,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Dragonbreath Hand Cannon\nBinds when picked upRangedGun99 - 173 DamageSpeed 3.10(43.9 damage per second)\n+14 Agility\n+7 Stamina\nDurability 90 / 90\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19368", + ["sourceUrl"] = "https://octowow.st/db/?item=19368", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15235,7 +15235,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrider's Bow\nBinds when picked upRangedBow62 - 114 DamageSpeed 2.40(36.7 damage per second)\n+11 Stamina\n+5 Agility\nDurability 75 / 75\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19558", + ["sourceUrl"] = "https://octowow.st/db/?item=19558", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15262,7 +15262,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrider's Bow\nBinds when picked upRangedBow46 - 86 DamageSpeed 2.40(27.5 damage per second)\n+8 Stamina\n+3 Agility\nDurability 75 / 75\nRequires Level 48", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19559", + ["sourceUrl"] = "https://octowow.st/db/?item=19559", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15289,7 +15289,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrider's Bow\nBinds when picked upRangedBow38 - 71 DamageSpeed 2.40(22.7 damage per second)\n+6 Stamina\n+3 Agility\nDurability 75 / 75\nRequires Level 38", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19560", + ["sourceUrl"] = "https://octowow.st/db/?item=19560", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15316,7 +15316,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrider's Bow\nBinds when picked upRangedBow28 - 52 DamageSpeed 2.40(16.7 damage per second)\nDurability 75 / 75\nRequires Level 28", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19561", + ["sourceUrl"] = "https://octowow.st/db/?item=19561", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15343,7 +15343,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrunner's Bow\nBinds when picked upRangedBow62 - 114 DamageSpeed 2.40(36.7 damage per second)\n+11 Stamina\n+5 Agility\nDurability 75 / 75\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19562", + ["sourceUrl"] = "https://octowow.st/db/?item=19562", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15370,7 +15370,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrunner's Bow\nBinds when picked upRangedBow46 - 86 DamageSpeed 2.40(27.5 damage per second)\n+8 Stamina\n+3 Agility\nDurability 75 / 75\nRequires Level 48", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19563", + ["sourceUrl"] = "https://octowow.st/db/?item=19563", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15397,7 +15397,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrunner's Bow\nBinds when picked upRangedBow38 - 71 DamageSpeed 2.40(22.7 damage per second)\n+6 Stamina\n+3 Agility\nDurability 75 / 75\nRequires Level 38", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19564", + ["sourceUrl"] = "https://octowow.st/db/?item=19564", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15424,7 +15424,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrunner's Bow\nBinds when picked upRangedBow28 - 52 DamageSpeed 2.40(16.7 damage per second)\nDurability 75 / 75\nRequires Level 28", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19565", + ["sourceUrl"] = "https://octowow.st/db/?item=19565", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15451,7 +15451,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Gurubashi Dwarf Destroyer\nBinds when picked up\nUniqueRangedGun76 - 142 DamageSpeed 2.80(38.9 damage per second)\nDurability 90 / 90\nRequires Level 60\nEquip: +30 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19853", + ["sourceUrl"] = "https://octowow.st/db/?item=19853", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15477,7 +15477,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Hoodoo Hunting Bow\nBinds when picked upRangedBow68 - 128 DamageSpeed 2.80(35.0 damage per second)\n+10 Agility\n+4 Stamina\nDurability 75 / 75\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=19993", + ["sourceUrl"] = "https://octowow.st/db/?item=19993", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15503,7 +15503,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Mandokir's Sting\nBinds when picked upRangedBow68 - 127 DamageSpeed 2.60(37.5 damage per second)\n+11 Agility\n+8 Stamina\nDurability 90 / 90\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20038", + ["sourceUrl"] = "https://octowow.st/db/?item=20038", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15536,7 +15536,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "90 Green Warrior Gun\nBinds when equippedRangedGun81 - 151 DamageSpeed 2.50(46.4 damage per second)\n+9 Strength\n+8 Stamina\nDurability 65 / 65\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20245", + ["sourceUrl"] = "https://octowow.st/db/?item=20245", ["sources"] = { }, }, @@ -15549,7 +15549,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "63 Green Warrior Gun\nBinds when equippedRangedGun51 - 96 DamageSpeed 2.50(29.4 damage per second)\n+6 Strength\n+6 Stamina\nDurability 65 / 65\nRequires Level 58", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20285", + ["sourceUrl"] = "https://octowow.st/db/?item=20285", ["sources"] = { }, }, @@ -15562,7 +15562,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "90 Green Rogue Bow\nBinds when picked upRangedBow81 - 151 DamageSpeed 2.50(46.4 damage per second)\n+6 Stamina\n+10 Agility\nDurability 65 / 65\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20299", + ["sourceUrl"] = "https://octowow.st/db/?item=20299", ["sources"] = { }, }, @@ -15575,7 +15575,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "63 Green Rogue Bow\nBinds when picked upRangedBow51 - 96 DamageSpeed 2.50(29.4 damage per second)\n+4 Stamina\n+7 Agility\nDurability 65 / 65\nRequires Level 58", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20313", + ["sourceUrl"] = "https://octowow.st/db/?item=20313", ["sources"] = { }, }, @@ -15588,7 +15588,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "This bow has no real variance.", ["tooltip"] = "Bland Bow of Steadiness\nBinds when picked upRangedBow46 - 46 DamageSpeed 1.50(30.7 damage per second)\nDurability 75 / 75\nRequires Level 60\n\"This bow has no real variance.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20368", + ["sourceUrl"] = "https://octowow.st/db/?item=20368", ["sources"] = { }, }, @@ -15601,7 +15601,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrider's Bow\nBinds when picked upRangedBow19 - 37 DamageSpeed 2.40(11.7 damage per second)\nDurability 65 / 65\nRequires Level 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20437", + ["sourceUrl"] = "https://octowow.st/db/?item=20437", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15628,7 +15628,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Outrunner's Bow\nBinds when picked upRangedBow19 - 37 DamageSpeed 2.40(11.7 damage per second)\nDurability 65 / 65\nRequires Level 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20438", + ["sourceUrl"] = "https://octowow.st/db/?item=20438", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -15655,7 +15655,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Rhok'delar, Longbow of the Ancient Keepers DEP\nBinds when picked up\nUniqueRangedBow89 - 166 DamageSpeed 2.90(44.0 damage per second)\nDurability 90 / 90\nClasses: Hunter\nRequires Level 60\nEquip: Improves your chance to get a critical strike by 1%.\nEquip: +17 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20488", + ["sourceUrl"] = "https://octowow.st/db/?item=20488", ["sources"] = { ["created-by"] = { ["template"] = "spell", @@ -15678,7 +15678,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Polished Ironwood Crossbow\nBinds when picked up\nUniqueRangedCrossbow101 - 177 DamageSpeed 3.10(44.8 damage per second)\n+6 Stamina\n+7 Nature Resistance\nDurability 90 / 90\nRequires Level 60\nEquip: +26 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20599", + ["sourceUrl"] = "https://octowow.st/db/?item=20599", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15703,7 +15703,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Sandstrider's Mark\nBinds when picked upRangedBow44 - 82 DamageSpeed 2.30(27.4 damage per second)\n+8 Stamina\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20646", + ["sourceUrl"] = "https://octowow.st/db/?item=20646", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -15722,7 +15722,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Deep Strike Bow\nBinds when picked upRangedBow58 - 108 DamageSpeed 2.70(30.7 damage per second)\n+6 Agility\n+6 Intellect\n+4 Stamina\nDurability 75 / 75\nRequires Level 55", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20663", + ["sourceUrl"] = "https://octowow.st/db/?item=20663", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15749,7 +15749,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Crystal Slugthrower\nBinds when equippedRangedGun65 - 122 DamageSpeed 2.80(33.4 damage per second)\n+4 Stamina\nDurability 75 / 75\nRequires Level 60\nEquip: +20 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=20722", + ["sourceUrl"] = "https://octowow.st/db/?item=20722", ["sources"] = { }, }, @@ -15761,7 +15761,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "The weapon is infused and reinforced with Elementium.", ["tooltip"] = "Blessed Qiraji Musket\nBinds when picked up\nUniqueRangedGun109 - 184 DamageSpeed 3.10(47.3 damage per second)\n+10 Stamina\nDurability 90 / 90\nEquip: +31 ranged Attack Power.\n\"The weapon is infused and reinforced with Elementium.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=21272", + ["sourceUrl"] = "https://octowow.st/db/?item=21272", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -15780,7 +15780,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Crossbow of Imminent Doom\nBinds when picked upRangedCrossbow103 - 155 DamageSpeed 3.10(41.6 damage per second)\n+7 Agility\n+5 Stamina\n+5 Strength\nDurability 90 / 90\nRequires Level 60\nEquip: Improves your chance to hit by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=21459", + ["sourceUrl"] = "https://octowow.st/db/?item=21459", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15806,7 +15806,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Bow of Taut Sinew\nBinds when picked upRangedBow59 - 111 DamageSpeed 2.20(38.6 damage per second)\n+8 Nature Resistance\nDurability 90 / 90\nRequires Level 60\nEquip: +22 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=21478", + ["sourceUrl"] = "https://octowow.st/db/?item=21478", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15832,7 +15832,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Monster - Bow, KaldoreiRangedBow1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=21550", + ["sourceUrl"] = "https://octowow.st/db/?item=21550", ["sources"] = { }, }, @@ -15845,7 +15845,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Gun, PvP HordeRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=21554", + ["sourceUrl"] = "https://octowow.st/db/?item=21554", ["sources"] = { }, }, @@ -15858,7 +15858,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Gun, Kaldorei PVP AllianceRangedGun1 - 2 DamageSpeed 2.00(0.8 damage per second)\nDurability 18 / 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=21564", + ["sourceUrl"] = "https://octowow.st/db/?item=21564", ["sources"] = { }, }, @@ -15871,7 +15871,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Huhuran's Stinger\nBinds when picked upRangedBow87 - 163 DamageSpeed 2.70(46.3 damage per second)\n+18 Agility\nDurability 90 / 90\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=21616", + ["sourceUrl"] = "https://octowow.st/db/?item=21616", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -15897,7 +15897,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Silithid Husked Launcher\nBinds when equippedRangedGun68 - 128 DamageSpeed 2.80(35.0 damage per second)\n+10 Stamina\n+4 Agility\nDurability 75 / 75\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=21800", + ["sourceUrl"] = "https://octowow.st/db/?item=21800", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16048,7 +16048,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Malgen's Long Bow\nBinds when picked upRangedBow63 - 118 DamageSpeed 2.90(31.2 damage per second)\n+4 Stamina\nDurability 75 / 75\nRequires Level 56\nEquip: +20 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=22318", + ["sourceUrl"] = "https://octowow.st/db/?item=22318", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16090,7 +16090,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Fahrad's Reloading Repeater\nBinds when picked upRangedCrossbow85 - 128 DamageSpeed 3.20(33.3 damage per second)\n+4 Agility\nDurability 75 / 75\nEquip: Improves your chance to hit by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=22347", + ["sourceUrl"] = "https://octowow.st/db/?item=22347", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16108,7 +16108,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "The Purifier\nBinds when picked upRangedGun74 - 138 DamageSpeed 3.00(35.3 damage per second)\nDurability 90 / 90\nEquip: Improves your chance to get a critical strike by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=22656", + ["sourceUrl"] = "https://octowow.st/db/?item=22656", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16127,7 +16127,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Toxin Injector\nBinds when picked upRangedGun68 - 135 DamageSpeed 2.00(50.8 damage per second)\n+10 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +28 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=22810", + ["sourceUrl"] = "https://octowow.st/db/?item=22810", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16153,7 +16153,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Soulstring\nBinds when picked upRangedBow113 - 201 DamageSpeed 2.90(54.1 damage per second)\n+6 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: Improves your chance to get a critical strike by 1%.\nEquip: +18 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=22811", + ["sourceUrl"] = "https://octowow.st/db/?item=22811", ["sources"] = { ["contained-in-object"] = { ["template"] = "object", @@ -16176,7 +16176,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Nerubian Slavemaker\nBinds when picked upRangedCrossbow128 - 238 DamageSpeed 3.20(57.2 damage per second)\nDurability 90 / 90\nRequires Level 60\nEquip: +24 Attack Power.\nEquip: Improves your chance to get a critical strike by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=22812", + ["sourceUrl"] = "https://octowow.st/db/?item=22812", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16209,7 +16209,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Larvae of the Great Worm\nBinds when picked upRangedGun103 - 192 DamageSpeed 3.00(49.2 damage per second)\nDurability 90 / 90\nRequires Level 60\nEquip: Improves your chance to get a critical strike by 1%.\nEquip: +18 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=23557", + ["sourceUrl"] = "https://octowow.st/db/?item=23557", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16234,7 +16234,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Water-logged Musket\nBinds when picked upRangedGun2 - 6 DamageSpeed 2.20(1.8 damage per second)\nDurability 20 / 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=41023", + ["sourceUrl"] = "https://octowow.st/db/?item=41023", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16252,7 +16252,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Bow of Alah'Thalas\nBinds when picked upRangedBow9 - 18 DamageSpeed 2.50(5.4 damage per second)\n+1 Agility\nDurability 35 / 35", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=41190", + ["sourceUrl"] = "https://octowow.st/db/?item=41190", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16271,7 +16271,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Dragonmaw Battle Bow\nBinds when picked upRangedBow22 - 58 DamageSpeed 2.40(16.7 damage per second)\n+5 Fire Resistance\nDurability 75 / 75\nRequires Level 28", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=41725", + ["sourceUrl"] = "https://octowow.st/db/?item=41725", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16297,7 +16297,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Shadowblaster\nBinds when picked upRangedGun126 - 65 DamageSpeed 3.10+2 - 5 Shadow Damage\n(31.9 damage per second)\n+9 Stamina\nDurability 75 / 75\nRequires Level 57\nEquip: +14 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=51746", + ["sourceUrl"] = "https://octowow.st/db/?item=51746", ["sources"] = { }, }, @@ -16310,7 +16310,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "Comes with an underbarrel bomb launcher.", ["tooltip"] = "Siege Bomber\nBinds when equippedRangedGun36 - 68 DamageSpeed 2.80(18.6 damage per second)\nDurability 65 / 65\nRequires Level 37\nUse: Inflicts 149 to 202 Fire damage and stuns targets in a 5 yard radius for 3 sec. Any damage will break the effect.\n\"Comes with an underbarrel bomb launcher.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=51759", + ["sourceUrl"] = "https://octowow.st/db/?item=51759", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16335,7 +16335,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Bow of Quel'Danil\nBinds when picked upRangedBow32 - 59 DamageSpeed 2.10(21.7 damage per second)\n+4 Agility\n+4 Stamina\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=51768", + ["sourceUrl"] = "https://octowow.st/db/?item=51768", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16354,7 +16354,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Ghoulslayer Shotgun\nBinds when picked upRangedGun81 - 151 DamageSpeed 3.10(37.4 damage per second)\nDurability 90 / 90\nRequires Level 60\nEquip: +24 Attack Power.\nEquip: +15 Attack Power when fighting Undead.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=51780", + ["sourceUrl"] = "https://octowow.st/db/?item=51780", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16379,7 +16379,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Otherworldly Rifle\nBinds when picked upRangedGun40 - 76 DamageSpeed 2.60+1 - 2 Fire Damage\n+1 - 2 Shadow Damage\n+1 - 2 Frost Damage\n(24.1 damage per second)\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=51794", + ["sourceUrl"] = "https://octowow.st/db/?item=51794", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16397,7 +16397,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Hawkwind Rifle\nBinds when picked upRangedGun6 - 12 DamageSpeed 2.60(3.5 damage per second)\nDurability 30 / 30", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=51828", + ["sourceUrl"] = "https://octowow.st/db/?item=51828", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16415,7 +16415,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "Cock the bow, aim, and fire!", ["tooltip"] = "Stunning Crossbow\nBinds when picked upRangedCrossbow40 - 73 DamageSpeed 3.40(16.6 damage per second)\n\"Cock the bow, aim, and fire!\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=51844", + ["sourceUrl"] = "https://octowow.st/db/?item=51844", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16433,7 +16433,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "Shu'halo warriors nickname this gun \"Greenboy\" for the way the sun reflects off it.", ["tooltip"] = "Brave's Rifle\nBinds when picked upRangedGun17 - 30 DamageSpeed 2.60(9.0 damage per second)\nDurability 50 / 50\n\"Shu'halo warriors nickname this gun \"Greenboy\" for the way the sun reflects off it.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=51862", + ["sourceUrl"] = "https://octowow.st/db/?item=51862", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16452,7 +16452,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "Always hits the heart.", ["tooltip"] = "Kavdan's Patient Watch\nBinds when picked upRangedCrossbow81 - 151 DamageSpeed 3.10(37.4 damage per second)\n+8 Stamina\nRequires Level 60\nRequires Syndicate - ExaltedEquip: +22 ranged Attack Power.\n\"Always hits the heart.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=55028", + ["sourceUrl"] = "https://octowow.st/db/?item=55028", ["sources"] = { }, }, @@ -16465,7 +16465,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "Needs spell: Use: Zap your target with radioactive energy, turning it into a Leper Gnome and reducing its damage dealt by 15 and its movement speed by 20% for 12 seconds. (5 min cd) actual quote :Still very much a prototype. Don't show it to other gnomes", ["tooltip"] = "Zan's Leperification Blaster\nBinds when picked upRangedGun49 - 92 DamageSpeed 2.70(26.1 damage per second)\n+4 Agility\n+4 Stamina\nDurability 75 / 75\nRequires Level 45\nRequires Ravenholdt - Revered\"Needs spell: Use: Zap your target with radioactive energy, turning it into a Leper Gnome and reducing its damage dealt by 15 and its movement speed by 20% for 12 seconds. (5 min cd) actual quote :Still very much a prototype. Don't show it to other gnomes\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=55068", + ["sourceUrl"] = "https://octowow.st/db/?item=55068", ["sources"] = { }, }, @@ -16478,7 +16478,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Phase-shifting Crossbow\nBinds when picked up\nUniqueRangedCrossbow111 - 203 DamageSpeed 2.80(56.1 damage per second)\n+9 Strength\nDurability 90 / 90\nRequires Level 60\nEquip: Improves your chance to get a critical strike by 1%.\nEquip: Improves your chance to hit by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=55096", + ["sourceUrl"] = "https://octowow.st/db/?item=55096", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16504,7 +16504,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Rain of Spiders\nBinds when picked up\nUniqueRangedBow144 - 255 DamageSpeed 3.10(64.4 damage per second)\n+10 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: +22 ranged Attack Power.\nEquip: Improves your chance to get a critical strike by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=55346", + ["sourceUrl"] = "https://octowow.st/db/?item=55346", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16530,7 +16530,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Slag Slugger\nBinds when picked upRangedGun15 - 28 DamageSpeed 2.30(9.3 damage per second)\nDurability 65 / 65\nRequires Level 18\nEquip: Chance to strike your ranged target with a Slag Bolt for 13 to 18 Fire damage immediately and 30 Fire damage over 6 sec.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=55379", + ["sourceUrl"] = "https://octowow.st/db/?item=55379", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16555,7 +16555,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Ashwood Bow\nBinds when picked upRangedBow29 - 50 DamageSpeed 2.30(17.2 damage per second)\n+5 Stamina\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=58016", + ["sourceUrl"] = "https://octowow.st/db/?item=58016", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16573,7 +16573,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Nippsy's Precision Rifle\nBinds when picked upRangedGun51 - 92 DamageSpeed 2.90(24.7 damage per second)\n+6 Agility\n+5 Intellect\nDurability 75 / 75", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=58083", + ["sourceUrl"] = "https://octowow.st/db/?item=58083", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16591,7 +16591,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Standard Grade Rifle\nBinds when picked upRangedGun4 - 9 DamageSpeed 2.60(2.5 damage per second)\nDurability 30 / 30", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=58086", + ["sourceUrl"] = "https://octowow.st/db/?item=58086", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16610,7 +16610,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Demon Hair Bow\nBinds when picked upRangedBow53 - 79 DamageSpeed 3.10(21.3 damage per second)\nDurability 75 / 75\nRequires Level 35\nEquip: +19 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=58193", + ["sourceUrl"] = "https://octowow.st/db/?item=58193", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16636,7 +16636,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Waterlogged Blunderbuss\nBinds when picked upRangedGun31 - 55 DamageSpeed 3.00(14.3 damage per second)\n+3 Stamina\n+3 Agility\nDurability 65 / 65\nRequires Level 30", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=58284", + ["sourceUrl"] = "https://octowow.st/db/?item=58284", ["sources"] = { ["contained-in-object"] = { ["template"] = "object", @@ -16658,7 +16658,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Wobblefree Fizz-rifle\nBinds when picked upRangedGun71 - 107 DamageSpeed 2.60(34.2 damage per second)\n+5 Intellect\nDurability 65 / 65\nEquip: +10 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60165", + ["sourceUrl"] = "https://octowow.st/db/?item=60165", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16676,7 +16676,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "The band is encrusted with bright jades", ["tooltip"] = "'Jadewood' Longbow\nBinds when picked upRangedBow23 - 45 DamageSpeed 1.90(17.9 damage per second)\n+6 Intellect\nDurability 65 / 65\n\"The band is encrusted with bright jades\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60309", + ["sourceUrl"] = "https://octowow.st/db/?item=60309", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16694,7 +16694,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Lordaeron Rusty Crossbow\nBinds when picked upRangedCrossbow12 - 23 DamageSpeed 3.00(5.8 damage per second)\nDurability 40 / 40", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60338", + ["sourceUrl"] = "https://octowow.st/db/?item=60338", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16712,7 +16712,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "High Elven Rotten Bow\nBinds when picked upRangedBow9 - 18 DamageSpeed 2.30(5.9 damage per second)\nDurability 40 / 40", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60339", + ["sourceUrl"] = "https://octowow.st/db/?item=60339", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16731,7 +16731,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Bloodscalp Longbow\nBinds when picked upRangedBow56 - 108 DamageSpeed 3.00(27.3 damage per second)\n+6 Agility\nDurability 75 / 75\nRequires Level 45", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60440", + ["sourceUrl"] = "https://octowow.st/db/?item=60440", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16758,7 +16758,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Vigilance\nBinds when picked upRangedCrossbow72 - 114 DamageSpeed 2.80(33.2 damage per second)\n+9 Stamina\n+5 Agility\nDurability 75 / 75\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60506", + ["sourceUrl"] = "https://octowow.st/db/?item=60506", ["sources"] = { ["contained-in-object"] = { ["template"] = "object", @@ -16781,7 +16781,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Techrifle X-TREME 5200\nBinds when picked upRangedGun47 - 93 DamageSpeed 2.60+1 - 5 Fire Damage\n(28.1 damage per second)\n+8 Agility\n+2 Intellect\nDurability 75 / 75\nRequires Level 51", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60545", + ["sourceUrl"] = "https://octowow.st/db/?item=60545", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16807,7 +16807,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Goldplated Royal Crossbow\nBinds when picked upRangedCrossbow86 - 131 DamageSpeed 3.30(32.9 damage per second)\n+5 Intellect\n+5 Agility\nDurability 75 / 75\nEquip: +12 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60624", + ["sourceUrl"] = "https://octowow.st/db/?item=60624", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16826,7 +16826,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Shieldbreaker Arbalest\nBinds when equippedRangedCrossbow73 - 133 DamageSpeed 3.00(34.3 damage per second)\nDurability 90 / 90\nRequires Level 56\nEquip: Your attacks ignore 35 of the target's armor.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60782", + ["sourceUrl"] = "https://octowow.st/db/?item=60782", ["sources"] = { ["worldDrop"] = true, ["droppedByCount"] = 348, @@ -16840,7 +16840,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Theramore Arbalest\nBinds when picked upRangedCrossbow57 - 106 DamageSpeed 3.40(24.0 damage per second)\n+8 Agility\nDurability 75 / 75", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60821", + ["sourceUrl"] = "https://octowow.st/db/?item=60821", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16859,7 +16859,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Magram Windstriker\nBinds when picked upRangedBow71 - 92 DamageSpeed 2.90+5 - 7 Nature Damage\n(30.2 damage per second)\n+4 Intellect\nDurability 75 / 75\nRequires Level 48\nRequires Magram Clan Centaur - ReveredEquip: +12 ranged Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60882", + ["sourceUrl"] = "https://octowow.st/db/?item=60882", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -16884,7 +16884,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Brackenwall Longbow\nBinds when picked upRangedBow49 - 90 DamageSpeed 2.90(24.0 damage per second)\n+8 Agility\nDurability 75 / 75", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=60953", + ["sourceUrl"] = "https://octowow.st/db/?item=60953", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16902,7 +16902,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "Also good for shooting woodchucks.", ["tooltip"] = "Flintlocke's Hand Cannon\nBinds when picked up\nUniqueRangedGun97 - 139 DamageSpeed 3.20(36.9 damage per second)\n+5 Stamina\nDurability 90 / 90\nRequires - ExaltedEquip: Improves your chance to hit by 1%.\n\"Also good for shooting woodchucks.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61011", + ["sourceUrl"] = "https://octowow.st/db/?item=61011", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -16927,7 +16927,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "The true embodiment of Dark Iron construction.", ["tooltip"] = "Dark Iron Desecrator\nBinds when picked upRangedGun72 - 142 DamageSpeed 2.90+5 - 10 Fire Damage\n(39.5 damage per second)\n+10 Fire Resistance\n+10 Shadow Resistance\nDurability 90 / 90\nUse: Blasts a target for 50 to 71 Fire damage.\nEquip: Improves your chance to get a critical strike by 1%.\n\"The true embodiment of Dark Iron construction.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61068", + ["sourceUrl"] = "https://octowow.st/db/?item=61068", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16945,7 +16945,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Bow of the Night Huntress\nBinds when picked upRangedBow22 - 41 DamageSpeed 2.20(14.3 damage per second)\n+5 Shadow Resistance\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61165", + ["sourceUrl"] = "https://octowow.st/db/?item=61165", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -16964,7 +16964,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "The initials G.G. are etched on the stock.", ["tooltip"] = "Beasthunter's Blunderbuss\nBinds when picked upRangedGun72 - 138 DamageSpeed 2.80(37.5 damage per second)\n+5 Agility\nDurability 90 / 90\nRequires Level 60\nEquip: +39 Attack Power when fighting Beasts.\n\"The initials G.G. are etched on the stock.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61248", + ["sourceUrl"] = "https://octowow.st/db/?item=61248", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -16990,7 +16990,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Worgen Hunter Musket\nBinds when picked upRangedGun50 - 92 DamageSpeed 2.90(24.5 damage per second)\n+4 Stamina\nDurability 75 / 75\nRequires Level 41\nEquip: +12 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61307", + ["sourceUrl"] = "https://octowow.st/db/?item=61307", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17016,7 +17016,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Intricate Gnomish Blunderbuss\nBinds when picked upRangedGun30 - 78 DamageSpeed 3.00(18.0 damage per second)\n+3 Intellect\nDurability 75 / 75", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61383", + ["sourceUrl"] = "https://octowow.st/db/?item=61383", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -17034,7 +17034,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Brigade Rifle\nBinds when picked upRangedGun42 - 76 DamageSpeed 2.80(21.1 damage per second)\n+4 Agility\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61472", + ["sourceUrl"] = "https://octowow.st/db/?item=61472", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -17053,7 +17053,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "Favored by the dryad.", ["tooltip"] = "Nature's Call\nBinds when picked up\nUniqueRangedBow103 - 191 DamageSpeed 3.00(49.0 damage per second)\n+8 Stamina\nDurability 90 / 90\nRequires Level 60\nEquip: Improves your chance to hit by 1%.\nEquip: Your attacks ignore 25 of the target's armor.\n\"Favored by the dryad.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61525", + ["sourceUrl"] = "https://octowow.st/db/?item=61525", ["sources"] = { }, }, @@ -17066,7 +17066,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Time Frozen Bow\nBinds when equippedRangedBow76 - 139 DamageSpeed 3.50+4 - 10 Fire Damage\n(32.7 damage per second)\n+-10 Frost Resistance\nDurability 75 / 75\nRequires Level 59\nEquip: Increases your attack and casting speed by 1%.\nEquip: Improves your chance to hit by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61569", + ["sourceUrl"] = "https://octowow.st/db/?item=61569", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17157,7 +17157,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Farmer's Musket\nBinds when picked upRangedGun38 - 76 DamageSpeed 2.60(21.9 damage per second)\n+6 Stamina\n+3 Agility\nDurability 65 / 65", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61629", + ["sourceUrl"] = "https://octowow.st/db/?item=61629", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -17176,7 +17176,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "Somehow still in working condition.", ["tooltip"] = "Battered ArbalestRangedCrossbow19 - 35 DamageSpeed 2.20(12.3 damage per second)\nDurability 65 / 65\nRequires Level 40\n\"Somehow still in working condition.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=61683", + ["sourceUrl"] = "https://octowow.st/db/?item=61683", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17201,7 +17201,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Worn CrossbowRangedCrossbow3 - 6 DamageSpeed 3.00(1.5 damage per second)\nDurability 20 / 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=70049", + ["sourceUrl"] = "https://octowow.st/db/?item=70049", ["sources"] = { }, }, @@ -17213,7 +17213,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Miscalibrated Rifle\nBinds when picked upRangedGun5 - 9 DamageSpeed 2.80(2.5 damage per second)\nDurability 25 / 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80106", + ["sourceUrl"] = "https://octowow.st/db/?item=80106", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -17231,7 +17231,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Polished Boomstick\nBinds when picked upRangedGun8 - 15 DamageSpeed 2.40(4.8 damage per second)\nDurability 35 / 35", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80127", + ["sourceUrl"] = "https://octowow.st/db/?item=80127", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -17249,7 +17249,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Worn Wooden Bow\nBinds when picked upRangedBow5 - 9 DamageSpeed 2.80(2.5 damage per second)\nDurability 25 / 25", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80207", + ["sourceUrl"] = "https://octowow.st/db/?item=80207", ["sources"] = { ["reward-of"] = { ["template"] = "quest", @@ -17268,7 +17268,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Farstrider Lodge Protector's Bow\nBinds when picked upRangedBow19 - 37 DamageSpeed 2.40(11.7 damage per second)\nDurability 65 / 65\nRequires Level 18", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80223", + ["sourceUrl"] = "https://octowow.st/db/?item=80223", ["sources"] = { }, }, @@ -17281,7 +17281,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Well-balanced Short Bow\nBinds when picked upRangedBow8 - 15 DamageSpeed 1.70(6.8 damage per second)\nDurability 40 / 40\nClasses: Warrior, Hunter, Rogue\nRequires Level 10\nRequires Silvermoon Remnant - Friendly", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80503", + ["sourceUrl"] = "https://octowow.st/db/?item=80503", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -17307,7 +17307,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Quel'dorei Ranger's Longbow\nBinds when picked upRangedBow75 - 118 DamageSpeed 2.90(33.3 damage per second)\n+5 Stamina\nDurability 75 / 75\nRequires Level 60\nRequires Silvermoon Remnant - ReveredEquip: +22 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80546", + ["sourceUrl"] = "https://octowow.st/db/?item=80546", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -17333,7 +17333,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Sturdy Short Bow\nBinds when picked upRangedBow7 - 14 DamageSpeed 1.70(6.2 damage per second)\nDurability 40 / 40\nClasses: Warrior, Hunter, Rogue\nRequires Level 10\nRequires Revantusk Trolls - Friendly", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80603", + ["sourceUrl"] = "https://octowow.st/db/?item=80603", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -17366,7 +17366,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Revantusk Shadow Hunter's Longbow\nBinds when picked upRangedBow75 - 118 DamageSpeed 2.90(33.3 damage per second)\n+5 Stamina\nDurability 75 / 75\nRequires Level 60\nRequires Revantusk Trolls - ReveredEquip: +22 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80646", + ["sourceUrl"] = "https://octowow.st/db/?item=80646", ["sources"] = { ["sold-by"] = { ["template"] = "npc", @@ -17399,7 +17399,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "Made from abomination ribs and stitching. Truly horrifying.", ["tooltip"] = "Abomination Crossbow\nBinds when picked upRangedCrossbow47 - 87 DamageSpeed 3.00(22.3 damage per second)\n+4 Strength\n+4 Stamina\nDurability 75 / 75\nRequires Level 37\n\"Made from abomination ribs and stitching. Truly horrifying.\"", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80745", + ["sourceUrl"] = "https://octowow.st/db/?item=80745", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17425,7 +17425,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Burstshot Harquebus\nBinds when equippedRangedGun52 - 98 DamageSpeed 2.60(28.8 damage per second)\n+8 Stamina\nDurability 75 / 75\nRequires Level 51\nEquip: +10 Attack Power.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80795", + ["sourceUrl"] = "https://octowow.st/db/?item=80795", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17718,7 +17718,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Quilflinger\nBinds when equippedRangedBow7 - 14 DamageSpeed 2.10+1 - 1 Nature Damage\n(5.5 damage per second)\nDurability 35 / 35\nRequires Level 8", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80820", + ["sourceUrl"] = "https://octowow.st/db/?item=80820", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17744,7 +17744,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Flamestring Bow\nBinds when equippedRangedBow44 - 84 DamageSpeed 2.60+2 - 7 Fire Damage\n(26.3 damage per second)\nRequires Level 42\nEquip: Adds 4 fire damage to your weapon attack.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80825", + ["sourceUrl"] = "https://octowow.st/db/?item=80825", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17770,7 +17770,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Silvermoon BowRangedBow2 - 5 DamageSpeed 2.30(1.5 damage per second)\nDurability 20 / 20", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=80876", + ["sourceUrl"] = "https://octowow.st/db/?item=80876", ["sources"] = { }, }, @@ -17783,7 +17783,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Monster - Rifle2H, MechagnomeRangedGun4 - 9 DamageSpeed 2.60(2.5 damage per second)\nDurability 30 / 30\nRequires Level 2", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=83094", + ["sourceUrl"] = "https://octowow.st/db/?item=83094", ["sources"] = { }, }, @@ -17796,7 +17796,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Bow of the Grove\nBinds when picked upRangedBow43 - 68 DamageSpeed 2.60(21.3 damage per second)\n+3 Agility\nDurability 75 / 75\nRequires Level 35", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=83225", + ["sourceUrl"] = "https://octowow.st/db/?item=83225", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17822,7 +17822,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "gun", ["description"] = "", ["tooltip"] = "Caer Darrow Reserve Rifle\nBinds when picked upRangedGun76 - 94 DamageSpeed 2.60(32.7 damage per second)\nDurability 75 / 75\nRequires Level 56\nEquip: Improves your chance to get a critical strike by 1%.", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=83257", + ["sourceUrl"] = "https://octowow.st/db/?item=83257", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17848,7 +17848,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "bow", ["description"] = "", ["tooltip"] = "Windbreaker\nBinds when picked upRangedBow44 - 96 DamageSpeed 2.30+7 - 14 Frost Damage\n(35.0 damage per second)\n+12 Agility\n+5 Frost Resistance\nDurability 75 / 75\nRequires Level 60", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=83452", + ["sourceUrl"] = "https://octowow.st/db/?item=83452", ["sources"] = { ["dropped-by"] = { ["template"] = "npc", @@ -17874,7 +17874,7 @@ MTH_DS_ItemOrigins = { ["subtype"] = "crossbow", ["description"] = "", ["tooltip"] = "Balanced Light CrossbowRangedCrossbow12 - 21 DamageSpeed 2.90(5.7 damage per second)\nDurability 40 / 40\nRequires Level 11", - ["sourceUrl"] = "https://database.turtlecraft.gg/?item=83517", + ["sourceUrl"] = "https://octowow.st/db/?item=83517", ["sources"] = { ["sold-by"] = { ["template"] = "npc", diff --git a/data/ds-pet-spells-trainer.lua b/data/ds-pet-spells-trainer.lua index 8f55840..c53bb07 100644 --- a/data/ds-pet-spells-trainer.lua +++ b/data/ds-pet-spells-trainer.lua @@ -51,7 +51,7 @@ local growlTrainerRows = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=2649", + ["sourceUrl"] = "https://octowow.st/db/?spell=2649", ["trainLevel"] = 1, }, { @@ -72,7 +72,7 @@ local growlTrainerRows = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=14916", + ["sourceUrl"] = "https://octowow.st/db/?spell=14916", ["trainLevel"] = 10, }, { @@ -93,7 +93,7 @@ local growlTrainerRows = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=14917", + ["sourceUrl"] = "https://octowow.st/db/?spell=14917", ["trainLevel"] = 20, }, { @@ -114,7 +114,7 @@ local growlTrainerRows = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=14918", + ["sourceUrl"] = "https://octowow.st/db/?spell=14918", ["trainLevel"] = 30, }, { @@ -135,7 +135,7 @@ local growlTrainerRows = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=14919", + ["sourceUrl"] = "https://octowow.st/db/?spell=14919", ["trainLevel"] = 40, }, { @@ -156,7 +156,7 @@ local growlTrainerRows = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=14920", + ["sourceUrl"] = "https://octowow.st/db/?spell=14920", ["trainLevel"] = 50, }, { @@ -177,7 +177,7 @@ local growlTrainerRows = { ["rank"] = "Rank 7", ["rankNumber"] = 7, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=14921", + ["sourceUrl"] = "https://octowow.st/db/?spell=14921", ["trainLevel"] = 60, }, } @@ -228,7 +228,7 @@ end) MTH_DS_PetSpells.byAbility["Growl"] = { ["ability"] = "Growl", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Growl", + ["searchUrl"] = "https://octowow.st/db/?search=Growl", ["spellCount"] = table.getn(growlTrainerRows), ["spells"] = growlTrainerRows, } diff --git a/data/ds-pet-spells.lua b/data/ds-pet-spells.lua index 718fcba..1d649fb 100644 --- a/data/ds-pet-spells.lua +++ b/data/ds-pet-spells.lua @@ -45,7 +45,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17253", + ["sourceUrl"] = "https://octowow.st/db/?spell=17253", ["trainLevel"] = 1, }, { @@ -65,7 +65,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17255", + ["sourceUrl"] = "https://octowow.st/db/?spell=17255", ["trainLevel"] = 8, }, { @@ -85,7 +85,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17256", + ["sourceUrl"] = "https://octowow.st/db/?spell=17256", ["trainLevel"] = 16, }, { @@ -105,7 +105,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17257", + ["sourceUrl"] = "https://octowow.st/db/?spell=17257", ["trainLevel"] = 24, }, { @@ -125,7 +125,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17258", + ["sourceUrl"] = "https://octowow.st/db/?spell=17258", ["trainLevel"] = 32, }, { @@ -145,7 +145,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17259", + ["sourceUrl"] = "https://octowow.st/db/?spell=17259", ["trainLevel"] = 40, }, { @@ -165,7 +165,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 7", ["rankNumber"] = 7, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17260", + ["sourceUrl"] = "https://octowow.st/db/?spell=17260", ["trainLevel"] = 48, }, { @@ -185,7 +185,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 8", ["rankNumber"] = 8, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17261", + ["sourceUrl"] = "https://octowow.st/db/?spell=17261", ["trainLevel"] = 56, }, { @@ -205,7 +205,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Frost", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36527", + ["sourceUrl"] = "https://octowow.st/db/?spell=36527", ["trainLevel"] = 10, }, { @@ -225,7 +225,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36523", + ["sourceUrl"] = "https://octowow.st/db/?spell=36523", ["trainLevel"] = 10, }, { @@ -245,7 +245,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36524", + ["sourceUrl"] = "https://octowow.st/db/?spell=36524", ["trainLevel"] = 24, }, { @@ -265,7 +265,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36525", + ["sourceUrl"] = "https://octowow.st/db/?spell=36525", ["trainLevel"] = 40, }, { @@ -285,7 +285,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36526", + ["sourceUrl"] = "https://octowow.st/db/?spell=36526", ["trainLevel"] = 56, }, { @@ -306,7 +306,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=22120", + ["sourceUrl"] = "https://octowow.st/db/?spell=22120", ["trainLevel"] = 20, }, { @@ -328,7 +328,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=7371", + ["sourceUrl"] = "https://octowow.st/db/?spell=7371", ["trainLevel"] = 1, }, { @@ -350,7 +350,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26177", + ["sourceUrl"] = "https://octowow.st/db/?spell=26177", ["trainLevel"] = 12, }, { @@ -372,7 +372,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26178", + ["sourceUrl"] = "https://octowow.st/db/?spell=26178", ["trainLevel"] = 24, }, { @@ -394,7 +394,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26179", + ["sourceUrl"] = "https://octowow.st/db/?spell=26179", ["trainLevel"] = 36, }, { @@ -416,7 +416,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26201", + ["sourceUrl"] = "https://octowow.st/db/?spell=26201", ["trainLevel"] = 48, }, { @@ -438,7 +438,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=27685", + ["sourceUrl"] = "https://octowow.st/db/?spell=27685", ["trainLevel"] = 60, }, { @@ -458,7 +458,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16827", + ["sourceUrl"] = "https://octowow.st/db/?spell=16827", ["trainLevel"] = 1, }, { @@ -478,7 +478,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16828", + ["sourceUrl"] = "https://octowow.st/db/?spell=16828", ["trainLevel"] = 8, }, { @@ -498,7 +498,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16829", + ["sourceUrl"] = "https://octowow.st/db/?spell=16829", ["trainLevel"] = 16, }, { @@ -518,7 +518,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16830", + ["sourceUrl"] = "https://octowow.st/db/?spell=16830", ["trainLevel"] = 24, }, { @@ -538,7 +538,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16831", + ["sourceUrl"] = "https://octowow.st/db/?spell=16831", ["trainLevel"] = 32, }, { @@ -558,7 +558,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16832", + ["sourceUrl"] = "https://octowow.st/db/?spell=16832", ["trainLevel"] = 40, }, { @@ -578,7 +578,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 7", ["rankNumber"] = 7, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=3010", + ["sourceUrl"] = "https://octowow.st/db/?spell=3010", ["trainLevel"] = 48, }, { @@ -598,7 +598,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 8", ["rankNumber"] = 8, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=3009", + ["sourceUrl"] = "https://octowow.st/db/?spell=3009", ["trainLevel"] = 56, }, { @@ -618,7 +618,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1742", + ["sourceUrl"] = "https://octowow.st/db/?spell=1742", ["trainLevel"] = 5, }, { @@ -638,7 +638,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1753", + ["sourceUrl"] = "https://octowow.st/db/?spell=1753", ["trainLevel"] = 15, }, { @@ -658,7 +658,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1754", + ["sourceUrl"] = "https://octowow.st/db/?spell=1754", ["trainLevel"] = 25, }, { @@ -678,7 +678,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1755", + ["sourceUrl"] = "https://octowow.st/db/?spell=1755", ["trainLevel"] = 35, }, { @@ -698,7 +698,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1756", + ["sourceUrl"] = "https://octowow.st/db/?spell=1756", ["trainLevel"] = 45, }, { @@ -718,7 +718,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16697", + ["sourceUrl"] = "https://octowow.st/db/?spell=16697", ["trainLevel"] = 55, }, { @@ -738,7 +738,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1850", + ["sourceUrl"] = "https://octowow.st/db/?spell=1850", ["trainLevel"] = 26, }, { @@ -758,7 +758,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=9821", + ["sourceUrl"] = "https://octowow.st/db/?spell=9821", ["trainLevel"] = 46, }, { @@ -778,7 +778,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=23110", + ["sourceUrl"] = "https://octowow.st/db/?spell=23110", ["trainLevel"] = 50, }, { @@ -799,7 +799,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36548", + ["sourceUrl"] = "https://octowow.st/db/?spell=36548", ["trainLevel"] = 10, }, { @@ -820,7 +820,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36549", + ["sourceUrl"] = "https://octowow.st/db/?spell=36549", ["trainLevel"] = 20, }, { @@ -841,7 +841,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36550", + ["sourceUrl"] = "https://octowow.st/db/?spell=36550", ["trainLevel"] = 30, }, { @@ -862,7 +862,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36551", + ["sourceUrl"] = "https://octowow.st/db/?spell=36551", ["trainLevel"] = 40, }, { @@ -883,7 +883,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36552", + ["sourceUrl"] = "https://octowow.st/db/?spell=36552", ["trainLevel"] = 50, }, { @@ -904,7 +904,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36553", + ["sourceUrl"] = "https://octowow.st/db/?spell=36553", ["trainLevel"] = 60, }, { @@ -924,7 +924,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=23145", + ["sourceUrl"] = "https://octowow.st/db/?spell=23145", ["trainLevel"] = 30, }, { @@ -944,7 +944,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=23147", + ["sourceUrl"] = "https://octowow.st/db/?spell=23147", ["trainLevel"] = 40, }, { @@ -964,7 +964,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=23148", + ["sourceUrl"] = "https://octowow.st/db/?spell=23148", ["trainLevel"] = 50, }, { @@ -984,7 +984,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24604", + ["sourceUrl"] = "https://octowow.st/db/?spell=24604", ["trainLevel"] = 10, }, { @@ -1004,7 +1004,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24605", + ["sourceUrl"] = "https://octowow.st/db/?spell=24605", ["trainLevel"] = 24, }, { @@ -1024,7 +1024,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24603", + ["sourceUrl"] = "https://octowow.st/db/?spell=24603", ["trainLevel"] = 40, }, { @@ -1044,7 +1044,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24597", + ["sourceUrl"] = "https://octowow.st/db/?spell=24597", ["trainLevel"] = 56, }, { @@ -1064,7 +1064,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46296", + ["sourceUrl"] = "https://octowow.st/db/?spell=46296", ["trainLevel"] = 10, }, { @@ -1084,7 +1084,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=15797", + ["sourceUrl"] = "https://octowow.st/db/?spell=15797", ["trainLevel"] = 20, }, { @@ -1104,7 +1104,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24844", + ["sourceUrl"] = "https://octowow.st/db/?spell=24844", ["trainLevel"] = 1, }, { @@ -1124,7 +1124,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25008", + ["sourceUrl"] = "https://octowow.st/db/?spell=25008", ["trainLevel"] = 12, }, { @@ -1144,7 +1144,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25009", + ["sourceUrl"] = "https://octowow.st/db/?spell=25009", ["trainLevel"] = 24, }, { @@ -1164,7 +1164,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25010", + ["sourceUrl"] = "https://octowow.st/db/?spell=25010", ["trainLevel"] = 36, }, { @@ -1184,7 +1184,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25011", + ["sourceUrl"] = "https://octowow.st/db/?spell=25011", ["trainLevel"] = 48, }, { @@ -1204,7 +1204,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25012", + ["sourceUrl"] = "https://octowow.st/db/?spell=25012", ["trainLevel"] = 60, }, { @@ -1224,7 +1224,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36532", + ["sourceUrl"] = "https://octowow.st/db/?spell=36532", ["trainLevel"] = 20, }, { @@ -1244,7 +1244,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46271", + ["sourceUrl"] = "https://octowow.st/db/?spell=46271", ["trainLevel"] = 15, }, { @@ -1264,7 +1264,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46272", + ["sourceUrl"] = "https://octowow.st/db/?spell=46272", ["trainLevel"] = 45, }, { @@ -1284,7 +1284,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46273", + ["sourceUrl"] = "https://octowow.st/db/?spell=46273", ["trainLevel"] = 60, }, { @@ -1306,7 +1306,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24450", + ["sourceUrl"] = "https://octowow.st/db/?spell=24450", ["trainLevel"] = 30, }, { @@ -1328,7 +1328,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24452", + ["sourceUrl"] = "https://octowow.st/db/?spell=24452", ["trainLevel"] = 40, }, { @@ -1350,7 +1350,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24453", + ["sourceUrl"] = "https://octowow.st/db/?spell=24453", ["trainLevel"] = 50, }, { @@ -1371,7 +1371,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36535", + ["sourceUrl"] = "https://octowow.st/db/?spell=36535", ["trainLevel"] = 36, }, { @@ -1392,7 +1392,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46282", + ["sourceUrl"] = "https://octowow.st/db/?spell=46282", ["trainLevel"] = 1, }, { @@ -1413,7 +1413,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36537", + ["sourceUrl"] = "https://octowow.st/db/?spell=36537", ["trainLevel"] = 12, }, { @@ -1434,7 +1434,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36538", + ["sourceUrl"] = "https://octowow.st/db/?spell=36538", ["trainLevel"] = 24, }, { @@ -1455,7 +1455,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36539", + ["sourceUrl"] = "https://octowow.st/db/?spell=36539", ["trainLevel"] = 36, }, { @@ -1476,7 +1476,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36540", + ["sourceUrl"] = "https://octowow.st/db/?spell=36540", ["trainLevel"] = 48, }, { @@ -1497,7 +1497,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36541", + ["sourceUrl"] = "https://octowow.st/db/?spell=36541", ["trainLevel"] = 60, }, { @@ -1518,7 +1518,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24640", + ["sourceUrl"] = "https://octowow.st/db/?spell=24640", ["trainLevel"] = 8, }, { @@ -1539,7 +1539,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24583", + ["sourceUrl"] = "https://octowow.st/db/?spell=24583", ["trainLevel"] = 24, }, { @@ -1560,7 +1560,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24586", + ["sourceUrl"] = "https://octowow.st/db/?spell=24586", ["trainLevel"] = 40, }, { @@ -1581,7 +1581,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24587", + ["sourceUrl"] = "https://octowow.st/db/?spell=24587", ["trainLevel"] = 56, }, { @@ -1603,7 +1603,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24423", + ["sourceUrl"] = "https://octowow.st/db/?spell=24423", ["trainLevel"] = 8, }, { @@ -1625,7 +1625,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24577", + ["sourceUrl"] = "https://octowow.st/db/?spell=24577", ["trainLevel"] = 24, }, { @@ -1647,7 +1647,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24578", + ["sourceUrl"] = "https://octowow.st/db/?spell=24578", ["trainLevel"] = 48, }, { @@ -1669,7 +1669,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24579", + ["sourceUrl"] = "https://octowow.st/db/?spell=24579", ["trainLevel"] = 56, }, { @@ -1690,7 +1690,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26064", + ["sourceUrl"] = "https://octowow.st/db/?spell=26064", ["trainLevel"] = 20, }, { @@ -1711,7 +1711,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36531", + ["sourceUrl"] = "https://octowow.st/db/?spell=36531", ["trainLevel"] = 10, }, { @@ -1731,7 +1731,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26090", + ["sourceUrl"] = "https://octowow.st/db/?spell=26090", ["trainLevel"] = 30, }, { @@ -1751,7 +1751,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26187", + ["sourceUrl"] = "https://octowow.st/db/?spell=26187", ["trainLevel"] = 40, }, { @@ -1771,7 +1771,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26188", + ["sourceUrl"] = "https://octowow.st/db/?spell=26188", ["trainLevel"] = 50, }, { @@ -1791,7 +1791,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=51156", + ["sourceUrl"] = "https://octowow.st/db/?spell=51156", ["trainLevel"] = 56, }, { @@ -1811,7 +1811,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36533", + ["sourceUrl"] = "https://octowow.st/db/?spell=36533", ["trainLevel"] = 22, }, { @@ -1826,14 +1826,14 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=42051", + ["sourceUrl"] = "https://octowow.st/db/?spell=42051", ["trainLevel"] = 1, }, }, ["byAbility"] = { ["Bite"] = { ["ability"] = "Bite", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Bite", + ["searchUrl"] = "https://octowow.st/db/?search=Bite", ["spellCount"] = 8, ["spells"] = { { @@ -1853,7 +1853,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17253", + ["sourceUrl"] = "https://octowow.st/db/?spell=17253", ["trainLevel"] = 1, }, { @@ -1873,7 +1873,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17255", + ["sourceUrl"] = "https://octowow.st/db/?spell=17255", ["trainLevel"] = 8, }, { @@ -1893,7 +1893,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17256", + ["sourceUrl"] = "https://octowow.st/db/?spell=17256", ["trainLevel"] = 16, }, { @@ -1913,7 +1913,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17257", + ["sourceUrl"] = "https://octowow.st/db/?spell=17257", ["trainLevel"] = 24, }, { @@ -1933,7 +1933,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17258", + ["sourceUrl"] = "https://octowow.st/db/?spell=17258", ["trainLevel"] = 32, }, { @@ -1953,7 +1953,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17259", + ["sourceUrl"] = "https://octowow.st/db/?spell=17259", ["trainLevel"] = 40, }, { @@ -1973,7 +1973,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 7", ["rankNumber"] = 7, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17260", + ["sourceUrl"] = "https://octowow.st/db/?spell=17260", ["trainLevel"] = 48, }, { @@ -1993,14 +1993,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 8", ["rankNumber"] = 8, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=17261", + ["sourceUrl"] = "https://octowow.st/db/?spell=17261", ["trainLevel"] = 56, }, }, }, ["Bubble Barrier"] = { ["ability"] = "Bubble Barrier", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Bubble+Barrier", + ["searchUrl"] = "https://octowow.st/db/?search=Bubble+Barrier", ["spellCount"] = 5, ["spells"] = { { @@ -2020,7 +2020,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Frost", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36527", + ["sourceUrl"] = "https://octowow.st/db/?spell=36527", ["trainLevel"] = 10, }, { @@ -2040,7 +2040,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36523", + ["sourceUrl"] = "https://octowow.st/db/?spell=36523", ["trainLevel"] = 10, }, { @@ -2060,7 +2060,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36524", + ["sourceUrl"] = "https://octowow.st/db/?spell=36524", ["trainLevel"] = 24, }, { @@ -2080,7 +2080,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36525", + ["sourceUrl"] = "https://octowow.st/db/?spell=36525", ["trainLevel"] = 40, }, { @@ -2100,14 +2100,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36526", + ["sourceUrl"] = "https://octowow.st/db/?spell=36526", ["trainLevel"] = 56, }, }, }, ["Charge"] = { ["ability"] = "Charge", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Charge", + ["searchUrl"] = "https://octowow.st/db/?search=Charge", ["spellCount"] = 7, ["spells"] = { { @@ -2128,7 +2128,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=22120", + ["sourceUrl"] = "https://octowow.st/db/?spell=22120", ["trainLevel"] = 20, }, { @@ -2150,7 +2150,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=7371", + ["sourceUrl"] = "https://octowow.st/db/?spell=7371", ["trainLevel"] = 1, }, { @@ -2172,7 +2172,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26177", + ["sourceUrl"] = "https://octowow.st/db/?spell=26177", ["trainLevel"] = 12, }, { @@ -2194,7 +2194,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26178", + ["sourceUrl"] = "https://octowow.st/db/?spell=26178", ["trainLevel"] = 24, }, { @@ -2216,7 +2216,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26179", + ["sourceUrl"] = "https://octowow.st/db/?spell=26179", ["trainLevel"] = 36, }, { @@ -2238,7 +2238,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26201", + ["sourceUrl"] = "https://octowow.st/db/?spell=26201", ["trainLevel"] = 48, }, { @@ -2260,14 +2260,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=27685", + ["sourceUrl"] = "https://octowow.st/db/?spell=27685", ["trainLevel"] = 60, }, }, }, ["Claw"] = { ["ability"] = "Claw", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Claw", + ["searchUrl"] = "https://octowow.st/db/?search=Claw", ["spellCount"] = 8, ["spells"] = { { @@ -2287,7 +2287,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16827", + ["sourceUrl"] = "https://octowow.st/db/?spell=16827", ["trainLevel"] = 1, }, { @@ -2307,7 +2307,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16828", + ["sourceUrl"] = "https://octowow.st/db/?spell=16828", ["trainLevel"] = 8, }, { @@ -2327,7 +2327,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16829", + ["sourceUrl"] = "https://octowow.st/db/?spell=16829", ["trainLevel"] = 16, }, { @@ -2347,7 +2347,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16830", + ["sourceUrl"] = "https://octowow.st/db/?spell=16830", ["trainLevel"] = 24, }, { @@ -2367,7 +2367,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16831", + ["sourceUrl"] = "https://octowow.st/db/?spell=16831", ["trainLevel"] = 32, }, { @@ -2387,7 +2387,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16832", + ["sourceUrl"] = "https://octowow.st/db/?spell=16832", ["trainLevel"] = 40, }, { @@ -2407,7 +2407,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 7", ["rankNumber"] = 7, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=3010", + ["sourceUrl"] = "https://octowow.st/db/?spell=3010", ["trainLevel"] = 48, }, { @@ -2427,14 +2427,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 8", ["rankNumber"] = 8, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=3009", + ["sourceUrl"] = "https://octowow.st/db/?spell=3009", ["trainLevel"] = 56, }, }, }, ["Cower"] = { ["ability"] = "Cower", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Cower", + ["searchUrl"] = "https://octowow.st/db/?search=Cower", ["spellCount"] = 6, ["spells"] = { { @@ -2454,7 +2454,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1742", + ["sourceUrl"] = "https://octowow.st/db/?spell=1742", ["trainLevel"] = 5, }, { @@ -2474,7 +2474,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1753", + ["sourceUrl"] = "https://octowow.st/db/?spell=1753", ["trainLevel"] = 15, }, { @@ -2494,7 +2494,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1754", + ["sourceUrl"] = "https://octowow.st/db/?spell=1754", ["trainLevel"] = 25, }, { @@ -2514,7 +2514,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1755", + ["sourceUrl"] = "https://octowow.st/db/?spell=1755", ["trainLevel"] = 35, }, { @@ -2534,7 +2534,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1756", + ["sourceUrl"] = "https://octowow.st/db/?spell=1756", ["trainLevel"] = 45, }, { @@ -2554,14 +2554,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=16697", + ["sourceUrl"] = "https://octowow.st/db/?spell=16697", ["trainLevel"] = 55, }, }, }, ["Dash"] = { ["ability"] = "Dash", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Dash", + ["searchUrl"] = "https://octowow.st/db/?search=Dash", ["spellCount"] = 3, ["spells"] = { { @@ -2581,7 +2581,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=1850", + ["sourceUrl"] = "https://octowow.st/db/?spell=1850", ["trainLevel"] = 26, }, { @@ -2601,7 +2601,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=9821", + ["sourceUrl"] = "https://octowow.st/db/?spell=9821", ["trainLevel"] = 46, }, { @@ -2621,14 +2621,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=23110", + ["sourceUrl"] = "https://octowow.st/db/?spell=23110", ["trainLevel"] = 50, }, }, }, ["Death Roll"] = { ["ability"] = "Death Roll", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Death+Roll", + ["searchUrl"] = "https://octowow.st/db/?search=Death+Roll", ["spellCount"] = 6, ["spells"] = { { @@ -2649,7 +2649,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36548", + ["sourceUrl"] = "https://octowow.st/db/?spell=36548", ["trainLevel"] = 10, }, { @@ -2670,7 +2670,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36549", + ["sourceUrl"] = "https://octowow.st/db/?spell=36549", ["trainLevel"] = 20, }, { @@ -2691,7 +2691,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36550", + ["sourceUrl"] = "https://octowow.st/db/?spell=36550", ["trainLevel"] = 30, }, { @@ -2712,7 +2712,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36551", + ["sourceUrl"] = "https://octowow.st/db/?spell=36551", ["trainLevel"] = 40, }, { @@ -2733,7 +2733,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36552", + ["sourceUrl"] = "https://octowow.st/db/?spell=36552", ["trainLevel"] = 50, }, { @@ -2754,14 +2754,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36553", + ["sourceUrl"] = "https://octowow.st/db/?spell=36553", ["trainLevel"] = 60, }, }, }, ["Dive"] = { ["ability"] = "Dive", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Dive", + ["searchUrl"] = "https://octowow.st/db/?search=Dive", ["spellCount"] = 3, ["spells"] = { { @@ -2781,7 +2781,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=23145", + ["sourceUrl"] = "https://octowow.st/db/?spell=23145", ["trainLevel"] = 30, }, { @@ -2801,7 +2801,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=23147", + ["sourceUrl"] = "https://octowow.st/db/?spell=23147", ["trainLevel"] = 40, }, { @@ -2821,14 +2821,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=23148", + ["sourceUrl"] = "https://octowow.st/db/?spell=23148", ["trainLevel"] = 50, }, }, }, ["Furious Howl"] = { ["ability"] = "Furious Howl", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Furious+Howl", + ["searchUrl"] = "https://octowow.st/db/?search=Furious+Howl", ["spellCount"] = 4, ["spells"] = { { @@ -2848,7 +2848,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24604", + ["sourceUrl"] = "https://octowow.st/db/?spell=24604", ["trainLevel"] = 10, }, { @@ -2868,7 +2868,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24605", + ["sourceUrl"] = "https://octowow.st/db/?spell=24605", ["trainLevel"] = 24, }, { @@ -2888,7 +2888,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24603", + ["sourceUrl"] = "https://octowow.st/db/?spell=24603", ["trainLevel"] = 40, }, { @@ -2908,14 +2908,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24597", + ["sourceUrl"] = "https://octowow.st/db/?spell=24597", ["trainLevel"] = 56, }, }, }, ["Grace"] = { ["ability"] = "Grace", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Grace", + ["searchUrl"] = "https://octowow.st/db/?search=Grace", ["spellCount"] = 1, ["spells"] = { { @@ -2935,14 +2935,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46296", + ["sourceUrl"] = "https://octowow.st/db/?spell=46296", ["trainLevel"] = 10, }, }, }, ["Lightning Breath"] = { ["ability"] = "Lightning Breath", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Lightning+Breath", + ["searchUrl"] = "https://octowow.st/db/?search=Lightning+Breath", ["spellCount"] = 7, ["spells"] = { { @@ -2962,7 +2962,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=15797", + ["sourceUrl"] = "https://octowow.st/db/?spell=15797", ["trainLevel"] = 20, }, { @@ -2982,7 +2982,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24844", + ["sourceUrl"] = "https://octowow.st/db/?spell=24844", ["trainLevel"] = 1, }, { @@ -3002,7 +3002,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25008", + ["sourceUrl"] = "https://octowow.st/db/?spell=25008", ["trainLevel"] = 12, }, { @@ -3022,7 +3022,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25009", + ["sourceUrl"] = "https://octowow.st/db/?spell=25009", ["trainLevel"] = 24, }, { @@ -3042,7 +3042,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25010", + ["sourceUrl"] = "https://octowow.st/db/?spell=25010", ["trainLevel"] = 36, }, { @@ -3062,7 +3062,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25011", + ["sourceUrl"] = "https://octowow.st/db/?spell=25011", ["trainLevel"] = 48, }, { @@ -3082,14 +3082,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=25012", + ["sourceUrl"] = "https://octowow.st/db/?spell=25012", ["trainLevel"] = 60, }, }, }, ["Packleader"] = { ["ability"] = "Packleader", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Packleader", + ["searchUrl"] = "https://octowow.st/db/?search=Packleader", ["spellCount"] = 1, ["spells"] = { { @@ -3109,14 +3109,14 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36532", + ["sourceUrl"] = "https://octowow.st/db/?spell=36532", ["trainLevel"] = 20, }, }, }, ["Poison Spit"] = { ["ability"] = "Poison Spit", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Poison+Spit", + ["searchUrl"] = "https://octowow.st/db/?search=Poison+Spit", ["spellCount"] = 3, ["spells"] = { { @@ -3136,7 +3136,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46271", + ["sourceUrl"] = "https://octowow.st/db/?spell=46271", ["trainLevel"] = 15, }, { @@ -3156,7 +3156,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46272", + ["sourceUrl"] = "https://octowow.st/db/?spell=46272", ["trainLevel"] = 45, }, { @@ -3176,14 +3176,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46273", + ["sourceUrl"] = "https://octowow.st/db/?spell=46273", ["trainLevel"] = 60, }, }, }, ["Prowl"] = { ["ability"] = "Prowl", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Prowl", + ["searchUrl"] = "https://octowow.st/db/?search=Prowl", ["spellCount"] = 3, ["spells"] = { { @@ -3205,7 +3205,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24450", + ["sourceUrl"] = "https://octowow.st/db/?spell=24450", ["trainLevel"] = 30, }, { @@ -3227,7 +3227,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24452", + ["sourceUrl"] = "https://octowow.st/db/?spell=24452", ["trainLevel"] = 40, }, { @@ -3249,14 +3249,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24453", + ["sourceUrl"] = "https://octowow.st/db/?spell=24453", ["trainLevel"] = 50, }, }, }, ["Roar of Fortitude"] = { ["ability"] = "Roar of Fortitude", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Roar+of+Fortitude", + ["searchUrl"] = "https://octowow.st/db/?search=Roar+of+Fortitude", ["spellCount"] = 1, ["spells"] = { { @@ -3277,14 +3277,14 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36535", + ["sourceUrl"] = "https://octowow.st/db/?spell=36535", ["trainLevel"] = 36, }, }, }, ["Savage Rend"] = { ["ability"] = "Savage Rend", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Savage+Rend", + ["searchUrl"] = "https://octowow.st/db/?search=Savage+Rend", ["spellCount"] = 6, ["spells"] = { { @@ -3305,7 +3305,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=46282", + ["sourceUrl"] = "https://octowow.st/db/?spell=46282", ["trainLevel"] = 1, }, { @@ -3326,7 +3326,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36537", + ["sourceUrl"] = "https://octowow.st/db/?spell=36537", ["trainLevel"] = 12, }, { @@ -3347,7 +3347,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36538", + ["sourceUrl"] = "https://octowow.st/db/?spell=36538", ["trainLevel"] = 24, }, { @@ -3368,7 +3368,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36539", + ["sourceUrl"] = "https://octowow.st/db/?spell=36539", ["trainLevel"] = 36, }, { @@ -3389,7 +3389,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 5", ["rankNumber"] = 5, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36540", + ["sourceUrl"] = "https://octowow.st/db/?spell=36540", ["trainLevel"] = 48, }, { @@ -3410,14 +3410,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 6", ["rankNumber"] = 6, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36541", + ["sourceUrl"] = "https://octowow.st/db/?spell=36541", ["trainLevel"] = 60, }, }, }, ["Scorpid Poison"] = { ["ability"] = "Scorpid Poison", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Scorpid+Poison", + ["searchUrl"] = "https://octowow.st/db/?search=Scorpid+Poison", ["spellCount"] = 4, ["spells"] = { { @@ -3438,7 +3438,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24640", + ["sourceUrl"] = "https://octowow.st/db/?spell=24640", ["trainLevel"] = 8, }, { @@ -3459,7 +3459,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24583", + ["sourceUrl"] = "https://octowow.st/db/?spell=24583", ["trainLevel"] = 24, }, { @@ -3480,7 +3480,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24586", + ["sourceUrl"] = "https://octowow.st/db/?spell=24586", ["trainLevel"] = 40, }, { @@ -3501,14 +3501,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24587", + ["sourceUrl"] = "https://octowow.st/db/?spell=24587", ["trainLevel"] = 56, }, }, }, ["Screech"] = { ["ability"] = "Screech", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Screech", + ["searchUrl"] = "https://octowow.st/db/?search=Screech", ["spellCount"] = 4, ["spells"] = { { @@ -3530,7 +3530,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24423", + ["sourceUrl"] = "https://octowow.st/db/?spell=24423", ["trainLevel"] = 8, }, { @@ -3552,7 +3552,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24577", + ["sourceUrl"] = "https://octowow.st/db/?spell=24577", ["trainLevel"] = 24, }, { @@ -3574,7 +3574,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24578", + ["sourceUrl"] = "https://octowow.st/db/?spell=24578", ["trainLevel"] = 48, }, { @@ -3596,14 +3596,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=24579", + ["sourceUrl"] = "https://octowow.st/db/?spell=24579", ["trainLevel"] = 56, }, }, }, ["Shell Shield"] = { ["ability"] = "Shell Shield", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Shell+Shield", + ["searchUrl"] = "https://octowow.st/db/?search=Shell+Shield", ["spellCount"] = 1, ["spells"] = { { @@ -3624,14 +3624,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26064", + ["sourceUrl"] = "https://octowow.st/db/?spell=26064", ["trainLevel"] = 20, }, }, }, ["Strider Presence"] = { ["ability"] = "Strider Presence", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Strider+Presence", + ["searchUrl"] = "https://octowow.st/db/?search=Strider+Presence", ["spellCount"] = 1, ["spells"] = { { @@ -3652,14 +3652,14 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36531", + ["sourceUrl"] = "https://octowow.st/db/?spell=36531", ["trainLevel"] = 10, }, }, }, ["Thunderstomp"] = { ["ability"] = "Thunderstomp", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Thunderstomp", + ["searchUrl"] = "https://octowow.st/db/?search=Thunderstomp", ["spellCount"] = 4, ["spells"] = { { @@ -3679,7 +3679,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 1", ["rankNumber"] = 1, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26090", + ["sourceUrl"] = "https://octowow.st/db/?spell=26090", ["trainLevel"] = 30, }, { @@ -3699,7 +3699,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 2", ["rankNumber"] = 2, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26187", + ["sourceUrl"] = "https://octowow.st/db/?spell=26187", ["trainLevel"] = 40, }, { @@ -3719,7 +3719,7 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 3", ["rankNumber"] = 3, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=26188", + ["sourceUrl"] = "https://octowow.st/db/?spell=26188", ["trainLevel"] = 50, }, { @@ -3739,14 +3739,14 @@ MTH_DS_PetSpells = { ["rank"] = "Rank 4", ["rankNumber"] = 4, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=51156", + ["sourceUrl"] = "https://octowow.st/db/?spell=51156", ["trainLevel"] = 56, }, }, }, ["Web"] = { ["ability"] = "Web", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Web", + ["searchUrl"] = "https://octowow.st/db/?search=Web", ["spellCount"] = 1, ["spells"] = { { @@ -3766,14 +3766,14 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Physical", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=36533", + ["sourceUrl"] = "https://octowow.st/db/?spell=36533", ["trainLevel"] = 22, }, }, }, ["Pollen Burst"] = { ["ability"] = "Pollen Burst", - ["searchUrl"] = "https://database.turtlecraft.gg/?search=Pollen+Burst", + ["searchUrl"] = "https://octowow.st/db/?search=Pollen+Burst", ["spellCount"] = 1, ["spells"] = { { @@ -3788,7 +3788,7 @@ MTH_DS_PetSpells = { ["rank"] = "", ["rankNumber"] = 0, ["school"] = "Nature", - ["sourceUrl"] = "https://database.turtlecraft.gg/?spell=42051", + ["sourceUrl"] = "https://octowow.st/db/?spell=42051", ["trainLevel"] = 1, }, }, @@ -4387,6 +4387,6 @@ MTH_DS_PetSpells = { }, ["droppedDuplicatesCount"] = 63, ["generatedAt"] = "2026-02-15 20:10:11", - ["source"] = "https://database.turtlecraft.gg/", + ["source"] = "https://octowow.st/db/", ["totalSpellRows"] = 88, } diff --git a/data/ds-racial.lua b/data/ds-racial.lua index 8e9d032..cf75e1b 100644 --- a/data/ds-racial.lua +++ b/data/ds-racial.lua @@ -6,7 +6,7 @@ MTH_DS_Racial = { version = "1.0", generatedAtUTC = "2026-02-20T00:00:00Z", source = { - site = "https://database.turtlecraft.gg", + site = "https://octowow.st/db", runRoot = ".TurtleDBparsing/racial-audit/runs/20260220-164554Z", selection = "user-curated-approved", }, @@ -25,46 +25,46 @@ MTH_DS_Racial = { ["Human"] = { 20599, 20600, 20597, 20864, 20598 }, }, spellsById = { - [2481] = { id = 2481, name = "Find Treasure", rank = "Racial", level = 1, school = 0, race = "Dwarf", sourceUrl = "https://database.turtlecraft.gg/?spell=2481" }, - [5227] = { id = 5227, name = "Underwater Breathing", rank = "Racial Passive", level = 0, school = 0, race = "Undead", sourceUrl = "https://database.turtlecraft.gg/?spell=5227" }, - [7340] = { id = 7340, name = "Language Gnomish", rank = "", level = 1, school = 0, race = "Gnome", sourceUrl = "https://database.turtlecraft.gg/?spell=7340" }, - [7744] = { id = 7744, name = "Will of the Forsaken", rank = "Racial", level = 0, school = 0, race = "Undead", sourceUrl = "https://database.turtlecraft.gg/?spell=7744" }, - [20549] = { id = 20549, name = "War Stomp", rank = "Racial", level = 0, school = 0, race = "Tauren", sourceUrl = "https://database.turtlecraft.gg/?spell=20549" }, - [20550] = { id = 20550, name = "Endurance", rank = "Racial Passive", level = 0, school = 0, race = "Tauren", sourceUrl = "https://database.turtlecraft.gg/?spell=20550" }, - [20551] = { id = 20551, name = "Nature Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Tauren", raceGuess = true, sourceUrl = "https://database.turtlecraft.gg/?spell=20551" }, - [20552] = { id = 20552, name = "Cultivation", rank = "Racial Passive", level = 0, school = 0, race = "Tauren", sourceUrl = "https://database.turtlecraft.gg/?spell=20552" }, - [20554] = { id = 20554, name = "Berserking", rank = "Racial", level = 0, school = 0, race = "Troll", sourceUrl = "https://database.turtlecraft.gg/?spell=20554" }, - [20555] = { id = 20555, name = "Regeneration", rank = "Racial Passive", level = 0, school = 0, race = "Troll", sourceUrl = "https://database.turtlecraft.gg/?spell=20555" }, - [20557] = { id = 20557, name = "Beast Slaying", rank = "Racial Passive", level = 0, school = 0, race = "Troll", sourceUrl = "https://database.turtlecraft.gg/?spell=20557" }, - [20558] = { id = 20558, name = "Throwing Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Troll", sourceUrl = "https://database.turtlecraft.gg/?spell=20558" }, - [20572] = { id = 20572, name = "Blood Fury", rank = "Racial", level = 0, school = 0, race = "Orc", sourceUrl = "https://database.turtlecraft.gg/?spell=20572" }, - [20573] = { id = 20573, name = "Hardiness", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://database.turtlecraft.gg/?spell=20573" }, - [20574] = { id = 20574, name = "Axe Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://database.turtlecraft.gg/?spell=20574" }, - [20575] = { id = 20575, name = "Command", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://database.turtlecraft.gg/?spell=20575" }, - [20576] = { id = 20576, name = "Command", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://database.turtlecraft.gg/?spell=20576" }, - [20577] = { id = 20577, name = "Cannibalize", rank = "Racial", level = 0, school = 0, race = "Undead", sourceUrl = "https://database.turtlecraft.gg/?spell=20577" }, - [20579] = { id = 20579, name = "Shadow Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Undead", sourceUrl = "https://database.turtlecraft.gg/?spell=20579" }, - [20580] = { id = 20580, name = "Shadowmeld", rank = "Racial", level = 1, school = 0, race = "Night Elf", sourceUrl = "https://database.turtlecraft.gg/?spell=20580" }, - [20582] = { id = 20582, name = "Quickness", rank = "Racial Passive", level = 0, school = 0, race = "Night Elf", sourceUrl = "https://database.turtlecraft.gg/?spell=20582" }, - [20583] = { id = 20583, name = "Nature Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Night Elf", raceGuess = true, sourceUrl = "https://database.turtlecraft.gg/?spell=20583" }, - [20585] = { id = 20585, name = "Wisp Spirit", rank = "Racial Passive", level = 0, school = 0, race = "Night Elf", sourceUrl = "https://database.turtlecraft.gg/?spell=20585" }, - [20589] = { id = 20589, name = "Escape Artist", rank = "Racial", level = 0, school = 0, race = "Gnome", sourceUrl = "https://database.turtlecraft.gg/?spell=20589" }, - [20591] = { id = 20591, name = "Expansive Mind", rank = "Racial Passive", level = 0, school = 0, race = "Gnome", sourceUrl = "https://database.turtlecraft.gg/?spell=20591" }, - [20592] = { id = 20592, name = "Arcane Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Gnome", sourceUrl = "https://database.turtlecraft.gg/?spell=20592" }, - [20593] = { id = 20593, name = "Engineering Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Gnome", sourceUrl = "https://database.turtlecraft.gg/?spell=20593" }, - [20594] = { id = 20594, name = "Stoneform", rank = "Racial", level = 0, school = 0, race = "Dwarf", sourceUrl = "https://database.turtlecraft.gg/?spell=20594" }, - [20595] = { id = 20595, name = "Gun Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Dwarf", sourceUrl = "https://database.turtlecraft.gg/?spell=20595" }, - [20596] = { id = 20596, name = "Frost Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Dwarf", sourceUrl = "https://database.turtlecraft.gg/?spell=20596" }, - [20597] = { id = 20597, name = "Sword Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Human", sourceUrl = "https://database.turtlecraft.gg/?spell=20597" }, - [20598] = { id = 20598, name = "The Human Spirit", rank = "Racial Passive", level = 0, school = 0, race = "Human", sourceUrl = "https://database.turtlecraft.gg/?spell=20598" }, - [20599] = { id = 20599, name = "Diplomacy", rank = "Racial Passive", level = 0, school = 0, race = "Human", sourceUrl = "https://database.turtlecraft.gg/?spell=20599" }, - [20600] = { id = 20600, name = "Perception", rank = "Racial", level = 0, school = 0, race = "Human", sourceUrl = "https://database.turtlecraft.gg/?spell=20600" }, - [20864] = { id = 20864, name = "Mace Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Human", sourceUrl = "https://database.turtlecraft.gg/?spell=20864" }, - [21009] = { id = 21009, name = "Shadowmeld Passive", rank = "Racial Passive", level = 0, school = 0, race = "Night Elf", sourceUrl = "https://database.turtlecraft.gg/?spell=21009" }, - [21563] = { id = 21563, name = "Command", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://database.turtlecraft.gg/?spell=21563" }, - [26290] = { id = 26290, name = "Bow Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Troll", sourceUrl = "https://database.turtlecraft.gg/?spell=26290" }, - [26296] = { id = 26296, name = "Berserking", rank = "Racial", level = 0, school = 0, race = "Troll", sourceUrl = "https://database.turtlecraft.gg/?spell=26296" }, - [26297] = { id = 26297, name = "Berserking", rank = "Racial", level = 0, school = 0, race = "Troll", sourceUrl = "https://database.turtlecraft.gg/?spell=26297" }, - [52522] = { id = 52522, name = "Vengeance", rank = "Racial Passive", level = 0, school = 0, race = "Undead", sourceUrl = "https://database.turtlecraft.gg/?spell=52522" }, + [2481] = { id = 2481, name = "Find Treasure", rank = "Racial", level = 1, school = 0, race = "Dwarf", sourceUrl = "https://octowow.st/db/?spell=2481" }, + [5227] = { id = 5227, name = "Underwater Breathing", rank = "Racial Passive", level = 0, school = 0, race = "Undead", sourceUrl = "https://octowow.st/db/?spell=5227" }, + [7340] = { id = 7340, name = "Language Gnomish", rank = "", level = 1, school = 0, race = "Gnome", sourceUrl = "https://octowow.st/db/?spell=7340" }, + [7744] = { id = 7744, name = "Will of the Forsaken", rank = "Racial", level = 0, school = 0, race = "Undead", sourceUrl = "https://octowow.st/db/?spell=7744" }, + [20549] = { id = 20549, name = "War Stomp", rank = "Racial", level = 0, school = 0, race = "Tauren", sourceUrl = "https://octowow.st/db/?spell=20549" }, + [20550] = { id = 20550, name = "Endurance", rank = "Racial Passive", level = 0, school = 0, race = "Tauren", sourceUrl = "https://octowow.st/db/?spell=20550" }, + [20551] = { id = 20551, name = "Nature Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Tauren", raceGuess = true, sourceUrl = "https://octowow.st/db/?spell=20551" }, + [20552] = { id = 20552, name = "Cultivation", rank = "Racial Passive", level = 0, school = 0, race = "Tauren", sourceUrl = "https://octowow.st/db/?spell=20552" }, + [20554] = { id = 20554, name = "Berserking", rank = "Racial", level = 0, school = 0, race = "Troll", sourceUrl = "https://octowow.st/db/?spell=20554" }, + [20555] = { id = 20555, name = "Regeneration", rank = "Racial Passive", level = 0, school = 0, race = "Troll", sourceUrl = "https://octowow.st/db/?spell=20555" }, + [20557] = { id = 20557, name = "Beast Slaying", rank = "Racial Passive", level = 0, school = 0, race = "Troll", sourceUrl = "https://octowow.st/db/?spell=20557" }, + [20558] = { id = 20558, name = "Throwing Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Troll", sourceUrl = "https://octowow.st/db/?spell=20558" }, + [20572] = { id = 20572, name = "Blood Fury", rank = "Racial", level = 0, school = 0, race = "Orc", sourceUrl = "https://octowow.st/db/?spell=20572" }, + [20573] = { id = 20573, name = "Hardiness", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://octowow.st/db/?spell=20573" }, + [20574] = { id = 20574, name = "Axe Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://octowow.st/db/?spell=20574" }, + [20575] = { id = 20575, name = "Command", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://octowow.st/db/?spell=20575" }, + [20576] = { id = 20576, name = "Command", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://octowow.st/db/?spell=20576" }, + [20577] = { id = 20577, name = "Cannibalize", rank = "Racial", level = 0, school = 0, race = "Undead", sourceUrl = "https://octowow.st/db/?spell=20577" }, + [20579] = { id = 20579, name = "Shadow Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Undead", sourceUrl = "https://octowow.st/db/?spell=20579" }, + [20580] = { id = 20580, name = "Shadowmeld", rank = "Racial", level = 1, school = 0, race = "Night Elf", sourceUrl = "https://octowow.st/db/?spell=20580" }, + [20582] = { id = 20582, name = "Quickness", rank = "Racial Passive", level = 0, school = 0, race = "Night Elf", sourceUrl = "https://octowow.st/db/?spell=20582" }, + [20583] = { id = 20583, name = "Nature Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Night Elf", raceGuess = true, sourceUrl = "https://octowow.st/db/?spell=20583" }, + [20585] = { id = 20585, name = "Wisp Spirit", rank = "Racial Passive", level = 0, school = 0, race = "Night Elf", sourceUrl = "https://octowow.st/db/?spell=20585" }, + [20589] = { id = 20589, name = "Escape Artist", rank = "Racial", level = 0, school = 0, race = "Gnome", sourceUrl = "https://octowow.st/db/?spell=20589" }, + [20591] = { id = 20591, name = "Expansive Mind", rank = "Racial Passive", level = 0, school = 0, race = "Gnome", sourceUrl = "https://octowow.st/db/?spell=20591" }, + [20592] = { id = 20592, name = "Arcane Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Gnome", sourceUrl = "https://octowow.st/db/?spell=20592" }, + [20593] = { id = 20593, name = "Engineering Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Gnome", sourceUrl = "https://octowow.st/db/?spell=20593" }, + [20594] = { id = 20594, name = "Stoneform", rank = "Racial", level = 0, school = 0, race = "Dwarf", sourceUrl = "https://octowow.st/db/?spell=20594" }, + [20595] = { id = 20595, name = "Gun Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Dwarf", sourceUrl = "https://octowow.st/db/?spell=20595" }, + [20596] = { id = 20596, name = "Frost Resistance", rank = "Racial Passive", level = 0, school = 0, race = "Dwarf", sourceUrl = "https://octowow.st/db/?spell=20596" }, + [20597] = { id = 20597, name = "Sword Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Human", sourceUrl = "https://octowow.st/db/?spell=20597" }, + [20598] = { id = 20598, name = "The Human Spirit", rank = "Racial Passive", level = 0, school = 0, race = "Human", sourceUrl = "https://octowow.st/db/?spell=20598" }, + [20599] = { id = 20599, name = "Diplomacy", rank = "Racial Passive", level = 0, school = 0, race = "Human", sourceUrl = "https://octowow.st/db/?spell=20599" }, + [20600] = { id = 20600, name = "Perception", rank = "Racial", level = 0, school = 0, race = "Human", sourceUrl = "https://octowow.st/db/?spell=20600" }, + [20864] = { id = 20864, name = "Mace Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Human", sourceUrl = "https://octowow.st/db/?spell=20864" }, + [21009] = { id = 21009, name = "Shadowmeld Passive", rank = "Racial Passive", level = 0, school = 0, race = "Night Elf", sourceUrl = "https://octowow.st/db/?spell=21009" }, + [21563] = { id = 21563, name = "Command", rank = "Racial Passive", level = 0, school = 0, race = "Orc", sourceUrl = "https://octowow.st/db/?spell=21563" }, + [26290] = { id = 26290, name = "Bow Specialization", rank = "Racial Passive", level = 0, school = 0, race = "Troll", sourceUrl = "https://octowow.st/db/?spell=26290" }, + [26296] = { id = 26296, name = "Berserking", rank = "Racial", level = 0, school = 0, race = "Troll", sourceUrl = "https://octowow.st/db/?spell=26296" }, + [26297] = { id = 26297, name = "Berserking", rank = "Racial", level = 0, school = 0, race = "Troll", sourceUrl = "https://octowow.st/db/?spell=26297" }, + [52522] = { id = 52522, name = "Vengeance", rank = "Racial Passive", level = 0, school = 0, race = "Undead", sourceUrl = "https://octowow.st/db/?spell=52522" }, }, } \ No newline at end of file diff --git a/data/init.lua b/data/init.lua index f586fff..bd8a1b6 100644 --- a/data/init.lua +++ b/data/init.lua @@ -212,7 +212,7 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() rank = src.rank, rankNumber = src.rankNumber, school = "Physical", - sourceUrl = "https://database.turtlecraft.gg/?spell=" .. tostring(src.id), + sourceUrl = "https://octowow.st/db/?spell=" .. tostring(src.id), trainLevel = src.trainLevel, } table.insert(growlSpells, row) @@ -262,7 +262,7 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() rank = src.rank, rankNumber = src.rankNumber, school = "Physical", - sourceUrl = "https://database.turtlecraft.gg/?spell=" .. tostring(src.id), + sourceUrl = "https://octowow.st/db/?spell=" .. tostring(src.id), trainLevel = src.trainLevel, } table.insert(staminaSpells, row) @@ -312,7 +312,7 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() rank = src.rank, rankNumber = src.rankNumber, school = "Physical", - sourceUrl = "https://database.turtlecraft.gg/?spell=" .. tostring(src.id), + sourceUrl = "https://octowow.st/db/?spell=" .. tostring(src.id), trainLevel = src.trainLevel, } table.insert(armorSpells, row) @@ -356,7 +356,7 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() rank = src.rank, rankNumber = src.rankNumber, school = "Physical", - sourceUrl = "https://database.turtlecraft.gg/?spell=" .. tostring(src.id), + sourceUrl = "https://octowow.st/db/?spell=" .. tostring(src.id), trainLevel = src.trainLevel, } table.insert(fireResSpells, row) @@ -400,7 +400,7 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() rank = src.rank, rankNumber = src.rankNumber, school = "Physical", - sourceUrl = "https://database.turtlecraft.gg/?spell=" .. tostring(src.id), + sourceUrl = "https://octowow.st/db/?spell=" .. tostring(src.id), trainLevel = src.trainLevel, } table.insert(frostResSpells, row) @@ -444,7 +444,7 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() rank = src.rank, rankNumber = src.rankNumber, school = "Physical", - sourceUrl = "https://database.turtlecraft.gg/?spell=" .. tostring(src.id), + sourceUrl = "https://octowow.st/db/?spell=" .. tostring(src.id), trainLevel = src.trainLevel, } table.insert(arcaneResSpells, row) @@ -488,7 +488,7 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() rank = src.rank, rankNumber = src.rankNumber, school = "Physical", - sourceUrl = "https://database.turtlecraft.gg/?spell=" .. tostring(src.id), + sourceUrl = "https://octowow.st/db/?spell=" .. tostring(src.id), trainLevel = src.trainLevel, } table.insert(natureResSpells, row) @@ -532,7 +532,7 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() rank = src.rank, rankNumber = src.rankNumber, school = "Physical", - sourceUrl = "https://database.turtlecraft.gg/?spell=" .. tostring(src.id), + sourceUrl = "https://octowow.st/db/?spell=" .. tostring(src.id), trainLevel = src.trainLevel, } table.insert(shadowResSpells, row) @@ -556,56 +556,56 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() MTH_DS_PetSpells.byAbility["Growl"] = { ability = "Growl", - searchUrl = "https://database.turtlecraft.gg/?search=Growl", + searchUrl = "https://octowow.st/db/?search=Growl", spellCount = table.getn(growlSpells), spells = growlSpells, } MTH_DS_PetSpells.byAbility["Great Stamina"] = { ability = "Great Stamina", - searchUrl = "https://database.turtlecraft.gg/?search=Great+Stamina", + searchUrl = "https://octowow.st/db/?search=Great+Stamina", spellCount = table.getn(staminaSpells), spells = staminaSpells, } MTH_DS_PetSpells.byAbility["Natural Armor"] = { ability = "Natural Armor", - searchUrl = "https://database.turtlecraft.gg/?search=Natural+Armor", + searchUrl = "https://octowow.st/db/?search=Natural+Armor", spellCount = table.getn(armorSpells), spells = armorSpells, } MTH_DS_PetSpells.byAbility["Fire Resistance"] = { ability = "Fire Resistance", - searchUrl = "https://database.turtlecraft.gg/?search=Fire+Resistance", + searchUrl = "https://octowow.st/db/?search=Fire+Resistance", spellCount = table.getn(fireResSpells), spells = fireResSpells, } MTH_DS_PetSpells.byAbility["Frost Resistance"] = { ability = "Frost Resistance", - searchUrl = "https://database.turtlecraft.gg/?search=Frost+Resistance", + searchUrl = "https://octowow.st/db/?search=Frost+Resistance", spellCount = table.getn(frostResSpells), spells = frostResSpells, } MTH_DS_PetSpells.byAbility["Arcane Resistance"] = { ability = "Arcane Resistance", - searchUrl = "https://database.turtlecraft.gg/?search=Arcane+Resistance", + searchUrl = "https://octowow.st/db/?search=Arcane+Resistance", spellCount = table.getn(arcaneResSpells), spells = arcaneResSpells, } MTH_DS_PetSpells.byAbility["Nature Resistance"] = { ability = "Nature Resistance", - searchUrl = "https://database.turtlecraft.gg/?search=Nature+Resistance", + searchUrl = "https://octowow.st/db/?search=Nature+Resistance", spellCount = table.getn(natureResSpells), spells = natureResSpells, } MTH_DS_PetSpells.byAbility["Shadow Resistance"] = { ability = "Shadow Resistance", - searchUrl = "https://database.turtlecraft.gg/?search=Shadow+Resistance", + searchUrl = "https://octowow.st/db/?search=Shadow+Resistance", spellCount = table.getn(shadowResSpells), spells = shadowResSpells, } @@ -613,6 +613,66 @@ local function MTH_DS_EnsurePetSpellsTrainerGrowl() MTH_DS_PetSpells.totalSpellRows = table.getn(MTH_DS_PetSpells.allSpells) end +-- Restore the "unique appearance" flag on beasts. +-- This is a curated allowlist of beasts whose look is genuinely one-of-a-kind +-- (named individuals / distinctive rares that a collector prizes). We do NOT derive +-- this from the skin datastore's unique flag: that flag only means "this texture +-- appears once in the DBC", which over-marks whole color-variant families (e.g. every +-- Moth and Fox colour) and would wrongly flag common mobs like Frenzied Dustwing. +-- Keyed by NPC id (names such as "Crimson Lynx" repeat across entries). Hand-maintained. +local MTH_DS_UniqueBeastIds = { + [5828] = true, -- Humar the Pridelord (Cats) + [61599] = true, -- Darksaber (Cats) + [61699] = true, -- Shar'lan (Cats) + [7434] = true, -- Frostsaber Pride Watcher (Cats) + [3068] = true, -- Mazzranache (Tallstriders) + [61789] = true, -- Moonfeather (Tallstriders) + [61488] = true, -- Azurebeak (Owls) + [59998] = true, -- Blue Owl (Owls) + [59997] = true, -- Red Owl (Owls) + [63147] = true, -- Withered Companion (Owls) + [62778] = true, -- Ahgk'tos the Pure (Spiders) + [8211] = true, -- Old Cliff Jumper (Wolves) + [4512] = true, -- Rotting Agam'ar (Boars) + [62750] = true, -- Magmalash Scorpid (Scorpids) + [11371] = true, -- Razzashi Serpent (Serpents) + [80260] = true, -- Spirit Fox (Foxes) + [61716] = true, -- Crimson Hawkstrider (Tallstriders) + [61691] = true, -- Forest Hawkstrider (Tallstriders) + [61695] = true, -- Ivory Hawkstrider (Tallstriders) + [62817] = true, -- Moonstrider Matriarch (Tallstriders) + [62818] = true, -- Moonstrider Rooster (Tallstriders) + [61774] = true, -- Brilliant Mana Wyrm (Wind Serpents) + [61775] = true, -- Emerald Mana Wyrm (Wind Serpents) + [61776] = true, -- Lavender Mana Wyrm (Wind Serpents) + [61480] = true, -- Brilliant Wind Serpent (Wind Serpents) + [1815] = true, -- Diseased Black Bear (Bears) + [80466] = true, -- Tirisfal Plagued Bear (Bears) + [62820] = true, -- Nightpelt Grizzly (Bears) + [62821] = true, -- Nightpelt Ursa (Bears) + [80816] = true, -- Hinterlands Eagle (Owls) + [61697] = true, -- Bright Lynx (Cats) + [61698] = true, -- Bright Lynx Matriarch (Cats) + [62338] = true, -- Stormwing Buzzard (Carrion Birds) + [62339] = true, -- Stormwing Vulture (Carrion Birds) +} + +local function MTH_DS_DeriveBeastUnique() + if type(MTH_DS_Beasts) ~= "table" then + return + end + + for npcId, beast in pairs(MTH_DS_Beasts) do + if type(beast) == "table" then + if MTH_DS_UniqueBeastIds[npcId] then + beast.unique = true + else + beast.unique = nil + end + end + end +end + function MTH_DS_OnLoad() if MTH_DS.loaded then return @@ -623,6 +683,7 @@ function MTH_DS_OnLoad() if MTH_DS_Beasts and MTH_DS_Vendors and MTH_DS_AmmoItems and MTH_DS_Zones and MTH_DS_Families then MTH_DS_EnsurePetSpellsTrainerGrowl() + MTH_DS_DeriveBeastUnique() MTH_DS.loaded = true -- Count entries diff --git a/locales/ui/enUS.lua b/locales/ui/enUS.lua index 31829b5..07b18d7 100644 --- a/locales/ui/enUS.lua +++ b/locales/ui/enUS.lua @@ -45,7 +45,7 @@ MTH_LocaleData.ui.enUS = { FOM_ERROR_TOGGLE_FAILED = "Failed to change FeedOMatic state: %s", FOM_TITLE = "Feed-O-Matic", - FOM_STATUS_NOTICE = "Feed-O-Matic, created by the great Fizzwidget, is not yet entirely ready for TurtleWoW because I am missing \na reliable list of all foods, mostly impossible to fetch from the DB.\n It still works much better in this version, but all stuff related to Food buff and Cooking isnt fully functional.", + FOM_STATUS_NOTICE = "Feed-O-Matic, created by the great Fizzwidget, is not yet entirely ready for Twow because I am missing \na reliable list of all foods, mostly impossible to fetch from the DB.\n It still works much better in this version, but all stuff related to Food buff and Cooking isnt fully functional.", FOM_ENABLE_MODULE = "Enable FeedOMatic module", FOM_HEADER_GENERAL = "General", FOM_BIND_HINT = "You should assign a key bind for feed-o-matic,\nin order to feed your pet automatically.\nThe key \"P\" is an excellent candidate !", @@ -119,12 +119,12 @@ MTH_LocaleData.ui.enUS = { CREDITS_ABOUT_CODE_INTRO = "This addon is, in others, a compilation of old vanilla addons that I loved and played with during years. But most of them had become very buggy with TwoW as new contain was added to the server. \n I fixed, reworked, enhanced, and melted them within a modern modular framework that I created.\nI want to make it crystal clear about what work is mine, and what is not and respectfully credit the original authors for the great stuff I've taken from them:", CREDITS_ABOUT_ZHUNTER = "- zBars, Antidaze and Autostrip were core functionalities of Vanilla zHunterMod addon. And I kept the \"z\" naming of bars/buttons to always remember it. On top of this I have created myself zAmmo, zCompanions, zMount, zToys.\nAlso the SmartAmmo feature idea is coming from Zhuntermod: there was a file in it with that functionality, but was like a work-in-progress, totally unfunctional and even \"dangerous\" in its current state, and was not active in the addon. Since it was a fucking great idea, I recoded it mostly from scratch and made it work reliably and safely.", CREDITS_ABOUT_FEEDOMATIC = "- Feed-o-Matic was entirely the work of Fizzwidget. I made it work for Twow, by melting it as a module calling Metahunt core to get pet's state and up-to-date data for new pet families and their diet.", - CREDITS_ABOUT_TOOLTIPS = "- Tooltips module is based on Hunter Helper, another vanilla addon of Fizzwidget. I kept the general idea, but there's not much original code left from it in Metahunt as I refactored everything so it plugs on MetaHunt controlled event handlers, can work with Turtle wow data, and is now usable on other things than Beasts.", + CREDITS_ABOUT_TOOLTIPS = "- Tooltips module is based on Hunter Helper, another vanilla addon of Fizzwidget. I kept the general idea, but there's not much original code left from it in Metahunt as I refactored everything so it plugs on MetaHunt controlled event handlers, can work with Twow data, and is now usable on other things than Beasts.", CREDITS_ABOUT_CHRONOMETER = "- Chronometer was an old and very popular vanilla addon working with ACE2 libraries. It has known uncountable iterations by various people over the years, and I can't credit them all of them here. The current version used by Metahunt is based on this Twow conversion made by \"wigan91\". I have reworked it to have it Hunter-focused-only: removed the non-hunter/race stuff, improved the definition for spells/effects config that were not correct, added missing hunter spells (not many), and implemented my own bar color scheme fitting better the hunter spells.", CREDITS_ABOUT_MAP = "- The Map/Marker system is the one of pfQuest from Master Shagu. Unfortunately he is no more reachable for some months and I could not have some talk with him about this. I mostly let it untouched, I just made slight modifications so it can integrate better within Metahunt, and doesn't conflict with pfQuest.", CREDITS_ABOUT_ATLAS_LINKS = "- The system allowing to fetch itemlinks in game (In the Weapon tab of the Hunter Book notably) is the one of Atlas Twow.", CREDITS_ABOUT_DATA_TITLE = "About data sources", - CREDITS_ABOUT_DATA_INTRO = "Metahunt ships with its own Turtle-WoW datastores, limited to hunter stuff only, and most of this data is up-to-date (Feb 2026).\n\nSources used to build the data include:", + CREDITS_ABOUT_DATA_INTRO = "Metahunt ships with its own Twow datastores, limited to hunter stuff only, and most of this data is up-to-date (Feb 2026).\n\nSources used to build the data include:", CREDITS_DATA_PFQUEST = "- pfQuest for Beasts and Ammo vendors NPCs and their spawn points (and beasts respawn times if any). Thank you again Shagu.", CREDITS_DATA_SHEET = "- This invaluable and very accurate spreadsheet for beasts having pet abilities. A big thanks to all twow players that crafted this! Now you dont need that sheet anymore, you have Metahunt! And the Great Book of Huntards is awesome and accurate also because of YOU.", CREDITS_DATA_OTHER = "- Atlas twow for ranged weapon sources\n\n- Twow wiki for pet diet (which was incomplete, missing Serpents and Foxes)", diff --git a/modules/feedomatic/FeedOMatic.lua b/modules/feedomatic/FeedOMatic.lua index 495209d..4f5c1c9 100644 --- a/modules/feedomatic/FeedOMatic.lua +++ b/modules/feedomatic/FeedOMatic.lua @@ -650,7 +650,24 @@ function MTH_GetMerchantFoodsByDiet() return result; end +local function FOM_ItemQuality(itemIdOrLink) + if (type(GetItemInfo) ~= "function" or itemIdOrLink == nil) then + return nil; + end + local _, _, quality = GetItemInfo(itemIdOrLink); + return quality; +end + local function FOM_DebugLog(message) + if (not (FOM_Config and FOM_Config.Debug)) then + return; + end + local text = "[FOM] " .. tostring(message); + if (MTH and MTH.Print) then + MTH:Print(text, "debug"); + elseif (DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage) then + DEFAULT_CHAT_FRAME:AddMessage(text); + end return; end @@ -1899,15 +1916,6 @@ end -- Add a food to a list function FOM_AddFood(diet, food) - if (FOM_Foods[diet] == nil) then - FOM_DebugLog("FOM_Foods[diet] == nil"); - end - if (FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil) then - FOM_DebugLog("FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil"); - end - if (FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil) then - FOM_DebugLog("FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil"); - end if ( GFWTable.IndexOf(FOM_Foods[diet], food) == 0 ) then if (FOM_AddedFoods == nil) then FOM_AddedFoods = {}; @@ -1941,15 +1949,6 @@ end -- Remove a food from a list function FOM_RemoveFood(diet, food) - if (FOM_Foods[diet] == nil) then - FOM_DebugLog("FOM_Foods[diet] == nil"); - end - if (FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil) then - FOM_DebugLog("FOM_AddedFoods == nil or FOM_AddedFoods[diet] == nil"); - end - if (FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil) then - FOM_DebugLog("FOM_RemovedFoods == nil or FOM_RemovedFoods[diet] == nil"); - end if ( GFWTable.IndexOf(FOM_Foods[diet], food) ~= 0 ) then if (FOM_RemovedFoods == nil) then FOM_RemovedFoods = {}; @@ -2388,7 +2387,12 @@ function FOM_Feed(aFood, options) foodLevel = selectedFoodLevel, }) - FOM_DebugLog("Picked "..FOM_LastFood.." (bag "..foodBag..", slot "..foodItem..") for feeding."); + FOM_DebugLog("Picked "..tostring(FOM_LastFood) + .." id="..tostring(selectedId) + .." foodLevel="..tostring(selectedFoodLevel) + .." itemQuality="..tostring(FOM_ItemQuality(selectedId)) + .." (bag "..tostring(foodBag)..", slot "..tostring(foodItem)..")" + .." reason="..tostring(FOM_LastChoiceReason)); if (FOM_Config.Debug) then -- don't actually feed anything, just show what we would choose return false; @@ -2606,6 +2610,19 @@ function FOM_FlatFoodList() if (table.getn(foodList) == 0 and table.getn(overflowFoodList) > 0) then foodList = overflowFoodList; end + if (FOM_Config and FOM_Config.Debug) then + FOM_DebugLog("Candidate foods for pet level "..tostring(petLevel).." ("..tostring(table.getn(foodList)).." in diet):"); + for i = 1, table.getn(foodList) do + local c = foodList[i]; + FOM_DebugLog(" ["..tostring(i).."] "..tostring(c.link) + .." id="..tostring(c.itemId) + .." qty="..tostring(c.count) + .." foodLevel="..tostring(c.quality) + .." itemQuality="..tostring(FOM_ItemQuality(c.itemId)) + .." known="..tostring(c.knownLevel) + .." useful="..tostring(c.useful)); + end + end return foodList; end @@ -2689,6 +2706,11 @@ function FOM_NewFindFood(fallback, excludedItemIds, precomputedFoodList, openSlo table.insert(reasonParts, "lower quality preferred"); end else + -- Bags are full: free a slot by eating a small stack, but still prefer foods + -- the pet can actually eat (known food level) so we don't pick an + -- unknown-level food the pet will refuse. Falls back to smallest stack + -- when nothing has a known level. + table.sort(FlatFoodList, FOM_SortKnownThenCount); table.insert(reasonParts, "small stack to free bag space"); end if (FOM_Config.AvoidUsefulFood and not fallback) then @@ -2730,6 +2752,15 @@ function FOM_SortCount(a, b) return a.count < b.count; end +function FOM_SortKnownThenCount(a, b) + local aKnown = a.knownLevel and 1 or 0; + local bKnown = b.knownLevel and 1 or 0; + if (aKnown ~= bKnown) then + return aKnown > bKnown; -- edible (known-level) foods first + end + return a.count < b.count; -- then smallest stack to free bag space +end + function FOM_SortQualityDescending(a, b) return a.quality > b.quality; end @@ -2821,9 +2852,6 @@ function FOM_IsInDiet(food, dietList) if (diet == nil) then diet = ""; end - if (FOM_Foods[diet] == nil) then - FOM_DebugLog("FOM_Foods[diet] == nil"); - end if (FOM_RemovedFoods ~= nil and FOM_RemovedFoods[diet] ~= nil and GFWTable.IndexOf(FOM_RemovedFoods[diet], food) ~= 0) then return false; end diff --git a/modules/tooltips/module.lua b/modules/tooltips/module.lua index f50becb..479b566 100644 --- a/modules/tooltips/module.lua +++ b/modules/tooltips/module.lua @@ -1055,6 +1055,15 @@ end local function MTH_TT_HunterKnowsAbility(abilityLower, rankNumber) if abilityLower == "" then return false end + -- Prefer the authoritative pet-training check: it is rank-strict and also consults the + -- hunter's learned-ability map, so a freshly learned rank counts even before a full + -- Beast Training rescan has rebuilt the spell-map rows the fallback below relies on. + if type(MTH_IsPetAbilityKnownByHunter) == "function" then + if MTH_IsPetAbilityKnownByHunter(abilityLower, rankNumber) then + return true + end + end + local spellMap = MTH_TT_GetKnownSpellMap() if type(spellMap) ~= "table" then return false @@ -1405,12 +1414,10 @@ local function MTH_TT_AddPetActionNotLearnedHint() local canonicalLower = MTH_TT_Lower(canonical) local hunterKnown = MTH_TT_HunterKnowsAbility(canonicalLower, rankNumber) - -- Only fall back to petKnown for ranked abilities: rankless abilities have no rank - -- filter in CurrentPetKnowsAbility, so they always match on name alone -> false positive. - local petKnown = (not hunterKnown) - and MTH_TT_AbilityHasPositiveRanks(canonical) - and MTH_TT_CurrentPetKnowsAbility(canonicalLower, rankNumber) - if hunterKnown or petKnown then + -- The hint reflects whether the HUNTER has learned this ability rank. We must NOT + -- suppress it just because the current pet knows it: taming a new beast is exactly + -- when you want to see which of its abilities you still have to learn. + if hunterKnown then return end From 9379668e4b5e6b4d9d7f10e995f0ca92c5238f12 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Wed, 9 Sep 2026 16:29:15 +0200 Subject: [PATCH 06/42] Remove dead code: SmartPet module + orphaned DBC datastores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SmartPet (module never wired into modules.xml) removed entirely — server-side pet AI on TWoW/OctoWoW makes it useless. Deleted modules/smartpet/ and api/options-smartpet.lua. Orphaned .tools DBC extracts (2026-03-20) never loaded via TOC, superseded by the curated/pfQuest datastores that ship: - data/ds-zones-dbc.lua, data/ds-zones-new-dbc.lua (bounds come from ds-zones-wma) - data/ds-stable-prices.lua (never wired to any UI) - data/ds-pet-spells-acquire.lua (learnMethod set at runtime by init.lua + trainer) Also removed api/beastlore-scan.lua (8-line deprecation stub) and its init/api.xml include. No shipped references or player-facing behaviour affected. --- api/beastlore-scan.lua | 8 - api/options-smartpet.lua | 321 ------- data/ds-pet-spells-acquire.lua | 140 --- data/ds-stable-prices.lua | 41 - data/ds-zones-dbc.lua | 97 --- data/ds-zones-new-dbc.lua | 1475 -------------------------------- init/api.xml | 1 - modules/smartpet/engine.lua | 1293 ---------------------------- modules/smartpet/loader.xml | 6 - 9 files changed, 3382 deletions(-) delete mode 100644 api/beastlore-scan.lua delete mode 100644 api/options-smartpet.lua delete mode 100644 data/ds-pet-spells-acquire.lua delete mode 100644 data/ds-stable-prices.lua delete mode 100644 data/ds-zones-dbc.lua delete mode 100644 data/ds-zones-new-dbc.lua delete mode 100644 modules/smartpet/engine.lua delete mode 100644 modules/smartpet/loader.xml diff --git a/api/beastlore-scan.lua b/api/beastlore-scan.lua deleted file mode 100644 index 13e2240..0000000 --- a/api/beastlore-scan.lua +++ /dev/null @@ -1,8 +0,0 @@ ------------------------------------------------------- --- MetaHunt: Beast Lore Scanner (removed) ------------------------------------------------------- --- This file previously contained a feature that recorded --- unknown beasts to SavedVariables when scanned with --- Beast Lore. The feature has been removed; the static --- beast database (ds-beasts) is now the sole data source. ------------------------------------------------------- diff --git a/api/options-smartpet.lua b/api/options-smartpet.lua deleted file mode 100644 index bfb9b51..0000000 --- a/api/options-smartpet.lua +++ /dev/null @@ -1,321 +0,0 @@ ------------------------------------------------------- --- MetaHunt: SmartPet Options Panel --- TWoW 1.18.1 ------------------------------------------------------- - -local function MTH_SP_OPT_Ready() - return type(MTH_GetFrame) == "function" - and type(MTH_CreateCheckbox) == "function" - and type(MTH_CreateSlider) == "function" -end - -local function MTH_SP_OPT_GetCfg() - if not MTH_SmartPet or not MTH_SmartPet.GetConfig then return {} end - return MTH_SmartPet.GetConfig() -end - -local MTH_SP_OPT_built = false - --- Layout constants -local OPT_L = 16 -- left column X -local OPT_R = 290 -- right column X -local OPT_CW = 250 -- column width (for tip text wrapping) - --- Spacing constants -local GAP_SECTION = 34 -- between sections -local GAP_CB = 24 -- checkbox height (icon + text) -local GAP_TIP = 16 -- small tip text height -local GAP_BTN = 28 -- button height + margin -local GAP_SLIDER = 52 -- slider total height (label + track + value) -local GAP_CB_SLIDER = 40 -- checkbox to slider (checkbox + slider label above track) - --- Helper: checkbox with click handler -local function SP_CB(container, name, label, yOff, xOff, isChecked, onClick) - local cb = MTH_CreateCheckbox(container, name, label, yOff, xOff or OPT_L) - if cb then - cb:SetChecked(isChecked and 1 or 0) - cb:SetScript("OnClick", function() - local v = this:GetChecked() - onClick(v == 1 or v == true) - end) - end - return cb -end - --- Helper: section header label -local function SP_Header(container, text, yOff, xOff) - local fs = container:CreateFontString(nil, "ARTWORK", "GameFontNormal") - fs:SetPoint("TOPLEFT", container, "TOPLEFT", xOff or OPT_L, yOff) - fs:SetText("|cffff9900" .. text .. "|r") - return fs -end - --- Helper: small descriptive text -local function SP_Tip(container, text, yOff, xOff) - local fs = container:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") - fs:SetPoint("TOPLEFT", container, "TOPLEFT", xOff or OPT_L, yOff) - fs:SetWidth(OPT_CW) - fs:SetJustifyH("LEFT") - fs:SetTextColor(0.4, 0.6, 1.0) - fs:SetText(text) - return fs -end - --- Taunt Mode cycle -local TAUNT_MODES = { "auto", "growl", "cower", "off" } -local TAUNT_LABELS = { - auto = "Auto (solo=Growl, tank=Cower)", - growl = "Always Growl", - cower = "Always Cower", - off = "Off (manual)", -} - -local function SP_NextTauntMode(current) - for i = 1, 4 do - if TAUNT_MODES[i] == current then - return TAUNT_MODES[math.mod(i, 4) + 1] - end - end - return "auto" -end - --- Channel cycle -local CHANNELS = { "SAY", "PARTY", "RAID", "GUILD" } - -local function SP_NextChannel(current) - for i = 1, 4 do - if CHANNELS[i] == current then - return CHANNELS[math.mod(i, 4) + 1] - end - end - return "SAY" -end - --- Build the UI -local function MTH_SP_OPT_BuildUI(container) - if MTH_SP_OPT_built then return end - MTH_SP_OPT_built = true - - local cfg = MTH_SP_OPT_GetCfg() - local api = MTH_SmartPet or {} - - --------------------------------------------------------------------------- - -- Title (large, like ExpAmmo) - --------------------------------------------------------------------------- - local title = container:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge") - title:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L, -16) - title:SetText("Smart Pet - Pet Combat Management") - - --------------------------------------------------------------------------- - -- Module enable checkbox - --------------------------------------------------------------------------- - local moduleEnabled = true - if MTH and MTH.IsModuleEnabled then - moduleEnabled = MTH:IsModuleEnabled("smartpet", false) and true or false - end - SP_CB(container, "MTH_SP_OPT_ModuleEnable", "Enable Smart Pet module", - -46, OPT_L, moduleEnabled, function(v) - if MTH and MTH.SetModuleEnabled then MTH:SetModuleEnabled("smartpet", v) end - end) - - -- Columns start below the module checkbox - local COL_TOP = -80 - - -- Vertical divider - local divider = container:CreateTexture(nil, "BACKGROUND") - divider:SetTexture(0.3, 0.3, 0.3, 0.6) - divider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R - 14, COL_TOP) - divider:SetPoint("BOTTOMLEFT", container, "TOPLEFT", OPT_R - 14, -530) - divider:SetWidth(1) - - --------------------------------------------------------------------------- - -- LEFT COLUMN — Focus Management - --------------------------------------------------------------------------- - local y = COL_TOP - - SP_Header(container, "Focus Management", y) - y = y - 22 - - -- Smart Focus - SP_CB(container, "MTH_SP_OPT_SmartFocus", "Enable Smart Focus", - y, OPT_L, cfg.smartFocus, function(v) - if api.SetSmartFocus then api.SetSmartFocus(v) end - end) - y = y - GAP_CB - SP_Tip(container, "Budget pet focus across DPS abilities.", y) - y = y - GAP_TIP - SP_Tip(container, "|cff80ff80High focus|r — all DPS abilities ON.", y) - y = y - GAP_TIP - SP_Tip(container, "|cffffcc00Mid focus|r — expensive ability first, others wait.", y) - y = y - GAP_TIP - SP_Tip(container, "|cffff8080Low focus|r — cheapest ability always ON (never idle).", y) - y = y - GAP_TIP - SP_Tip(container, "Taunt cost is always reserved.", y) - y = y - GAP_SECTION - - --------------------------------------------------------------------------- - -- LEFT COLUMN — Taunt Management - --------------------------------------------------------------------------- - SP_Header(container, "Taunt Management", y) - y = y - GAP_TIP - SP_Tip(container, "Controls Growl/Cower autocast based on party.", y) - y = y - 22 - - local tauntBtn = CreateFrame("Button", "MTH_SP_OPT_TauntBtn", container, "UIPanelButtonTemplate") - tauntBtn:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L, y) - tauntBtn:SetWidth(250) - tauntBtn:SetHeight(22) - tauntBtn:SetText("Mode: " .. (TAUNT_LABELS[cfg.tauntMode] or "Auto")) - tauntBtn:SetScript("OnClick", function() - local c = MTH_SP_OPT_GetCfg() - local newMode = SP_NextTauntMode(c.tauntMode) - if api.SetTauntMode then api.SetTauntMode(newMode) end - this:SetText("Mode: " .. (TAUNT_LABELS[newMode] or newMode)) - end) - y = y - GAP_BTN - - SP_CB(container, "MTH_SP_OPT_PvpDetaunt", "PVP Auto-Detaunt", - y, OPT_L, cfg.pvpDetaunt, function(v) - if api.SetPvpDetaunt then api.SetPvpDetaunt(v) end - end) - y = y - GAP_SECTION - - --------------------------------------------------------------------------- - -- LEFT COLUMN — CC-Break Prevention - --------------------------------------------------------------------------- - SP_Header(container, "CC-Break Prevention", y) - y = y - 22 - - SP_CB(container, "MTH_SP_OPT_CcBreak", "Check for breakable CC", - y, OPT_L, cfg.ccBreakCheck, function(v) - if api.SetCcBreakCheck then api.SetCcBreakCheck(v) end - end) - y = y - GAP_CB - - local ccLabels = { block = "Block attack", warn = "Warn only" } - local ccModeBtn = CreateFrame("Button", "MTH_SP_OPT_CcModeBtn", container, "UIPanelButtonTemplate") - ccModeBtn:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L + 20, y) - ccModeBtn:SetWidth(180) - ccModeBtn:SetHeight(20) - ccModeBtn:SetText("CC Mode: " .. (ccLabels[cfg.ccBreakMode] or "Block")) - ccModeBtn:SetScript("OnClick", function() - local c = MTH_SP_OPT_GetCfg() - local newMode = (c.ccBreakMode == "block") and "warn" or "block" - if api.SetCcBreakMode then api.SetCcBreakMode(newMode) end - this:SetText("CC Mode: " .. (ccLabels[newMode] or newMode)) - end) - y = y - GAP_SECTION - - --------------------------------------------------------------------------- - -- RIGHT COLUMN — General - --------------------------------------------------------------------------- - local ry = COL_TOP - - -- The Button - SP_Header(container, "The Button", ry, OPT_R) - ry = ry - 22 - - SP_CB(container, "MTH_SP_OPT_TheButton", "Enable The Button keybind", - ry, OPT_R, cfg.theButton, function(v) - if api.SetTheButton then api.SetTheButton(v) end - end) - ry = ry - GAP_CB - SP_Tip(container, "Enemy=attack, friendly=assist, none=recall.", ry, OPT_R) - ry = ry - GAP_SECTION - - -- Rush on Attack - SP_CB(container, "MTH_SP_OPT_Rush", "Rush on initial attack", - ry, OPT_R, cfg.rushOnAttack, function(v) - if api.SetRushOnAttack then api.SetRushOnAttack(v) end - end) - ry = ry - GAP_CB - SP_Tip(container, "Charge / Dash / Dive when pet is sent to attack.", ry, OPT_R) - ry = ry - GAP_SECTION - - -- NoChase - SP_CB(container, "MTH_SP_OPT_NoChase", "NoChase (recall on mob flee)", - ry, OPT_R, cfg.noChase, function(v) - if api.SetNoChase then api.SetNoChase(v) end - end) - ry = ry - GAP_CB - SP_Tip(container, "Recall pet when target attempts to flee.", ry, OPT_R) - ry = ry - GAP_SECTION - - -- AutoCower - SP_Header(container, "AutoCower", ry, OPT_R) - ry = ry - 22 - - SP_CB(container, "MTH_SP_OPT_AutoCower", "Enable AutoCower on low HP", - ry, OPT_R, cfg.autoCower, function(v) - if api.SetAutoCower then api.SetAutoCower(v) end - end) - ry = ry - GAP_CB_SLIDER - - local acSlider = MTH_CreateSlider(container, "MTH_SP_OPT_AutoCowerSlider", - "HP Threshold (%)", 5, 80, 5, ry) - if acSlider then - acSlider:ClearAllPoints() - acSlider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, ry) - acSlider:SetWidth(200) - acSlider:SetValue(cfg.autoCowerPct or 30) - local valText = getglobal("MTH_SP_OPT_AutoCowerSliderValue") - if valText then valText:SetText(tostring(cfg.autoCowerPct or 30)) end - acSlider.onChange = function(val) - if api.SetAutoCowerPct then api.SetAutoCowerPct(val) end - end - end - ry = ry - GAP_SLIDER - - -- AutoWarn - SP_Header(container, "AutoWarn", ry, OPT_R) - ry = ry - 22 - - SP_CB(container, "MTH_SP_OPT_AutoWarn", "Warn when pet HP is low", - ry, OPT_R, cfg.autoWarn, function(v) - if api.SetAutoWarn then api.SetAutoWarn(v) end - end) - ry = ry - GAP_CB_SLIDER - - local awSlider = MTH_CreateSlider(container, "MTH_SP_OPT_AutoWarnSlider", - "HP Threshold (%)", 5, 60, 5, ry) - if awSlider then - awSlider:ClearAllPoints() - awSlider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, ry) - awSlider:SetWidth(200) - awSlider:SetValue(cfg.autoWarnPct or 20) - local valText = getglobal("MTH_SP_OPT_AutoWarnSliderValue") - if valText then valText:SetText(tostring(cfg.autoWarnPct or 20)) end - awSlider.onChange = function(val) - if api.SetAutoWarnPct then api.SetAutoWarnPct(val) end - end - end - ry = ry - GAP_SLIDER - - local chanBtn = CreateFrame("Button", "MTH_SP_OPT_ChanBtn", container, "UIPanelButtonTemplate") - chanBtn:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, ry) - chanBtn:SetWidth(180) - chanBtn:SetHeight(22) - chanBtn:SetText("Channel: " .. (cfg.autoWarnChannel or "SAY")) - chanBtn:SetScript("OnClick", function() - local c = MTH_SP_OPT_GetCfg() - local newChan = SP_NextChannel(c.autoWarnChannel or "SAY") - if api.SetAutoWarnChannel then api.SetAutoWarnChannel(newChan) end - this:SetText("Channel: " .. newChan) - end) - ry = ry - GAP_CB - SP_Tip(container, "Chat channel for low-HP warnings.", ry, OPT_R) -end - --- Public setup function (called by options-shell.lua) -function MTH_SetupSmartPetOptions() - if not MTH_SP_OPT_Ready() then return end - local container = MTH_GetFrame("MetaHuntOptionsSmartPet") - if not container then return end - local ok, err = pcall(MTH_SP_OPT_BuildUI, container) - if not ok then - if MTH and MTH.Print then - MTH:Print("[SmartPet Options] " .. tostring(err), "error") - end - MTH_SP_OPT_built = false - end -end diff --git a/data/ds-pet-spells-acquire.lua b/data/ds-pet-spells-acquire.lua deleted file mode 100644 index df0a7d1..0000000 --- a/data/ds-pet-spells-acquire.lua +++ /dev/null @@ -1,140 +0,0 @@ --- MetaHunt: Pet spell DBC acquire-method overlay (learnMethod, minRank, nextSpellId) --- AUTO-GENERATED by .tools — do not edit by hand. --- Source DBC(s): SkillLineAbility.dbc, SkillLine.dbc --- Generated: 2026-03-20 13:57 UTC - --- This file patches MTH_DS_PetSpells.allSpells entries with DBC-authoritative --- learnMethod, minRank, and nextSpellId values. --- Load order: after ds-pet-spells.lua and ds-pet-spells-trainer.lua - -if not MTH_DS_PetSpells or not MTH_DS_PetSpells.allSpells then return end - -local _acquire = { - [1747] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [1748] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [1749] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [1750] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [1751] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [1853] = { learnMethod="unknown(1)", minRank=1, nextSpellId=0, skillLineId=261 }, - [2975] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [2976] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [2977] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [2980] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [2981] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [2982] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [3666] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [3667] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [4195] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [4196] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [4197] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [4198] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [4199] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [4200] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [4201] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [4202] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [5048] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [5049] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [5149] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [7370] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [14922] = { learnMethod="unknown(1)", minRank=1, nextSpellId=0, skillLineId=261 }, - [14923] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [14924] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [14925] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [14926] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [14927] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [16698] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [17254] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [17262] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [17263] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [17264] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [17265] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [17266] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [17267] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [17268] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23100] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23111] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23112] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23146] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23149] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23150] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23163] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23166] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [23167] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24424] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24440] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24441] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24451] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24454] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24455] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24463] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24464] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24475] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24476] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24477] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24478] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24490] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24494] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24495] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24508] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24509] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24510] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24511] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24512] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24513] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24514] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24515] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24516] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24547] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24556] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24557] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24558] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24559] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24560] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24561] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24562] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24580] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24581] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24582] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24584] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24588] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24589] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24599] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24607] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24608] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24609] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24631] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24632] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24641] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [24845] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [25013] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [25014] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [25015] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [25016] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [25017] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [25077] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [26065] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [26094] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [26184] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [26185] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [26186] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [26189] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [26190] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [26202] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [28343] = { learnMethod="trainer", minRank=1, nextSpellId=0, skillLineId=261 }, - [46023] = { learnMethod="trainer", minRank=0, nextSpellId=0, skillLineId=261 }, - [46303] = { learnMethod="trainer", minRank=0, nextSpellId=0, skillLineId=261 }, - [46307] = { learnMethod="trainer", minRank=0, nextSpellId=0, skillLineId=261 }, - [51154] = { learnMethod="trainer", minRank=0, nextSpellId=0, skillLineId=261 }, - [51155] = { learnMethod="trainer", minRank=0, nextSpellId=0, skillLineId=261 }, - [51157] = { learnMethod="trainer", minRank=0, nextSpellId=0, skillLineId=261 }, -} - -for _, spell in ipairs(MTH_DS_PetSpells.allSpells) do - local meta = _acquire[spell.id] - if meta then - spell.learnMethod = meta.learnMethod - spell.minRank = meta.minRank - spell.nextSpellId = meta.nextSpellId - spell.skillLineId = meta.skillLineId - end -end diff --git a/data/ds-stable-prices.lua b/data/ds-stable-prices.lua deleted file mode 100644 index 9bced7e..0000000 --- a/data/ds-stable-prices.lua +++ /dev/null @@ -1,41 +0,0 @@ --- MetaHunt: Stable slot prices --- AUTO-GENERATED by .tools — do not edit by hand. --- Source DBC(s): StableSlotPrices.dbc --- Generated: 2026-03-20 13:57 UTC - -if not MTH_DS then MTH_DS = {} end - -MTH_DS_StableSlotPrices = { - [1] = { - ["cost_copper"] = 500, - ["cost_gold"] = 0, - ["cost_silver"] = 5, - ["cost_copper_remainder"] = 0, - ["display"] = "0g 5s 0c", - ["acquireMethod"] = "gold", - }, - [2] = { - ["cost_copper"] = 50000, - ["cost_gold"] = 5, - ["cost_silver"] = 0, - ["cost_copper_remainder"] = 0, - ["display"] = "5g 0s 0c", - ["acquireMethod"] = "gold", - }, - [3] = { - ["cost_copper"] = 2500000, - ["cost_gold"] = 250, - ["cost_silver"] = 0, - ["cost_copper_remainder"] = 0, - ["display"] = "250g 0s 0c", - ["acquireMethod"] = "gold", - }, - [4] = { - ["cost_copper"] = 0, - ["cost_gold"] = 0, - ["cost_silver"] = 0, - ["cost_copper_remainder"] = 0, - ["display"] = "Shop", - ["acquireMethod"] = "shop", - }, -} diff --git a/data/ds-zones-dbc.lua b/data/ds-zones-dbc.lua deleted file mode 100644 index d784623..0000000 --- a/data/ds-zones-dbc.lua +++ /dev/null @@ -1,97 +0,0 @@ --- MetaHunt: Zone DBC enrichment (worldBounds, dbcAreaId, dbcMapId, dbcName) --- AUTO-GENERATED by .tools — do not edit by hand. --- Source DBC(s): WorldMapArea.dbc, AreaTable.dbc --- Generated: 2026-03-20 13:57 UTC - -if not MTH_DS_Zones then return end - --- WMA ID=301 map=0 'Stormwind' -if MTH_DS_Zones[1519] then - MTH_DS_Zones[1519]["dbcAreaId"] = 1519 - MTH_DS_Zones[1519]["dbcMapId"] = 0 - MTH_DS_Zones[1519]["dbcName"] = "Stormwind" - MTH_DS_Zones[1519]["worldBounds"] = { - ["left"] = 1722.92, - ["right"] = -14.58, - ["top"] = -7995.8301, - ["bottom"] = -9154.1699, -} -end - --- WMA ID=321 map=1 'Ogrimmar' -if MTH_DS_Zones[1637] then - MTH_DS_Zones[1637]["dbcAreaId"] = 1637 - MTH_DS_Zones[1637]["dbcMapId"] = 1 - MTH_DS_Zones[1637]["dbcName"] = "Ogrimmar" - MTH_DS_Zones[1637]["worldBounds"] = { - ["left"] = -3680.6001, - ["right"] = -5083.21, - ["top"] = 2273.8799, - ["bottom"] = 1338.46, -} -end - --- WMA ID=362 map=1 'ThunderBluff' -if MTH_DS_Zones[1638] then - MTH_DS_Zones[1638]["dbcAreaId"] = 1638 - MTH_DS_Zones[1638]["dbcMapId"] = 1 - MTH_DS_Zones[1638]["dbcName"] = "ThunderBluff" - MTH_DS_Zones[1638]["worldBounds"] = { - ["left"] = 516.67, - ["right"] = -527.08, - ["top"] = -850.0, - ["bottom"] = -1545.83, -} -end - --- WMA ID=381 map=1 'Darnassis' -if MTH_DS_Zones[1657] then - MTH_DS_Zones[1657]["dbcAreaId"] = 1657 - MTH_DS_Zones[1657]["dbcMapId"] = 1 - MTH_DS_Zones[1657]["dbcName"] = "Darnassis" - MTH_DS_Zones[1657]["worldBounds"] = { - ["left"] = 2938.3601, - ["right"] = 1880.03, - ["top"] = 10238.3203, - ["bottom"] = 9532.5898, -} -end - --- WMA ID=382 map=0 'Undercity' -if MTH_DS_Zones[1497] then - MTH_DS_Zones[1497]["dbcAreaId"] = 1497 - MTH_DS_Zones[1497]["dbcMapId"] = 0 - MTH_DS_Zones[1497]["dbcName"] = "Undercity" - MTH_DS_Zones[1497]["worldBounds"] = { - ["left"] = 873.19, - ["right"] = -86.18, - ["top"] = 1877.95, - ["bottom"] = 1237.84, -} -end - --- WMA ID=508 map=1 'AmaniAlor' -if MTH_DS_Zones[2041] then - MTH_DS_Zones[2041]["dbcAreaId"] = 2041 - MTH_DS_Zones[2041]["dbcMapId"] = 1 - MTH_DS_Zones[2041]["dbcName"] = "AmaniAlor" - MTH_DS_Zones[2041]["worldBounds"] = { - ["left"] = 3112.0, - ["right"] = 1599.0, - ["top"] = 3437.0, - ["bottom"] = 2434.0, -} -end - --- WMA ID=509 map=0 'AlahThalas' -if MTH_DS_Zones[2040] then - MTH_DS_Zones[2040]["dbcAreaId"] = 2040 - MTH_DS_Zones[2040]["dbcMapId"] = 0 - MTH_DS_Zones[2040]["dbcName"] = "AlahThalas" - MTH_DS_Zones[2040]["worldBounds"] = { - ["left"] = -2169.0, - ["right"] = -3637.0, - ["top"] = 4907.0, - ["bottom"] = 3931.0, -} -end diff --git a/data/ds-zones-new-dbc.lua b/data/ds-zones-new-dbc.lua deleted file mode 100644 index 12985b4..0000000 --- a/data/ds-zones-new-dbc.lua +++ /dev/null @@ -1,1475 +0,0 @@ --- MetaHunt: Zones present in DBC but not in MTH_DS_Zones (TWoW custom zones) --- AUTO-GENERATED by .tools — do not edit by hand. --- Source DBC(s): WorldMapArea.dbc, AreaTable.dbc --- Generated: 2026-03-20 13:57 UTC - -if not MTH_DS_Zones then MTH_DS_Zones = {} end - --- DBC WMA ID=4 MapID=1 'Durotar' -if not MTH_DS_Zones[14] then - MTH_DS_Zones[14] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 14, - ["dbcMapId"] = 1, - ["dbcName"] = "Durotar", - ["worldBounds"] = { left=-1962.50, right=-7250.00, top=1808.33, bottom=-1716.67 }, - ["names"] = { ["enUS"] = "Durotar", ["esES"] = "Durotar", ["ptBR"] = "Durotar", ["zhCN"] = "杜隆塔尔" }, - } -end - --- DBC WMA ID=9 MapID=1 'Mulgore' -if not MTH_DS_Zones[215] then - MTH_DS_Zones[215] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 215, - ["dbcMapId"] = 1, - ["dbcName"] = "Mulgore", - ["worldBounds"] = { left=2047.92, right=-3089.58, top=-272.92, bottom=-3697.92 }, - ["names"] = { ["enUS"] = "Mulgore", ["esES"] = "Mulgore", ["ptBR"] = "Mulgore", ["zhCN"] = "莫高雷" }, - } -end - --- DBC WMA ID=11 MapID=1 'Barrens' -if not MTH_DS_Zones[17] then - MTH_DS_Zones[17] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 17, - ["dbcMapId"] = 1, - ["dbcName"] = "Barrens", - ["worldBounds"] = { left=2622.92, right=-7510.42, top=1612.50, bottom=-5143.75 }, - ["names"] = { ["enUS"] = "The Barrens", ["esES"] = "Los Baldíos", ["ptBR"] = "Os Barrens", ["zhCN"] = "贫瘠之地" }, - } -end - --- DBC WMA ID=13 MapID=1 'Kalimdor' -if not MTH_DS_Zones[0] then - MTH_DS_Zones[0] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 0, - ["dbcMapId"] = 1, - ["dbcName"] = "Kalimdor", - ["worldBounds"] = { left=17066.60, right=-19733.21, top=12799.90, bottom=-11733.30 }, - ["names"] = { ["enUS"] = "Kalimdor" }, - } -end - --- DBC WMA ID=14 MapID=0 'Azeroth' -if not MTH_DS_Zones[0] then - MTH_DS_Zones[0] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 0, - ["dbcMapId"] = 0, - ["dbcName"] = "Azeroth", - ["worldBounds"] = { left=16000.00, right=-19199.90, top=7466.60, bottom=-16000.00 }, - ["names"] = { ["enUS"] = "Azeroth" }, - } -end - --- DBC WMA ID=15 MapID=0 'Alterac' -if not MTH_DS_Zones[36] then - MTH_DS_Zones[36] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 36, - ["dbcMapId"] = 0, - ["dbcName"] = "Alterac", - ["worldBounds"] = { left=783.33, right=-2016.67, top=1500.00, bottom=-366.67 }, - ["names"] = { ["enUS"] = "Alterac Mountains", ["esES"] = "Montañas de Alterac", ["ptBR"] = "Montanhas de Alterac", ["zhCN"] = "奥特兰克山脉" }, - } -end - --- DBC WMA ID=16 MapID=0 'Arathi' -if not MTH_DS_Zones[45] then - MTH_DS_Zones[45] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 45, - ["dbcMapId"] = 0, - ["dbcName"] = "Arathi", - ["worldBounds"] = { left=-866.67, right=-4466.67, top=-133.33, bottom=-2533.33 }, - ["names"] = { ["enUS"] = "Arathi Highlands", ["esES"] = "Tierras Altas de Arathi", ["ptBR"] = "Planalto Arathi", ["zhCN"] = "阿拉希高地" }, - } -end - --- DBC WMA ID=17 MapID=0 'Badlands' -if not MTH_DS_Zones[3] then - MTH_DS_Zones[3] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 3, - ["dbcMapId"] = 0, - ["dbcName"] = "Badlands", - ["worldBounds"] = { left=-2079.17, right=-4566.67, top=-5889.58, bottom=-7547.92 }, - ["names"] = { ["enUS"] = "Badlands", ["esES"] = "Tierras Inhóspitas", ["ptBR"] = "Ermos", ["zhCN"] = "荒芜之地" }, - } -end - --- DBC WMA ID=19 MapID=0 'BlastedLands' -if not MTH_DS_Zones[4] then - MTH_DS_Zones[4] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 4, - ["dbcMapId"] = 0, - ["dbcName"] = "BlastedLands", - ["worldBounds"] = { left=-1241.67, right=-4591.67, top=-10566.67, bottom=-12800.00 }, - ["names"] = { ["enUS"] = "Blasted Lands", ["esES"] = "Tierras Devastadas", ["ptBR"] = "Terras Devastadas", ["zhCN"] = "诅咒之地" }, - } -end - --- DBC WMA ID=20 MapID=0 'Tirisfal' -if not MTH_DS_Zones[85] then - MTH_DS_Zones[85] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 85, - ["dbcMapId"] = 0, - ["dbcName"] = "Tirisfal", - ["worldBounds"] = { left=3033.33, right=-1485.42, top=3837.50, bottom=825.00 }, - ["names"] = { ["enUS"] = "Tirisfal Glades", ["esES"] = "Claros de Tirisfal", ["ptBR"] = "Os Bosques de Tirisfal", ["zhCN"] = "提瑞斯法林地" }, - } -end - --- DBC WMA ID=21 MapID=0 'Silverpine' -if not MTH_DS_Zones[130] then - MTH_DS_Zones[130] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 130, - ["dbcMapId"] = 0, - ["dbcName"] = "Silverpine", - ["worldBounds"] = { left=3450.00, right=-750.00, top=1666.67, bottom=-1133.33 }, - ["names"] = { ["enUS"] = "Silverpine Forest", ["esES"] = "Bosque de Argénteos", ["ptBR"] = "Floresta Silverpine", ["zhCN"] = "银松森林" }, - } -end - --- DBC WMA ID=22 MapID=0 'WesternPlaguelands' -if not MTH_DS_Zones[28] then - MTH_DS_Zones[28] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 28, - ["dbcMapId"] = 0, - ["dbcName"] = "WesternPlaguelands", - ["worldBounds"] = { left=416.67, right=-3883.33, top=3366.67, bottom=500.00 }, - ["names"] = { ["enUS"] = "Western Plaguelands", ["esES"] = "Tierras de la Peste del Oeste", ["ptBR"] = "Plaguelands Ocidentais", ["zhCN"] = "西瘟疫之地" }, - } -end - --- DBC WMA ID=23 MapID=0 'EasternPlaguelands' -if not MTH_DS_Zones[139] then - MTH_DS_Zones[139] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 139, - ["dbcMapId"] = 0, - ["dbcName"] = "EasternPlaguelands", - ["worldBounds"] = { left=-2185.42, right=-6056.25, top=3800.00, bottom=1218.75 }, - ["names"] = { ["enUS"] = "Eastern Plaguelands", ["esES"] = "Tierras de la Peste del Este", ["ptBR"] = "Terras Pestilentas Orientais", ["zhCN"] = "东瘟疫之地" }, - } -end - --- DBC WMA ID=24 MapID=0 'Hilsbrad' -if not MTH_DS_Zones[267] then - MTH_DS_Zones[267] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 267, - ["dbcMapId"] = 0, - ["dbcName"] = "Hilsbrad", - ["worldBounds"] = { left=1066.67, right=-2133.33, top=400.00, bottom=-1733.33 }, - ["names"] = { ["enUS"] = "Hillsbrad Foothills", ["esES"] = "Laderas de Trabalomas", ["ptBR"] = "Contrafortes de Eira dos Montes", ["zhCN"] = "希尔斯布莱德丘陵" }, - } -end - --- DBC WMA ID=26 MapID=0 'Hinterlands' -if not MTH_DS_Zones[47] then - MTH_DS_Zones[47] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 47, - ["dbcMapId"] = 0, - ["dbcName"] = "Hinterlands", - ["worldBounds"] = { left=-1575.00, right=-5425.00, top=1466.67, bottom=-1100.00 }, - ["names"] = { ["enUS"] = "The Hinterlands", ["esES"] = "Tierras del Interior", ["ptBR"] = "As Terras Interiores", ["zhCN"] = "辛特兰" }, - } -end - --- DBC WMA ID=27 MapID=0 'DunMorogh' -if not MTH_DS_Zones[1] then - MTH_DS_Zones[1] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 1, - ["dbcMapId"] = 0, - ["dbcName"] = "DunMorogh", - ["worldBounds"] = { left=1802.08, right=-3122.92, top=-3877.08, bottom=-7160.42 }, - ["names"] = { ["enUS"] = "Dun Morogh", ["esES"] = "Dun Morogh", ["ptBR"] = "Dun Morogh", ["zhCN"] = "丹莫罗" }, - } -end - --- DBC WMA ID=28 MapID=0 'SearingGorge' -if not MTH_DS_Zones[51] then - MTH_DS_Zones[51] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 51, - ["dbcMapId"] = 0, - ["dbcName"] = "SearingGorge", - ["worldBounds"] = { left=-322.92, right=-2554.17, top=-6100.00, bottom=-7587.50 }, - ["names"] = { ["enUS"] = "Searing Gorge", ["esES"] = "La Garganta de Fuego", ["ptBR"] = "Desfiladeiro Searing", ["zhCN"] = "灼热峡谷" }, - } -end - --- DBC WMA ID=29 MapID=0 'BurningSteppes' -if not MTH_DS_Zones[46] then - MTH_DS_Zones[46] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 46, - ["dbcMapId"] = 0, - ["dbcName"] = "BurningSteppes", - ["worldBounds"] = { left=-266.67, right=-3195.83, top=-7031.25, bottom=-8983.33 }, - ["names"] = { ["enUS"] = "Burning Steppes", ["esES"] = "Las Estepas Ardientes", ["ptBR"] = "Estepes Ardentes", ["zhCN"] = "燃烧平原" }, - } -end - --- DBC WMA ID=30 MapID=0 'Elwynn' -if not MTH_DS_Zones[12] then - MTH_DS_Zones[12] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 12, - ["dbcMapId"] = 0, - ["dbcName"] = "Elwynn", - ["worldBounds"] = { left=1535.42, right=-1935.42, top=-7939.58, bottom=-10254.17 }, - ["names"] = { ["enUS"] = "Elwynn Forest", ["esES"] = "Bosque de Elwynn", ["ptBR"] = "Floresta de Elwynn", ["zhCN"] = "艾尔文森林" }, - } -end - --- DBC WMA ID=32 MapID=0 'DeadwindPass' -if not MTH_DS_Zones[41] then - MTH_DS_Zones[41] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 41, - ["dbcMapId"] = 0, - ["dbcName"] = "DeadwindPass", - ["worldBounds"] = { left=-833.33, right=-3333.33, top=-9866.67, bottom=-11533.33 }, - ["names"] = { ["enUS"] = "Deadwind Pass", ["esES"] = "Paso de la Muerte", ["ptBR"] = "Desfiladeiro da Morte", ["zhCN"] = "逆风小径" }, - } -end - --- DBC WMA ID=34 MapID=0 'Duskwood' -if not MTH_DS_Zones[10] then - MTH_DS_Zones[10] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 10, - ["dbcMapId"] = 0, - ["dbcName"] = "Duskwood", - ["worldBounds"] = { left=833.33, right=-1866.67, top=-9716.67, bottom=-11516.67 }, - ["names"] = { ["enUS"] = "Duskwood", ["esES"] = "Bosque del Ocaso", ["ptBR"] = "Floresta do Crepúsculo", ["zhCN"] = "暮色森林" }, - } -end - --- DBC WMA ID=35 MapID=0 'LochModan' -if not MTH_DS_Zones[38] then - MTH_DS_Zones[38] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 38, - ["dbcMapId"] = 0, - ["dbcName"] = "LochModan", - ["worldBounds"] = { left=-1993.75, right=-4752.08, top=-4487.50, bottom=-6327.08 }, - ["names"] = { ["enUS"] = "Loch Modan", ["esES"] = "Loch Modan", ["ptBR"] = "Loch Modan", ["zhCN"] = "洛克莫丹" }, - } -end - --- DBC WMA ID=36 MapID=0 'Redridge' -if not MTH_DS_Zones[44] then - MTH_DS_Zones[44] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 44, - ["dbcMapId"] = 0, - ["dbcName"] = "Redridge", - ["worldBounds"] = { left=-1570.83, right=-3741.67, top=-8575.00, bottom=-10022.92 }, - ["names"] = { ["enUS"] = "Redridge Mountains", ["esES"] = "Montañas Crestagrana", ["ptBR"] = "Montanhas Redridge", ["zhCN"] = "赤脊山" }, - } -end - --- DBC WMA ID=37 MapID=0 'Stranglethorn' -if not MTH_DS_Zones[33] then - MTH_DS_Zones[33] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 33, - ["dbcMapId"] = 0, - ["dbcName"] = "Stranglethorn", - ["worldBounds"] = { left=2220.83, right=-4160.42, top=-11168.75, bottom=-15422.92 }, - ["names"] = { ["enUS"] = "Stranglethorn Vale", ["esES"] = "Vega de Tuercespina", ["ptBR"] = "Vale Stranglethorn", ["zhCN"] = "荆棘谷" }, - } -end - --- DBC WMA ID=38 MapID=0 'SwampOfSorrows' -if not MTH_DS_Zones[8] then - MTH_DS_Zones[8] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 8, - ["dbcMapId"] = 0, - ["dbcName"] = "SwampOfSorrows", - ["worldBounds"] = { left=-2222.92, right=-4516.67, top=-9620.83, bottom=-11150.00 }, - ["names"] = { ["enUS"] = "Swamp of Sorrows", ["esES"] = "Pantano de las Penas", ["ptBR"] = "Pantano das Tristezas", ["zhCN"] = "悲伤沼泽" }, - } -end - --- DBC WMA ID=39 MapID=0 'Westfall' -if not MTH_DS_Zones[40] then - MTH_DS_Zones[40] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 40, - ["dbcMapId"] = 0, - ["dbcName"] = "Westfall", - ["worldBounds"] = { left=3016.67, right=-483.33, top=-9400.00, bottom=-11733.33 }, - ["names"] = { ["enUS"] = "Westfall", ["esES"] = "Páramos del Poniente", ["ptBR"] = "Westfall", ["zhCN"] = "西部荒野" }, - } -end - --- DBC WMA ID=40 MapID=0 'Wetlands' -if not MTH_DS_Zones[11] then - MTH_DS_Zones[11] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 11, - ["dbcMapId"] = 0, - ["dbcName"] = "Wetlands", - ["worldBounds"] = { left=-389.58, right=-4525.00, top=-2147.92, bottom=-4904.17 }, - ["names"] = { ["enUS"] = "Wetlands", ["esES"] = "Los Humedales", ["ptBR"] = "Pântanos", ["zhCN"] = "湿地" }, - } -end - --- DBC WMA ID=41 MapID=1 'Teldrassil' -if not MTH_DS_Zones[141] then - MTH_DS_Zones[141] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 141, - ["dbcMapId"] = 1, - ["dbcName"] = "Teldrassil", - ["worldBounds"] = { left=3814.58, right=-1277.08, top=11831.25, bottom=8437.50 }, - ["names"] = { ["enUS"] = "Teldrassil", ["esES"] = "Teldrassil", ["ptBR"] = "Teldrassil", ["zhCN"] = "泰达希尔" }, - } -end - --- DBC WMA ID=42 MapID=1 'Darkshore' -if not MTH_DS_Zones[148] then - MTH_DS_Zones[148] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 148, - ["dbcMapId"] = 1, - ["dbcName"] = "Darkshore", - ["worldBounds"] = { left=2941.67, right=-3608.33, top=8333.33, bottom=3966.67 }, - ["names"] = { ["enUS"] = "Darkshore", ["esES"] = "Costa Oscura", ["ptBR"] = "Costa Negra", ["zhCN"] = "黑海岸" }, - } -end - --- DBC WMA ID=43 MapID=1 'Ashenvale' -if not MTH_DS_Zones[331] then - MTH_DS_Zones[331] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 331, - ["dbcMapId"] = 1, - ["dbcName"] = "Ashenvale", - ["worldBounds"] = { left=1700.00, right=-4066.67, top=4672.92, bottom=829.17 }, - ["names"] = { ["enUS"] = "Ashenvale", ["esES"] = "Vallefresno", ["ptBR"] = "Vale das Cinzas", ["zhCN"] = "灰谷" }, - } -end - --- DBC WMA ID=61 MapID=1 'ThousandNeedles' -if not MTH_DS_Zones[400] then - MTH_DS_Zones[400] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 400, - ["dbcMapId"] = 1, - ["dbcName"] = "ThousandNeedles", - ["worldBounds"] = { left=-433.33, right=-4833.33, top=-3966.67, bottom=-6900.00 }, - ["names"] = { ["enUS"] = "Thousand Needles", ["esES"] = "Las Mil Agujas", ["ptBR"] = "As Mil Agulhas", ["zhCN"] = "千针石林" }, - } -end - --- DBC WMA ID=81 MapID=1 'StonetalonMountains' -if not MTH_DS_Zones[406] then - MTH_DS_Zones[406] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 406, - ["dbcMapId"] = 1, - ["dbcName"] = "StonetalonMountains", - ["worldBounds"] = { left=3245.83, right=-1637.50, top=2916.67, bottom=-339.58 }, - ["names"] = { ["enUS"] = "Stonetalon Mountains", ["esES"] = "Montañas de Colina Roca", ["ptBR"] = "Montanhas Stonetalon", ["zhCN"] = "石爪山脉" }, - } -end - --- DBC WMA ID=101 MapID=1 'Desolace' -if not MTH_DS_Zones[405] then - MTH_DS_Zones[405] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 405, - ["dbcMapId"] = 1, - ["dbcName"] = "Desolace", - ["worldBounds"] = { left=4233.33, right=-262.50, top=452.08, bottom=-2545.83 }, - ["names"] = { ["enUS"] = "Desolace", ["esES"] = "Desolace", ["ptBR"] = "Desolação", ["zhCN"] = "凄凉之地" }, - } -end - --- DBC WMA ID=121 MapID=1 'Feralas' -if not MTH_DS_Zones[357] then - MTH_DS_Zones[357] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 357, - ["dbcMapId"] = 1, - ["dbcName"] = "Feralas", - ["worldBounds"] = { left=5441.67, right=-1508.33, top=-2366.67, bottom=-7000.00 }, - ["names"] = { ["enUS"] = "Feralas", ["esES"] = "Feralas", ["ptBR"] = "Feralas", ["zhCN"] = "菲拉斯" }, - } -end - --- DBC WMA ID=141 MapID=1 'Dustwallow' -if not MTH_DS_Zones[15] then - MTH_DS_Zones[15] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 15, - ["dbcMapId"] = 1, - ["dbcName"] = "Dustwallow", - ["worldBounds"] = { left=-975.00, right=-6225.00, top=-2033.33, bottom=-5533.33 }, - ["names"] = { ["enUS"] = "Dustwallow Marsh", ["esES"] = "Marjal Revolcafango", ["ptBR"] = "Pântano Vadeante", ["zhCN"] = "尘泥沼泽" }, - } -end - --- DBC WMA ID=161 MapID=1 'Tanaris' -if not MTH_DS_Zones[440] then - MTH_DS_Zones[440] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 440, - ["dbcMapId"] = 1, - ["dbcName"] = "Tanaris", - ["worldBounds"] = { left=-218.75, right=-7118.75, top=-5875.00, bottom=-10475.00 }, - ["names"] = { ["enUS"] = "Tanaris", ["esES"] = "Tanaris", ["ptBR"] = "Tanaris", ["zhCN"] = "塔纳利斯" }, - } -end - --- DBC WMA ID=181 MapID=1 'Aszhara' -if not MTH_DS_Zones[16] then - MTH_DS_Zones[16] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 16, - ["dbcMapId"] = 1, - ["dbcName"] = "Aszhara", - ["worldBounds"] = { left=-3277.08, right=-8347.92, top=5341.67, bottom=1960.42 }, - ["names"] = { ["enUS"] = "Azshara", ["esES"] = "Azshara", ["ptBR"] = "Azshara", ["zhCN"] = "艾萨拉" }, - } -end - --- DBC WMA ID=182 MapID=1 'Felwood' -if not MTH_DS_Zones[361] then - MTH_DS_Zones[361] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 361, - ["dbcMapId"] = 1, - ["dbcName"] = "Felwood", - ["worldBounds"] = { left=1641.67, right=-4108.33, top=7133.33, bottom=3300.00 }, - ["names"] = { ["enUS"] = "Felwood", ["esES"] = "Frondavil", ["ptBR"] = "Selva Maleva", ["zhCN"] = "费伍德森林" }, - } -end - --- DBC WMA ID=201 MapID=1 'UngoroCrater' -if not MTH_DS_Zones[490] then - MTH_DS_Zones[490] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 490, - ["dbcMapId"] = 1, - ["dbcName"] = "UngoroCrater", - ["worldBounds"] = { left=533.33, right=-3166.67, top=-5966.67, bottom=-8433.33 }, - ["names"] = { ["enUS"] = "Un'Goro Crater", ["esES"] = "Crater Un'Goro", ["ptBR"] = "Cratera Un'Goro", ["zhCN"] = "安戈洛环形山" }, - } -end - --- DBC WMA ID=241 MapID=1 'Moonglade' -if not MTH_DS_Zones[493] then - MTH_DS_Zones[493] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 493, - ["dbcMapId"] = 1, - ["dbcName"] = "Moonglade", - ["worldBounds"] = { left=-1381.25, right=-3689.58, top=8491.67, bottom=6952.08 }, - ["names"] = { ["enUS"] = "Moonglade", ["esES"] = "Claro de la Luna", ["ptBR"] = "Moonglade", ["zhCN"] = "月光林地" }, - } -end - --- DBC WMA ID=261 MapID=1 'Silithus' -if not MTH_DS_Zones[1377] then - MTH_DS_Zones[1377] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 1377, - ["dbcMapId"] = 1, - ["dbcName"] = "Silithus", - ["worldBounds"] = { left=2537.50, right=-945.83, top=-5958.33, bottom=-8281.25 }, - ["names"] = { ["enUS"] = "Silithus", ["esES"] = "Silithus", ["ptBR"] = "Silithus", ["zhCN"] = "希利苏斯" }, - } -end - --- DBC WMA ID=281 MapID=1 'Winterspring' -if not MTH_DS_Zones[618] then - MTH_DS_Zones[618] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 618, - ["dbcMapId"] = 1, - ["dbcName"] = "Winterspring", - ["worldBounds"] = { left=-316.67, right=-7416.67, top=8533.33, bottom=3800.00 }, - ["names"] = { ["enUS"] = "Winterspring", ["esES"] = "Winterspring", ["ptBR"] = "Inverno", ["zhCN"] = "冬泉谷" }, - } -end - --- DBC WMA ID=341 MapID=0 'Ironforge' -if not MTH_DS_Zones[1537] then - MTH_DS_Zones[1537] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 1537, - ["dbcMapId"] = 0, - ["dbcName"] = "Ironforge", - ["worldBounds"] = { left=-713.59, right=-1504.22, top=-4569.24, bottom=-5096.85 }, - ["names"] = { ["enUS"] = "Ironforge", ["esES"] = "Forjaz", ["ptBR"] = "Altaforja", ["zhCN"] = "铁炉堡" }, - } -end - --- DBC WMA ID=401 MapID=30 'AlteracValley' -if not MTH_DS_Zones[2597] then - MTH_DS_Zones[2597] = { - ["continent"] = 30, - ["parent"] = 30, - ["dbcAreaId"] = 2597, - ["dbcMapId"] = 30, - ["dbcName"] = "AlteracValley", - ["worldBounds"] = { left=1781.25, right=-2456.25, top=1085.42, bottom=-1739.58 }, - ["names"] = { ["enUS"] = "Alterac Valley", ["esES"] = "Valle de Alterac", ["ptBR"] = "Vale Alterac", ["zhCN"] = "奥特兰克山谷" }, - } -end - --- DBC WMA ID=443 MapID=489 'WarsongGulch' -if not MTH_DS_Zones[3277] then - MTH_DS_Zones[3277] = { - ["continent"] = 489, - ["parent"] = 489, - ["dbcAreaId"] = 3277, - ["dbcMapId"] = 489, - ["dbcName"] = "WarsongGulch", - ["worldBounds"] = { left=2041.67, right=895.83, top=1627.08, bottom=862.50 }, - ["names"] = { ["enUS"] = "Warsong Gulch", ["esES"] = "Garganta de Warsong", ["ptBR"] = "Garganta Grito de Guerra", ["zhCN"] = "战歌峡谷" }, - } -end - --- DBC WMA ID=461 MapID=529 'ArathiBasin' -if not MTH_DS_Zones[3358] then - MTH_DS_Zones[3358] = { - ["continent"] = 529, - ["parent"] = 529, - ["dbcAreaId"] = 3358, - ["dbcMapId"] = 529, - ["dbcName"] = "ArathiBasin", - ["worldBounds"] = { left=1858.33, right=102.08, top=1508.33, bottom=337.50 }, - ["names"] = { ["enUS"] = "Arathi Basin", ["esES"] = "Cuenca de Arathi", ["ptBR"] = "Bacia Arathi", ["zhCN"] = "阿拉希盆地" }, - } -end - --- DBC WMA ID=500 MapID=1 'GMIsland' -if not MTH_DS_Zones[876] then - MTH_DS_Zones[876] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 876, - ["dbcMapId"] = 1, - ["dbcName"] = "GMIsland", - ["worldBounds"] = { left=16748.00, right=15920.00, top=16525.00, bottom=15984.00 }, - ["names"] = { ["enUS"] = "GM Island", ["esES"] = "Isla GM", ["ptBR"] = "Ilha GM", ["zhCN"] = "GM岛" }, - } -end - --- DBC WMA ID=501 MapID=1 'Hyjal' -if not MTH_DS_Zones[616] then - MTH_DS_Zones[616] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 616, - ["dbcMapId"] = 1, - ["dbcName"] = "Hyjal", - ["worldBounds"] = { left=-1080.00, right=-4286.00, top=6125.00, bottom=3983.00 }, - ["names"] = { ["enUS"] = "Hyjal", ["esES"] = "Hyjal", ["ptBR"] = "Hyjal", ["zhCN"] = "海加尔山" }, - } -end - --- DBC WMA ID=502 MapID=0 'ScarletEnclave' -if not MTH_DS_Zones[4012] then - MTH_DS_Zones[4012] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 4012, - ["dbcMapId"] = 0, - ["dbcName"] = "ScarletEnclave", - ["worldBounds"] = { left=-4050.00, right=-7209.00, top=3087.00, bottom=979.00 }, - ["names"] = { ["enUS"] = "Scarlet Enclave", ["esES"] = "Enclave Escarlata", ["ptBR"] = "Enclave Scarlet", ["zhCN"] = "东瘟疫之地:血色领地" }, - } -end - --- DBC WMA ID=503 MapID=27 'Sunnyglade' -if not MTH_DS_Zones[5023] then - MTH_DS_Zones[5023] = { - ["continent"] = 27, - ["parent"] = 27, - ["dbcAreaId"] = 5023, - ["dbcMapId"] = 27, - ["dbcName"] = "Sunnyglade", - ["worldBounds"] = { left=719.47, right=-269.16, top=2000.77, bottom=607.60 }, - ["names"] = { ["enUS"] = "Sunnyglade Valley", ["esES"] = "Valle Claro Sol", ["ptBR"] = "Vale Sunnyglade", ["zhCN"] = "阳光林地山谷" }, - } -end - --- DBC WMA ID=504 MapID=0 'Lapidis' -if not MTH_DS_Zones[409] then - MTH_DS_Zones[409] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 409, - ["dbcMapId"] = 0, - ["dbcName"] = "Lapidis", - ["worldBounds"] = { left=4933.33, right=2031.88, top=-11043.48, bottom=-12959.42 }, - ["names"] = { ["enUS"] = "Lapidis Isle", ["esES"] = "Isla Lapidis", ["ptBR"] = "Isla Lapidis", ["zhCN"] = "拉匹迪斯之岛" }, - } -end - --- DBC WMA ID=505 MapID=0 'Gillijim' -if not MTH_DS_Zones[408] then - MTH_DS_Zones[408] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 408, - ["dbcMapId"] = 0, - ["dbcName"] = "Gillijim", - ["worldBounds"] = { left=4436.22, right=1344.12, top=-12528.71, bottom=-14575.72 }, - ["names"] = { ["enUS"] = "Gillijim's Isle", ["esES"] = "Isla de Gillijim", ["ptBR"] = "Ilha de Gillijim", ["zhCN"] = "吉利吉姆之岛" }, - } -end - --- DBC WMA ID=506 MapID=31 'AlteracValleyClassic' -if not MTH_DS_Zones[2597] then - MTH_DS_Zones[2597] = { - ["continent"] = 31, - ["parent"] = 31, - ["dbcAreaId"] = 2597, - ["dbcMapId"] = 31, - ["dbcName"] = "AlteracValleyClassic", - ["worldBounds"] = { left=1781.25, right=-2456.25, top=1085.42, bottom=-1739.58 }, - ["names"] = { ["enUS"] = "Alterac Valley", ["esES"] = "Valle de Alterac", ["ptBR"] = "Vale Alterac", ["zhCN"] = "奥特兰克山谷" }, - } -end - --- DBC WMA ID=507 MapID=1 'TelAbim' -if not MTH_DS_Zones[5121] then - MTH_DS_Zones[5121] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 5121, - ["dbcMapId"] = 1, - ["dbcName"] = "TelAbim", - ["worldBounds"] = { left=-5179.00, right=-8406.00, top=-7184.00, bottom=-9371.00 }, - ["names"] = { ["enUS"] = "Tel'Abim", ["esES"] = "Tel'Abim", ["ptBR"] = "Tel'Abim", ["zhCN"] = "泰拉比姆" }, - } -end - --- DBC WMA ID=510 MapID=0 'Gilneas' -if not MTH_DS_Zones[5179] then - MTH_DS_Zones[5179] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 5179, - ["dbcMapId"] = 0, - ["dbcName"] = "Gilneas", - ["worldBounds"] = { left=3253.00, right=-413.00, top=-359.00, bottom=-2801.00 }, - ["names"] = { ["enUS"] = "Gilneas", ["esES"] = "Gilneas", ["ptBR"] = "Gilneas", ["zhCN"] = "吉尔尼斯" }, - } -end - --- DBC WMA ID=511 MapID=1 'Icepoint' -if not MTH_DS_Zones[5024] then - MTH_DS_Zones[5024] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 5024, - ["dbcMapId"] = 1, - ["dbcName"] = "Icepoint", - ["worldBounds"] = { left=-5595.00, right=-7203.00, top=14405.00, bottom=13330.00 }, - ["names"] = { ["enUS"] = "Icepoint Rock", ["esES"] = "Roca Punto de Hielo", ["ptBR"] = "Roca Punto de Hielo", ["zhCN"] = "冰点岩" }, - } -end - --- DBC WMA ID=512 MapID=1 'BlackstoneIsland' -if not MTH_DS_Zones[5536] then - MTH_DS_Zones[5536] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 5536, - ["dbcMapId"] = 1, - ["dbcName"] = "BlackstoneIsland", - ["worldBounds"] = { left=-6274.00, right=-8746.00, top=799.00, bottom=-866.00 }, - ["names"] = { ["enUS"] = "Blackstone Island", ["esES"] = "Isla Piedra Negra", ["ptBR"] = "Ilha Negrito", ["zhCN"] = "黑石岛" }, - } -end - --- DBC WMA ID=513 MapID=0 'ThalassianHighlands' -if not MTH_DS_Zones[5225] then - MTH_DS_Zones[5225] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 5225, - ["dbcMapId"] = 0, - ["dbcName"] = "ThalassianHighlands", - ["worldBounds"] = { left=-1005.00, right=-4087.00, top=4952.00, bottom=2891.00 }, - ["names"] = { ["enUS"] = "Thalassian Highlands", ["esES"] = "Tierras Altas Thalassianas", ["ptBR"] = "Terras Altas Thalassianas", ["zhCN"] = "萨拉斯高地" }, - } -end - --- DBC WMA ID=514 MapID=0 'DeadminesEntrance' -if not MTH_DS_Zones[1581] then - MTH_DS_Zones[1581] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 1581, - ["dbcMapId"] = 0, - ["dbcName"] = "DeadminesEntrance", - ["worldBounds"] = { left=1793.18, right=1343.29, top=-11054.94, bottom=-11354.86 }, - ["names"] = { ["enUS"] = "The Deadmines", ["esES"] = "Las Minas de la Muerte", ["ptBR"] = "As Minas Mortas", ["zhCN"] = "死亡矿井" }, - } -end - --- DBC WMA ID=516 MapID=1 'WailingCavernsEntrance' -if not MTH_DS_Zones[718] then - MTH_DS_Zones[718] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 718, - ["dbcMapId"] = 1, - ["dbcName"] = "WailingCavernsEntrance", - ["worldBounds"] = { left=-1900.87, right=-2473.65, top=-488.64, bottom=-870.49 }, - ["names"] = { ["enUS"] = "Wailing Caverns", ["esES"] = "Cuevas de los Lamentos", ["ptBR"] = "Cavernas Uivantes", ["zhCN"] = "哀嚎洞穴" }, - } -end - --- DBC WMA ID=517 MapID=1 'MaraudonEntrance' -if not MTH_DS_Zones[2100] then - MTH_DS_Zones[2100] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 2100, - ["dbcMapId"] = 1, - ["dbcName"] = "MaraudonEntrance", - ["worldBounds"] = { left=3208.00, right=2384.00, top=-1096.00, bottom=-1646.00 }, - ["names"] = { ["enUS"] = "Maraudon", ["esES"] = "Maraudon", ["ptBR"] = "Maraudon", ["zhCN"] = "玛拉顿" }, - } -end - --- DBC WMA ID=518 MapID=0 'GnomereganEntrance' -if not MTH_DS_Zones[721] then - MTH_DS_Zones[721] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 721, - ["dbcMapId"] = 0, - ["dbcName"] = "GnomereganEntrance", - ["worldBounds"] = { left=1027.64, right=456.45, top=-4806.32, bottom=-5185.46 }, - ["names"] = { ["enUS"] = "Gnomeregan", ["esES"] = "Gnomeregan", ["ptBR"] = "Gnomeregan", ["zhCN"] = "诺莫瑞根" }, - } -end - --- DBC WMA ID=519 MapID=0 'BlackrockMountain' -if not MTH_DS_Zones[25] then - MTH_DS_Zones[25] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 25, - ["dbcMapId"] = 0, - ["dbcName"] = "BlackrockMountain", - ["worldBounds"] = { left=-760.94, right=-1472.50, top=-7327.82, bottom=-7796.50 }, - ["names"] = { ["enUS"] = "Blackrock Mountain", ["esES"] = "Montaña Roca Negra", ["ptBR"] = "Montanha Rocha Negra", ["zhCN"] = "黑石山" }, - } -end - --- DBC WMA ID=520 MapID=0 'ScarletMonasteryEntrance' -if not MTH_DS_Zones[796] then - MTH_DS_Zones[796] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 796, - ["dbcMapId"] = 0, - ["dbcName"] = "ScarletMonasteryEntrance", - ["worldBounds"] = { left=-659.96, right=-863.62, top=2947.37, bottom=2812.33 }, - ["names"] = { ["enUS"] = "Scarlet Monastery", ["esES"] = "Monasterio Escarlata", ["ptBR"] = "Monastério Scarlet", ["zhCN"] = "血色修道院" }, - } -end - --- DBC WMA ID=521 MapID=0 'UldamanEntrance' -if not MTH_DS_Zones[1337] then - MTH_DS_Zones[1337] = { - ["continent"] = 0, - ["parent"] = 0, - ["dbcAreaId"] = 1337, - ["dbcMapId"] = 0, - ["dbcName"] = "UldamanEntrance", - ["worldBounds"] = { left=-2747.15, right=-3310.46, top=-5953.41, bottom=-6329.51 }, - ["names"] = { ["enUS"] = "Uldaman", ["esES"] = "Uldaman", ["ptBR"] = "Uldaman", ["zhCN"] = "奥达曼" }, - } -end - --- DBC WMA ID=522 MapID=1 'AhnQirajEntrance' -if not MTH_DS_Zones[3478] then - MTH_DS_Zones[3478] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 3478, - ["dbcMapId"] = 1, - ["dbcName"] = "AhnQirajEntrance", - ["worldBounds"] = { left=3932.67, right=-206.35, top=-8005.20, bottom=-10951.25 }, - ["names"] = { ["enUS"] = "Gates of Ahn'Qiraj", ["esES"] = "Puertas de Ahn'Qiraj", ["ptBR"] = "Portões de Ahn'Qiraj", ["zhCN"] = "安其拉之门" }, - } -end - --- DBC WMA ID=523 MapID=1 'CavernsOfTime' -if not MTH_DS_Zones[1941] then - MTH_DS_Zones[1941] = { - ["continent"] = 1, - ["parent"] = 1, - ["dbcAreaId"] = 1941, - ["dbcMapId"] = 1, - ["dbcName"] = "CavernsOfTime", - ["worldBounds"] = { left=-3695.85, right=-5044.09, top=-8023.09, bottom=-8911.18 }, - ["names"] = { ["enUS"] = "Caverns of Time", ["esES"] = "Cavernas del Tiempo", ["ptBR"] = "Cavernas do Tempo", ["zhCN"] = "时光之穴" }, - } -end - --- DBC WMA ID=601 MapID=813 'WinterVeilVale' -if not MTH_DS_Zones[5130] then - MTH_DS_Zones[5130] = { - ["continent"] = 813, - ["parent"] = 813, - ["dbcAreaId"] = 5130, - ["dbcMapId"] = 813, - ["dbcName"] = "WinterVeilVale", - ["worldBounds"] = { left=1916.00, right=484.00, top=-2332.00, bottom=-3309.00 }, - ["names"] = { ["enUS"] = "Winter Veil Vale", ["esES"] = "Valle del Velo Invernal", ["ptBR"] = "Vale do Véu de Inverno", ["zhCN"] = "冬幕谷" }, - } -end - --- DBC WMA ID=603 MapID=389 'Ragefire' -if not MTH_DS_Zones[2437] then - MTH_DS_Zones[2437] = { - ["continent"] = 389, - ["parent"] = 389, - ["dbcAreaId"] = 2437, - ["dbcMapId"] = 389, - ["dbcName"] = "Ragefire", - ["worldBounds"] = { left=452.87, right=-285.99, top=39.62, bottom=-452.95 }, - ["names"] = { ["enUS"] = "Ragefire Chasm", ["esES"] = "Sima Ígnea", ["ptBR"] = "Cavernas Ragefire", ["zhCN"] = "怒焰裂谷" }, - } -end - --- DBC WMA ID=605 MapID=209 'ZulFarrak' -if not MTH_DS_Zones[1176] then - MTH_DS_Zones[1176] = { - ["continent"] = 209, - ["parent"] = 209, - ["dbcAreaId"] = 1176, - ["dbcMapId"] = 209, - ["dbcName"] = "ZulFarrak", - ["worldBounds"] = { left=1625.00, right=241.67, top=2052.08, bottom=1129.17 }, - ["names"] = { ["enUS"] = "Zul'Farrak", ["esES"] = "Zul'Farrak", ["ptBR"] = "Zul'Farrak", ["zhCN"] = "祖尔法拉克" }, - } -end - --- DBC WMA ID=607 MapID=109 'TheTempleOfAtalHakkar' -if not MTH_DS_Zones[1477] then - MTH_DS_Zones[1477] = { - ["continent"] = 109, - ["parent"] = 109, - ["dbcAreaId"] = 1477, - ["dbcMapId"] = 109, - ["dbcName"] = "TheTempleOfAtalHakkar", - ["worldBounds"] = { left=442.76, right=-252.27, top=-255.17, bottom=-718.52 }, - ["names"] = { ["enUS"] = "The Temple of Atal'Hakkar", ["esES"] = "Templo de Atal'Hakkar", ["ptBR"] = "O Templo de Atal'Hakkar", ["zhCN"] = "阿塔哈卡神庙" }, - } -end - --- DBC WMA ID=609 MapID=48 'BlackFathomDeeps' -if not MTH_DS_Zones[719] then - MTH_DS_Zones[719] = { - ["continent"] = 48, - ["parent"] = 48, - ["dbcAreaId"] = 719, - ["dbcMapId"] = 48, - ["dbcName"] = "BlackFathomDeeps", - ["worldBounds"] = { left=485.41, right=-736.46, top=-107.91, bottom=-914.33 }, - ["names"] = { ["enUS"] = "Blackfathom Deeps", ["esES"] = "Cavernas de Brazanegra", ["ptBR"] = "Profundezas Negras", ["zhCN"] = "黑暗深渊" }, - } -end - --- DBC WMA ID=611 MapID=34 'TheStockade' -if not MTH_DS_Zones[717] then - MTH_DS_Zones[717] = { - ["continent"] = 34, - ["parent"] = 34, - ["dbcAreaId"] = 717, - ["dbcMapId"] = 34, - ["dbcName"] = "TheStockade", - ["worldBounds"] = { left=189.82, right=-188.33, top=220.63, bottom=-31.47 }, - ["names"] = { ["enUS"] = "The Stockade", ["esES"] = "La Mazmorras", ["ptBR"] = "A Prisão", ["zhCN"] = "监狱" }, - } -end - --- DBC WMA ID=613 MapID=90 'Gnomeregan' -if not MTH_DS_Zones[721] then - MTH_DS_Zones[721] = { - ["continent"] = 90, - ["parent"] = 90, - ["dbcAreaId"] = 721, - ["dbcMapId"] = 90, - ["dbcName"] = "Gnomeregan", - ["worldBounds"] = { left=491.90, right=-277.77, top=-180.89, bottom=-694.00 }, - ["names"] = { ["enUS"] = "Gnomeregan", ["esES"] = "Gnomeregan", ["ptBR"] = "Gnomeregan", ["zhCN"] = "诺莫瑞根" }, - } -end - --- DBC WMA ID=615 MapID=70 'Uldaman' -if not MTH_DS_Zones[1337] then - MTH_DS_Zones[1337] = { - ["continent"] = 70, - ["parent"] = 70, - ["dbcAreaId"] = 1337, - ["dbcMapId"] = 70, - ["dbcName"] = "Uldaman", - ["worldBounds"] = { left=645.12, right=-248.55, top=204.31, bottom=-391.47 }, - ["names"] = { ["enUS"] = "Uldaman", ["esES"] = "Uldaman", ["ptBR"] = "Uldaman", ["zhCN"] = "奥达曼" }, - } -end - --- DBC WMA ID=617 MapID=409 'MoltenCore' -if not MTH_DS_Zones[2717] then - MTH_DS_Zones[2717] = { - ["continent"] = 409, - ["parent"] = 409, - ["dbcAreaId"] = 2717, - ["dbcMapId"] = 409, - ["dbcName"] = "MoltenCore", - ["worldBounds"] = { left=-130.76, right=-1395.56, top=1303.06, bottom=459.86 }, - ["names"] = { ["enUS"] = "Molten Core", ["esES"] = "Núcleo de Magma", ["ptBR"] = "Núcleo Derretido", ["zhCN"] = "熔火之心" }, - } -end - --- DBC WMA ID=619 MapID=309 'ZulGurub' -if not MTH_DS_Zones[1977] then - MTH_DS_Zones[1977] = { - ["continent"] = 309, - ["parent"] = 309, - ["dbcAreaId"] = 1977, - ["dbcMapId"] = 309, - ["dbcName"] = "ZulGurub", - ["worldBounds"] = { left=-612.50, right=-2733.33, top=-11225.00, bottom=-12639.58 }, - ["names"] = { ["enUS"] = "Zul'Gurub", ["esES"] = "Zul'Gurub", ["ptBR"] = "Zul'Gurub", ["zhCN"] = "祖尔格拉布" }, - } -end - --- DBC WMA ID=621 MapID=429 'DireMaul' -if not MTH_DS_Zones[2557] then - MTH_DS_Zones[2557] = { - ["continent"] = 429, - ["parent"] = 429, - ["dbcAreaId"] = 2557, - ["dbcMapId"] = 429, - ["dbcName"] = "DireMaul", - ["worldBounds"] = { left=887.50, right=-387.50, top=1050.00, bottom=200.00 }, - ["names"] = { ["enUS"] = "Dire Maul", ["esES"] = "La Masacre", ["ptBR"] = "Gládio Cruel", ["zhCN"] = "厄运之槌" }, - } -end - --- DBC WMA ID=623 MapID=230 'BlackrockDepths' -if not MTH_DS_Zones[1584] then - MTH_DS_Zones[1584] = { - ["continent"] = 230, - ["parent"] = 230, - ["dbcAreaId"] = 1584, - ["dbcMapId"] = 230, - ["dbcName"] = "BlackrockDepths", - ["worldBounds"] = { left=522.34, right=-884.72, top=1186.68, bottom=248.64 }, - ["names"] = { ["enUS"] = "Blackrock Depths", ["esES"] = "Profundidades de Roca Negra", ["ptBR"] = "Abismo Rocha Negra", ["zhCN"] = "黑石深渊" }, - } -end - --- DBC WMA ID=625 MapID=509 'RuinsofAhnQiraj' -if not MTH_DS_Zones[3429] then - MTH_DS_Zones[3429] = { - ["continent"] = 509, - ["parent"] = 509, - ["dbcAreaId"] = 3429, - ["dbcMapId"] = 509, - ["dbcName"] = "RuinsofAhnQiraj", - ["worldBounds"] = { left=3035.42, right=522.92, top=-8233.33, bottom=-9908.33 }, - ["names"] = { ["enUS"] = "Ruins of Ahn'Qiraj", ["esES"] = "Ruinas de Ahn'Qiraj", ["ptBR"] = "Ruínas de Ahn'Qiraj", ["zhCN"] = "安其拉废墟" }, - } -end - --- DBC WMA ID=627 MapID=249 'OnyxiasLair' -if not MTH_DS_Zones[2159] then - MTH_DS_Zones[2159] = { - ["continent"] = 249, - ["parent"] = 249, - ["dbcAreaId"] = 2159, - ["dbcMapId"] = 249, - ["dbcName"] = "OnyxiasLair", - ["worldBounds"] = { left=111.38, right=-371.73, top=98.25, bottom=-223.83 }, - ["names"] = { ["enUS"] = "Onyxia's Lair", ["esES"] = "Guarida de Onyxia", ["ptBR"] = "Caverna de Onyxia", ["zhCN"] = "奥妮克希亚的巢穴" }, - } -end - --- DBC WMA ID=629 MapID=229 'BlackrockSpire' -if not MTH_DS_Zones[1583] then - MTH_DS_Zones[1583] = { - ["continent"] = 229, - ["parent"] = 229, - ["dbcAreaId"] = 1583, - ["dbcMapId"] = 229, - ["dbcName"] = "BlackrockSpire", - ["worldBounds"] = { left=10.59, right=-876.25, top=304.40, bottom=-286.83 }, - ["names"] = { ["enUS"] = "Blackrock Spire", ["esES"] = "Cumbre de Roca Negra", ["ptBR"] = "Pico Rocha Negra", ["zhCN"] = "黑石塔" }, - } -end - --- DBC WMA ID=631 MapID=43 'WailingCaverns' -if not MTH_DS_Zones[718] then - MTH_DS_Zones[718] = { - ["continent"] = 43, - ["parent"] = 43, - ["dbcAreaId"] = 718, - ["dbcMapId"] = 43, - ["dbcName"] = "WailingCaverns", - ["worldBounds"] = { left=790.00, right=-380.00, top=215.00, bottom=-570.00 }, - ["names"] = { ["enUS"] = "Wailing Caverns", ["esES"] = "Cuevas de los Lamentos", ["ptBR"] = "Cavernas Uivantes", ["zhCN"] = "哀嚎洞穴" }, - } -end - --- DBC WMA ID=633 MapID=349 'Maraudon' -if not MTH_DS_Zones[2100] then - MTH_DS_Zones[2100] = { - ["continent"] = 349, - ["parent"] = 349, - ["dbcAreaId"] = 2100, - ["dbcMapId"] = 349, - ["dbcName"] = "Maraudon", - ["worldBounds"] = { left=485.41, right=-1626.68, top=1199.76, bottom=-211.13 }, - ["names"] = { ["enUS"] = "Maraudon", ["esES"] = "Maraudon", ["ptBR"] = "Maraudon", ["zhCN"] = "玛拉顿" }, - } -end - --- DBC WMA ID=635 MapID=469 'BlackwingLair' -if not MTH_DS_Zones[2677] then - MTH_DS_Zones[2677] = { - ["continent"] = 469, - ["parent"] = 469, - ["dbcAreaId"] = 2677, - ["dbcMapId"] = 469, - ["dbcName"] = "BlackwingLair", - ["worldBounds"] = { left=-844.62, right=-1344.05, top=-7394.12, bottom=-7727.07 }, - ["names"] = { ["enUS"] = "Blackwing Lair", ["esES"] = "Guarida Alanegra", ["ptBR"] = "Covil Asa Negra", ["zhCN"] = "黑翼之巢" }, - } -end - --- DBC WMA ID=637 MapID=36 'TheDeadmines' -if not MTH_DS_Zones[5138] then - MTH_DS_Zones[5138] = { - ["continent"] = 36, - ["parent"] = 36, - ["dbcAreaId"] = 5138, - ["dbcMapId"] = 36, - ["dbcName"] = "TheDeadmines", - ["worldBounds"] = { left=-322.94, right=-979.53, top=36.93, bottom=-398.04 }, - ["names"] = { ["enUS"] = "The Deadmines", ["esES"] = "Las Minas de la Muerte", ["ptBR"] = "As Minas Mortas", ["zhCN"] = "死亡矿井" }, - } -end - --- DBC WMA ID=639 MapID=129 'RazorfenDowns' -if not MTH_DS_Zones[722] then - MTH_DS_Zones[722] = { - ["continent"] = 129, - ["parent"] = 129, - ["dbcAreaId"] = 722, - ["dbcMapId"] = 129, - ["dbcName"] = "RazorfenDowns", - ["worldBounds"] = { left=1279.94, right=570.89, top=2682.55, bottom=2209.85 }, - ["names"] = { ["enUS"] = "Razorfen Downs", ["esES"] = "Tierras Bajas de Rajacieno", ["ptBR"] = "Razorfen Downs", ["zhCN"] = "剃刀高地" }, - } -end - --- DBC WMA ID=641 MapID=47 'RazorfenKraul' -if not MTH_DS_Zones[491] then - MTH_DS_Zones[491] = { - ["continent"] = 47, - ["parent"] = 47, - ["dbcAreaId"] = 491, - ["dbcMapId"] = 47, - ["dbcName"] = "RazorfenKraul", - ["worldBounds"] = { left=2058.92, right=1322.47, top=2349.64, bottom=1858.68 }, - ["names"] = { ["enUS"] = "Razorfen Kraul", ["esES"] = "Madriguera de Rajacieno", ["ptBR"] = "Razorfen Kraul", ["zhCN"] = "剃刀沼泽" }, - } -end - --- DBC WMA ID=643 MapID=189 'ScarletMonastery' -if not MTH_DS_Zones[5136] then - MTH_DS_Zones[5136] = { - ["continent"] = 189, - ["parent"] = 189, - ["dbcAreaId"] = 5136, - ["dbcMapId"] = 189, - ["dbcName"] = "ScarletMonastery", - ["worldBounds"] = { left=1567.97, right=947.99, top=2030.18, bottom=1616.86 }, - ["names"] = { ["enUS"] = "Scarlet Monastery Graveyard", ["esES"] = "Cementerio Monasterio Escarlata", ["ptBR"] = "Cemitério do Monastério Escarlate", ["zhCN"] = "血色修道院-墓地" }, - } -end - --- DBC WMA ID=644 MapID=189 'ScarletMonastery2f' -if not MTH_DS_Zones[5135] then - MTH_DS_Zones[5135] = { - ["continent"] = 189, - ["parent"] = 189, - ["dbcAreaId"] = 5135, - ["dbcMapId"] = 189, - ["dbcName"] = "ScarletMonastery2f", - ["worldBounds"] = { left=-162.27, right=-482.46, top=307.37, bottom=93.91 }, - ["names"] = { ["enUS"] = "Scarlet Monastery Library", ["esES"] = "Biblioteca Monasterio Escarlata", ["ptBR"] = "Biblioteca do Monastério Escarlate", ["zhCN"] = "血色修道院-图书馆" }, - } -end - --- DBC WMA ID=645 MapID=189 'ScarletMonastery3f' -if not MTH_DS_Zones[5153] then - MTH_DS_Zones[5153] = { - ["continent"] = 189, - ["parent"] = 189, - ["dbcAreaId"] = 5153, - ["dbcMapId"] = 189, - ["dbcName"] = "ScarletMonastery3f", - ["worldBounds"] = { left=50.27, right=-562.42, top=2009.10, bottom=1600.64 }, - ["names"] = { ["enUS"] = "Scarlet Monastery Armory", ["esES"] = "Armería Monasterio Escarlata", ["ptBR"] = "Arsenal do Monastério Escarlate", ["zhCN"] = "血色修道院-军械库" }, - } -end - --- DBC WMA ID=646 MapID=189 'ScarletMonastery4f' -if not MTH_DS_Zones[5163] then - MTH_DS_Zones[5163] = { - ["continent"] = 189, - ["parent"] = 189, - ["dbcAreaId"] = 5163, - ["dbcMapId"] = 189, - ["dbcName"] = "ScarletMonastery4f", - ["worldBounds"] = { left=1743.99, right=1040.69, top=1281.29, bottom=812.42 }, - ["names"] = { ["enUS"] = "Scarlet Monastery Cathedral", ["esES"] = "Catedral Monasterio Escarlata", ["ptBR"] = "Catedral do Monastério Escarlate", ["zhCN"] = "血色修道院-大教堂" }, - } -end - --- DBC WMA ID=648 MapID=289 'Scholomance' -if not MTH_DS_Zones[2057] then - MTH_DS_Zones[2057] = { - ["continent"] = 289, - ["parent"] = 289, - ["dbcAreaId"] = 2057, - ["dbcMapId"] = 289, - ["dbcName"] = "Scholomance", - ["worldBounds"] = { left=251.41, right=-68.64, top=317.70, bottom=104.33 }, - ["names"] = { ["enUS"] = "Scholomance", ["esES"] = "Scholomance", ["ptBR"] = "Scholomance", ["zhCN"] = "通灵学院" }, - } -end - --- DBC WMA ID=650 MapID=33 'ShadowfangKeep' -if not MTH_DS_Zones[209] then - MTH_DS_Zones[209] = { - ["continent"] = 33, - ["parent"] = 33, - ["dbcAreaId"] = 209, - ["dbcMapId"] = 33, - ["dbcName"] = "ShadowfangKeep", - ["worldBounds"] = { left=2436.00, right=2055.00, top=-61.00, bottom=-315.00 }, - ["names"] = { ["enUS"] = "Shadowfang Keep", ["esES"] = "Fortaleza Colmillo Oscuro", ["ptBR"] = "Fortaleza Shadowfang", ["zhCN"] = "影牙城堡" }, - } -end - --- DBC WMA ID=652 MapID=329 'Stratholme' -if not MTH_DS_Zones[2017] then - MTH_DS_Zones[2017] = { - ["continent"] = 329, - ["parent"] = 329, - ["dbcAreaId"] = 2017, - ["dbcMapId"] = 329, - ["dbcName"] = "Stratholme", - ["worldBounds"] = { left=-2639.83, right=-3825.18, top=4163.90, bottom=3374.04 }, - ["names"] = { ["enUS"] = "Stratholme", ["esES"] = "Stratholme", ["ptBR"] = "Stratholme", ["zhCN"] = "斯坦索姆" }, - } -end - --- DBC WMA ID=654 MapID=531 'AhnQiraj' -if not MTH_DS_Zones[3428] then - MTH_DS_Zones[3428] = { - ["continent"] = 531, - ["parent"] = 531, - ["dbcAreaId"] = 3428, - ["dbcMapId"] = 531, - ["dbcName"] = "AhnQiraj", - ["worldBounds"] = { left=2515.63, right=1538.07, top=-8051.80, bottom=-8703.50 }, - ["names"] = { ["enUS"] = "Ahn'Qiraj", ["esES"] = "Ahn'Qiraj", ["ptBR"] = "Ahn'Qiraj", ["zhCN"] = "安其拉" }, - } -end - --- DBC WMA ID=655 MapID=531 'AhnQiraj2f' -if not MTH_DS_Zones[5147] then - MTH_DS_Zones[5147] = { - ["continent"] = 531, - ["parent"] = 531, - ["dbcAreaId"] = 5147, - ["dbcMapId"] = 531, - ["dbcName"] = "AhnQiraj2f", - ["worldBounds"] = { left=2915.62, right=138.08, top=-7659.29, bottom=-9510.98 }, - ["names"] = { ["enUS"] = "Ahn'Qiraj", ["esES"] = "Ahn'Qiraj", ["ptBR"] = "Ahn'Qiraj", ["zhCN"] = "安其拉" }, - } -end - --- DBC WMA ID=657 MapID=532 'Karazhan' -if not MTH_DS_Zones[3457] then - MTH_DS_Zones[3457] = { - ["continent"] = 532, - ["parent"] = 532, - ["dbcAreaId"] = 3457, - ["dbcMapId"] = 532, - ["dbcName"] = "Karazhan", - ["worldBounds"] = { left=-1614.00, right=-2212.00, top=-10780.00, bottom=-11179.00 }, - ["names"] = { ["enUS"] = "Tower of Karazhan", ["esES"] = "Torre de Karazhan", ["ptBR"] = "Karazhan", ["zhCN"] = "卡拉赞" }, - } -end - --- DBC WMA ID=659 MapID=369 'DeeprunTram' -if not MTH_DS_Zones[2257] then - MTH_DS_Zones[2257] = { - ["continent"] = 369, - ["parent"] = 369, - ["dbcAreaId"] = 2257, - ["dbcMapId"] = 369, - ["dbcName"] = "DeeprunTram", - ["worldBounds"] = { left=2623.50, right=2311.50, top=95.50, bottom=-112.50 }, - ["names"] = { ["enUS"] = "Deeprun Tram", ["esES"] = "Tranvía Subterráneo", ["ptBR"] = "Metrô Correfundo", ["zhCN"] = "矿道地铁" }, - } -end - --- DBC WMA ID=660 MapID=369 'DeeprunTram2f' -if not MTH_DS_Zones[5144] then - MTH_DS_Zones[5144] = { - ["continent"] = 369, - ["parent"] = 369, - ["dbcAreaId"] = 5144, - ["dbcMapId"] = 369, - ["dbcName"] = "DeeprunTram2f", - ["worldBounds"] = { left=187.50, right=-121.50, top=95.50, bottom=-112.50 }, - ["names"] = { ["enUS"] = "Deeprun Tram", ["esES"] = "Tranvía Profundocircuito", ["ptBR"] = "Metrô Correfundo", ["zhCN"] = "矿道地铁" }, - } -end - --- DBC WMA ID=662 MapID=269 'BlackMorass' -if not MTH_DS_Zones[5204] then - MTH_DS_Zones[5204] = { - ["continent"] = 269, - ["parent"] = 269, - ["dbcAreaId"] = 5204, - ["dbcMapId"] = 269, - ["dbcName"] = "BlackMorass", - ["worldBounds"] = { left=7338.47, right=6066.48, top=-1247.55, bottom=-2093.03 }, - ["names"] = { ["enUS"] = "The Black Morass", ["esES"] = "El Morass Negro", ["ptBR"] = "Lamaçal Negro", ["zhCN"] = "黑色沼泽" }, - } -end - --- DBC WMA ID=663 MapID=269 'BlackMorass2f' -if not MTH_DS_Zones[2366] then - MTH_DS_Zones[2366] = { - ["continent"] = 269, - ["parent"] = 269, - ["dbcAreaId"] = 2366, - ["dbcMapId"] = 269, - ["dbcName"] = "BlackMorass2f", - ["worldBounds"] = { left=7651.85, right=6565.99, top=-1500.47, bottom=-2227.08 }, - ["names"] = { ["enUS"] = "The Black Morass", ["esES"] = "La Ciénaga Negra", ["ptBR"] = "O Morass Negro", ["zhCN"] = "黑色沼泽" }, - } -end - --- DBC WMA ID=665 MapID=815 'GilneasCity' -if not MTH_DS_Zones[5208] then - MTH_DS_Zones[5208] = { - ["continent"] = 815, - ["parent"] = 815, - ["dbcAreaId"] = 5208, - ["dbcMapId"] = 815, - ["dbcName"] = "GilneasCity", - ["worldBounds"] = { left=3290.32, right=2040.14, top=-760.72, bottom=-1598.16 }, - ["names"] = { ["enUS"] = "Gilneas City", ["esES"] = "Ciudad Gilneas", ["ptBR"] = "Cidade de Gilneas", ["zhCN"] = "吉尔尼斯城" }, - } -end - --- DBC WMA ID=667 MapID=533 'Naxxramas' -if not MTH_DS_Zones[3456] then - MTH_DS_Zones[3456] = { - ["continent"] = 533, - ["parent"] = 533, - ["dbcAreaId"] = 3456, - ["dbcMapId"] = 533, - ["dbcName"] = "Naxxramas", - ["worldBounds"] = { left=-2375.27, right=-4366.96, top=3656.70, bottom=2338.28 }, - ["names"] = { ["enUS"] = "Naxxramas", ["esES"] = "Naxxramas", ["ptBR"] = "Naxxramas", ["zhCN"] = "纳克萨玛斯" }, - } -end - --- DBC WMA ID=668 MapID=533 'Naxxramas2f' -if not MTH_DS_Zones[5148] then - MTH_DS_Zones[5148] = { - ["continent"] = 533, - ["parent"] = 533, - ["dbcAreaId"] = 5148, - ["dbcMapId"] = 533, - ["dbcName"] = "Naxxramas2f", - ["worldBounds"] = { left=-4869.36, right=-5521.46, top=3816.56, bottom=3376.89 }, - ["names"] = { ["enUS"] = "The Upper Necropolis", ["esES"] = "La Necropolis Superior", ["ptBR"] = "Necrópole Superior", ["zhCN"] = "上层大墓地" }, - } -end - --- DBC WMA ID=670 MapID=802 'CrescentGrove' -if not MTH_DS_Zones[5077] then - MTH_DS_Zones[5077] = { - ["continent"] = 802, - ["parent"] = 802, - ["dbcAreaId"] = 5077, - ["dbcMapId"] = 802, - ["dbcName"] = "CrescentGrove", - ["worldBounds"] = { left=1207.72, right=-1435.49, top=882.98, bottom=-868.18 }, - ["names"] = { ["enUS"] = "Crescent Grove", ["esES"] = "Claro de la Media Luna", ["ptBR"] = "Bosque Crescente", ["zhCN"] = "新月林地" }, - } -end - --- DBC WMA ID=672 MapID=808 'HateforgeQuarry' -if not MTH_DS_Zones[5103] then - MTH_DS_Zones[5103] = { - ["continent"] = 808, - ["parent"] = 808, - ["dbcAreaId"] = 5103, - ["dbcMapId"] = 808, - ["dbcName"] = "HateforgeQuarry", - ["worldBounds"] = { left=-3024.33, right=-3776.45, top=-7888.28, bottom=-8398.61 }, - ["names"] = { ["enUS"] = "Hateforge Quarry", ["esES"] = "Cantera Hateforge", ["ptBR"] = "Pedreira Forja do Ódio", ["zhCN"] = "仇恨熔炉采石场" }, - } -end - --- DBC WMA ID=674 MapID=800 'KarazhanCrypt' -if not MTH_DS_Zones[5086] then - MTH_DS_Zones[5086] = { - ["continent"] = 800, - ["parent"] = 800, - ["dbcAreaId"] = 5086, - ["dbcMapId"] = 800, - ["dbcName"] = "KarazhanCrypt", - ["worldBounds"] = { left=-1302.51, right=-1849.26, top=-10916.17, bottom=-11308.14 }, - ["names"] = { ["enUS"] = "Karazhan Crypt", ["esES"] = "Cripta de Karazhan", ["ptBR"] = "Cripta Karazhan", ["zhCN"] = "卡拉赞墓穴" }, - } -end - --- DBC WMA ID=676 MapID=35 'StormwindVault' -if not MTH_DS_Zones[5087] then - MTH_DS_Zones[5087] = { - ["continent"] = 35, - ["parent"] = 35, - ["dbcAreaId"] = 5087, - ["dbcMapId"] = 35, - ["dbcName"] = "StormwindVault", - ["worldBounds"] = { left=314.34, right=-40.16, top=89.30, bottom=-145.44 }, - ["names"] = { ["enUS"] = "Stormwind Vault", ["esES"] = "Bóveda de Ventormenta", ["ptBR"] = "Cofre Stormwind", ["zhCN"] = "暴风城地牢" }, - } -end - --- DBC WMA ID=678 MapID=807 'EmeraldSanctum' -if not MTH_DS_Zones[5097] then - MTH_DS_Zones[5097] = { - ["continent"] = 807, - ["parent"] = 807, - ["dbcAreaId"] = 5097, - ["dbcMapId"] = 807, - ["dbcName"] = "EmeraldSanctum", - ["worldBounds"] = { left=3939.10, right=2666.00, top=3519.72, bottom=2666.00 }, - ["names"] = { ["enUS"] = "Emerald Sanctum", ["esES"] = "Sanctum Esmeralda", ["ptBR"] = "Santuário Esmeralda", ["zhCN"] = "翡翠圣殿" }, - } -end - --- DBC WMA ID=680 MapID=801 'Moomoo' -if not MTH_DS_Zones[5053] then - MTH_DS_Zones[5053] = { - ["continent"] = 801, - ["parent"] = 801, - ["dbcAreaId"] = 5053, - ["dbcMapId"] = 801, - ["dbcName"] = "Moomoo", - ["worldBounds"] = { left=17296.30, right=16288.62, top=17158.52, bottom=16486.73 }, - ["names"] = { ["enUS"] = "Moomoo Grove", ["esES"] = "Claro Moomoo", ["ptBR"] = "Bosque Moomoo", ["zhCN"] = "奶牛树林" }, - } -end - --- DBC WMA ID=682 MapID=814 'UpperKarazhan' -if not MTH_DS_Zones[3457] then - MTH_DS_Zones[3457] = { - ["continent"] = 814, - ["parent"] = 814, - ["dbcAreaId"] = 3457, - ["dbcMapId"] = 814, - ["dbcName"] = "UpperKarazhan", - ["worldBounds"] = { left=-1378.00, right=-2201.00, top=-10887.00, bottom=-11436.00 }, - ["names"] = { ["enUS"] = "Tower of Karazhan", ["esES"] = "Torre de Karazhan", ["ptBR"] = "Karazhan", ["zhCN"] = "卡拉赞" }, - } -end - --- DBC WMA ID=683 MapID=814 'UpperKarazhan2f' -if not MTH_DS_Zones[5557] then - MTH_DS_Zones[5557] = { - ["continent"] = 814, - ["parent"] = 814, - ["dbcAreaId"] = 5557, - ["dbcMapId"] = 814, - ["dbcName"] = "UpperKarazhan2f", - ["worldBounds"] = { left=-1483.00, right=-4283.00, top=-5800.00, bottom=-7668.00 }, - ["names"] = { ["enUS"] = "???", ["esES"] = "???", ["ptBR"] = "???", ["zhCN"] = "外域" }, - } -end diff --git a/init/api.xml b/init/api.xml index 9bc6d81..a73951e 100644 --- a/init/api.xml +++ b/init/api.xml @@ -43,7 +43,6 @@ - diff --git a/modules/smartpet/engine.lua b/modules/smartpet/engine.lua deleted file mode 100644 index 5517f6d..0000000 --- a/modules/smartpet/engine.lua +++ /dev/null @@ -1,1293 +0,0 @@ ------------------------------------------------------- --- MetaHunt: SmartPet — Pet Combat Management --- TWoW 1.18.1 ------------------------------------------------------- --- --- Integrated pet combat manager inspired by SmartPet v2.5.1. --- Handles taunt management, focus budgeting, CC-break --- prevention, PVP auto-detaunt, The Button keybind, --- NoChase, Dash/Dive on attack, auto-cower, and --- low-health chat warnings. ------------------------------------------------------- - --- ── Localised ability names ────────────────────────────────── --- Resolved from MTH_LocaleData.spells via MTH:LocalizeSpell(). --- PET_ACTION_FOLLOW / PET_ACTION_ATTACK are built-in WoW tokens, --- not localised spell names. -local L_GROWL, L_COWER, L_CLAW, L_BITE, L_DASH, L_DIVE -local L_LIGHTNING_BREATH, L_SCREECH, L_SCORPID_POISON -local L_CHARGE, L_DEATH_ROLL, L_FURIOUS_HOWL, L_SAVAGE_REND -local L_THUNDERSTOMP, L_POISON_SPIT, L_POLLEN_BURST -local L_PROWL, L_SHELL_SHIELD, L_GRACE, L_BUBBLE_BARRIER -local L_WEB, L_ROAR_OF_FORTITUDE, L_PACKLEADER, L_STRIDER_PRESENCE -local L_FOLLOW = "PET_ACTION_FOLLOW" -local L_ATTACK = "PET_ACTION_ATTACK" -local L_FLEE = "attempts to run away in fear" - --- ── CC debuff token list (English keys into spell locale) ──── -local CC_DEBUFF_TOKENS = { - "Gouge", "Sap", "Charm", "Seduction", "Sheep", "Polymorph", - "Tame Beast", "Scare Beast", "Sleep", "Hibernate", "Fear", - "Mind Control", "Blind", "Scatter Shot", "Enslave Demon", - "Shackle Undead", "Reckless Charge", "Freezing Trap Effect", - "Intimidating Shout", "Repentance", "Wyvern Sting", -} - --- Populated once MTH:LocalizeSpell is available (VARIABLES_LOADED) -local CC_DEBUFF_SET = {} - --- Maps localised ability names to rt.slot keys. -local ABILITY_MAP = {} -local function BuildAbilityMap() - ABILITY_MAP = { - [L_GROWL] = "growl", - [L_COWER] = "cower", - [L_CLAW] = "claw", - [L_BITE] = "bite", - [L_FOLLOW] = "follow", - [L_ATTACK] = "attack", - [L_DASH] = "dash", - [L_DIVE] = "dive", - } - -- Conditionally add family-specific abilities (nil-safe: L_ vars may be nil before locale resolution) - if L_LIGHTNING_BREATH then ABILITY_MAP[L_LIGHTNING_BREATH] = "lightningBreath" end - if L_SCREECH then ABILITY_MAP[L_SCREECH] = "screech" end - if L_SCORPID_POISON then ABILITY_MAP[L_SCORPID_POISON] = "scorpidPoison" end - if L_CHARGE then ABILITY_MAP[L_CHARGE] = "charge" end - if L_DEATH_ROLL then ABILITY_MAP[L_DEATH_ROLL] = "deathRoll" end - if L_FURIOUS_HOWL then ABILITY_MAP[L_FURIOUS_HOWL] = "furiousHowl" end - if L_SAVAGE_REND then ABILITY_MAP[L_SAVAGE_REND] = "savageRend" end - if L_THUNDERSTOMP then ABILITY_MAP[L_THUNDERSTOMP] = "thunderstomp" end - if L_POISON_SPIT then ABILITY_MAP[L_POISON_SPIT] = "poisonSpit" end - if L_POLLEN_BURST then ABILITY_MAP[L_POLLEN_BURST] = "pollenBurst" end - if L_PROWL then ABILITY_MAP[L_PROWL] = "prowl" end - if L_SHELL_SHIELD then ABILITY_MAP[L_SHELL_SHIELD] = "shellShield" end - if L_GRACE then ABILITY_MAP[L_GRACE] = "grace" end - if L_BUBBLE_BARRIER then ABILITY_MAP[L_BUBBLE_BARRIER] = "bubbleBarrier" end - if L_WEB then ABILITY_MAP[L_WEB] = "web" end - if L_ROAR_OF_FORTITUDE then ABILITY_MAP[L_ROAR_OF_FORTITUDE] = "roarOfFortitude" end - if L_PACKLEADER then ABILITY_MAP[L_PACKLEADER] = "packleader" end - if L_STRIDER_PRESENCE then ABILITY_MAP[L_STRIDER_PRESENCE] = "striderPresence" end -end - --- Called at VARIABLES_LOADED when the locale system is ready -local function ResolveLocaleStrings() - if not (MTH and MTH.LocalizeSpell) then return end - - L_GROWL = MTH:LocalizeSpell("Growl") - L_COWER = MTH:LocalizeSpell("Cower") - L_CLAW = MTH:LocalizeSpell("Claw") - L_BITE = MTH:LocalizeSpell("Bite") - L_DASH = MTH:LocalizeSpell("Dash") - L_DIVE = MTH:LocalizeSpell("Dive") - - -- Family-specific abilities - L_LIGHTNING_BREATH = MTH:LocalizeSpell("Lightning Breath") - L_SCREECH = MTH:LocalizeSpell("Screech") - L_SCORPID_POISON = MTH:LocalizeSpell("Scorpid Poison") - L_CHARGE = MTH:LocalizeSpell("Charge") - L_DEATH_ROLL = MTH:LocalizeSpell("Death Roll") - L_FURIOUS_HOWL = MTH:LocalizeSpell("Furious Howl") - L_SAVAGE_REND = MTH:LocalizeSpell("Savage Rend") - L_THUNDERSTOMP = MTH:LocalizeSpell("Thunderstomp") - L_POISON_SPIT = MTH:LocalizeSpell("Poison Spit") - L_POLLEN_BURST = MTH:LocalizeSpell("Pollen Burst") - L_PROWL = MTH:LocalizeSpell("Prowl") - L_SHELL_SHIELD = MTH:LocalizeSpell("Shell Shield") - L_GRACE = MTH:LocalizeSpell("Grace") - L_BUBBLE_BARRIER = MTH:LocalizeSpell("Bubble Barrier") - L_WEB = MTH:LocalizeSpell("Web") - L_ROAR_OF_FORTITUDE = MTH:LocalizeSpell("Roar of Fortitude") - L_PACKLEADER = MTH:LocalizeSpell("Packleader") - L_STRIDER_PRESENCE = MTH:LocalizeSpell("Strider Presence") - - -- Flee pattern from spell locale (special key) - local spellLocales = MTH_LocaleData and MTH_LocaleData.spells - if spellLocales then - local locale = MTH.currentLocale or "enUS" - local aliases = { enGB = "enUS", esMX = "esES", zhTW = "zhCN" } - if aliases[locale] then locale = aliases[locale] end - local map = spellLocales[locale] or spellLocales.enUS - if map and map["SP_FLEE_PATTERN"] then - L_FLEE = map["SP_FLEE_PATTERN"] - end - end - - -- Rebuild CC debuff set from localised names - CC_DEBUFF_SET = {} - for _, token in ipairs(CC_DEBUFF_TOKENS) do - local localized = MTH:LocalizeSpell(token) - CC_DEBUFF_SET[localized] = true - end - - -- Rebuild ability map with resolved names - BuildAbilityMap() -end - --- ── Constants ──────────────────────────────────────────────── -local FOCUS_REGEN_INTERVAL = 5 -- pet focus ticks roughly every 5s - --- Pet action bar slot count (WoW 1.12) -local NUM_PET_SLOTS = NUM_PET_ACTION_SLOTS or 10 - --- Ability metadata: focus cost and behaviour type. --- type: "taunt", "autocastDPS" (spammable, no meaningful CD), --- "semiAutocast" (short CD, still autocasts frequently), --- "cdDPS" (significant cooldown), "mobility", "defensive", --- "utility", "partyBuff" -local ABILITY_INFO = { - growl = { cost = 15, type = "taunt" }, - cower = { cost = 15, type = "taunt" }, - bite = { cost = 35, type = "autocastDPS" }, - claw = { cost = 25, type = "autocastDPS" }, - lightningBreath = { cost = 50, type = "autocastDPS" }, - scorpidPoison = { cost = 25, type = "semiAutocast" }, - screech = { cost = 20, type = "semiAutocast" }, - charge = { cost = 35, type = "cdDPS" }, - deathRoll = { cost = 50, type = "cdDPS" }, - furiousHowl = { cost = 50, type = "cdDPS" }, - savageRend = { cost = 25, type = "cdDPS" }, - thunderstomp = { cost = 60, type = "cdDPS" }, - poisonSpit = { cost = 35, type = "cdDPS" }, - pollenBurst = { cost = 40, type = "cdDPS" }, - dash = { cost = 20, type = "mobility" }, - dive = { cost = 20, type = "mobility" }, - prowl = { cost = 40, type = "utility" }, - shellShield = { cost = 10, type = "defensive" }, - grace = { cost = 10, type = "defensive" }, - bubbleBarrier = { cost = 20, type = "defensive" }, - web = { cost = 0, type = "utility" }, - roarOfFortitude = { cost = 50, type = "partyBuff" }, - packleader = { cost = 50, type = "partyBuff" }, - striderPresence = { cost = 50, type = "partyBuff" }, -} - --- Ability keys whose autocast is managed by the focus budget system. -local MANAGED_DPS_TYPES = { autocastDPS = true, semiAutocast = true, cdDPS = true } - --- ── Config defaults (persisted per-character) ──────────────── -local CONFIG_DEFAULTS = { - tauntMode = "auto", -- "auto" | "growl" | "cower" | "off" - pvpDetaunt = true, - ccBreakCheck = true, - ccBreakMode = "block", -- "block" | "warn" - theButton = true, - smartFocus = false, - autoCower = false, - autoCowerPct = 30, - noChase = false, - rushOnAttack = true, - autoWarn = false, - autoWarnPct = 20, - autoWarnChannel = "SAY", -} - --- ── Runtime state (not persisted) ──────────────────────────── -local rt = { - inCombat = false, - inPVP = false, - -- Backed-up pre-PVP autocast states - pvpBackup = nil, -- { growl=bool, cower=bool, autoCower=bool } - -- Pet action bar slot indices (-1 = not found) - slot = { - attack = -1, - follow = -1, - growl = -1, - cower = -1, - bite = -1, - claw = -1, - dash = -1, - dive = -1, - lightningBreath = -1, - screech = -1, - scorpidPoison = -1, - charge = -1, - deathRoll = -1, - furiousHowl = -1, - savageRend = -1, - thunderstomp = -1, - poisonSpit = -1, - pollenBurst = -1, - prowl = -1, - shellShield = -1, - grace = -1, - bubbleBarrier = -1, - web = -1, - roarOfFortitude = -1, - packleader = -1, - striderPresence = -1, - }, - -- Pet spellbook indices for rush abilities (used for CastSpell) - spellbook = { - dash = -1, - dive = -1, - charge = -1, - }, - -- Focus tracking - focus1 = 100, - focus2 = 100, - lastFocusTick = 0, - nextFocusTick = 0, - -- Smart Focus: remember which DPS autocasts were ON at combat start - preCombatAutocast = {}, -- { [slotKey] = bool } - -- AutoCower: track if we swapped to cower - autoCowered = false, - -- AutoWarn throttle - lastWarnTime = 0, - lastWarnPct = 0, - -- NoChase recall flag - recallPending = false, - -- TheButton context - theButtonRecall = false, - -- Party has a tank (cached) - partyHasTank = false, -} - --- ── Hidden tooltip for debuff scanning (legacy path) ───────── -local debuffTip = nil -local function EnsureDebuffTip() - if debuffTip then return debuffTip end - debuffTip = CreateFrame("GameTooltip", "MTH_SP_DebuffTip", UIParent, "GameTooltipTemplate") - debuffTip:SetOwner(UIParent, "ANCHOR_NONE") - return debuffTip -end - --- ── Config helpers ─────────────────────────────────────────── -local function GetCfg() - if type(MTH_SavedVariables) ~= "table" then return CONFIG_DEFAULTS end - if type(MTH_SavedVariables.modules) ~= "table" then - MTH_SavedVariables.modules = {} - end - local cfg = MTH_SavedVariables.modules["smartpet"] - if type(cfg) ~= "table" then - cfg = {} - MTH_SavedVariables.modules["smartpet"] = cfg - end - -- Fill defaults for missing keys - for k, v in pairs(CONFIG_DEFAULTS) do - if cfg[k] == nil then - cfg[k] = v - end - end - return cfg -end - -local function IsEnabled() - if not (MTH and MTH.IsModuleEnabled) then return false end - return MTH:IsModuleEnabled("smartpet", false) -end - --- ── Pet action bar helpers ─────────────────────────────────── -local function GetAutocast(slotIndex) - if slotIndex < 1 then return false end - local name, subtext, texture, isToken, isActive, autoCastAllowed, autoCastEnabled = GetPetActionInfo(slotIndex) - return autoCastEnabled and true or false -end - -local function SetAutocast(slotIndex, enabled) - if slotIndex < 1 then return end - local current = GetAutocast(slotIndex) - if (current and not enabled) or (not current and enabled) then - TogglePetAutocast(slotIndex) - end -end - --- ── Pet action bar scanning ────────────────────────────────── - -local function ScanPetActionBar() - for key in pairs(rt.slot) do - rt.slot[key] = -1 - end - for i = 1, NUM_PET_SLOTS do - local name = GetPetActionInfo(i) - if name then - local key = ABILITY_MAP[name] - if key then - rt.slot[key] = i - end - end - end -end - --- Scan pet spellbook for rush ability indices (for CastSpell) -local function ScanPetSpellbook() - rt.spellbook.dash = -1 - rt.spellbook.dive = -1 - rt.spellbook.charge = -1 - for i = 1, 20 do - local spellName = GetSpellName(i, BOOKTYPE_PET) - if not spellName then break end - if spellName == L_DASH then - rt.spellbook.dash = i - elseif spellName == L_DIVE then - rt.spellbook.dive = i - elseif spellName == L_CHARGE then - rt.spellbook.charge = i - end - end -end - --- ── Party/tank detection ───────────────────────────────────── -local TANK_CLASSES = { WARRIOR = true, PALADIN = true } --- Druid tanks detected by class + bear form buff (approximate) - -local function ScanPartyForTank() - local members = GetNumPartyMembers and GetNumPartyMembers() or 0 - local raid = GetNumRaidMembers and GetNumRaidMembers() or 0 - - if members == 0 and raid == 0 then - rt.partyHasTank = false - return - end - - -- Raid scan - if raid > 0 then - for i = 1, raid do - local unit = "raid" .. i - if UnitExists(unit) and not UnitIsUnit(unit, "player") then - local _, cls = UnitClass(unit) - if cls and TANK_CLASSES[cls] then - rt.partyHasTank = true - return - end - if cls == "DRUID" then - -- Check for bear form via power type (rage = 1) - if UnitPowerType and UnitPowerType(unit) == 1 then - rt.partyHasTank = true - return - end - end - end - end - rt.partyHasTank = false - return - end - - -- Party scan - for i = 1, members do - local unit = "party" .. i - if UnitExists(unit) then - local _, cls = UnitClass(unit) - if cls and TANK_CLASSES[cls] then - rt.partyHasTank = true - return - end - if cls == "DRUID" then - if UnitPowerType and UnitPowerType(unit) == 1 then - rt.partyHasTank = true - return - end - end - end - end - rt.partyHasTank = false -end - --- ── Taunt Management ───────────────────────────────────────── -local function ApplyTauntMode() - if not IsEnabled() then return end - local cfg = GetCfg() - local mode = cfg.tauntMode - - if mode == "off" then return end - if mode == "growl" then - SetAutocast(rt.slot.growl, true) - SetAutocast(rt.slot.cower, false) - return - end - if mode == "cower" then - SetAutocast(rt.slot.growl, false) - SetAutocast(rt.slot.cower, true) - return - end - -- auto: solo → growl, party with tank → cower - if rt.partyHasTank then - SetAutocast(rt.slot.growl, false) - if rt.slot.cower > 0 then - SetAutocast(rt.slot.cower, true) - end - else - SetAutocast(rt.slot.growl, true) - SetAutocast(rt.slot.cower, false) - end -end - --- ── PVP Auto-Detaunt ───────────────────────────────────────── -local function StartPVP() - if rt.inPVP then return end - local cfg = GetCfg() - if not cfg.pvpDetaunt then return end - - rt.inPVP = true - rt.pvpBackup = { - growl = GetAutocast(rt.slot.growl), - cower = GetAutocast(rt.slot.cower), - autoCower = cfg.autoCower, - } - SetAutocast(rt.slot.growl, false) - SetAutocast(rt.slot.cower, false) -end - -local function EndPVP() - if not rt.inPVP then return end - rt.inPVP = false - if rt.pvpBackup then - SetAutocast(rt.slot.growl, rt.pvpBackup.growl) - SetAutocast(rt.slot.cower, rt.pvpBackup.cower) - -- Restore autoCower setting — it doesn't get toggled on the bar, just the config flag - rt.pvpBackup = nil - end -end - --- ── CC-Break Prevention ────────────────────────────────────── --- Returns true if the target has a breakable CC debuff. -local function TargetHasBreakableCC() - local cfg = GetCfg() - if not cfg.ccBreakCheck then return false end - - -- NamPower path: GetUnitField aura array - if type(GetUnitField) == "function" then - local auras = GetUnitField("target", "aura") - if type(auras) == "table" then - for _, aura in ipairs(auras) do - if type(aura) == "table" then - local auraName = aura.name or (aura.spellId and GetSpellRecField and GetSpellRecField(aura.spellId, "name")) - if auraName and CC_DEBUFF_SET[auraName] then - return true - end - end - end - return false - end - end - - -- Legacy path: hidden tooltip scan - local tip = EnsureDebuffTip() - tip:ClearLines() - for j = 1, 16 do - if not UnitDebuff("target", j) then break end - tip:SetUnitDebuff("target", j) - local nameRegion = getglobal("MTH_SP_DebuffTipTextLeft1") - if nameRegion then - local debuffName = nameRegion:GetText() - if debuffName and CC_DEBUFF_SET[debuffName] then - return true - end - end - end - return false -end - --- ── Rush on attack (Dash / Dive / Charge) ──────────────────── -local function CastChargeAbility() - local cfg = GetCfg() - if not cfg.rushOnAttack then return end - if rt.inCombat then return end -- only on initial engage - - -- Try Charge first (Boar — rush + damage), then Dash, then Dive - local idx = -1 - if rt.spellbook.charge > 0 then - idx = rt.spellbook.charge - elseif rt.spellbook.dash > 0 then - idx = rt.spellbook.dash - elseif rt.spellbook.dive > 0 then - idx = rt.spellbook.dive - end - if idx < 1 then return end - - local start, duration = GetSpellCooldown(idx, BOOKTYPE_PET) - if start == 0 and duration == 0 then - CastSpell(idx, BOOKTYPE_PET) - end -end - --- ── Smart Focus ────────────────────────────────────────────── --- Estimate how many focus-regen ticks will occur during an ability's cooldown. -local function EstimatedFocusRegen(slotIndex) - if slotIndex < 1 then return 0 end - local start, duration = GetPetActionCooldown(slotIndex) - if not start or start == 0 then return 0 end - local remaining = (start + duration) - GetTime() - if remaining <= 0 then return 0 end - -- Each tick gives ~FOCUS_REGEN_INTERVAL worth of focus ticks at ~24 focus per 5s cycle - -- SmartPet used FocusManager * 4.8; we use a simpler remaining/5 * 24 estimate - local ticks = remaining / FOCUS_REGEN_INTERVAL - return ticks * 24 -end - -local function HandleFocusEvent() - if not IsEnabled() then return end - local cfg = GetCfg() - if not cfg.smartFocus and cfg.tauntMode == "off" then return end - if not rt.inCombat then return end - if rt.inPVP then return end - - -- Track focus regen tick timing - rt.focus2 = rt.focus1 - rt.focus1 = UnitMana("pet") or 0 - if rt.focus1 > rt.focus2 then - rt.lastFocusTick = GetTime() - rt.nextFocusTick = rt.lastFocusTick + 4.5 - end - - local currentFocus = rt.focus1 - - -- Determine taunt cost (must always be reservable) - local tauntCost = 0 - if cfg.tauntMode ~= "off" then - if GetAutocast(rt.slot.cower) then - tauntCost = ABILITY_INFO.cower.cost - elseif rt.slot.growl > 0 then - tauntCost = ABILITY_INFO.growl.cost - end - end - - -- Collect managed DPS abilities (on bar AND pre-combat autocast was ON) - local managed = {} -- { {key, cost, slot}, ... } - local managedN = 0 - for key, wasOn in pairs(rt.preCombatAutocast) do - if wasOn and rt.slot[key] > 0 and ABILITY_INFO[key] then - managedN = managedN + 1 - managed[managedN] = { key = key, cost = ABILITY_INFO[key].cost, slot = rt.slot[key] } - end - end - - if managedN == 0 then return end - - -- Sort by cost descending (most expensive = highest priority to keep active) - table.sort(managed, function(a, b) return a.cost > b.cost end) - - if cfg.smartFocus and managedN >= 2 then - -- Smart Focus: reserve taunt cost once, then budget the rest across DPS. - -- Always keep the cheapest ability ON so the pet never sits idle; - -- the worst case is growl is delayed by one regen tick (~5s). - local budget = currentFocus - tauntCost - if budget < 0 then budget = 0 end - if MTH and MTH.Print then - MTH:Print("[SP] focus=" .. currentFocus .. " reserve=" .. tauntCost .. " budget=" .. budget .. " managed=" .. managedN, "debug") - end - local anyEnabled = false - for i = 1, managedN do - local m = managed[i] - if budget >= m.cost then - SetAutocast(m.slot, true) - anyEnabled = true - if MTH and MTH.Print then - MTH:Print("[SP] " .. m.key .. " ON (cost=" .. m.cost .. " left=" .. (budget - m.cost) .. ")", "debug") - end - budget = budget - m.cost - elseif i == managedN and not anyEnabled then - -- Cheapest ability: keep ON so pet isn't idle - SetAutocast(m.slot, true) - if MTH and MTH.Print then - MTH:Print("[SP] " .. m.key .. " ON* (cheapest, waiting for regen)", "debug") - end - else - SetAutocast(m.slot, false) - if MTH and MTH.Print then - MTH:Print("[SP] " .. m.key .. " OFF (cost=" .. m.cost .. " budget=" .. budget .. ")", "debug") - end - end - end - return - end - - -- Basic taunt-priority: disable any DPS ability that would starve the taunt - if cfg.tauntMode ~= "off" and tauntCost > 0 then - for i = 1, managedN do - local m = managed[i] - local regen = EstimatedFocusRegen(m.slot) - if (currentFocus + regen) - m.cost < tauntCost then - SetAutocast(m.slot, false) - if MTH and MTH.Print then - MTH:Print("[SP] tauntGuard " .. m.key .. " OFF (focus=" .. currentFocus .. " regen=" .. math.floor(regen) .. ")", "debug") - end - else - SetAutocast(m.slot, true) - end - end - end -end - --- ── AutoCower on low HP ────────────────────────────────────── -local function HandlePetHealth() - if not IsEnabled() then return end - if not rt.inCombat then return end - local cfg = GetCfg() - - local hp = UnitHealth("pet") or 0 - local hpMax = UnitHealthMax("pet") or 1 - if hpMax == 0 then hpMax = 1 end - local pct = (100 * hp) / hpMax - - -- AutoCower - if cfg.autoCower and rt.slot.cower > 0 then - if pct < cfg.autoCowerPct then - if not rt.autoCowered then - rt.autoCowered = true - SetAutocast(rt.slot.growl, false) - SetAutocast(rt.slot.cower, true) - end - else - if rt.autoCowered then - rt.autoCowered = false - ApplyTauntMode() - end - end - end - - -- AutoWarn - if cfg.autoWarn then - local warnInterval - if pct >= 60 then - warnInterval = 10 - elseif pct >= 50 then - warnInterval = 8 - elseif pct >= 30 then - warnInterval = 6 - else - warnInterval = 4 - end - if pct < cfg.autoWarnPct - and (GetTime() - rt.lastWarnTime) > warnInterval - and rt.lastWarnPct > pct then - local petName = UnitName("pet") or "Pet" - local msg = petName .. " needs healing! (" .. string.format("%d", pct) .. "% Health)" - local chan = string.upper(cfg.autoWarnChannel or "SAY") - -- Validate channel - if chan == "PARTY" then - if not (UnitInParty and UnitInParty("player")) then chan = "SAY" end - elseif chan == "RAID" then - if not (GetNumRaidMembers and GetNumRaidMembers() > 0) then chan = "SAY" end - elseif chan == "GUILD" then - if not (IsInGuild and IsInGuild()) then chan = "SAY" end - elseif chan ~= "SAY" then - chan = "SAY" - end - SendChatMessage(msg, chan) - rt.lastWarnTime = GetTime() - end - rt.lastWarnPct = pct - end -end - --- ── NoChase ────────────────────────────────────────────────── -local function HandleMonsterEmote(emoteText, sourceName) - if not IsEnabled() then return end - local cfg = GetCfg() - if not cfg.noChase then return end - - local petTarget = UnitName("pettarget") - if sourceName and petTarget and sourceName == petTarget - and strfind(emoteText or "", L_FLEE) then - PetFollow() - rt.theButtonRecall = true - if UIErrorsFrame then - UIErrorsFrame:AddMessage("Recall Pet", 0.0, 0.0, 1.0, 1.0, UIERRORS_HOLD_TIME) - end - end -end - --- ── Pet Damage Meter ───────────────────────────────────────── -local meter = { - active = false, - recording = false, -- true while actively collecting data - startTime = 0, - duration = 0, -- 0 = unlimited, >0 = auto-stop after N seconds - totalDmg = 0, - totalHits = 0, - totalCrits = 0, - totalMisses = 0, - abilities = {}, -- [name] = { dmg=N, hits=N, crits=N, misses=N } - petName = "", - autocastSnapshot = "", -- "Bite=ON Claw=ON Growl=OFF ..." -} - -local MeterPrintSummary -- forward declaration -local MeterStop -- forward declaration - -local function MeterReset() - meter.startTime = GetTime() - meter.recording = true - meter.totalDmg = 0 - meter.totalHits = 0 - meter.totalCrits = 0 - meter.totalMisses = 0 - meter.abilities = {} - -- Snapshot pet name - meter.petName = UnitName("pet") or "Unknown" - -- Ensure slot map is current before reading autocast states - ScanPetActionBar() - -- Snapshot all known ability autocast states - local parts = {} - local partN = 0 - for key, info in pairs(ABILITY_INFO) do - if rt.slot[key] and rt.slot[key] > 0 then - local on = GetAutocast(rt.slot[key]) - partN = partN + 1 - parts[partN] = key .. "=" .. (on and "ON" or "OFF") - end - end - table.sort(parts) - meter.autocastSnapshot = table.concat(parts, " ") -end - -MeterStop = function() - meter.recording = false - meter.active = false - if meter.totalDmg > 0 then MeterPrintSummary() end - MTH:Print("|cffff9900[SP Meter]|r Stopped.") -end - -local function MeterGetAbility(name) - if not meter.abilities[name] then - meter.abilities[name] = { dmg = 0, hits = 0, crits = 0, misses = 0 } - end - return meter.abilities[name] -end - -local function MeterParseCombatMsg(msg) - if not meter.recording or not msg then return end - - local ability, dmg = nil, nil - - -- hit: "'s hits for ." - _, _, ability, dmg = strfind(msg, "'s ([%a ]-) hits .+ for (%d+)") - if ability and dmg then - dmg = tonumber(dmg) or 0 - local a = MeterGetAbility(ability) - a.hits = a.hits + 1 - a.dmg = a.dmg + dmg - meter.totalHits = meter.totalHits + 1 - meter.totalDmg = meter.totalDmg + dmg - return - end - - -- crit: "'s crits for ." - _, _, ability, dmg = strfind(msg, "'s ([%a ]-) crits .+ for (%d+)") - if ability and dmg then - dmg = tonumber(dmg) or 0 - local a = MeterGetAbility(ability) - a.crits = a.crits + 1 - a.dmg = a.dmg + dmg - meter.totalCrits = meter.totalCrits + 1 - meter.totalDmg = meter.totalDmg + dmg - return - end - - -- miss/resist: "'s misses" / "'s was resisted" - _, _, ability = strfind(msg, "'s ([%a ]-) misses") - if not ability then - _, _, ability = strfind(msg, "'s ([%a ]-) was ") - end - if ability then - local a = MeterGetAbility(ability) - a.misses = a.misses + 1 - meter.totalMisses = meter.totalMisses + 1 - return - end -end - -local function MeterParsePetMelee(msg) - if not meter.recording or not msg then return end - - local dmg = nil - - -- hit: " hits for ." - _, _, dmg = strfind(msg, " hits .+ for (%d+)") - if dmg then - dmg = tonumber(dmg) or 0 - local a = MeterGetAbility("Melee") - a.hits = a.hits + 1 - a.dmg = a.dmg + dmg - meter.totalHits = meter.totalHits + 1 - meter.totalDmg = meter.totalDmg + dmg - return - end - - -- crit: " crits for ." - _, _, dmg = strfind(msg, " crits .+ for (%d+)") - if dmg then - dmg = tonumber(dmg) or 0 - local a = MeterGetAbility("Melee") - a.crits = a.crits + 1 - a.dmg = a.dmg + dmg - meter.totalCrits = meter.totalCrits + 1 - meter.totalDmg = meter.totalDmg + dmg - return - end - - -- miss: " misses ." - if strfind(msg, " misses ") then - local a = MeterGetAbility("Melee") - a.misses = a.misses + 1 - meter.totalMisses = meter.totalMisses + 1 - return - end -end - -MeterPrintSummary = function() - if not MTH or not MTH.Print then return end - local elapsed = GetTime() - meter.startTime - if elapsed < 1 then elapsed = 1 end - local dps = meter.totalDmg / elapsed - local smartLabel = GetCfg().smartFocus and "ON" or "OFF" - local totalSwings = meter.totalHits + meter.totalCrits + meter.totalMisses - - MTH:Print(string.format( - "[SP Meter] Pet=%s SmartFocus=%s Duration=%.1fs TotalDmg=%d DPS=%.1f Swings=%d (H:%d C:%d M:%d)", - meter.petName, smartLabel, elapsed, meter.totalDmg, dps, totalSwings, - meter.totalHits, meter.totalCrits, meter.totalMisses), "debug") - - MTH:Print("[SP Meter] Autocasts: " .. meter.autocastSnapshot, "debug") - - -- Per-ability breakdown, sorted by damage - local sorted = {} - local sortN = 0 - for name, data in pairs(meter.abilities) do - sortN = sortN + 1 - sorted[sortN] = { name = name, dmg = data.dmg, hits = data.hits, crits = data.crits, misses = data.misses } - end - table.sort(sorted, function(a, b) return a.dmg > b.dmg end) - for i = 1, sortN do - local e = sorted[i] - local uses = e.hits + e.crits + e.misses - local pct = (meter.totalDmg > 0) and (100 * e.dmg / meter.totalDmg) or 0 - MTH:Print(string.format( - " %s: %d dmg (%.0f%%) %d uses (H:%d C:%d M:%d)", - e.name, e.dmg, pct, uses, e.hits, e.crits, e.misses), "debug") - end -end - --- ── Combat lifecycle ───────────────────────────────────────── -local function OnCombatStart() - if not IsEnabled() then return end - ScanPetActionBar() - rt.inCombat = true - rt.theButtonRecall = false - rt.autoCowered = false - rt.lastWarnTime = 0 - rt.lastWarnPct = (100 * (UnitHealth("pet") or 0)) / math.max(UnitHealthMax("pet") or 1, 1) - - -- Reset meter on new fight (only for untimed meters; timed meters keep recording across fights) - if meter.active and meter.duration == 0 then - MeterReset() - end - - -- Record pre-combat autocast states for all managed DPS abilities - rt.preCombatAutocast = {} - for key, info in pairs(ABILITY_INFO) do - if MANAGED_DPS_TYPES[info.type] and rt.slot[key] and rt.slot[key] > 0 then - rt.preCombatAutocast[key] = GetAutocast(rt.slot[key]) - if MTH and MTH.Print then - MTH:Print("[SP] snapshot " .. key .. "=" .. (GetAutocast(rt.slot[key]) and "ON" or "OFF") .. " slot=" .. rt.slot[key] .. " cost=" .. ABILITY_INFO[key].cost, "debug") - end - end - end - - -- Smart Focus initial state: keep only the most expensive ability enabled - local cfg = GetCfg() - if cfg.smartFocus then - local managed = {} - local managedN = 0 - for key, wasOn in pairs(rt.preCombatAutocast) do - if wasOn and ABILITY_INFO[key] then - managedN = managedN + 1 - managed[managedN] = { key = key, cost = ABILITY_INFO[key].cost, slot = rt.slot[key] } - end - end - if MTH and MTH.Print then - MTH:Print("[SP] combatStart smartFocus=ON managedON=" .. managedN, "debug") - end - if managedN >= 2 then - table.sort(managed, function(a, b) return a.cost > b.cost end) - for i = 1, managedN do - local state = (i == 1) and "ON" or "OFF" - if MTH and MTH.Print then - MTH:Print("[SP] init " .. managed[i].key .. "=" .. state .. " (cost=" .. managed[i].cost .. ")", "debug") - end - SetAutocast(managed[i].slot, i == 1) - end - end - else - if MTH and MTH.Print then - MTH:Print("[SP] combatStart smartFocus=OFF", "debug") - end - end -end - -local function OnCombatEnd() - -- Print meter summary on combat end (only for untimed meters) - if meter.active and meter.duration == 0 and meter.totalDmg > 0 then - MeterPrintSummary() - end - - -- Restore all managed DPS autocasts to pre-combat state - if rt.preCombatAutocast then - for key, wasOn in pairs(rt.preCombatAutocast) do - if rt.slot[key] and rt.slot[key] > 0 then - SetAutocast(rt.slot[key], wasOn) - if MTH and MTH.Print then - MTH:Print("[SP] restore " .. key .. "=" .. (wasOn and "ON" or "OFF"), "debug") - end - end - end - end - - if rt.inPVP then EndPVP() end - - rt.inCombat = false - rt.autoCowered = false - rt.theButtonRecall = false - - -- Re-apply taunt mode (restores growl/cower to config state) - ApplyTauntMode() -end - --- ── PetAttack wrapper (used by The Button and hook) ────────── -local MTH_SP_OriginalPetAttack = nil - -local function SmartPetAttack() - if not IsEnabled() then - if MTH_SP_OriginalPetAttack then MTH_SP_OriginalPetAttack() end - return - end - - local cfg = GetCfg() - - -- CC-Break check - if cfg.ccBreakCheck and UnitExists("target") then - if TargetHasBreakableCC() then - if cfg.ccBreakMode == "block" then - if UIErrorsFrame then - UIErrorsFrame:AddMessage("Breakable debuff found — pet attack blocked", 1.0, 0.3, 0.0, 1.0, UIERRORS_HOLD_TIME) - end - return - else - if UIErrorsFrame then - UIErrorsFrame:AddMessage("Warning: target has breakable CC", 1.0, 1.0, 0.0, 1.0, UIERRORS_HOLD_TIME) - end - end - end - end - - -- PVP check - if UnitExists("target") and UnitIsPlayer("target") and UnitCanAttack("player", "target") then - StartPVP() - end - - -- Dash/Dive on attack - CastChargeAbility() - - -- Issue the actual PetAttack - if MTH_SP_OriginalPetAttack then MTH_SP_OriginalPetAttack() end -end - --- ── The Button ─────────────────────────────────────────────── --- Global function called from Bindings.xml -function MTH_SmartPet_TheButton() - if not IsEnabled() then return end - local cfg = GetCfg() - if not cfg.theButton then return end - - -- NoChase recall pending - if rt.theButtonRecall then - PetFollow() - rt.theButtonRecall = false - return - end - - -- No pet alive - if not UnitExists("pet") or UnitIsDead("pet") then return end - - -- No target or dead target → recall - if not UnitExists("target") or UnitIsDead("target") then - PetFollow() - return - end - - -- Friendly target → assist (attack their target) - if UnitIsPlayer("target") and UnitCanCooperate("player", "target") then - if UnitExists("targettarget") and not UnitIsDead("targettarget") - and UnitCanAttack("player", "targettarget") then - AssistUnit("target") - SmartPetAttack() - end - return - end - - -- Enemy target → attack - if UnitCanAttack("player", "target") and not UnitIsDead("target") then - SmartPetAttack() - return - end - - -- Fallback: recall - PetFollow() -end - --- ── Taunt toggle (keybind) ─────────────────────────────────── -function MTH_SmartPet_TauntToggle() - if not IsEnabled() then return end - local growlOn = GetAutocast(rt.slot.growl) - local cowerOn = GetAutocast(rt.slot.cower) - - if growlOn then - -- Switch to cower - SetAutocast(rt.slot.growl, false) - if rt.slot.cower > 0 then SetAutocast(rt.slot.cower, true) end - elseif cowerOn then - -- Switch to growl - SetAutocast(rt.slot.cower, false) - if rt.slot.growl > 0 then SetAutocast(rt.slot.growl, true) end - else - -- Neither on: enable growl - if rt.slot.growl > 0 then SetAutocast(rt.slot.growl, true) end - end -end - --- ── Event frame ────────────────────────────────────────────── -local frame = CreateFrame("Frame", "MTH_SmartPetFrame") -frame:Hide() - --- Meter auto-stop ticker (runs only while meter is active with a duration) -local meterTickFrame = CreateFrame("Frame") -meterTickFrame:Hide() -meterTickFrame:SetScript("OnUpdate", function() - if not meter.active or not meter.recording then - meterTickFrame:Hide() - return - end - if meter.duration > 0 and (GetTime() - meter.startTime) >= meter.duration then - MeterStop() - meterTickFrame:Hide() - end -end) - -frame:RegisterEvent("VARIABLES_LOADED") -frame:RegisterEvent("PET_BAR_UPDATE") -frame:RegisterEvent("PET_UI_UPDATE") -frame:RegisterEvent("PET_ATTACK_START") -frame:RegisterEvent("PET_ATTACK_STOP") -frame:RegisterEvent("PLAYER_DEAD") -frame:RegisterEvent("UNIT_HEALTH") -frame:RegisterEvent("UNIT_FOCUS") -frame:RegisterEvent("PARTY_MEMBERS_CHANGED") -frame:RegisterEvent("RAID_ROSTER_UPDATE") -frame:RegisterEvent("CHAT_MSG_MONSTER_EMOTE") -frame:RegisterEvent("PLAYER_ENTERING_WORLD") -frame:RegisterEvent("CHAT_MSG_SPELL_PET_DAMAGE") -frame:RegisterEvent("CHAT_MSG_COMBAT_PET_HITS") -frame:RegisterEvent("CHAT_MSG_COMBAT_PET_MISSES") - -frame:SetScript("OnEvent", function() - -- Meter events bypass the module-enabled check - if event == "CHAT_MSG_SPELL_PET_DAMAGE" then - if meter.recording then - MTH:Print("[SP Meter] " .. event .. ": " .. (arg1 or "nil"), "debug") - MeterParseCombatMsg(arg1) - end - return - end - if event == "CHAT_MSG_COMBAT_PET_HITS" or event == "CHAT_MSG_COMBAT_PET_MISSES" then - if meter.recording then - MTH:Print("[SP Meter] " .. event .. ": " .. (arg1 or "nil"), "debug") - MeterParsePetMelee(arg1) - end - return - end - - if not IsEnabled() then - -- Even with module off, handle meter start/stop (untimed only) - if meter.active and meter.duration == 0 then - if event == "PET_ATTACK_START" then - MeterReset() - elseif event == "PET_ATTACK_STOP" or event == "PLAYER_DEAD" then - if meter.totalDmg > 0 then MeterPrintSummary() end - end - end - return - end - - if event == "VARIABLES_LOADED" or event == "PLAYER_ENTERING_WORLD" then - ResolveLocaleStrings() - ScanPetActionBar() - ScanPetSpellbook() - ScanPartyForTank() - -- Hook PetAttack once - if not MTH_SP_OriginalPetAttack and type(PetAttack) == "function" then - MTH_SP_OriginalPetAttack = PetAttack - PetAttack = SmartPetAttack - end - ApplyTauntMode() - return - end - - if event == "PET_BAR_UPDATE" or event == "PET_UI_UPDATE" then - ScanPetActionBar() - ScanPetSpellbook() - return - end - - if event == "PET_ATTACK_START" then - OnCombatStart() - return - end - - if event == "PET_ATTACK_STOP" or event == "PLAYER_DEAD" then - OnCombatEnd() - return - end - - if event == "UNIT_FOCUS" and arg1 == "pet" then - HandleFocusEvent() - return - end - - if event == "UNIT_HEALTH" and arg1 == "pet" then - HandlePetHealth() - return - end - - if event == "PARTY_MEMBERS_CHANGED" or event == "RAID_ROSTER_UPDATE" then - ScanPartyForTank() - if not rt.inCombat then - ApplyTauntMode() - end - return - end - - if event == "CHAT_MSG_MONSTER_EMOTE" then - HandleMonsterEmote(arg1, arg2) - return - end -end) - --- ── Public API (for options panel and external access) ──────── -MTH_SmartPet = { - GetConfig = GetCfg, - - SetTauntMode = function(mode) - local cfg = GetCfg() - if mode == "auto" or mode == "growl" or mode == "cower" or mode == "off" then - cfg.tauntMode = mode - if not rt.inCombat then ApplyTauntMode() end - end - end, - - SetPvpDetaunt = function(v) GetCfg().pvpDetaunt = v and true or false end, - - SetCcBreakCheck = function(v) GetCfg().ccBreakCheck = v and true or false end, - SetCcBreakMode = function(mode) - if mode == "block" or mode == "warn" then - GetCfg().ccBreakMode = mode - end - end, - - SetTheButton = function(v) GetCfg().theButton = v and true or false end, - SetSmartFocus = function(v) GetCfg().smartFocus = v and true or false end, - - SetAutoCower = function(v) GetCfg().autoCower = v and true or false end, - SetAutoCowerPct = function(v) - v = tonumber(v) - if v and v >= 1 and v <= 99 then GetCfg().autoCowerPct = v end - end, - - SetNoChase = function(v) GetCfg().noChase = v and true or false end, - SetRushOnAttack = function(v) GetCfg().rushOnAttack = v and true or false end, - - SetAutoWarn = function(v) GetCfg().autoWarn = v and true or false end, - SetAutoWarnPct = function(v) - v = tonumber(v) - if v and v >= 1 and v <= 99 then GetCfg().autoWarnPct = v end - end, - SetAutoWarnChannel = function(chan) - chan = string.upper(tostring(chan or "SAY")) - if chan == "SAY" or chan == "PARTY" or chan == "RAID" or chan == "GUILD" then - GetCfg().autoWarnChannel = chan - end - end, - - -- Force a rescan (useful after pet swap) - Rescan = function() - ScanPetActionBar() - ScanPetSpellbook() - ScanPartyForTank() - if not rt.inCombat then ApplyTauntMode() end - end, - - -- Damage meter (duration=0 for unlimited, >0 for timed auto-stop) - StartMeter = function(duration) - meter.active = true - meter.duration = duration or 0 - MeterReset() - if duration and duration > 0 then - meterTickFrame:Show() - MTH:Print(string.format("|cffff9900[SP Meter]|r ON — recording for %ds.", duration)) - else - MTH:Print("|cffff9900[SP Meter]|r ON — /sp meter to stop.") - end - end, - - StopMeter = function() - if meter.active then - MeterStop() - meterTickFrame:Hide() - else - MTH:Print("|cffff9900[SP Meter]|r Not running.") - end - end, - - -- Reprint last summary - PrintMeter = function() - if meter.totalDmg > 0 then - MeterPrintSummary() - else - MTH:Print("|cffff9900[SP Meter]|r No data recorded.") - end - end, -} - --- ── Register with MTH framework so IsModuleEnabled works ───── -local MTH_SmartPetModule = { - name = "smartpet", - enabled = false, - events = {}, -} - -function MTH_SmartPetModule:init() - self.initialized = true -end - -function MTH_SmartPetModule:setEnabled(enabled) - -- No-op: SmartPet checks IsEnabled() on every event -end - -if MTH and MTH.RegisterModule then - MTH:RegisterModule("smartpet", MTH_SmartPetModule) -end - --- ── Slash command ──────────────────────────────────────────── -SLASH_MTHSmartPet1 = "/sp" -SlashCmdList["MTHSmartPet"] = function(msg) - msg = string.lower(msg or "") - if msg == "meter" then - if meter.active then - MTH_SmartPet.StopMeter() - else - MTH_SmartPet.StartMeter(0) - end - elseif msg == "meter print" then - MTH_SmartPet.PrintMeter() - else - -- "/sp meter 60" — timed start - local _, _, secs = strfind(msg, "^meter (%d+)$") - if secs then - MTH_SmartPet.StartMeter(tonumber(secs)) - else - MTH:Print("|cffff9900[SmartPet]|r /sp meter — toggle damage meter") - MTH:Print("|cffff9900[SmartPet]|r /sp meter 60 — record for 60s then stop") - MTH:Print("|cffff9900[SmartPet]|r /sp meter print — reprint last fight") - end - end -end diff --git a/modules/smartpet/loader.xml b/modules/smartpet/loader.xml deleted file mode 100644 index 6d68df7..0000000 --- a/modules/smartpet/loader.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - From da645fa53c39543de5014793ebbbfe23ebdd7a4c Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Wed, 9 Sep 2026 16:33:39 +0200 Subject: [PATCH 07/42] Remove empty placeholder init XMLs init/env.xml, compat.xml, libs.xml, skins.xml contained only comments (no frames, scripts, or includes) and were loaded as no-ops at startup. Deleted the files and their MetaHunt.toc include lines. init/ now holds only the three real XMLs (api, localization, modules). --- MetaHunt.toc | 4 ---- init/compat.xml | 4 ---- init/env.xml | 4 ---- init/libs.xml | 5 ----- init/skins.xml | 4 ---- 5 files changed, 21 deletions(-) delete mode 100644 init/compat.xml delete mode 100644 init/env.xml delete mode 100644 init/libs.xml delete mode 100644 init/skins.xml diff --git a/MetaHunt.toc b/MetaHunt.toc index d553386..d26b3c4 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -11,12 +11,8 @@ data\init.lua Bindings.xml ## Load initialization and core framework -init\env.xml -init\compat.xml -init\libs.xml init\api.xml api\core-feed-tracking.lua -init\skins.xml ## Load database files (DB files must come before data\init.lua) data\ds-beasts.lua diff --git a/init/compat.xml b/init/compat.xml deleted file mode 100644 index 314f140..0000000 --- a/init/compat.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/init/env.xml b/init/env.xml deleted file mode 100644 index b2363b9..0000000 --- a/init/env.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/init/libs.xml b/init/libs.xml deleted file mode 100644 index 2161052..0000000 --- a/init/libs.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/init/skins.xml b/init/skins.xml deleted file mode 100644 index 227d434..0000000 --- a/init/skins.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - From de6621f41f9e5ed232b6b110a0493347fb78a440 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Wed, 9 Sep 2026 16:58:19 +0200 Subject: [PATCH 08/42] Add requirements.txt for the .tools Python venv Freezes the dev toolchain (numpy, pandas, pillow, mpyq, odfpy, asarPy, defusedxml, python-dateutil, six) so any machine rebuilds .venv from this instead of copying the 176MB virtualenv. Supports the multi-machine workflow: clone via git, then 'python -m venv .venv && .venv/bin/pip install -r requirements.txt'. --- requirements.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3c6cf62 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +asarPy==1.0.1 +defusedxml==0.7.1 +mpyq==0.2.5 +numpy==2.4.3 +odfpy==1.4.1 +pandas==3.0.1 +pillow==12.1.1 +python-dateutil==2.9.0.post0 +six==1.17.0 From 060fc555fe6a42302d841ade359d08370ce9f352 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Wed, 9 Sep 2026 22:52:48 +0200 Subject: [PATCH 09/42] Unify SavedVariables into two clean roots with one-time migration Collapse all legacy/scattered SavedVariables from absorbed standalone addons (FeedOMatic, ZHunter/zButtons, SmartAmmo, AutoQuest, AntiDaze, AutoStrip, MinimapButton) into two persisted globals with clean per-module nesting under .modules.. Migration engine (api/savedvariables.lua): - MTH_SV_EnsureSchema() with schemaVersion gate, idempotent + non-destructive - Per-module migrators map old locations -> canonical nested stores - Driven by an ADDON_LOADED("MetaHunt") handler so it runs only AFTER WoW loads the real SavedVariables (fixes announce-every-session bug that ran the migration on empty pre-load defaults) - One-time player-facing chat announce, gated on real legacy data present Framework/config: - core-framework InitSavedVariables no longer runs migration at file-load; unconditionally strips account-root module aliases - config.lua stops re-creating root[module] aliases (was duplicating data onto disk); canonical store stays under .modules. Modules: - feedomatic: bind FOM_* globals as runtime aliases into nested store, drop persisted duplicates and .legacy blob - zhunter: ZHunterMod_Saved re-pointed at .modules.zhunter - smartammo/autoquest/antidaze/autostrip/minimapbutton read nested store TOC trimmed to MTH_SavedVariables + MTH_CharSavedVariables only. Profiles unchanged (already snapshot/apply the nested .modules structure). Also includes pet-scan refinements: canonical pet key derivation and level-independent pet signature (schema v3). --- CHANGELOG.md | 11 + MetaHunt.toc | 4 +- README.MD | 39 +-- api/AntiDaze.lua | 19 +- api/AutoStrip.lua | 16 +- api/config.lua | 5 +- api/core-framework.lua | 29 +- api/core-scan-stablemaster.lua | 175 ++++++++++-- api/core.lua | 10 + api/minimap-button.lua | 15 +- api/options-autoquest.lua | 32 ++- api/options-credits.lua | 8 +- api/savedvariables.lua | 434 ++++++++++++++++++++++++++++++ init/api.xml | 1 + modules/autoquest/module.lua | 105 ++------ modules/feedomatic/FeedOMatic.lua | 98 ------- modules/feedomatic/module.lua | 114 ++------ modules/smartammo/engine.lua | 95 +++---- modules/smartammo/module.lua | 77 +----- modules/tooltips/module.lua | 23 +- modules/zhunter/ZHunterMod.lua | 31 ++- modules/zhunter/module.lua | 17 +- 22 files changed, 855 insertions(+), 503 deletions(-) create mode 100644 api/savedvariables.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index e0daa37..8481e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to MetaHunt will be documented in this file. +## [2.0.0] - Unreleased + +### Changed + +- **Settings storage tidied into one clean layout**: The settings MetaHunt inherited from the older stand-alone addons it now replaces (Feed-O-Matic, ZHunter/zButtons, SmartAmmo, and the quest/anti-daze/auto-strip/minimap helpers) used to be scattered across many separate saved entries, several of them duplicated on disk. MetaHunt now keeps everything in one organised place, grouped per module. The clean-up runs automatically and once, the first time you log in after updating: all of your existing settings and layouts are carried over unchanged — nothing is reset — and the old duplicate copies are removed so your saved-variables file stays small and consistent. + +### Fixed + +- **Pets — duplicate and phantom entries in your pet collection**: MetaHunt could record the same tamed pet more than once — a fresh copy appeared after you logged out and back in, or after your pet leveled up — and a few very early pets you had long since dismissed lingered as though they were still with you. MetaHunt now recognises each pet reliably from one session to the next (and as it levels), automatically merges the duplicate entries it had already created while keeping the correct taming details (where, and at what level, you tamed each one), and tidies the leftover phantom pets away into your pet history. Your full taming history is preserved — nothing is deleted. + + ## [1.5.2] - 2026-09-08 Res on Octowow server. diff --git a/MetaHunt.toc b/MetaHunt.toc index d26b3c4..50a0b74 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -3,8 +3,8 @@ ## Title: MetaHunt - |cff00ff00Hunter ## Author: Metasploit and his Copilot ;) ## Notes: Unified addon suite for huntards making old addons compatible with Twow, and adding an arsenal of never seen Hunter's utilities. -## SavedVariables: MTH_SavedVariables, FOM_Config, FOM_FoodQuality, FOM_AddedFoods, FOM_RemovedFoods, FOM_Cooking, FOM_QuestFood, FOM_LocaleInfo -## SavedVariablesPerCharacter: ZHunterMod_Saved, MTH_CharSavedVariables +## SavedVariables: MTH_SavedVariables +## SavedVariablesPerCharacter: MTH_CharSavedVariables data\init.lua diff --git a/README.MD b/README.MD index f6f7024..e916721 100644 --- a/README.MD +++ b/README.MD @@ -1,14 +1,15 @@ # MetaHunt MetaHunt is a Twow Hunter toolkit. -It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken on Twow. +It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken on twow. It provides a bunch of useful tools for all huntards, wheter they are still levelling, lone-wolf, or HL raider. ### Important notes + - Altough Nampower isn't mandatory for most of the features, it is a must-have for some. - If you were using old versions of Feed-O-Matic, ICU, zHunterMod and HunterHelper, you dont need those with MetaHunt and you should disable them to avoid conflicts. -- MetaHunt is fully compatible with Quiver and is not a replacement for it. -- Zero support for Capycraft and Ravencraft mafia. +- MetaHunt is fully compatible with QUIVER, and is not a replacement for it. +- MetaHunt don't and will never support Capycraft/Ravencraft (mafia) servers, after the DDOS attacks they conducted on other servers, to only name that. Fuck you. ## Twow Data @@ -23,6 +24,14 @@ MetaHunt ships with large Twow datastores : You can browse all the beasts in game within the Book of Huntards, but also on the github site : https://DuvelCorp.github.io/MetaHunt-Web/ + ## The Great Book of Huntards + + The Book is the main GUI of MetaHunt, it allows to display the data collected about you and your pet, and browse the addon's data to find anything a Hunter needs. + +- Browse the MetaHunt datastores of NPCs (Beast, vendors, masters) and open the map to locate them like in pfQuest +- Track all info about the pet abilities that you know and don't know yet +- Track all info about your stabled pets +- Keep an history of all your pets after you abandon them or if they run away. ## Smart Ammo @@ -31,7 +40,7 @@ You can browse all the beasts in game within the Book of Huntards, but also on t - Auto equip the right ammo when you swap from a box/xbow to a gun and the other way around. ## MM Widget - Lets Make MM great again. Tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock&Load procs, and has a dynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. + Lets Make MM great again. Tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock&Load procs, and has a dynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. You can also bind it to turn it into a very efficient one-button rotation. No more excuse to not be amongst top DPS. ## Tooltips @@ -45,11 +54,13 @@ You can browse all the beasts in game within the Book of Huntards, but also on t Old zHunterMod addon on steroids. Fully compatible with Twow, and enhanced with new functionalities - zAmmo : Track all your ammo Live and swap them in a click or bind -- zPet : Intelligent collapsible pet Bar regrouping all pet spells and providing an intelligent button that always show you the right spell to use depending on the situation +- zPet : Intelligent collapsible pet Bar regrouping all pet spells and providing a main button that always swap on the right spell to use depending on the situation - zTrack: All your trackings in one collapsible bar - zAspect: All your trackings in one collapsible bar - zRanged : All the ranged weapons currently in your bags to swap them fast -- But also zBar, zCrafts, zMounts, zToys and zCompanions. +- But also zCrafts, zMounts, zToys and zCompanions. + +Additionally, a zBar can regroup all the zButtons above into a single bar for people that like clean and ordered stuff. ## Feed-O-Matic @@ -63,26 +74,22 @@ The good old Chronometer addon, purged of non-necessary things and reconfigured Old addon used to display Targets' enhanced information when you click on your minimap tracking things. MetaHunt offers many more customization options for it. + ## Ammo labels + + You can display on all ammo in your bags and/or bank their type and/or dps to instantly see which of those damn same arrows icons really are. ## Auto-Buy -Define your own simple rules for auto buy ammo and pet food when you open a vendor. +Define your own simple rules for auto buy ammo and pet food when you open a vendor. You can also make a food-auto-buy planning based on all your stabled pet's diets. ## Auto-Quest -Just spam SHIFT-click on the NPC to validate quests Scorpok and Thorium arrows. Additionally you can have an enhanced tooltip that display your actual Scorpok-related items when you mouseover the mobs involved. +Just spam SHIFT-click on the NPC to validate quests Scorpok and Thorium arrows. Additionally you can have an enhanced tooltip that display your actual Scorpok-related items when you mouseover the Blasted Land mobs involved. ## Beast model Viewer Display the model of all beasts in the world. Always know what you are going to tame exactly before running there ! You can also browse all skin models of the same pet family to find the best-looking pet according to your tastes. - ## The Great Book of Huntards - - The Book is the main GUI of MetaHunt, it allows to display the data constantly collected about you and your pet, and browse the addon's data to find anything a Hunter needs. - -- Browse the MetaHunt datastores of NPCs (Beast, vendors, masters) and open the map to locate them like in pfQuest -- Track all info about the pet abilities that you know and don't know yet -- Track all info about your stabled pets -- Keep an history of all your pets after you abandon them or if they run away. + diff --git a/api/AntiDaze.lua b/api/AntiDaze.lua index d20ffba..40079be 100644 --- a/api/AntiDaze.lua +++ b/api/AntiDaze.lua @@ -10,10 +10,23 @@ local function AntiDaze_GetSaved() if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.antiDaze) ~= "table" then - MTH_CharSavedVariables.antiDaze = {} + local store + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("antidaze") end - return MTH_CharSavedVariables.antiDaze + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} + end + if type(MTH_CharSavedVariables.modules.antidaze) ~= "table" then + MTH_CharSavedVariables.modules.antidaze = {} + end + store = MTH_CharSavedVariables.modules.antidaze + end + if type(store.settings) ~= "table" then + store.settings = {} + end + return store.settings end local function AntiDaze_IsEnabled() diff --git a/api/AutoStrip.lua b/api/AutoStrip.lua index 762e448..b153b28 100644 --- a/api/AutoStrip.lua +++ b/api/AutoStrip.lua @@ -33,10 +33,20 @@ function AutoStrip_GetSaved() if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.autoStrip) ~= "table" then - MTH_CharSavedVariables.autoStrip = {} + local store + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("autostrip") end - return MTH_CharSavedVariables.autoStrip + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} + end + if type(MTH_CharSavedVariables.modules.autostrip) ~= "table" then + MTH_CharSavedVariables.modules.autostrip = {} + end + store = MTH_CharSavedVariables.modules.autostrip + end + return store end local AUTO_STRIP_ORDER = {16, 17, 18, 5, 7, 1, 3, 10, 8, 6, 9} diff --git a/api/config.lua b/api/config.lua index ecdb00b..772d006 100644 --- a/api/config.lua +++ b/api/config.lua @@ -38,7 +38,10 @@ local function MTH_ConfigEnsureStore(module_name) root.modules[module_name] = legacy end - root[module_name] = root.modules[module_name] + -- NOTE: we intentionally do NOT mirror root[module_name] = root.modules[module_name]. + -- That old alias made WoW serialize a full DUPLICATE of every module store at the + -- account root on disk. The canonical store is root.modules[module_name] only. + root[module_name] = nil MTH_SavedVariables = root return root.modules[module_name] end diff --git a/api/core-framework.lua b/api/core-framework.lua index f7e47c3..cd17a90 100644 --- a/api/core-framework.lua +++ b/api/core-framework.lua @@ -1187,8 +1187,6 @@ function MTH:InitSavedVariables() if not MTH_SavedVariables then MTH_SavedVariables = { modules = {}, - feedomatic = {}, - zhunter = {}, } end @@ -1198,12 +1196,6 @@ function MTH:InitSavedVariables() if not MTH_SavedVariables.moduleStates then MTH_SavedVariables.moduleStates = {} end - if not MTH_SavedVariables.modules.feedomatic then - MTH_SavedVariables.modules.feedomatic = MTH_SavedVariables.feedomatic or {} - end - if not MTH_SavedVariables.modules.zhunter then - MTH_SavedVariables.modules.zhunter = MTH_SavedVariables.zhunter or {} - end if type(MTH_SavedVariables.messages) ~= "table" then MTH_SavedVariables.messages = {} end @@ -1215,9 +1207,6 @@ function MTH:InitSavedVariables() end end - MTH_SavedVariables.feedomatic = MTH_SavedVariables.modules.feedomatic - MTH_SavedVariables.zhunter = MTH_SavedVariables.modules.zhunter - if not MTH_CharSavedVariables then MTH_CharSavedVariables = {} end @@ -1271,6 +1260,24 @@ function MTH:InitSavedVariables() end end end + + -- NOTE: the legacy/scattered SavedVariables consolidation (MTH_SV_EnsureSchema) + -- is intentionally NOT called here. InitSavedVariables also runs at file-load time + -- (before WoW has loaded the SV file from disk), when the roots are still empty + -- defaults. Running the migration there operated on throwaway tables and made it + -- re-announce every session. The migration is now driven from an ADDON_LOADED + -- handler in api/savedvariables.lua, which fires once the real saved data exists. + + -- Account-root module aliases must NEVER persist: the canonical stores live under + -- .modules.. feedomatic/zhunter used to be mirrored here (config.lua alias + + -- InitSavedVariables seeds), which made WoW serialize a full DUPLICATE on disk. + -- Strip them unconditionally on every load — even after the one-shot migration gate + -- has closed — so any stray copy re-created by older code, or already sitting in an + -- on-disk file from a previous version, is removed. + if type(MTH_SavedVariables) == "table" then + MTH_SavedVariables.feedomatic = nil + MTH_SavedVariables.zhunter = nil + end end function MTH:GetModuleSavedVariables(name) diff --git a/api/core-scan-stablemaster.lua b/api/core-scan-stablemaster.lua index 196981e..45b4bf1 100644 --- a/api/core-scan-stablemaster.lua +++ b/api/core-scan-stablemaster.lua @@ -2,7 +2,7 @@ if not MTH then error("MetaHunt core framework missing: api/core-framework.lua must load before api/core-scan-stablemaster.lua") end -local MTH_PETS_SCHEMA_VERSION = 2 +local MTH_PETS_SCHEMA_VERSION = 3 local MTH_PETS_CORE_HOOK_BOUNDARY_KEY = "core-pet-rename-hook" local MTH_ST_FULL_DEBUG_TRACE = false MTH_PETS_TRACE_CONSISTENCY = false @@ -914,6 +914,21 @@ local function MTH_PETS_ParseCreatureIdFromGuid(guid) return nil end +-- Canonical pet key: the pet GUID with its low 16 bits (last 4 hex chars) zeroed. +-- On this core the low 16 bits change every session while the high portion stays +-- stable per physical pet, so the full GUID cannot be used as a durable identity +-- key. The canonical key is that durable identity, derived purely from the GUID. +function MTH_PETS_CanonicalKey(guid) + if type(guid) ~= "string" or guid == "" then + return nil + end + local body = string.gsub(guid, "^0[xX]", "") + if string.len(body) < 5 then + return nil + end + return "0x" .. string.sub(body, 1, string.len(body) - 4) .. "0000" +end + local function MTH_PETS_ParseBeastLevelBounds(levelField) local levelText = tostring(levelField or "") if levelText == "" then @@ -1252,10 +1267,11 @@ local function MTH_PETS_ResolveConfirmedTameBeastId(source, snapshot) end local function MTH_PETS_MakeSignature(name, family, level) + -- Level intentionally excluded: it mutates on level-up, so baking it into the + -- identity signature made the same pet look like a new one after each level. local cleanName = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(name)) local cleanFamily = MTH_PETS_SafeLower(MTH_PETS_NormalizeText(family)) - local numericLevel = tonumber(level) or 0 - return cleanName .. "|" .. cleanFamily .. "|" .. tostring(numericLevel) + return cleanName .. "|" .. cleanFamily end local function MTH_PETS_RowHasAnyTameMetadata(row) @@ -1546,11 +1562,14 @@ local function MTH_PETS_RepairActiveRowsByGuid(pets) local groupedByGuid = {} for petId, row in pairs(pets.petStore.activeById) do if type(row) == "table" and type(row.guid) == "string" and row.guid ~= "" then - local guid = row.guid - if type(groupedByGuid[guid]) ~= "table" then - groupedByGuid[guid] = {} + -- Group by the CANONICAL key, not the full guid: the full guid is volatile + -- across sessions, so grouping on it never catches cross-session duplicates + -- of the same physical pet (the root cause of the duplicate-row bug). + local key = MTH_PETS_CanonicalKey(row.guid) or row.guid + if type(groupedByGuid[key]) ~= "table" then + groupedByGuid[key] = {} end - table.insert(groupedByGuid[guid], tostring(petId)) + table.insert(groupedByGuid[key], tostring(petId)) end end @@ -1627,6 +1646,9 @@ local function MTH_PETS_EnsurePetStoreSchema(pets) row.tameVerified = true end end + -- Re-sign level-free every load so the signature index can never retain a + -- level-baked key from older data. + row.signature = MTH_PETS_MakeSignature(row.name, row.family) if type(row.signature) == "string" and row.signature ~= "" then if type(petStore.signatureIndex[row.signature]) ~= "table" then petStore.signatureIndex[row.signature] = {} @@ -1634,7 +1656,12 @@ local function MTH_PETS_EnsurePetStoreSchema(pets) table.insert(petStore.signatureIndex[row.signature], resolvedPetId) end if type(row.guid) == "string" and row.guid ~= "" then - petStore.guidIndex[row.guid] = resolvedPetId + petStore.guidIndex[MTH_PETS_CanonicalKey(row.guid) or row.guid] = resolvedPetId + end + -- beastId is mislabeled (per-pet number, not creature id); expose petNumber + -- additively without breaking existing readers. + if row.beastId ~= nil and row.petNumber == nil then + row.petNumber = row.beastId end if row.loyaltyLevel == nil and type(row.loyalty) == "string" then row.loyaltyLevel = MTH_PETS_ParseLoyaltyLevelFromText(row.loyalty) @@ -1800,8 +1827,16 @@ local function MTH_PETS_ApplyCurrentPetFromActiveRow(cp, petId, row) end end +-- Forward declaration: the one-shot orphan cleanup is defined later (it reuses +-- MTH_PETS_MoveToHistory, declared further down), but MTH_PETS_EnsureSchema needs +-- to reference it here. By call time (a load-time event) it is assigned. +local MTH_PETS_ArchiveOrphanActiveRows + local function MTH_PETS_EnsureSchema(pets) local now = time() + -- Capture BEFORE the stamp block rewrites schemaVersion, so we know whether the + -- one-time v3 orphan cleanup still needs to run for this character. + local needsOrphanCleanup = (tonumber(pets.schemaVersion) or 0) < 3 if pets.schemaVersion ~= MTH_PETS_SCHEMA_VERSION then pets.schemaVersion = MTH_PETS_SCHEMA_VERSION pets.schemaMigratedAt = now @@ -1810,7 +1845,7 @@ local function MTH_PETS_EnsureSchema(pets) end pets.schemaMigration.version = MTH_PETS_SCHEMA_VERSION pets.schemaMigration.at = now - pets.schemaMigration.note = "beta canonical store" + pets.schemaMigration.note = "canonical identity keys + orphan cleanup" end if pets.updatedAt == nil then pets.updatedAt = 0 @@ -1839,6 +1874,11 @@ local function MTH_PETS_EnsureSchema(pets) end MTH_PETS_EnsureCurrentPetSchema(pets) MTH_PETS_EnsurePetStoreSchema(pets) + -- After dedup + index rebuild (done inside EnsurePetStoreSchema), sweep legacy + -- orphan rows (active but neither the current pet nor stabled) into history once. + if needsOrphanCleanup and type(MTH_PETS_ArchiveOrphanActiveRows) == "function" then + MTH_PETS_ArchiveOrphanActiveRows(pets, "migration-recovered") + end end local function MTH_PETS_MarkStableVisited(pets, source) @@ -2067,19 +2107,22 @@ local function MTH_PETS_SelectPetIdBySnapshot(store, snapshot, previousPetId) if previousPetId and store.activeById[previousPetId] then local previousRow = store.activeById[previousPetId] - if previousRow.guid and snapshot.guid and previousRow.guid == snapshot.guid then + if previousRow.guid and snapshot.guid + and MTH_PETS_CanonicalKey(previousRow.guid) == MTH_PETS_CanonicalKey(snapshot.guid) then return previousPetId end end - if snapshot.guid and store.guidIndex[snapshot.guid] and store.activeById[store.guidIndex[snapshot.guid]] then - return store.guidIndex[snapshot.guid] + local snapshotKey = snapshot.guid and MTH_PETS_CanonicalKey(snapshot.guid) or nil + if snapshotKey and store.guidIndex[snapshotKey] and store.activeById[store.guidIndex[snapshotKey]] then + return store.guidIndex[snapshotKey] end - if snapshot.guid and snapshot.guid ~= "" then + if snapshotKey then for petId, row in pairs(store.activeById or {}) do - if type(row) == "table" and row.guid == snapshot.guid then - store.guidIndex[snapshot.guid] = tostring(petId) + if type(row) == "table" and type(row.guid) == "string" + and MTH_PETS_CanonicalKey(row.guid) == snapshotKey then + store.guidIndex[snapshotKey] = tostring(petId) return tostring(petId) end end @@ -2240,16 +2283,18 @@ local function MTH_PETS_UpsertActivePetFromSnapshot(pets, snapshot, source, opti petStore.stableSlotIndex[row.stableSlot] = nil end row.stableSlot = nil - if row.guid and row.guid ~= snapshot.guid and petStore.guidIndex[row.guid] == petId then - petStore.guidIndex[row.guid] = nil + local oldGuidKey = row.guid and MTH_PETS_CanonicalKey(row.guid) or nil + local newGuidKey = snapshot.guid and MTH_PETS_CanonicalKey(snapshot.guid) or nil + if oldGuidKey and oldGuidKey ~= newGuidKey and petStore.guidIndex[oldGuidKey] == petId then + petStore.guidIndex[oldGuidKey] = nil end local context = MTH_PETS_CaptureContext() MTH_PETS_ApplySnapshotToRow(row, snapshot, source, context) row.firstSeen = row.firstSeen or time() - if snapshot.guid and snapshot.guid ~= "" then - petStore.guidIndex[snapshot.guid] = petId + if newGuidKey then + petStore.guidIndex[newGuidKey] = petId end if snapshot.signature and snapshot.signature ~= "" then MTH_PETS_AddSignatureIndex(petStore, snapshot.signature, petId) @@ -2573,8 +2618,9 @@ local function MTH_PETS_MoveToHistory(pets, petId, source, context, reason) if row.signature then MTH_PETS_RemoveSignatureIndex(petStore, row.signature, resolvedPetId) end - if row.guid and petStore.guidIndex[row.guid] == resolvedPetId then - petStore.guidIndex[row.guid] = nil + local guidKey = row.guid and MTH_PETS_CanonicalKey(row.guid) or nil + if guidKey and petStore.guidIndex[guidKey] == resolvedPetId then + petStore.guidIndex[guidKey] = nil end if row.stableSlot and petStore.stableSlotIndex[row.stableSlot] == resolvedPetId then petStore.stableSlotIndex[row.stableSlot] = nil @@ -2597,6 +2643,56 @@ local function MTH_PETS_MoveToHistory(pets, petId, source, context, reason) return true end +-- One-shot legacy cleanup (v3 migration): move "orphan" active rows into history. +-- An orphan is an active row that is neither the current pet nor associated with a +-- stable slot in any way. In normal play a pet is always either summoned or stabled, +-- so such rows are abandoned pets that older code failed to archive. They are moved +-- (not deleted): all tame history is preserved. Assigned to the forward-declared +-- local so MTH_PETS_EnsureSchema (defined earlier) can call it. +function MTH_PETS_ArchiveOrphanActiveRows(pets, reason) + local petStore = type(pets) == "table" and pets.petStore or nil + if type(petStore) ~= "table" or type(petStore.activeById) ~= "table" then + return 0 + end + local currentId = tostring(pets.currentPetId or petStore.activeCurrentId or "") + local orphanIds = {} + for petId, row in pairs(petStore.activeById) do + if type(row) == "table" then + local id = tostring(petId) + local isCurrent = (currentId ~= "" and id == currentId) + local inSlotIndex = false + if type(petStore.stableSlotIndex) == "table" then + for _, mappedId in pairs(petStore.stableSlotIndex) do + if tostring(mappedId or "") == id then + inSlotIndex = true + break + end + end + end + local hasStableSlot = tonumber(row.stableSlot) and tonumber(row.stableSlot) > 0 + local hasStableInfo = type(row.stableInfo) == "table" + if not isCurrent and not inSlotIndex and not hasStableSlot and not hasStableInfo then + table.insert(orphanIds, id) + end + end + end + local archived = 0 + for i = 1, table.getn(orphanIds) do + local id = orphanIds[i] + local row = petStore.activeById[id] + if type(row) == "table" then + row.migrationNote = "Auto-archived by 2.0 pet migration (orphaned active row)." + if MTH_PETS_MoveToHistory(pets, id, "migration-v3", nil, reason or "migration-recovered") then + archived = archived + 1 + end + end + end + if archived > 0 and MTH and MTH.DebugPrint then + MTH:DebugPrint("Pet migration: archived " .. tostring(archived) .. " orphan pet row(s) to history.") + end + return archived +end + function MTH_PETS_RecordPetAbandon(source, explicitName) local pets = MTH_PETS_GetRootStore() if type(pets) ~= "table" then @@ -3952,6 +4048,43 @@ function MTH_CommandPetsReset() return true end +-- Dev-only: force the v3 pet-store migration to run again (dedup by canonical key, +-- level-free re-sign, index rebuild, orphan cleanup). Resets the pet schema flag +-- and re-runs EnsureSchema. Does NOT touch MTH.version, so nothing is broadcast. +-- Non-destructive: history is preserved. Invoke via /mth dev petmigrate or /run. +function MTH_CommandPetsMigrate() + local pets = MTH_PETS_GetRootStore() + if type(pets) ~= "table" then + MTH:Print("Pet migration failed: datastore unavailable.") + return false + end + local store = MTH_PETS_GetStoreTables(pets) + local function countTable(t) + local n = 0 + if type(t) == "table" then for _ in pairs(t) do n = n + 1 end end + return n + end + local beforeActive = countTable(store and store.activeById) + local beforeHistory = countTable(store and store.historyById) + + -- Force the one-shot path to re-run regardless of current stamp. + pets.schemaVersion = 2 + MTH_PETS_EnsureSchema(pets) + + store = MTH_PETS_GetStoreTables(pets) + local afterActive = countTable(store and store.activeById) + local afterHistory = countTable(store and store.historyById) + pets.updatedAt = time() + if type(MTH_CharSavedVariables) == "table" then + MTH_CharSavedVariables.MTH_Pets = pets + MTH_CharSavedVariables.petStore = pets.petStore + end + MTH:Print("Pet migration (v3) complete: active " .. tostring(beforeActive) .. "->" .. tostring(afterActive) + .. ", history " .. tostring(beforeHistory) .. "->" .. tostring(afterHistory) + .. ", schemaVersion=" .. tostring(pets.schemaVersion)) + return true +end + function MTH_CommandPetsDump() local pets = MTH_PETS_GetRootStore() MTH_PETS_RefreshCurrentPet() diff --git a/api/core.lua b/api/core.lua index 4308a8e..dc1d2da 100644 --- a/api/core.lua +++ b/api/core.lua @@ -422,6 +422,16 @@ function SlashCmdList.MTH(msg, editbox) MTH:Print("|cffaaffaaArmed.|r Open any vendor, trainer, or stable master to capture their ID.") end end + elseif lowerCmd == "dev" then + if lowerArg == "petmigrate" then + if type(MTH_CommandPetsMigrate) == "function" then + MTH_CommandPetsMigrate() + else + MTH:Print("Pet migration command unavailable.") + end + else + MTH:Print("Dev commands: /mth dev petmigrate") + end elseif lowerCmd == "food" then local sub = lowerArg local function MTH_FoodItemLabel(itemId) diff --git a/api/minimap-button.lua b/api/minimap-button.lua index 947bd89..26eed87 100644 --- a/api/minimap-button.lua +++ b/api/minimap-button.lua @@ -25,10 +25,19 @@ local function MTH_MB_GetStore() if not MTH_CharSavedVariables then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.minimapButton) ~= "table" then - MTH_CharSavedVariables.minimapButton = {} + local store + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("minimapbutton") + end + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} + end + if type(MTH_CharSavedVariables.modules.minimapbutton) ~= "table" then + MTH_CharSavedVariables.modules.minimapbutton = {} + end + store = MTH_CharSavedVariables.modules.minimapbutton end - local store = MTH_CharSavedVariables.minimapButton if store.angle == nil then store.angle = 220 end diff --git a/api/options-autoquest.lua b/api/options-autoquest.lua index 032ade4..13c7a5f 100644 --- a/api/options-autoquest.lua +++ b/api/options-autoquest.lua @@ -16,26 +16,30 @@ local function MTH_AQ_EnsureStore() if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.autoquest) ~= "table" then - MTH_CharSavedVariables.autoquest = {} + local store = nil + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("autoquest") end - local store = MTH_CharSavedVariables.autoquest - if type(MTH_CharSavedVariables.questautomation) == "table" then - local legacy = MTH_CharSavedVariables.questautomation - if store.scorpokDrazial == nil and legacy.scorpokDrazial ~= nil then - store.scorpokDrazial = legacy.scorpokDrazial and true or false + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} end - if store.scorpokTooltip == nil and legacy.scorpokTooltip ~= nil then - store.scorpokTooltip = legacy.scorpokTooltip and true or false + if type(MTH_CharSavedVariables.modules.autoquest) ~= "table" then + MTH_CharSavedVariables.modules.autoquest = {} end + store = MTH_CharSavedVariables.modules.autoquest end - if store.scorpokDrazial == nil then - store.scorpokDrazial = false + if type(store.settings) ~= "table" then + store.settings = {} end - if store.arrowsForSissies == nil then - store.arrowsForSissies = false + local settings = store.settings + if settings.scorpokDrazial == nil then + settings.scorpokDrazial = false end - return store + if settings.arrowsForSissies == nil then + settings.arrowsForSissies = false + end + return settings end function MTH_SetupAutoQuestOptions() diff --git a/api/options-credits.lua b/api/options-credits.lua index 194b303..25603ac 100644 --- a/api/options-credits.lua +++ b/api/options-credits.lua @@ -129,7 +129,7 @@ function MTH_SetupCreditsOptions() cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_ABOUT_ZHUNTER", - "- zBars, Antidaze and Autostrip were core functionalities of Vanilla zHunterMod addon. And I kept the \"z\" naming of bars/buttons to always remember it. On top of this I have created myself zAmmo, zCompanions, zMount, zToys.\nAlso the SmartAmmo feature idea is coming from Zhuntermod: there was a file in it with that functionality, but was like a work-in-progress, totally unfunctional and even \"dangerous\" in its current state, and was not active in the addon. Since it was a fucking great idea, I recoded it mostly from scratch and made it work reliably and safely."), + "- zBars, Antidaze and Autostrip were core functionalities of Vanilla zHunterMod addon. And I kept the \"z\" naming of bars/buttons to always remember it. On top of this I have created myself zBars, zAmmo, zCompanions, zMount, zToys, zCraft.\nAlso the SmartAmmo feature idea is coming from Zhuntermod: there was a file in it with that functionality, but was like a work-in-progress, totally unfunctional and even \"dangerous\" in its current state, and was not active in the addon. Since it was a fucking great idea, I recoded it mostly from scratch and made it work reliably and safely."), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, @@ -170,7 +170,7 @@ function MTH_SetupCreditsOptions() -4) cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_ABOUT_MAP", "- The Map/Marker system is the one of pfQuest from Master Shagu. Unfortunately he is no more reachable for some months and I could not have some talk with him about this. I mostly let it untouched, I just made slight modifications so it can integrate better within Metahunt, and doesn't conflict with pfQuest."), + MTH_CR_L("CREDITS_ABOUT_MAP", "- The Map/Marker system is the one of pfQuest from Master Shagu. I mostly let it untouched, I just made slight modifications so it can integrate better within Metahunt, and doesn't conflict with pfQuest."), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, @@ -179,8 +179,8 @@ function MTH_SetupCreditsOptions() cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_ABOUT_DATA_TITLE", "About data sources"), "GameFontNormal", -14, 1.00, 0.82, 0.00) cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_ABOUT_DATA_INTRO", "Metahunt ships with its own Twow datastores, limited to hunter stuff only, and most of this data is up-to-date (Feb 2026).\n\n" - .. "Sources used to build the data include:"), + MTH_CR_L("CREDITS_ABOUT_DATA_INTRO", "Metahunt ships with its own Twow datastores, limited to hunter stuff only, and most of this data is up-to-date (twow 1.18.1). The beast information is coming directly from a extract of live twow DB, made for MetaHunt by twow dev lead in March 2026.\n\n" + .. "Other sources used to build the data include:"), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, diff --git a/api/savedvariables.lua b/api/savedvariables.lua new file mode 100644 index 0000000..d6687fe --- /dev/null +++ b/api/savedvariables.lua @@ -0,0 +1,434 @@ +------------------------------------------------------ +-- MetaHunt — Unified SavedVariables schema + migration engine +------------------------------------------------------ +-- PURPOSE +-- Collapse every legacy / scattered SavedVariable into just TWO persisted +-- globals and one consistent shape, so old-addon names (FOM_*, ZHunterMod_Saved, +-- MTHSmartAmmo) and ad-hoc key styles disappear. +-- +-- TARGET SHAPE (canonical) +-- MTH_SavedVariables (account-wide) = { +-- schemaVersion = , +-- modules = { +-- = { schemaVersion = N, settings = { ...camelCase... }, }, +-- feedomatic = { settings{...}, foodQuality, cooking, questFood, +-- addedFoods, removedFoods, localeInfo }, +-- }, +-- messages = {...}, moduleStates = {...}, profiles = {...}, +-- charSnapshots = {...}, versionCheck = {...}, +-- } +-- MTH_CharSavedVariables (per-character) = { +-- schemaVersion = , +-- modules = { +-- = { schemaVersion = N, settings{...}, }, +-- zhunter = { buttons{...}, bar{...}, widgetSpawnLayout }, +-- antidaze = { settings{ enabled } }, +-- autostrip = { settings{ enabled, display }, frame{ point, relativePoint, x, y } }, +-- minimapbutton = { settings{ angle } }, +-- }, +-- moduleStates = {...}, +-- -- pet-system core stays at root (out of scope, heavily referenced): +-- MTH_Pets, feedTracking, petTraining, trainScan, stableScan, petSpellScan, +-- } +-- +-- RULES +-- * One-shot, gated by schemaVersion on each root. Idempotent. Non-destructive +-- (data is COPIED into the new location before the old location is cleared). +-- * Lua 5.0-safe (no '#', no string.match/gmatch; pairs()/ipairs() only). +-- * Testable offline: MTH_SV_EnsureSchema(account, char) accepts explicit roots +-- (defaults to the live globals) and returns a report table. +------------------------------------------------------ + +MTH_SV_SCHEMA_VERSION = 1 + +------------------------------------------------------ +-- Helpers +------------------------------------------------------ + +local function MTH_SV_IsTable(v) + return type(v) == "table" +end + +local function MTH_SV_DeepCopy(orig) + if type(orig) ~= "table" then + return orig + end + local copy = {} + for k, v in pairs(orig) do + copy[MTH_SV_DeepCopy(k)] = MTH_SV_DeepCopy(v) + end + return copy +end + +-- Ensure parent[key] is a table and return it. +local function MTH_SV_Ensure(parent, key) + if type(parent[key]) ~= "table" then + parent[key] = {} + end + return parent[key] +end + +-- Pick the first non-nil value from a list of candidates; else default. +local function MTH_SV_Coalesce(candidates, default) + for _, v in ipairs(candidates) do + if v ~= nil then + return v + end + end + return default +end + +-- Copy every key from src into dst, applying a rename map (oldKey -> newKey). +-- Keys not present in the map keep their original name. Existing dst values win +-- (so re-running never clobbers already-migrated data). Returns count copied. +local function MTH_SV_CopyRenamed(src, dst, renameMap) + local moved = 0 + if not MTH_SV_IsTable(src) then + return moved + end + for oldKey, value in pairs(src) do + local newKey = (renameMap and renameMap[oldKey]) or oldKey + if dst[newKey] == nil then + dst[newKey] = value + moved = moved + 1 + end + end + return moved +end + +-- Count entries in a table (Lua 5.0-safe). +local function MTH_SV_Count(t) + local n = 0 + if MTH_SV_IsTable(t) then + for _ in pairs(t) do + n = n + 1 + end + end + return n +end + +------------------------------------------------------ +-- Per-module migrators +-- Each takes the relevant module store table (already ensured) plus context, +-- transforms in place, and records notes into the shared report. +------------------------------------------------------ + +-- SMARTAMMO (per-character) +-- OLD: modules.smartammo = { enabled, smartEnabled, reloadEnabled, +-- weaponSwapEnabled, legacy = { MTHSmartAmmo = { +-- enabled, reload, weaponSwap } } } +-- + optional global MTHSmartAmmo = { enabled, reload, weaponSwap } +-- NEW: modules.smartammo = { schemaVersion, settings = { +-- smartEnabled, reloadEnabled, weaponSwapEnabled } } +local function MTH_SV_Migrate_SmartAmmo(store, globalMTHSmartAmmo, report) + if not MTH_SV_IsTable(store) then + return + end + local legacy = MTH_SV_IsTable(store.legacy) and store.legacy.MTHSmartAmmo or nil + local g = MTH_SV_IsTable(globalMTHSmartAmmo) and globalMTHSmartAmmo or nil + + local settings = MTH_SV_Ensure(store, "settings") + + -- smartEnabled: modern flat -> settings -> legacy.enabled -> global.enabled -> old `enabled` -> true + if settings.smartEnabled == nil then + settings.smartEnabled = MTH_SV_Coalesce({ + store.smartEnabled, + legacy and legacy.enabled, + g and g.enabled, + store.enabled, + }, true) and true or false + end + if settings.reloadEnabled == nil then + settings.reloadEnabled = MTH_SV_Coalesce({ + store.reloadEnabled, + legacy and legacy.reload, + g and g.reload, + }, true) and true or false + end + if settings.weaponSwapEnabled == nil then + settings.weaponSwapEnabled = MTH_SV_Coalesce({ + store.weaponSwapEnabled, + legacy and legacy.weaponSwap, + g and g.weaponSwap, + }, true) and true or false + end + + -- Clear the old flat/legacy shape now that settings holds the truth. + store.legacy = nil + store.enabled = nil + store.smartEnabled = nil + store.reloadEnabled = nil + store.weaponSwapEnabled = nil + store.schemaVersion = 1 + + report.smartammo = "settings{smartEnabled=" .. tostring(settings.smartEnabled) + .. ",reloadEnabled=" .. tostring(settings.reloadEnabled) + .. ",weaponSwapEnabled=" .. tostring(settings.weaponSwapEnabled) .. "}" +end + +-- AUTOQUEST (per-character) +-- OLD (canonical): MTH_CharSavedVariables.autoquest = { scorpokDrazial, +-- arrowsForSissies, scorpokTooltip, _migrated } mirrored into +-- modules.autoquest = { scorpokDrazial, arrowsForSissies, scorpokTooltip, enabled } +-- plus oldest legacy MTH_CharSavedVariables.questautomation. +-- NEW: modules.autoquest = { schemaVersion, settings = { +-- scorpokDrazial, arrowsForSissies, scorpokTooltip } } +local function MTH_SV_Migrate_AutoQuest(char, report) + local modules = MTH_SV_Ensure(char, "modules") + local store = MTH_SV_Ensure(modules, "autoquest") + local settings = MTH_SV_Ensure(store, "settings") + + local root = MTH_SV_IsTable(char.autoquest) and char.autoquest or {} + local qa = MTH_SV_IsTable(char.questautomation) and char.questautomation or {} + + local keys = { "scorpokDrazial", "arrowsForSissies", "scorpokTooltip" } + for _, k in ipairs(keys) do + if settings[k] == nil then + settings[k] = MTH_SV_Coalesce({ + store[k], -- flat mirror on modules.autoquest + root[k], -- former canonical root .autoquest + qa[k], -- oldest legacy .questautomation + }, false) and true or false + end + end + + -- Clear old / duplicate locations. + store.scorpokDrazial = nil + store.arrowsForSissies = nil + store.scorpokTooltip = nil + store._migrated = nil + char.autoquest = nil + char.questautomation = nil + store.schemaVersion = 1 + + report.autoquest = "settings{scorpokDrazial=" .. tostring(settings.scorpokDrazial) + .. ",arrowsForSissies=" .. tostring(settings.arrowsForSissies) + .. ",scorpokTooltip=" .. tostring(settings.scorpokTooltip) .. "}" +end + +-- ANTIDAZE (per-character): char-root .antiDaze{enabled} -> modules.antidaze.settings{enabled} +local function MTH_SV_Migrate_AntiDaze(char, report) + local modules = MTH_SV_Ensure(char, "modules") + local store = MTH_SV_Ensure(modules, "antidaze") + local settings = MTH_SV_Ensure(store, "settings") + local old = MTH_SV_IsTable(char.antiDaze) and char.antiDaze or {} + if settings.enabled == nil and old.enabled ~= nil then + settings.enabled = old.enabled and true or false + end + char.antiDaze = nil + store.schemaVersion = 1 + report.antidaze = "settings{enabled=" .. tostring(settings.enabled) .. "}" +end + +-- AUTOSTRIP (per-character): char-root .autoStrip{autostrip,display,point,relativePoint,x,y} +-- -> modules.autostrip{autostrip,display,point,relativePoint,x,y} +-- (keys preserved verbatim; only the storage location moves under .modules) +local function MTH_SV_Migrate_AutoStrip(char, report) + local modules = MTH_SV_Ensure(char, "modules") + local store = MTH_SV_Ensure(modules, "autostrip") + local old = MTH_SV_IsTable(char.autoStrip) and char.autoStrip or {} + local carry = { "autostrip", "display", "point", "relativePoint", "x", "y" } + for _, k in ipairs(carry) do + if store[k] == nil and old[k] ~= nil then + store[k] = old[k] + end + end + char.autoStrip = nil + store.schemaVersion = 1 + report.autostrip = "store{autostrip=" .. tostring(store.autostrip) + .. ",display=" .. tostring(store.display) .. "}" +end + +-- MINIMAPBUTTON (per-character): char-root .minimapButton{angle} -> modules.minimapbutton{angle} +local function MTH_SV_Migrate_MinimapButton(char, report) + local modules = MTH_SV_Ensure(char, "modules") + local store = MTH_SV_Ensure(modules, "minimapbutton") + local old = MTH_SV_IsTable(char.minimapButton) and char.minimapButton or {} + if store.angle == nil and old.angle ~= nil then + store.angle = old.angle + end + char.minimapButton = nil + store.schemaVersion = 1 + report.minimapbutton = "store{angle=" .. tostring(store.angle) .. "}" +end + +-- FEEDOMATIC (account-wide): the 7 old FeedOMatic globals were stored under +-- modules.feedomatic.legacy.FOM_* (with _G.FOM_* bound as runtime aliases) and +-- duplicated at the account-root alias MTH_SavedVariables.feedomatic. +-- NEW: modules.feedomatic = { schemaVersion, settings, foodQuality, addedFoods, +-- removedFoods, cooking, questFood, localeInfo } — no .legacy wrapper, no root +-- alias. The _G.FOM_* globals stay as RUNTIME aliases (re-bound to these nested +-- tables by MTH_FeedOMatic_SyncSavedVariables) so FeedOMatic.lua is untouched. +local MTH_SV_FOM_MAP = { + { global = "FOM_Config", key = "settings" }, + { global = "FOM_FoodQuality", key = "foodQuality" }, + { global = "FOM_AddedFoods", key = "addedFoods" }, + { global = "FOM_RemovedFoods", key = "removedFoods" }, + { global = "FOM_Cooking", key = "cooking" }, + { global = "FOM_QuestFood", key = "questFood" }, + { global = "FOM_LocaleInfo", key = "localeInfo" }, +} + +local function MTH_SV_Migrate_FeedOMatic(account, report) + local modules = MTH_SV_Ensure(account, "modules") + local store = MTH_SV_Ensure(modules, "feedomatic") + local legacy = MTH_SV_IsTable(store.legacy) and store.legacy or {} + + for _, m in ipairs(MTH_SV_FOM_MAP) do + if store[m.key] == nil then + local src = legacy[m.global] + if type(src) ~= "table" and _G then + src = _G[m.global] + end + if type(src) == "table" then + store[m.key] = src + end + end + end + + store.legacy = nil + account.feedomatic = nil -- drop the account-root alias duplicate + store.schemaVersion = 1 + report.feedomatic = "keys{settings,foodQuality,addedFoods,removedFoods,cooking,questFood,localeInfo}" +end + +-- ZHUNTER (per-character): the zButton layout used to live in the TOC-declared +-- global ZHunterMod_Saved AND was mirrored into modules.zhunter (two on-disk +-- copies). NEW: modules.zhunter is the single canonical store (button tables, +-- _zbar, enabled, _mth_widget_spawn_layout_v1). ZHunterMod_Saved becomes a pure +-- RUNTIME alias re-bound by MTH_ZH_GetSavedRoot, and is dropped from the TOC. +-- The vestigial account-root alias MTH_SavedVariables.zhunter is removed too. +local function MTH_SV_Migrate_ZHunter(account, char, report) + local modules = MTH_SV_Ensure(char, "modules") + local store = MTH_SV_Ensure(modules, "zhunter") + + -- If the nested store is still empty but a per-char global carries data + -- (legacy TOC copy present during the transition), adopt it once. + if _G and MTH_SV_IsTable(_G.ZHunterMod_Saved) and _G.ZHunterMod_Saved ~= store then + if MTH_SV_Count(store) == 0 then + for k, v in pairs(_G.ZHunterMod_Saved) do + store[k] = v + end + end + end + + account.zhunter = nil -- drop the vestigial account-root alias duplicate + report.zhunter = "modules.zhunter canonical; ZHunterMod_Saved -> runtime alias" +end + +------------------------------------------------------ +-- Orchestrator +------------------------------------------------------ + +-- MTH_SV_EnsureSchema(account, char) +-- account / char default to the live globals. Returns a report table. +-- Gated by schemaVersion on each root so it only runs once; idempotent anyway. +function MTH_SV_EnsureSchema(account, char) + if account == nil then account = MTH_SavedVariables end + if char == nil then char = MTH_CharSavedVariables end + + local report = { ran = false } + + if not MTH_SV_IsTable(account) or not MTH_SV_IsTable(char) then + report.error = "roots unavailable" + return report + end + + local accountDone = (tonumber(account.schemaVersion) or 0) >= MTH_SV_SCHEMA_VERSION + local charDone = (tonumber(char.schemaVersion) or 0) >= MTH_SV_SCHEMA_VERSION + if accountDone and charDone then + report.skipped = true + return report + end + report.ran = true + + local accountModules = MTH_SV_Ensure(account, "modules") + local charModules = MTH_SV_Ensure(char, "modules") + + -- Detect whether there is genuine legacy data to relocate BEFORE the migrators + -- consume/delete it. On a brand-new install there is nothing to move, so we still + -- stamp the schema (to avoid re-checking) but skip the player-facing announcement. + local smartLegacy = MTH_SV_IsTable(charModules.smartammo) and charModules.smartammo.legacy or nil + local feedLegacy = MTH_SV_IsTable(accountModules.feedomatic) and accountModules.feedomatic.legacy or nil + report.changed = (account.feedomatic ~= nil) + or (account.zhunter ~= nil) + or (char.antiDaze ~= nil) + or (char.autoStrip ~= nil) + or (char.minimapButton ~= nil) + or (char.autoquest ~= nil) + or (char.questautomation ~= nil) + or (feedLegacy ~= nil) + or (smartLegacy ~= nil) + or (_G ~= nil and (_G.FOM_Config ~= nil or _G.ZHunterMod_Saved ~= nil or _G.MTHSmartAmmo ~= nil)) + or false + + -- ---- account-wide modules ---- + MTH_SV_Migrate_FeedOMatic(account, report) + + -- ---- per-character modules ---- + MTH_SV_Migrate_SmartAmmo( + MTH_SV_Ensure(charModules, "smartammo"), + _G and _G.MTHSmartAmmo or nil, + report + ) + if _G then _G.MTHSmartAmmo = nil end + + MTH_SV_Migrate_AutoQuest(char, report) + MTH_SV_Migrate_AntiDaze(char, report) + MTH_SV_Migrate_AutoStrip(char, report) + MTH_SV_Migrate_MinimapButton(char, report) + MTH_SV_Migrate_ZHunter(account, char, report) + + -- ---- stamp ---- + account.schemaVersion = MTH_SV_SCHEMA_VERSION + char.schemaVersion = MTH_SV_SCHEMA_VERSION + + -- ---- player-facing announcement (only when real legacy data was relocated) ---- + -- Guarded on MTH_Log so the offline test harness (no MetaHunt runtime) stays silent. + if report.changed and type(MTH_Log) == "function" then + local order = { + "feedomatic", "smartammo", "autoquest", + "antidaze", "autostrip", "minimapbutton", "zhunter", + } + local done = {} + for _, id in ipairs(order) do + if report[id] ~= nil then + table.insert(done, id) + end + end + MTH_Log("Tidying your saved settings into the new 2.0 layout (one-time)...") + if table.getn(done) > 0 then + MTH_Log(" consolidated: " .. table.concat(done, ", ")) + end + MTH_Log("Saved-settings cleanup complete. Your options were carried over unchanged.") + end + + return report +end + +------------------------------------------------------ +-- Load trigger +------------------------------------------------------ +-- Run the migration exactly once, AFTER WoW has loaded MetaHunt's SavedVariables +-- from disk. ADDON_LOADED with arg1 == "MetaHunt" is the earliest point at which the +-- real account + per-character saved data is actually available. Running it any earlier +-- (e.g. from a top-level MTH:InitSavedVariables() at file-load) operates on the empty +-- default tables WoW then overwrites when it loads the SV file — which is exactly why +-- the migration used to re-announce every login and /reload. CreateFrame is guarded so +-- the offline test harness (no WoW API) simply skips this. +local MTH_SV_LoadFrame = CreateFrame and CreateFrame("Frame") +if MTH_SV_LoadFrame then + MTH_SV_LoadFrame:RegisterEvent("ADDON_LOADED") + MTH_SV_LoadFrame:SetScript("OnEvent", function() + if event ~= "ADDON_LOADED" or arg1 ~= "MetaHunt" then + return + end + MTH_SV_LoadFrame:UnregisterEvent("ADDON_LOADED") + if MTH and MTH.InitSavedVariables then + MTH:InitSavedVariables() + end + if type(MTH_SV_EnsureSchema) == "function" then + MTH_SV_EnsureSchema(MTH_SavedVariables, MTH_CharSavedVariables) + end + end) +end diff --git a/init/api.xml b/init/api.xml index a73951e..c964985 100644 --- a/init/api.xml +++ b/init/api.xml @@ -1,6 +1,7 @@ + diff --git a/modules/autoquest/module.lua b/modules/autoquest/module.lua index cb3fed7..69b836a 100644 --- a/modules/autoquest/module.lua +++ b/modules/autoquest/module.lua @@ -56,70 +56,33 @@ local function QA_GetOptionsStore() if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end - if type(MTH_CharSavedVariables.autoquest) ~= "table" then - MTH_CharSavedVariables.autoquest = {} - end - local store = MTH_CharSavedVariables.autoquest - - if store._migrated ~= true then - if type(MTH_CharSavedVariables.questautomation) == "table" then - local legacyStore = MTH_CharSavedVariables.questautomation - if legacyStore.scorpokDrazial ~= nil and store.scorpokDrazial == nil then - store.scorpokDrazial = legacyStore.scorpokDrazial and true or false - end - if legacyStore.scorpokTooltip ~= nil and store.scorpokTooltip == nil then - store.scorpokTooltip = legacyStore.scorpokTooltip and true or false - end - end - local legacyChar = nil - local legacyAccount = nil - if MTH and MTH.GetModuleCharSavedVariables then - legacyChar = MTH:GetModuleCharSavedVariables("autoquest") - if type(legacyChar) ~= "table" then - legacyChar = MTH:GetModuleCharSavedVariables("questautomation") - end - end - if MTH and MTH.GetModuleSavedVariables then - legacyAccount = MTH:GetModuleSavedVariables("autoquest") - if type(legacyAccount) ~= "table" then - legacyAccount = MTH:GetModuleSavedVariables("questautomation") - end - end - if type(legacyChar) == "table" and legacyChar.scorpokDrazial ~= nil and store.scorpokDrazial == nil then - store.scorpokDrazial = legacyChar.scorpokDrazial and true or false - elseif type(legacyAccount) == "table" and legacyAccount.scorpokDrazial ~= nil and store.scorpokDrazial == nil then - store.scorpokDrazial = legacyAccount.scorpokDrazial and true or false - elseif store.scorpokDrazial == nil and MTH and MTH.GetModuleCharSavedVariables then - local autobuyStore = MTH:GetModuleCharSavedVariables("autobuy") - if type(autobuyStore) == "table" - and type(autobuyStore.questAutomation) == "table" - and autobuyStore.questAutomation.scorpokDrazial ~= nil then - store.scorpokDrazial = autobuyStore.questAutomation.scorpokDrazial and true or false - end - end - store._migrated = true - end - - if store.scorpokDrazial == nil then - store.scorpokDrazial = false - end - if store.arrowsForSissies == nil then - store.arrowsForSissies = false - end - if store.scorpokTooltip == nil then - store.scorpokTooltip = false - end - + local store = nil if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.scorpokDrazial = store.scorpokDrazial and true or false - moduleStore.arrowsForSissies = store.arrowsForSissies and true or false - moduleStore.scorpokTooltip = store.scorpokTooltip and true or false - end + store = MTH:GetModuleCharSavedVariables("autoquest") end - - return store + if type(store) ~= "table" then + if type(MTH_CharSavedVariables.modules) ~= "table" then + MTH_CharSavedVariables.modules = {} + end + if type(MTH_CharSavedVariables.modules.autoquest) ~= "table" then + MTH_CharSavedVariables.modules.autoquest = {} + end + store = MTH_CharSavedVariables.modules.autoquest + end + if type(store.settings) ~= "table" then + store.settings = {} + end + local settings = store.settings + if settings.scorpokDrazial == nil then + settings.scorpokDrazial = false + end + if settings.arrowsForSissies == nil then + settings.arrowsForSissies = false + end + if settings.scorpokTooltip == nil then + settings.scorpokTooltip = false + end + return settings end function MTH_AutoQuest:GetStore() @@ -129,36 +92,18 @@ end function MTH_AutoQuest:SetScorpokDrazialEnabled(enabled) local store = QA_GetOptionsStore() store.scorpokDrazial = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.scorpokDrazial = store.scorpokDrazial and true or false - end - end QA_Debug("option write: scorpokDrazial=" .. tostring(store.scorpokDrazial and true or false)) end function MTH_AutoQuest:SetArrowsForSissiesEnabled(enabled) local store = QA_GetOptionsStore() store.arrowsForSissies = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.arrowsForSissies = store.arrowsForSissies and true or false - end - end QA_Debug("option write: arrowsForSissies=" .. tostring(store.arrowsForSissies and true or false)) end function MTH_AutoQuest:SetScorpokTooltipEnabled(enabled) local store = QA_GetOptionsStore() store.scorpokTooltip = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.scorpokTooltip = store.scorpokTooltip and true or false - end - end QA_Debug("option write: scorpokTooltip=" .. tostring(store.scorpokTooltip and true or false)) end diff --git a/modules/feedomatic/FeedOMatic.lua b/modules/feedomatic/FeedOMatic.lua index 4f5c1c9..322fc92 100644 --- a/modules/feedomatic/FeedOMatic.lua +++ b/modules/feedomatic/FeedOMatic.lua @@ -650,58 +650,6 @@ function MTH_GetMerchantFoodsByDiet() return result; end -local function FOM_ItemQuality(itemIdOrLink) - if (type(GetItemInfo) ~= "function" or itemIdOrLink == nil) then - return nil; - end - local _, _, quality = GetItemInfo(itemIdOrLink); - return quality; -end - -local function FOM_DebugLog(message) - if (not (FOM_Config and FOM_Config.Debug)) then - return; - end - local text = "[FOM] " .. tostring(message); - if (MTH and MTH.Print) then - MTH:Print(text, "debug"); - elseif (DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage) then - DEFAULT_CHAT_FRAME:AddMessage(text); - end - return; -end - -local function FOM_DebugLogMerchantFoods(sourceEvent) - local merchantFoods = MTH_GetMerchantFoodsByDiet(); - local eventLabel = tostring(sourceEvent or "MERCHANT"); - if (merchantFoods == nil) then - FOM_DebugLog(eventLabel .. ": no merchant data available."); - return; - end - - local knownCount = table.getn(merchantFoods.items or {}); - local unknownCount = table.getn(merchantFoods.unknown or {}); - local merchantName = UnitName("npc") or UnitName("target") or "unknown"; - - FOM_DebugLog(eventLabel .. ": " .. tostring(merchantName) .. " | known foods=" .. tostring(knownCount) .. " | unknown items=" .. tostring(unknownCount)); - - if (type(merchantFoods.byDiet) == "table") then - for dietName, foodRows in merchantFoods.byDiet do - local dietCount = table.getn(foodRows or {}); - FOM_DebugLog("diet " .. tostring(dietName) .. ": " .. tostring(dietCount) .. " item(s)"); - for _, row in foodRows do - FOM_DebugLog(" - [" .. tostring(row.id) .. "] " .. tostring(row.name) .. " (merchantIndex=" .. tostring(row.index) .. ")"); - end - end - end - - if (unknownCount > 0) then - for _, row in merchantFoods.unknown do - FOM_DebugLog("unknown food map: [" .. tostring(row.id) .. "] " .. tostring(row.name) .. " (merchantIndex=" .. tostring(row.index) .. ")"); - end - end -end - function MTH_FOM_FeedButton_OnClick() if (arg1 == "RightButton") then if (MTH_OpenOptions) then @@ -920,10 +868,6 @@ function FOM_OnEvent(event, arg1) end return; - elseif ( event == "MERCHANT_SHOW" or event == "MERCHANT_UPDATE" ) then - FOM_DebugLogMerchantFoods(event); - return; - elseif ( event == "PET_ATTACK_START" ) then -- Set Flag @@ -950,7 +894,6 @@ function FOM_OnEvent(event, arg1) end if (foodID) then FOM_LastFood = GFWUtils.ItemLink(foodID); - FOM_DebugLog("Manually fed "..FOM_LastFood); end end return; @@ -1269,31 +1212,24 @@ end function FOM_CanFeed() local petInfo = MTH_FOM_GetCorePetInfo(); if ( not (petInfo and petInfo.liveExists) ) then - FOM_DebugLog("Can't feed; pet doesn't exist."); return false; end if ( tonumber(petInfo.health) and tonumber(petInfo.health) <= 0 ) then - FOM_DebugLog("Can't feed; pet is dead."); return false; end if ( UnitHealth("player") <= 0 ) then - FOM_DebugLog("Can't feed; I'm dead."); return false; end if ( CastingBarFrameStatusBar:IsVisible() ) then - FOM_DebugLog("Can't feed; casting a spell / tradeksill."); return false; end if ( UnitOnTaxi("player") ) then - FOM_DebugLog("Can't feed; flying."); return false; end if ( FOM_State.InCombat ) or ( PlayerFrame.inCombat ) then - FOM_DebugLog("Can't feed; in combat."); return false; end if ( LootFrame:IsVisible() ) then - FOM_DebugLog("Shouldn't feed; loot window is open."); return false; end @@ -1315,14 +1251,12 @@ function FOM_CanFeed() local name = GetSpellRecField(sid, "name") if name then if dontFeedNames[name] then - FOM_DebugLog("Can't feed; buff detected: " .. name) return false end if UnitLevel("player") >= 40 then local lname = string.lower(name) for _, mountSub in FOM_MOUNT_NAME_SUBSTRINGS do if string.find(lname, mountSub) then - FOM_DebugLog("Can't feed; mounted (" .. name .. ").") return false end end @@ -1351,7 +1285,6 @@ function FOM_CanFeed() if ( buff ~= nil) then for _, buffTexture in dontFeedBuffTextures do if ( buff == buffTexture ) then - FOM_DebugLog("Can't feed; currently, eating, drinking, or feigning death."); return false; end end @@ -1364,7 +1297,6 @@ function FOM_CanFeed() msg = string.lower(msg); for _, mountName in FOM_MOUNT_NAME_SUBSTRINGS do if (string.find(msg, mountName)) then - FOM_DebugLog("Can't feed; mounted."); return false; end end @@ -2387,17 +2319,6 @@ function FOM_Feed(aFood, options) foodLevel = selectedFoodLevel, }) - FOM_DebugLog("Picked "..tostring(FOM_LastFood) - .." id="..tostring(selectedId) - .." foodLevel="..tostring(selectedFoodLevel) - .." itemQuality="..tostring(FOM_ItemQuality(selectedId)) - .." (bag "..tostring(foodBag)..", slot "..tostring(foodItem)..")" - .." reason="..tostring(FOM_LastChoiceReason)); - if (FOM_Config.Debug) then - -- don't actually feed anything, just show what we would choose - return false; - end - -- Actually feed the item to the pet PickupContainerItem(foodBag, foodItem); if ( CursorHasItem() ) then @@ -2610,19 +2531,6 @@ function FOM_FlatFoodList() if (table.getn(foodList) == 0 and table.getn(overflowFoodList) > 0) then foodList = overflowFoodList; end - if (FOM_Config and FOM_Config.Debug) then - FOM_DebugLog("Candidate foods for pet level "..tostring(petLevel).." ("..tostring(table.getn(foodList)).." in diet):"); - for i = 1, table.getn(foodList) do - local c = foodList[i]; - FOM_DebugLog(" ["..tostring(i).."] "..tostring(c.link) - .." id="..tostring(c.itemId) - .." qty="..tostring(c.count) - .." foodLevel="..tostring(c.quality) - .." itemQuality="..tostring(FOM_ItemQuality(c.itemId)) - .." known="..tostring(c.knownLevel) - .." useful="..tostring(c.useful)); - end - end return foodList; end @@ -2786,12 +2694,10 @@ end function FOM_IsUsefulFood(itemID, quantity) local foodName = GetItemInfo(itemID); if (foodName == nil) then - FOM_DebugLog("Can't get info for item ID "..itemID..", assuming it's OK to eat."); return false; end if (FOM_Cooking and FOM_Cooking[FOM_RealmPlayer] and FOM_Cooking[FOM_RealmPlayer][itemID]) then if (FOM_Cooking[FOM_RealmPlayer][itemID] >= FOM_Config.SaveForCookingLevel) then - FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; is good for cooking."); return true; end end @@ -2803,19 +2709,15 @@ function FOM_IsUsefulFood(itemID, quantity) FOM_Quantity[foodName] = FOM_Quantity[foodName] + quantity; end if (FOM_Quantity[foodName] > FOM_QuestFood[FOM_RealmPlayer][foodName]) then - FOM_DebugLog("Not skipping "..quantity.."x "..foodName.."; is needed for quest, but we have more than enough."); return false; else - FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; is needed for quest."); return true; end end end if (FOM_Config.AvoidBonusFood and FOM_IsInDiet(itemID, FOM_DIET_BONUS)) then - FOM_DebugLog("Skipping "..quantity.."x "..foodName.."; has bonus effect when eaten by player."); return true; end - --FOM_DebugLog("Not skipping "..quantity.."x "..foodName.."; doesn't have other uses."); return false; end diff --git a/modules/feedomatic/module.lua b/modules/feedomatic/module.lua index 8dad1d8..47637e5 100644 --- a/modules/feedomatic/module.lua +++ b/modules/feedomatic/module.lua @@ -8,8 +8,6 @@ local MTH_FeedOMatic = { enabled = false, events = { "VARIABLES_LOADED", - "MERCHANT_SHOW", - "MERCHANT_UPDATE", "MTH_PET_LIVE_STATE_CHANGED", "PET_ATTACK_START", "PET_ATTACK_STOP", @@ -31,7 +29,6 @@ local MTH_FeedOMatic = { } local MTH_PETL_HOOK_BOUNDARY_KEY = "pet-lifecycle-hooks" -local MTH_FOM_MerchantProbe = nil function MTH_FeedOMatic_OnUpdate(elapsed) if not elapsed then @@ -128,104 +125,34 @@ local function MTH_FeedOMatic_SyncSavedVariables() end local moduleStore = MTH:GetModuleSavedVariables("feedomatic") - if not moduleStore.legacy then - moduleStore.legacy = {} - end - local function bindLegacyTable(globalName) - local legacyValue = moduleStore.legacy[globalName] - local globalValue = _G and _G[globalName] or nil - - if type(legacyValue) ~= "table" then + -- Bind each legacy FeedOMatic global to its clean nested location under + -- modules.feedomatic. The globals are runtime aliases only (NOT persisted via + -- the TOC any more); the real data lives in these nested tables inside + -- MTH_SavedVariables. Adopt any pre-existing runtime global on first bind. + local function bindGlobal(globalName, key) + local target = moduleStore[key] + if type(target) ~= "table" then + local globalValue = _G and _G[globalName] or nil if type(globalValue) == "table" then - legacyValue = globalValue + target = globalValue else - legacyValue = {} + target = {} end - moduleStore.legacy[globalName] = legacyValue + moduleStore[key] = target end - if _G then - _G[globalName] = legacyValue + _G[globalName] = target end end - bindLegacyTable("FOM_Config") - bindLegacyTable("FOM_FoodQuality") - bindLegacyTable("FOM_AddedFoods") - bindLegacyTable("FOM_RemovedFoods") - bindLegacyTable("FOM_Cooking") - bindLegacyTable("FOM_QuestFood") - bindLegacyTable("FOM_LocaleInfo") -end - -local function MTH_FeedOMatic_Log(message, severity) - if type(MTH_Log) == "function" then - MTH_Log("[FeedOMatic] " .. tostring(message or ""), severity) - end -end - -local function MTH_FeedOMatic_DebugMerchantScan(sourceEvent) - if type(MTH_GetMerchantFoodsByDiet) ~= "function" then - MTH_FeedOMatic_Log(tostring(sourceEvent) .. ": MTH_GetMerchantFoodsByDiet is not available yet") - return - end - - local merchantFoods = MTH_GetMerchantFoodsByDiet() - local knownCount = 0 - local unknownCount = 0 - if type(merchantFoods) == "table" then - if type(merchantFoods.items) == "table" then - knownCount = table.getn(merchantFoods.items) - end - if type(merchantFoods.unknown) == "table" then - unknownCount = table.getn(merchantFoods.unknown) - end - end - - local merchantName = UnitName("npc") or UnitName("target") or "unknown" - MTH_FeedOMatic_Log(tostring(sourceEvent) .. ": wrapper scan for " .. tostring(merchantName) .. " | known=" .. tostring(knownCount) .. " | unknown=" .. tostring(unknownCount)) - - if type(merchantFoods.byDiet) == "table" then - for dietName, rows in merchantFoods.byDiet do - local dietCount = 0 - if type(rows) == "table" then - dietCount = table.getn(rows) - end - if type(rows) == "table" then - for _, row in rows do - end - end - end - end - - if unknownCount > 0 and type(merchantFoods.unknown) == "table" then - for _, row in merchantFoods.unknown do - end - end -end - -local function MTH_FeedOMatic_SetMerchantProbeEnabled(enabled) - if not enabled then - if MTH_FOM_MerchantProbe then - MTH_FOM_MerchantProbe:UnregisterEvent("MERCHANT_SHOW") - MTH_FOM_MerchantProbe:UnregisterEvent("MERCHANT_UPDATE") - end - return - end - - if not MTH_FOM_MerchantProbe then - MTH_FOM_MerchantProbe = CreateFrame("Frame", "MTH_FOM_MerchantProbe") - MTH_FOM_MerchantProbe:SetScript("OnEvent", function() - local evt = event - if evt == "MERCHANT_SHOW" or evt == "MERCHANT_UPDATE" then - MTH_FeedOMatic_DebugMerchantScan(evt) - end - end) - end - - MTH_FOM_MerchantProbe:RegisterEvent("MERCHANT_SHOW") - MTH_FOM_MerchantProbe:RegisterEvent("MERCHANT_UPDATE") + bindGlobal("FOM_Config", "settings") + bindGlobal("FOM_FoodQuality", "foodQuality") + bindGlobal("FOM_AddedFoods", "addedFoods") + bindGlobal("FOM_RemovedFoods", "removedFoods") + bindGlobal("FOM_Cooking", "cooking") + bindGlobal("FOM_QuestFood", "questFood") + bindGlobal("FOM_LocaleInfo", "localeInfo") end function MTH_FeedOMatic:init() @@ -265,7 +192,6 @@ function MTH_FeedOMatic:setEnabled(enabled) MTH_FeedOMatic_CaptureHookBoundary() MTH_FeedOMatic_SyncSavedVariables() MTH_FeedOMatic_EnsureVariablesLoaded(self) - MTH_FeedOMatic_SetMerchantProbeEnabled(false) if MTH_FOM_FeedButton then MTH_FOM_FeedButton:Show() end @@ -275,7 +201,6 @@ function MTH_FeedOMatic:setEnabled(enabled) if type(FOM_State) == "table" then FOM_State.ShouldFeed = false end - MTH_FeedOMatic_SetMerchantProbeEnabled(false) if MTH_FOM_FeedButton then MTH_FOM_FeedButton:Hide() end @@ -304,7 +229,6 @@ function MTH_FeedOMatic:cleanup() if type(FOM_State) == "table" then FOM_State.ShouldFeed = false end - MTH_FeedOMatic_SetMerchantProbeEnabled(false) if MTH_FOM_FeedButton then MTH_FOM_FeedButton:Hide(); end diff --git a/modules/smartammo/engine.lua b/modules/smartammo/engine.lua index 8cc19c2..6d6f14f 100644 --- a/modules/smartammo/engine.lua +++ b/modules/smartammo/engine.lua @@ -1,7 +1,3 @@ -local function MTHSmartAmmo_GetSaved() - return MTH_SA_GetSavedTable("MTHSmartAmmo") -end - local function MTHSmartAmmo_GetModuleStore() if MTH and MTH.GetModuleCharSavedVariables then return MTH:GetModuleCharSavedVariables("smartammo") @@ -9,40 +5,43 @@ local function MTHSmartAmmo_GetModuleStore() return nil end -local function MTHSmartAmmo_IsSmartEnabled() +-- Single source of truth: moduleStore.settings.{smartEnabled,reloadEnabled,weaponSwapEnabled} +local function MTHSmartAmmo_GetSettings() local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" and moduleStore.smartEnabled ~= nil then - return moduleStore.smartEnabled and true or false + if type(moduleStore) ~= "table" then + return nil end - local saved = MTHSmartAmmo_GetSaved() - if saved["enabled"] == nil then - return true + if type(moduleStore.settings) ~= "table" then + moduleStore.settings = {} end - return saved["enabled"] and true or false + return moduleStore.settings +end + +local function MTHSmartAmmo_ReadFlag(key) + local settings = MTHSmartAmmo_GetSettings() + if settings and settings[key] ~= nil then + return settings[key] and true or false + end + return true -- default enabled +end + +local function MTHSmartAmmo_WriteFlag(key, enabled) + local settings = MTHSmartAmmo_GetSettings() + if settings then + settings[key] = enabled and true or false + end +end + +local function MTHSmartAmmo_IsSmartEnabled() + return MTHSmartAmmo_ReadFlag("smartEnabled") end local function MTHSmartAmmo_IsReloadEnabled() - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" and moduleStore.reloadEnabled ~= nil then - return moduleStore.reloadEnabled and true or false - end - local saved = MTHSmartAmmo_GetSaved() - if saved["reload"] == nil then - return true - end - return saved["reload"] and true or false + return MTHSmartAmmo_ReadFlag("reloadEnabled") end local function MTHSmartAmmo_IsWeaponSwapEnabled() - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" and moduleStore.weaponSwapEnabled ~= nil then - return moduleStore.weaponSwapEnabled and true or false - end - local saved = MTHSmartAmmo_GetSaved() - if saved["weaponSwap"] == nil then - return true - end - return saved["weaponSwap"] and true or false + return MTHSmartAmmo_ReadFlag("weaponSwapEnabled") end local MTHSmartAmmo_InitializeHooks @@ -58,9 +57,7 @@ local MTH_SA_SPELL_EVENTS = { } function MTHSmartAmmo_SetSmartEnabled(enabled, silent) - local saved = MTHSmartAmmo_GetSaved() if enabled then - saved["enabled"] = 1 local evFrame = getglobal("MTH_SA_EventFrame") if evFrame then for _, ev in ipairs(MTH_SA_SPELL_EVENTS) do @@ -77,7 +74,6 @@ function MTHSmartAmmo_SetSmartEnabled(enabled, silent) MTH_SA_Print("Smart Ammo Enabled.") end else - saved["enabled"] = false local evFrame = getglobal("MTH_SA_EventFrame") if evFrame then for _, ev in ipairs(MTH_SA_SPELL_EVENTS) do @@ -89,10 +85,7 @@ function MTHSmartAmmo_SetSmartEnabled(enabled, silent) end end - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" then - moduleStore.smartEnabled = enabled and true or false - end + MTHSmartAmmo_WriteFlag("smartEnabled", enabled and true or false) end function MTHSmartAmmo_GetSmartEnabled() @@ -100,16 +93,7 @@ function MTHSmartAmmo_GetSmartEnabled() end function MTHSmartAmmo_SetReloadEnabled(enabled, silent) - local saved = MTHSmartAmmo_GetSaved() - if enabled then - saved["reload"] = 1 - else - saved["reload"] = false - end - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" then - moduleStore.reloadEnabled = enabled and true or false - end + MTHSmartAmmo_WriteFlag("reloadEnabled", enabled and true or false) if not silent and DEFAULT_CHAT_FRAME then MTH_SA_Print("Smart Ammo reload fallback " .. (enabled and "Enabled." or "Disabled.")) end @@ -120,16 +104,7 @@ function MTHSmartAmmo_GetReloadEnabled() end function MTHSmartAmmo_SetWeaponSwapEnabled(enabled, silent) - local saved = MTHSmartAmmo_GetSaved() - if enabled then - saved["weaponSwap"] = 1 - else - saved["weaponSwap"] = false - end - local moduleStore = MTHSmartAmmo_GetModuleStore() - if type(moduleStore) == "table" then - moduleStore.weaponSwapEnabled = enabled and true or false - end + MTHSmartAmmo_WriteFlag("weaponSwapEnabled", enabled and true or false) if not silent and DEFAULT_CHAT_FRAME then MTH_SA_Print("Smart Ammo weapon-swap auto-equip " .. (enabled and "Enabled." or "Disabled.")) end @@ -807,15 +782,15 @@ SlashCmdList["MTHSmartAmmo"] = function(msg) return end - local saved = MTHSmartAmmo_GetSaved() + local smartEnabled = MTHSmartAmmo_GetSmartEnabled() if msg == "status" then local castHook, actionHook, byNameHook = MTHSmartAmmo_AreHooksInstalled() - MTH_SA_Print("Smart Ammo status: smart=" .. tostring(saved["enabled"] and true or false) .. ", module=" .. tostring(MTH_SA_IsModuleEnabled()) .. ", hooks=" .. tostring(castHook) .. "/" .. tostring(actionHook) .. "/" .. tostring(byNameHook)) + MTH_SA_Print("Smart Ammo status: smart=" .. tostring(smartEnabled) .. ", module=" .. tostring(MTH_SA_IsModuleEnabled()) .. ", hooks=" .. tostring(castHook) .. "/" .. tostring(actionHook) .. "/" .. tostring(byNameHook)) return end - - if saved["enabled"] then + + if smartEnabled then MTHSmartAmmo_SetSmartEnabled(nil) else MTHSmartAmmo_SetSmartEnabled(1) diff --git a/modules/smartammo/module.lua b/modules/smartammo/module.lua index ccb4c11..54723d4 100644 --- a/modules/smartammo/module.lua +++ b/modules/smartammo/module.lua @@ -15,39 +15,6 @@ local MTH_SmartAmmo = { local MTH_SA_BOUNDARY_KEY = "smartammo.core" -function MTH_SA_GetSavedTable(tableName) - if not MTH or not MTH.GetModuleCharSavedVariables then - if type(_G[tableName]) ~= "table" then - _G[tableName] = {} - end - return _G[tableName] - end - - local moduleStore = MTH:GetModuleCharSavedVariables("smartammo") - if type(moduleStore) ~= "table" then - moduleStore = {} - end - - if not moduleStore.legacy then - moduleStore.legacy = {} - end - if type(moduleStore.legacy[tableName]) ~= "table" then - moduleStore.legacy[tableName] = {} - end - - if type(_G[tableName]) == "table" then - local legacyTable = moduleStore.legacy[tableName] - if not next(legacyTable) then - for key, value in pairs(_G[tableName]) do - legacyTable[key] = value - end - end - end - _G[tableName] = moduleStore.legacy[tableName] - - return moduleStore.legacy[tableName] -end - function MTH_SA_IsModuleEnabled() if MTH and MTH.IsModuleEnabled then return MTH:IsModuleEnabled("smartammo", true) and true or false @@ -70,43 +37,22 @@ local function MTH_SA_ApplySavedState() return end - local saved = MTH_SA_GetSavedTable("MTHSmartAmmo") local moduleStore = MTH and MTH.GetModuleCharSavedVariables and MTH:GetModuleCharSavedVariables("smartammo") or nil - if type(moduleStore) == "table" then - if moduleStore.smartEnabled ~= nil then - saved["enabled"] = moduleStore.smartEnabled and 1 or false - end - if moduleStore.reloadEnabled ~= nil then - saved["reload"] = moduleStore.reloadEnabled and 1 or false - end - if moduleStore.weaponSwapEnabled ~= nil then - saved["weaponSwap"] = moduleStore.weaponSwapEnabled and 1 or false - end - end - if saved["enabled"] == nil then - saved["enabled"] = 1 - if type(moduleStore) == "table" then - moduleStore.smartEnabled = true - end - end - if saved["reload"] == nil and type(moduleStore) == "table" then - moduleStore.reloadEnabled = saved["reload"] ~= false - end - if saved["weaponSwap"] == nil then - saved["weaponSwap"] = 1 - if type(moduleStore) == "table" then - moduleStore.weaponSwapEnabled = true - end - elseif type(moduleStore) == "table" and moduleStore.weaponSwapEnabled == nil then - moduleStore.weaponSwapEnabled = saved["weaponSwap"] ~= false - end + local settings = (type(moduleStore) == "table" and type(moduleStore.settings) == "table") and moduleStore.settings or {} - MTHSmartAmmo_SetSmartEnabled(saved["enabled"] and 1 or nil, 1) + local smart = settings.smartEnabled + if smart == nil then smart = true end + local reload = settings.reloadEnabled + if reload == nil then reload = true end + local swap = settings.weaponSwapEnabled + if swap == nil then swap = true end + + MTHSmartAmmo_SetSmartEnabled(smart and 1 or nil, 1) if type(MTHSmartAmmo_SetReloadEnabled) == "function" then - MTHSmartAmmo_SetReloadEnabled(saved["reload"] ~= false and 1 or nil, 1) + MTHSmartAmmo_SetReloadEnabled(reload and 1 or nil, 1) end if type(MTHSmartAmmo_SetWeaponSwapEnabled) == "function" then - MTHSmartAmmo_SetWeaponSwapEnabled(saved["weaponSwap"] ~= false and 1 or nil, 1) + MTHSmartAmmo_SetWeaponSwapEnabled(swap and 1 or nil, 1) end if type(MTHSmartAmmo_EnsureHooks) == "function" then MTHSmartAmmo_EnsureHooks("module-apply") @@ -154,7 +100,6 @@ local function MTH_SA_RestoreHooks() end function MTH_SmartAmmo:init() - MTH_SA_GetSavedTable("MTHSmartAmmo") if self.enabled then MTH_SA_ApplySavedState() end diff --git a/modules/tooltips/module.lua b/modules/tooltips/module.lua index 479b566..5f8f494 100644 --- a/modules/tooltips/module.lua +++ b/modules/tooltips/module.lua @@ -115,23 +115,24 @@ local function MTH_TT_IsOwnPetTooltipsEnabled() end local function MTH_TT_GetAutoQuestStore() - if type(MTH_CharSavedVariables) ~= "table" then - return nil - end - local store = MTH_CharSavedVariables.autoquest - if type(store) ~= "table" then - store = MTH_CharSavedVariables.questautomation + local store = nil + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("autoquest") end if type(store) ~= "table" then return nil end - if store.scorpokDrazial == nil then - store.scorpokDrazial = false + if type(store.settings) ~= "table" then + store.settings = {} end - if store.scorpokTooltip == nil then - store.scorpokTooltip = false + local settings = store.settings + if settings.scorpokDrazial == nil then + settings.scorpokDrazial = false end - return store + if settings.scorpokTooltip == nil then + settings.scorpokTooltip = false + end + return settings end local function MTH_TT_IsScorpokAutomationEnabled() diff --git a/modules/zhunter/ZHunterMod.lua b/modules/zhunter/ZHunterMod.lua index cfeb15a..9f80e61 100644 --- a/modules/zhunter/ZHunterMod.lua +++ b/modules/zhunter/ZHunterMod.lua @@ -43,10 +43,35 @@ function MTH_ZH_IsModuleEnabled() end function MTH_ZH_GetSavedRoot() - if type(ZHunterMod_Saved) ~= "table" then - ZHunterMod_Saved = {} + -- Canonical store is MTH_CharSavedVariables.modules.zhunter. The old global + -- ZHunterMod_Saved is kept only as a RUNTIME alias so the many zButton files + -- and options-zbuttons.lua can keep referencing it unchanged. + local store = nil + if MTH and MTH.GetModuleCharSavedVariables then + store = MTH:GetModuleCharSavedVariables("zhunter") end - return ZHunterMod_Saved + + if type(store) ~= "table" then + -- Framework not ready yet: fall back to a temporary global holder; + -- MTH_ZH_SyncSavedVariables re-binds to the nested store later. + if type(ZHunterMod_Saved) ~= "table" then + ZHunterMod_Saved = {} + end + return ZHunterMod_Saved + end + + -- Adopt a pre-existing global (legacy TOC copy) once, only if the nested + -- store is still empty, so no layout is lost during the transition. + if type(ZHunterMod_Saved) == "table" and ZHunterMod_Saved ~= store then + if next(store) == nil then + for k, v in pairs(ZHunterMod_Saved) do + store[k] = v + end + end + end + + ZHunterMod_Saved = store -- pure runtime alias into the canonical store + return store end function MTH_ZH_GetSavedTable(key) diff --git a/modules/zhunter/module.lua b/modules/zhunter/module.lua index 2c2b438..4308c33 100644 --- a/modules/zhunter/module.lua +++ b/modules/zhunter/module.lua @@ -165,18 +165,11 @@ MTH_ZH_ApplyEnabledRuntimeState = function(source) end MTH_ZH_SyncSavedVariables = function() - if not MTH or not MTH.GetModuleCharSavedVariables then - return - end - - local moduleStore = MTH:GetModuleCharSavedVariables("zhunter") - - if type(ZHunterMod_Saved) == "table" then - if MTH_CharSavedVariables and MTH_CharSavedVariables.modules then - MTH_CharSavedVariables.modules.zhunter = ZHunterMod_Saved - end - elseif type(moduleStore) == "table" then - ZHunterMod_Saved = moduleStore + -- Bind the ZHunterMod_Saved global as a runtime alias into the canonical + -- nested store (MTH_CharSavedVariables.modules.zhunter). GetSavedRoot does the + -- adoption + aliasing; there is no second on-disk copy anymore. + if type(MTH_ZH_GetSavedRoot) == "function" then + MTH_ZH_GetSavedRoot() end end From 566f3f4925775ca8dbfeb955d68ba06893e9ad36 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 14:41:26 +0200 Subject: [PATCH 10/42] Add Animations feature + fix own-pet tooltip false positive Animations (all optional, default on, per-effect toggles): - New Options -> Animations tab with master switch + per-effect checkboxes - Embed PizzaSauce animation library (api/PizzaSauce.lua) - fx-celebrate: firework + banner when a pet learns a new ability/rank, triggered from core-scan-beasttraining pet-learn hook - Hunter's Book: slide + fade on open, results list fade on tab switch - Options window: slide + fade on open - All new user-facing strings keyed (ANIM_* namespace) Fixes: - Tooltip #19: own-pet lines (mood/loyalty/happiness/XP) no longer misfire on party members / targets. Removed the fragile title-name fallback in MTH_TT_IsTooltipOnOwnPet; ownership now requires positive unit identity. Temp (remove before release): /mth fx and /mth fxdebug diagnostics. --- CHANGELOG.md | 37 +- README.MD | 59 +- api/PizzaSauce.lua | 1347 +++++++++++++++++++++++++++++++ api/core-scan-beasttraining.lua | 3 + api/core.lua | 63 ++ api/fx-celebrate.lua | 297 +++++++ api/hunterbook.lua | 32 + api/options-animations.lua | 173 ++++ api/options-shell.lua | 18 + api/options-tree.lua | 2 + api/options.xml | 1 + init/api.xml | 3 + 12 files changed, 1992 insertions(+), 43 deletions(-) create mode 100644 api/PizzaSauce.lua create mode 100644 api/fx-celebrate.lua create mode 100644 api/options-animations.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 8481e41..03852b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,25 +5,31 @@ All notable changes to MetaHunt will be documented in this file. ## [2.0.0] - Unreleased -### Changed - -- **Settings storage tidied into one clean layout**: The settings MetaHunt inherited from the older stand-alone addons it now replaces (Feed-O-Matic, ZHunter/zButtons, SmartAmmo, and the quest/anti-daze/auto-strip/minimap helpers) used to be scattered across many separate saved entries, several of them duplicated on disk. MetaHunt now keeps everything in one organised place, grouped per module. The clean-up runs automatically and once, the first time you log in after updating: all of your existing settings and layouts are carried over unchanged — nothing is reset — and the old duplicate copies are removed so your saved-variables file stays small and consistent. - -### Fixed - -- **Pets — duplicate and phantom entries in your pet collection**: MetaHunt could record the same tamed pet more than once — a fresh copy appeared after you logged out and back in, or after your pet leveled up — and a few very early pets you had long since dismissed lingered as though they were still with you. MetaHunt now recognises each pet reliably from one session to the next (and as it levels), automatically merges the duplicate entries it had already created while keeping the correct taming details (where, and at what level, you tamed each one), and tidies the leftover phantom pets away into your pet history. Your full taming history is preserved — nothing is deleted. - - -## [1.5.2] - 2026-09-08 - -Res on Octowow server. +Resurrecting on Octowow. ### Added +- **Animations**: MetaHunt now plays optional animations that make the interface feel alive. A firework-and-banner celebration bursts on screen when your pet learns a new ability or a new rank, the Great Book slides up and fades in when you open it and gently fades its list when you switch tabs, and the options window slides in when opened. Everything is optional and off-switchable: a new **Animations** tab (Options → Animations) lets you toggle each effect on its own — or disable them all at once. Powered by the PizzaSauce animation library. + - **zTrack — Find Fish**: Added new spell Find Fish to the zTrack list of trackings. +- **DDOSers Realm Blocking**: The Addon will auto-disable itself on all Mafia's realms, we know who they are. + +### Changed + +- **Complete revamp of SavedVariables**: All your settings now live in one tidy place instead of being scattered around. A migration happens the first time you log in, and you shouldn't lose any settings. + + ### Fixed +- **Tooltips — your pet's info showing on other players**: In a party, mousing over or targeting another player could wrongly show YOUR pet's details (mood, loyalty, happiness, XP) on their tooltip. The own-pet detection no longer misfires on other units. + +- **zCraft — Smelting**: Smelting was previously not appearing in zCraft because incorrectly hardcoded as "Mining", this is fixed. + +- **Wrong icons on pet spells**: Fixed the Rank 0 of Bubble Barrier and Charge that had incorrect icons. + +- **Fixed tracking data in Book's Stables**: Fixed an issue with pets' GUID returned by the server, that was not really unique and caused issues with pets' tracking, notably when you had several pets with the same name. Taming info was disapearing, amongst other weirdness. A migration is done on first load to fix your existing data. + - **FeedOMatic — "Set Key" button error**: Clicking **Set Key** in Options → Feed-o-Matic threw an error and wouldn't let you bind a feed key. It now works again. - **FeedOMatic — pet left hungry when bags were full**: When your bags were full, FeedOMatic could grab a random tiny stack (like a single meat your pet won't eat) to free a bag slot, so your pet stayed hungry. It now always picks food your pet can actually eat. @@ -32,13 +38,8 @@ Res on Octowow server. - **Pet training — new rank of a known ability not registered**: When you trained a higher rank of an ability you already had (e.g. learning Charge Rank 2 after Rank 1), MetaHunt treated it as already known — so the "Pet ability learned and recorded" message never appeared and the pet-bar tooltip kept saying "Not learned yet" for the new rank. Learning a new rank is now recognised on its own, announces correctly, and the tooltip immediately reflects the rank you just learned. -- **Hunter's Book — missing "Unique Appearance" beasts**: The unique-appearance flag had gone missing from the beast database, so the "Unique" badge and filter in the Hunter's Book came up empty. Restored a curated list of the genuinely one-of-a-kind beasts a collector hunts for (Humar the Pridelord, Darksaber, Shar'lan, Frostsaber Pride Watcher, Mazzranache, Moonfeather, Azurebeak, Old Cliff Jumper, Rotting Agam'ar, Razzashi Serpent, Magmalash Scorpid, Spirit Fox and more), so the Unique badge and filter work again without wrongly flagging common colour-variant mobs. +- **Hunter's Book — fixed "Unique" beasts**: Restored a curated list of the one-of-a-kind beasts (Humar the Pridelord, Darksaber, Shar'lan, Frostsaber Pride Watcher, Mazzranache, Moonfeather, Azurebeak, Old Cliff Jumper, Rotting Agam'ar, Razzashi Serpent, Magmalash Scorpid, Spirit Fox and more). -- **ds-pet-spells — wrong rank 0 icons**: Rank 0 (triggered/sub-spell) entries for Bubble Barrier and Charge had incorrect icons (`Spell_Frost_FrostNova` and `Ability_Warrior_Charge` respectively) instead of matching their ranked ability icons (`spell_bubl1` and `Ability_Hunter_Pet_Boar`). Fixed in both `allSpells` and `byAbility` sections. - -- **zCraft — Smelting**: Smelting was previously not appearing in zCraft because incorrectly hardcoded as "Mining", this is fixed. - -- **Realm Blocking**: The Addon will auto-disable itself on all Mafia's realms. ## [1.5.1] - 2026-04-01 diff --git a/README.MD b/README.MD index e916721..8f68b75 100644 --- a/README.MD +++ b/README.MD @@ -1,37 +1,32 @@ # MetaHunt MetaHunt is a Twow Hunter toolkit. -It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken on twow. -It provides a bunch of useful tools for all huntards, wheter they are still levelling, lone-wolf, or HL raider. + +It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken or disfunctional with twow. + +It provides a bunch of useful functionalities for all huntards, wheter they are still levelling, lone-wolf, or HL raider. +You play the best class, now you have the best companion for it (and its not your pet). ### Important notes - Altough Nampower isn't mandatory for most of the features, it is a must-have for some. - If you were using old versions of Feed-O-Matic, ICU, zHunterMod and HunterHelper, you dont need those with MetaHunt and you should disable them to avoid conflicts. - MetaHunt is fully compatible with QUIVER, and is not a replacement for it. -- MetaHunt don't and will never support Capycraft/Ravencraft (mafia) servers, after the DDOS attacks they conducted on other servers, to only name that. Fuck you. +- The addon purposely disable itself on all Capycraft/Ravencraft Realms, following the DDOS attacks they conducted on other servers. ## Twow Data -MetaHunt ships with large Twow datastores : +MetaHunt ships with large twow datastores, and notably: - 100% accurate beasts' data, their locations, and the abilities they can learn you - All Pet families, their abilities and their diet - All pet abilities ranks -- All Ammo (Arrows and Bullets) -- All Stable, Hunter and Pet Masters +- All Ammo and ranged weapons +- All Hunter/Pet/Stable Masters - All Ammo vendors -You can browse all the beasts in game within the Book of Huntards, but also on the github site : https://DuvelCorp.github.io/MetaHunt-Web/ +Online bestiary is also available here https://DuvelCorp.github.io/MetaHunt-Web/ - ## The Great Book of Huntards - - The Book is the main GUI of MetaHunt, it allows to display the data collected about you and your pet, and browse the addon's data to find anything a Hunter needs. - -- Browse the MetaHunt datastores of NPCs (Beast, vendors, masters) and open the map to locate them like in pfQuest -- Track all info about the pet abilities that you know and don't know yet -- Track all info about your stabled pets -- Keep an history of all your pets after you abandon them or if they run away. ## Smart Ammo @@ -40,7 +35,8 @@ You can browse all the beasts in game within the Book of Huntards, but also on t - Auto equip the right ammo when you swap from a box/xbow to a gun and the other way around. ## MM Widget - Lets Make MM great again. Tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock&Load procs, and has a dynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. You can also bind it to turn it into a very efficient one-button rotation. No more excuse to not be amongst top DPS. + + 1.18.1 weapon of mass-destruction. The widget is composed of 5 cells, tracking the 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock & Load procs, and a smart dynamic cell. The smart cell shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any, and it bypasses the proc spell if your current target is immune to the school. You can bind the smart cell to turn the widget into a very efficient one-button rotation. Lets make MM great again ! ## Tooltips @@ -56,27 +52,27 @@ Old zHunterMod addon on steroids. Fully compatible with Twow, and enhanced with - zAmmo : Track all your ammo Live and swap them in a click or bind - zPet : Intelligent collapsible pet Bar regrouping all pet spells and providing a main button that always swap on the right spell to use depending on the situation - zTrack: All your trackings in one collapsible bar -- zAspect: All your trackings in one collapsible bar +- zAspect: All your aspects in one collapsible bar - zRanged : All the ranged weapons currently in your bags to swap them fast - But also zCrafts, zMounts, zToys and zCompanions. -Additionally, a zBar can regroup all the zButtons above into a single bar for people that like clean and ordered stuff. +Additionally, a special _zBar_ can regroup all the zButtons above into a single bar for people that like clean and ordered stuff. ## Feed-O-Matic -Now compatible with Twow, feed your pet with one key bind, it will always pickup the best food for your pet. +Now compatible with Twow, feed your pet with one key bind, it will always pickup the best food for your pet, according to your preferences. ## Chronometer -The good old Chronometer addon, purged of non-necessary things and reconfigured for a pure hunter use. +The good old Chronometer addon, purged of non-necessary things and reconfigured for a pure hunter usage. ## ICU (I see you) -Old addon used to display Targets' enhanced information when you click on your minimap tracking things. MetaHunt offers many more customization options for it. +This old addon is mostly a PVP weapon used to display targets' enhanced information when you click on your minimap tracked things. MetaHunt offers many more customization options for it. ## Ammo labels - You can display on all ammo in your bags and/or bank their type and/or dps to instantly see which of those damn same arrows icons really are. + You can display some labels on all ammo in your bags/bank to instantly see which of those damn same arrows icons really are. ## Auto-Buy @@ -86,10 +82,23 @@ Define your own simple rules for auto buy ammo and pet food when you open a vend Just spam SHIFT-click on the NPC to validate quests Scorpok and Thorium arrows. Additionally you can have an enhanced tooltip that display your actual Scorpok-related items when you mouseover the Blasted Land mobs involved. - ## Beast model Viewer - -Display the model of all beasts in the world. Always know what you are going to tame exactly before running there ! You can also browse all skin models of the same pet family to find the best-looking pet according to your tastes. + ## The Great Book of Huntards + The Book is the main GUI of MetaHunt, it allows to browse the addon's data to find anything a Hunter needs. + +- Search Hunter-related NPCs (Beasts, vendors, masters) and open the map to locate them like in pfQuest. +- Beast model Viewer : Display the model of all beasts in the world. You can also browse all skin models of the same family to find the best-looking pet according to your tastes. +- Instantly find where the beasts having a specific ability are lying and plan ahead your tracking sessions. +- Get a full understanding of all Pet families. +- Display meaningfully the pet abilities that you know and don't know yet +- Get your stable's state everywhere and track all possible information about all your pets +- Keep a detailed history of all the pets you abandonned or those that ran away. + + ## Tune it the way you like + +Metahunt option panel is huge. There are hundreds of settings you can configure in order to tweak it like a boss so it fits YOUR preferences. +Its modular architecture also allows you to completely disable any module in order to decrease the addon's memory footprint and background processes. + ### Happy Hunting Fellas ! diff --git a/api/PizzaSauce.lua b/api/PizzaSauce.lua new file mode 100644 index 0000000..c07de99 --- /dev/null +++ b/api/PizzaSauce.lua @@ -0,0 +1,1347 @@ +-- PizzaSauce: Smooth, declarative animation library for WoW 1.12 + +local _G = getfenv(0) + +local lib = {} +lib.version = "0.1.1" +_G["PizzaSauce"] = lib + +lib._types = {} +lib._easing = {} +lib._active = {} +lib._pending = {} +lib._pendingN = 0 +lib._count = 0 +lib._finBuf = {} + +lib._frame = CreateFrame("Frame", nil, UIParent) +lib._frame:Hide() + +local lastTime = GetTime() + +local function activate(anim) + if anim._active then return end + anim._active = true + lib._count = lib._count + 1 + lib._active[anim] = true + if lib._count == 1 then + lastTime = GetTime() + lib._frame:Show() + end +end + +local function deactivate(anim) + if not anim._active then return end + anim._active = false + lib._count = lib._count - 1 + lib._active[anim] = nil + if lib._count <= 0 then + lib._count = 0 + if lib._pendingN == 0 then lib._frame:Hide() end + end +end + +lib._frame:SetScript("OnUpdate", function() + local now = GetTime() + local dt = now - lastTime + lastTime = now + if dt <= 0 then return end + + -- Process pending auto-play animations + local pendingN = lib._pendingN + if pendingN > 0 then + lib._pendingN = 0 + for i = 1, pendingN do + local anim = lib._pending[i] + lib._pending[i] = nil + if anim._autoplay then anim:Play() end + end + end + + -- Hide driver if nothing to do + if lib._count == 0 then + lib._frame:Hide() + return + end + + -- Tick all active, collect finished into scratch buffer. + -- Can't finish inline because _finish() callbacks may create new animations, + -- which would modify _active while we're iterating it. + local finN = 0 + for anim in pairs(lib._active) do + if anim:_tick(dt, now) then + finN = finN + 1 + lib._finBuf[finN] = anim + end + end + + for i = 1, finN do + lib._finBuf[i]:_finish() + lib._finBuf[i] = nil + end +end) + +lib._activate = activate +lib._deactivate = deactivate + +function lib._schedulePending(anim) + anim._autoplay = true + lib._pendingN = lib._pendingN + 1 + lib._pending[lib._pendingN] = anim + lastTime = GetTime() + lib._frame:Show() +end + +function lib:RegisterType(name, handlers) + lib._types[name] = handlers +end + +function lib:RegisterEasing(name, fn) + lib._easing[name] = fn +end + +function lib:RegisterHelper(name, fn) + if lib[name] then + DEFAULT_CHAT_FRAME:AddMessage( + "PizzaSauce: RegisterHelper('" .. name .. "') would overwrite an existing method") + return + end + lib[name] = fn +end +local E = lib._easing + +local pi = math.pi +local sin = math.sin +local cos = math.cos +local pow = math.pow +local sqrt = math.sqrt + +E["linear"] = function(t) return t end +E["swing"] = function(t) return 0.5 - cos(t * pi) / 2 end + +-- Quad (power of 2) +E["inQuad"] = function(t) return t * t end +E["outQuad"] = function(t) return t * (2 - t) end +E["inOutQuad"] = function(t) + if t < 0.5 then return 2 * t * t end + return -1 + (4 - 2 * t) * t +end + +-- Cubic (power of 3) +E["inCubic"] = function(t) return t * t * t end +E["outCubic"] = function(t) local u = t - 1; return u * u * u + 1 end +E["inOutCubic"] = function(t) + if t < 0.5 then return 4 * t * t * t end + local u = 2 * t - 2; return (u * u * u + 2) / 2 +end + +-- Quart (power of 4) +E["inQuart"] = function(t) return t * t * t * t end +E["outQuart"] = function(t) local u = t - 1; return 1 - u * u * u * u end +E["inOutQuart"] = function(t) + if t < 0.5 then return 8 * t * t * t * t end + local u = t - 1; return 1 - 8 * u * u * u * u +end + +-- Quint (power of 5) +E["inQuint"] = function(t) return t * t * t * t * t end +E["outQuint"] = function(t) local u = t - 1; return 1 + u * u * u * u * u end +E["inOutQuint"] = function(t) + if t < 0.5 then return 16 * t * t * t * t * t end + local u = t - 1; return 1 + 16 * u * u * u * u * u +end + +-- Sine +E["inSine"] = function(t) return 1 - cos(t * pi / 2) end +E["outSine"] = function(t) return sin(t * pi / 2) end +E["inOutSine"] = function(t) return 0.5 - cos(t * pi) / 2 end + +-- Expo +E["inExpo"] = function(t) + if t == 0 then return 0 end + return pow(2, 10 * (t - 1)) +end +E["outExpo"] = function(t) + if t == 1 then return 1 end + return 1 - pow(2, -10 * t) +end +E["inOutExpo"] = function(t) + if t == 0 or t == 1 then return t end + if t < 0.5 then return pow(2, 20 * t - 10) / 2 end + return (2 - pow(2, -20 * t + 10)) / 2 +end + +-- Circ +E["inCirc"] = function(t) return 1 - sqrt(1 - t * t) end +E["outCirc"] = function(t) local u = t - 1; return sqrt(1 - u * u) end +E["inOutCirc"] = function(t) + if t < 0.5 then return (1 - sqrt(1 - 4 * t * t)) / 2 end + local u = 2 * t - 2; return (sqrt(1 - u * u) + 1) / 2 +end + +-- Elastic +E["inElastic"] = function(t) + if t == 0 or t == 1 then return t end + return -pow(2, 10 * t - 10) * sin((t * 10 - 10.75) * (2 * pi) / 3) +end +E["outElastic"] = function(t) + if t == 0 or t == 1 then return t end + return pow(2, -10 * t) * sin((t * 10 - 0.75) * (2 * pi) / 3) + 1 +end +E["inOutElastic"] = function(t) + if t == 0 or t == 1 then return t end + if t < 0.5 then + return -pow(2, 20 * t - 10) * sin((20 * t - 11.125) * (2 * pi) / 4.5) / 2 + end + return pow(2, -20 * t + 10) * sin((20 * t - 11.125) * (2 * pi) / 4.5) / 2 + 1 +end + +-- Back +E["inBack"] = function(t) + local s = 1.70158 + return t * t * ((s + 1) * t - s) +end +E["outBack"] = function(t) + local s = 1.70158 + local u = t - 1 + return u * u * ((s + 1) * u + s) + 1 +end +E["inOutBack"] = function(t) + local s = 1.70158 * 1.525 + if t < 0.5 then + return (2 * t) * (2 * t) * ((s + 1) * (2 * t) - s) / 2 + end + local u = 2 * t - 2 + return (u * u * ((s + 1) * u + s) + 2) / 2 +end + +-- Bounce +E["outBounce"] = function(t) + if t < 1 / 2.75 then + return 7.5625 * t * t + elseif t < 2 / 2.75 then + t = t - 1.5 / 2.75 + return 7.5625 * t * t + 0.75 + elseif t < 2.5 / 2.75 then + t = t - 2.25 / 2.75 + return 7.5625 * t * t + 0.9375 + else + t = t - 2.625 / 2.75 + return 7.5625 * t * t + 0.984375 + end +end +E["inBounce"] = function(t) return 1 - E["outBounce"](1 - t) end +E["inOutBounce"] = function(t) + if t < 0.5 then return (1 - E["outBounce"](1 - 2 * t)) / 2 end + return (1 + E["outBounce"](2 * t - 1)) / 2 +end +local function lerp(a, b, t) return a + (b - a) * t end + +local function rotCorner(a) + local r = math.rad(a) + return 0.5 + math.cos(r) / 1.4142, 0.5 + math.sin(r) / 1.4142 +end + +local function applyRotation(texture, angle) + local a = -angle + local ULx, ULy = rotCorner(a + 225) + local LLx, LLy = rotCorner(a + 135) + local URx, URy = rotCorner(a - 45) + local LRx, LRy = rotCorner(a + 45) + texture:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy) +end + +lib:RegisterType("alpha", { + init = function(target, from) + return from or target:GetAlpha() + end, + apply = function(target, from, to, t) + target:SetAlpha(lerp(from, to, t)) + end, +}) + +lib:RegisterType("scale", { + init = function(target, from, to, tween) + tween._baseW = target:GetWidth() + tween._baseH = target:GetHeight() + return from or 1.0 + end, + apply = function(target, from, to, t, tween) + local s = lerp(from, to, t) + target:SetWidth(tween._baseW * s) + target:SetHeight(tween._baseH * s) + end, +}) + +lib:RegisterType("size", { + init = function(target, from) + return from or { target:GetWidth(), target:GetHeight() } + end, + apply = function(target, from, to, t) + target:SetWidth(lerp(from[1], to[1], t)) + target:SetHeight(lerp(from[2], to[2], t)) + end, +}) + +lib:RegisterType("position", { + init = function(target, from, to, tween) + local point, rel, relPoint, x, y = target:GetPoint(1) + tween._point = point or "CENTER" + tween._rel = rel + tween._relPoint = relPoint or "CENTER" + return from or { x or 0, y or 0 } + end, + apply = function(target, from, to, t, tween) + target:ClearAllPoints() + target:SetPoint(tween._point, tween._rel, tween._relPoint, + lerp(from[1], to[1], t), lerp(from[2], to[2], t)) + end, +}) + +lib:RegisterType("color", { + init = function(target, from) + return from or { 1, 1, 1, 1 } + end, + apply = function(target, from, to, t) + target:SetVertexColor( + lerp(from[1], to[1], t), + lerp(from[2], to[2], t), + lerp(from[3], to[3], t), + lerp(from[4] or 1, to[4] or 1, t)) + end, +}) + +lib:RegisterType("rotation", { + init = function(target, from) + return from or 0 + end, + apply = function(target, from, to, t) + applyRotation(target, lerp(from, to, t)) + end, +}) + +lib:RegisterType("custom", { + init = function(target, from, to, tween) + if from ~= nil then return from end + if tween._getter then return tween._getter(target) end + return 0 + end, + apply = function(target, from, to, t, tween) + if tween._setter then + tween._setter(target, lerp(from, to, t)) + end + end, +}) +local function resolveEasing(easing) + if type(easing) == "function" then return easing end + return lib._easing[easing] or lib._easing["inOutQuad"] +end + +local Tween = {} + +function Tween:Play() + self._elapsed = 0 + self._started = false + self._done = false + lib._activate(self) +end + +function Tween:Cancel() + if not self._active then return end + lib._deactivate(self) + self._done = true + if self._onCancel then self:_onCancel() end +end + +function Tween:IsPlaying() + return self._active == true +end + +function Tween:_start() + local handler = lib._types[self._type] + if handler and handler.init then + self._from = handler.init(self._target, self._from, self._to, self) + end + self._started = true + if self._onStart then self:_onStart() end +end + +function Tween:_tick(dt) + self._elapsed = self._elapsed + dt + + if self._elapsed < self._delay then return false end + + if not self._started then self:_start() end + + local t = self._duration > 0 + and math.min((self._elapsed - self._delay) / self._duration, 1) + or 1 + + local handler = lib._types[self._type] + if handler and handler.apply and self._target then + handler.apply(self._target, self._from, self._to, resolveEasing(self._easing)(t), self) + end + + if self._onUpdate then self:_onUpdate(t) end + + return t >= 1 +end + +function Tween:_finish() + lib._deactivate(self) + self._done = true + + -- Apply one final time at exactly t=1 -- the last _tick may have landed + -- slightly before 1.0 depending on frame timing. + local handler = lib._types[self._type] + if handler and handler.apply and self._target then + handler.apply(self._target, self._from, self._to, 1, self) + end + if handler and handler.cleanup then + handler.cleanup(self._target, self) + end + + if self._onFinish then self:_onFinish() end +end + +function Tween:SetFrom(val) self._from = val end +function Tween:SetTo(val) self._to = val end +function Tween:SetDuration(val) self._duration = val end +function Tween:SetDelay(val) self._delay = val end +function Tween:SetTarget(val) self._target = val end + +function Tween:_reset() + self._elapsed = 0 + self._started = false + self._done = false +end + +local TweenMT = { __index = Tween } + +local KNOWN_OPTS = { + type = true, from = true, to = true, duration = true, easing = true, + delay = true, onStart = true, onUpdate = true, onFinish = true, + onCancel = true, getter = true, setter = true, defer = true, +} + +function lib:Tween(target, opts) + local tween = setmetatable({ + _target = target, + _type = opts.type or "alpha", + _from = opts.from, + _to = opts.to, + _duration = opts.duration or 0.3, + _easing = opts.easing or "inOutQuad", + _delay = opts.delay or 0, + _onStart = opts.onStart, + _onUpdate = opts.onUpdate, + _onFinish = opts.onFinish, + _onCancel = opts.onCancel, + _getter = opts.getter, + _setter = opts.setter, + }, TweenMT) + + -- Forward extra opts to the tween for custom type handlers. + -- Internal fields use underscore prefix, so user params (vx, radius, etc.) + -- are accessible as tween.vx, tween.radius without collision. + for k, v in pairs(opts) do + if not KNOWN_OPTS[k] then + tween[k] = v + end + end + + if not opts.defer then + lib._schedulePending(tween) + end + + return tween +end +local Sequence = {} + +function Sequence:Play() + self._index = 1 + self._loopCount = 0 + self._elapsed = 0 + self._done = false + self:_resetChildren() + lib._activate(self) +end + +function Sequence:Cancel() + if not self._active then return end + local child = self._children[self._index] + if child and child._started and child.Cancel then child:Cancel() end + lib._deactivate(self) + self._done = true + if self._onCancel then self:_onCancel() end +end + +function Sequence:IsPlaying() + return self._active == true +end + +function Sequence:_tick(dt) + self._elapsed = self._elapsed + dt + if self._elapsed < self._delay then return false end + + local child = self._children[self._index] + if not child then return true end + + if not child._started then child:_start() end + + if child:_tick(dt) then + child:_finish() + self._index = self._index + 1 + + if self._index > table.getn(self._children) then + return self:_handleLoop() + end + end + + return false +end + +function Sequence:_finish() + lib._deactivate(self) + self._done = true + if self._onFinish then self:_onFinish() end +end + +function Sequence:_start() + self._started = true +end + +function Sequence:_reset() + self._index = 1 + self._loopCount = 0 + self._elapsed = 0 + self._done = false + self._started = false + self:_resetChildren() +end + +function Sequence:_resetChildren() + for i = 1, table.getn(self._children) do + self._children[i]:_reset() + end +end + +function Sequence:_handleLoop() + self._loopCount = self._loopCount + 1 + if self._loop ~= 0 and self._loopCount >= self._loop then + return true + end + + if self._yoyo then + local c = self._children + local n = table.getn(c) + for i = 1, math.floor(n / 2) do + c[i], c[n - i + 1] = c[n - i + 1], c[i] + end + end + + self._index = 1 + self:_resetChildren() + return false +end + +local SequenceMT = { __index = Sequence } + +function lib:Sequence(children, opts) + opts = opts or {} + + for i = 1, table.getn(children) do + children[i]._autoplay = false + end + + local seq = setmetatable({ + _children = children, + _index = 1, + _loop = opts.loop or 1, + _yoyo = opts.yoyo or false, + _loopCount = 0, + _delay = opts.delay or 0, + _elapsed = 0, + _onFinish = opts.onFinish, + _onCancel = opts.onCancel, + _done = false, + }, SequenceMT) + + if not opts.defer then + lib._schedulePending(seq) + end + + return seq +end +local Group = {} + +function Group:Play() + self._loopCount = 0 + self._elapsed = 0 + self._done = false + self:_resetChildren() + lib._activate(self) +end + +function Group:Cancel() + if not self._active then return end + for i = 1, table.getn(self._children) do + local child = self._children[i] + if child._active or child._started then + if child.Cancel then child:Cancel() end + end + end + lib._deactivate(self) + self._done = true + if self._onCancel then self:_onCancel() end +end + +function Group:IsPlaying() + return self._active == true +end + +function Group:_tick(dt) + self._elapsed = self._elapsed + dt + if self._elapsed < self._delay then return false end + + local allDone = true + + for i = 1, table.getn(self._children) do + local child = self._children[i] + if not child._done then + if not child._started then child:_start() end + if child:_tick(dt) then + child:_finish() + else + allDone = false + end + end + end + + if allDone then + return self:_handleLoop() + end + return false +end + +function Group:_handleLoop() + self._loopCount = self._loopCount + 1 + if self._loop ~= 0 and self._loopCount >= self._loop then + return true + end + + if self._yoyo then + local c = self._children + local n = table.getn(c) + for i = 1, math.floor(n / 2) do + c[i], c[n - i + 1] = c[n - i + 1], c[i] + end + end + + self:_resetChildren() + return false +end + +function Group:_finish() + lib._deactivate(self) + self._done = true + if self._onFinish then self:_onFinish() end +end + +function Group:_start() + self._started = true +end + +function Group:_reset() + self._loopCount = 0 + self._elapsed = 0 + self._done = false + self._started = false + self:_resetChildren() +end + +function Group:_resetChildren() + for i = 1, table.getn(self._children) do + self._children[i]:_reset() + end +end + +local GroupMT = { __index = Group } + +function lib:Group(children, opts) + opts = opts or {} + + for i = 1, table.getn(children) do + children[i]._autoplay = false + end + + local grp = setmetatable({ + _children = children, + _loop = opts.loop or 1, + _yoyo = opts.yoyo or false, + _loopCount = 0, + _delay = opts.delay or 0, + _elapsed = 0, + _onFinish = opts.onFinish, + _onCancel = opts.onCancel, + _done = false, + }, GroupMT) + + if not opts.defer then + lib._schedulePending(grp) + end + + return grp +end +-- Directional offsets for slide animations +local directions = { + LEFT = function(d) return { -d, 0 } end, + RIGHT = function(d) return { d, 0 } end, + UP = function(d) return { 0, d } end, + DOWN = function(d) return { 0,-d } end, +} + +local function dirOffset(dir, dist) + local fn = directions[string.upper(dir or "LEFT")] + return fn and fn(dist) or { 0, 0 } +end + +local function getAnchorPos(frame) + local _, _, _, x, y = frame:GetPoint(1) + return { x or 0, y or 0 } +end + +local EMPTY = {} +local function opts(o) + if type(o) == "table" then return o end + return EMPTY +end + +-- ============================================================ +-- Alpha +-- ============================================================ + +function lib:FadeTo(target, toAlpha, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "alpha", to = toAlpha, + duration = duration, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:FadeIn(target, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "alpha", from = 0, to = 1, + duration = duration, easing = easing, + onStart = function() + target:SetAlpha(0) + target:Show() + if o.onStart then o.onStart() end + end, + delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:FadeOut(target, duration, easing, o) + o = opts(o) + local onFinish = o.onFinish + return lib:FadeTo(target, 0, duration, easing, { + delay = o.delay, onCancel = o.onCancel, defer = o.defer, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + }) +end + +function lib:Flash(target, count, duration, o) + o = opts(o) + count = count or 3 + duration = duration or 0.6 + local half = duration / count / 2 + local steps = {} + for i = 1, count do + table.insert(steps, lib:Tween(target, { type = "alpha", to = 0, duration = half, easing = "inQuad", defer = true })) + table.insert(steps, lib:Tween(target, { type = "alpha", to = 1, duration = half, easing = "outQuad", defer = true })) + end + return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:Breathe(target, minAlpha, maxAlpha, duration, o) + o = opts(o) + duration = duration or 1.6 + local half = duration / 2 + return lib:Sequence({ + lib:Tween(target, { type = "alpha", from = minAlpha, to = maxAlpha, duration = half, easing = "inOutQuad", defer = true }), + lib:Tween(target, { type = "alpha", from = maxAlpha, to = minAlpha, duration = half, easing = "inOutQuad", defer = true }), + }, { loop = 0, delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +-- ============================================================ +-- Scale +-- ============================================================ + +function lib:ScaleIn(target, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "scale", from = 0, to = 1, + duration = duration or 0.3, easing = easing or "outBack", + onStart = function() + target:Show() + if o.onStart then o.onStart() end + end, + delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:ScaleOut(target, duration, easing, o) + o = opts(o) + local onFinish = o.onFinish + return lib:Tween(target, { + type = "scale", from = 1, to = 0, + duration = duration or 0.3, easing = easing or "inBack", + onStart = o.onStart, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + onCancel = o.onCancel, delay = o.delay, defer = o.defer, + }) +end + +function lib:Pulse(target, scale, duration, o) + o = opts(o) + scale = scale or 1.3 + local half = (duration or 0.4) / 2 + local baseW, baseH = target:GetWidth(), target:GetHeight() + local function set(f, s) + f:SetWidth(baseW * s) + f:SetHeight(baseH * s) + end + return lib:Sequence({ + lib:Tween(target, { type = "custom", from = 1.0, to = scale, duration = half, easing = "outQuad", setter = set, defer = true }), + lib:Tween(target, { type = "custom", from = scale, to = 1.0, duration = half, easing = "inQuad", setter = set, defer = true }), + }, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:Rubber(target, scale, duration, o) + o = opts(o) + return lib:Tween(target, { + type = "scale", from = scale or 1.3, to = 1.0, + duration = duration or 0.5, easing = "outElastic", + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:Flip(target, axis, duration, o) + o = opts(o) + local quarter = (duration or 0.8) / 4 + local horizontal = axis ~= "y" + local base = horizontal and target:GetWidth() or target:GetHeight() + local set = horizontal + and function(f, v) f:SetWidth(v) end + or function(f, v) f:SetHeight(v) end + + -- Find child textures to mirror via SetTexCoord + local textures = {} + if target.GetTexture then + table.insert(textures, target) + else + local children = { target:GetRegions() } + for i = 1, table.getn(children) do + if children[i].GetTexture then table.insert(textures, children[i]) end + end + end + + local function mirror() + for i = 1, table.getn(textures) do + if horizontal then + textures[i]:SetTexCoord(1, 0, 0, 1) + else + textures[i]:SetTexCoord(0, 1, 1, 0) + end + end + end + + local function unmirror() + for i = 1, table.getn(textures) do + textures[i]:SetTexCoord(0, 1, 0, 1) + end + end + + local shrink = function(f, v) set(f, v) end + return lib:Sequence({ + -- 0°→90°: shrink to invisible + lib:Tween(target, { type = "custom", from = base, to = 0.01, duration = quarter, easing = "inQuad", setter = shrink, defer = true }), + -- 90°→180°: mirror and expand (showing flipped content) + lib:Tween(target, { type = "custom", from = 0.01, to = base, duration = quarter, easing = "outQuad", setter = shrink, defer = true, + onStart = function() mirror() end }), + -- 180°→270°: shrink again + lib:Tween(target, { type = "custom", from = base, to = 0.01, duration = quarter, easing = "inQuad", setter = shrink, defer = true }), + -- 270°→360°: unmirror and expand back to original + lib:Tween(target, { type = "custom", from = 0.01, to = base, duration = quarter, easing = "outQuad", setter = shrink, defer = true, + onStart = function() unmirror() end }), + }, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +-- ============================================================ +-- Position +-- ============================================================ + +function lib:Move(target, from, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "position", from = from, to = to, + duration = duration or 0.3, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:MoveTo(target, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "position", to = to, + duration = duration, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:SlideIn(target, direction, distance, duration, easing, o) + o = opts(o) + local dest = getAnchorPos(target) + local offset = dirOffset(direction, distance or 100) + local from = { dest[1] - offset[1], dest[2] - offset[2] } + return lib:Group({ + lib:Tween(target, { + type = "position", from = from, to = dest, duration = duration or 0.3, easing = easing or "outCubic", + onStart = function() + target:SetAlpha(0) + target:Show() + if o.onStart then o.onStart() end + end, + defer = true, + }), + lib:Tween(target, { + type = "alpha", + from = 0, + to = 1, + duration = (duration or 0.3) * 0.6, + easing = "outQuad", + defer = true, + }), + }, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:SlideOut(target, direction, distance, duration, easing, o) + o = opts(o) + local from = getAnchorPos(target) + local offset = dirOffset(direction, distance or 100) + local dest = { from[1] + offset[1], from[2] + offset[2] } + local onFinish = o.onFinish + return lib:Group({ + lib:Tween(target, { type = "position", from = from, to = dest, duration = duration or 0.3, easing = easing or "inCubic", defer = true }), + lib:Tween(target, { type = "alpha", to = 0, duration = duration or 0.3, easing = "inQuad", defer = true }), + }, { + delay = o.delay, onCancel = o.onCancel, defer = o.defer, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + }) +end + +function lib:Bounce(target, height, count, duration, o) + o = opts(o) + height = height or 30 + count = count or 3 + duration = duration or 0.6 + local pos = getAnchorPos(target) + local steps = {} + for i = 1, count do + local h = height * (1 - (i - 1) / count) -- decay + table.insert(steps, lib:Tween(target, { + type = "position", from = pos, to = { pos[1], pos[2] + h }, + duration = duration / count / 2, easing = "outQuad", defer = true, + })) + table.insert(steps, lib:Tween(target, { + type = "position", from = { pos[1], pos[2] + h }, to = pos, + duration = duration / count / 2, easing = "inQuad", defer = true, + })) + end + return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:Shake(target, intensity, duration, o) + o = opts(o) + intensity = intensity or 5 + duration = duration or 0.3 + local pos = getAnchorPos(target) + local steps = {} + local stepDur = 0.03 + local numSteps = math.floor(duration / stepDur) + for i = 1, numSteps do + local decay = 1 - (i - 1) / numSteps + local dx = (math.random() * intensity * 2 - intensity) * decay + local dy = (math.random() * intensity * 2 - intensity) * decay + table.insert(steps, lib:Tween(target, { + type = "position", to = { pos[1] + dx, pos[2] + dy }, + duration = stepDur, easing = "linear", defer = true, + })) + end + -- Snap back to original position + table.insert(steps, lib:Tween(target, { + type = "position", to = pos, + duration = stepDur, easing = "linear", defer = true, + })) + return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:FlyTo(frame, targetFrame, duration, easing, o) + o = opts(o) + local toX = targetFrame:GetLeft() + targetFrame:GetWidth() / 2 - UIParent:GetWidth() / 2 + local toY = targetFrame:GetBottom() + targetFrame:GetHeight() / 2 - UIParent:GetHeight() / 2 + return lib:Tween(frame, { + type = "position", to = { toX, toY }, + duration = duration or 0.5, easing = easing or "inOutQuad", + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +-- ============================================================ +-- Rotation +-- ============================================================ + +function lib:Spin(texture, degreesPerSecond, o) + o = opts(o) + local dps = degreesPerSecond or 90 + return lib:Sequence({ + lib:Tween(texture, { + type = "rotation", + from = 0, + to = dps > 0 and 360 or -360, + duration = 360 / math.abs(dps), + easing = "linear", + defer = true, + }), + }, { loop = 0, delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +-- ============================================================ +-- Color +-- ============================================================ + +function lib:Color(target, from, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "color", from = from, to = to, + duration = duration or 0.3, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:ColorFlash(texture, r, g, b, duration, o) + o = opts(o) + duration = duration or 0.4 + local half = duration / 2 + return lib:Sequence({ + lib:Tween(texture, { type = "color", to = { r, g, b, 1 }, duration = half, easing = "outQuad", defer = true }), + lib:Tween(texture, { type = "color", from = { r, g, b, 1 }, to = { 1, 1, 1, 1 }, duration = half, easing = "inQuad", defer = true }), + }, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +-- ============================================================ +-- Size +-- ============================================================ + +function lib:Size(target, from, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "size", from = from, to = to, + duration = duration or 0.3, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:Morph(target, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "size", to = to, + duration = duration or 0.3, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +-- ============================================================ +-- Value +-- ============================================================ + +function lib:Progress(statusbar, toValue, duration, easing, o) + o = opts(o) + return lib:Tween(statusbar, { + type = "custom", + to = toValue, + duration = duration or 0.4, + easing = easing or "outCubic", + getter = function(bar) return bar:GetValue() end, + setter = function(bar, val) bar:SetValue(val) end, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +-- ============================================================ +-- Text +-- ============================================================ + +function lib:Typewriter(fontstring, text, duration, o) + o = opts(o) + local len = string.len(text) + return lib:Tween(fontstring, { + type = "custom", + from = 0, to = len, + duration = duration or len * 0.05, + easing = "linear", + setter = function(fs, val) + fs:SetText(string.sub(text, 1, math.floor(val))) + end, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +-- ============================================================ +-- Clipping (Reveal / Conceal) +-- ============================================================ + +-- Progressively reveal a texture from a direction by re-anchoring +-- to one edge, animating width/height, and shifting tex coords. +local clipAnchors = { + LEFT = { "TOPLEFT", "TOPLEFT" }, + RIGHT = { "TOPRIGHT", "TOPRIGHT" }, + UP = { "TOPLEFT", "TOPLEFT" }, + DOWN = { "BOTTOMLEFT", "BOTTOMLEFT" }, +} + +local function clipTween(target, direction, duration, easing, from, to, o) + o = opts(o) + direction = string.upper(direction or "LEFT") + duration = duration or 0.3 + + local parent = target:GetParent() + local baseW = target._clipBaseW or parent:GetWidth() + local baseH = target._clipBaseH or parent:GetHeight() + target._clipBaseW = baseW + target._clipBaseH = baseH + + local horizontal = (direction == "LEFT" or direction == "RIGHT") + local reversed = (direction == "RIGHT" or direction == "UP") + local anchor = clipAnchors[direction] + + target:ClearAllPoints() + target:SetPoint(anchor[1], parent, anchor[2], 0, 0) + + return lib:Tween(target, { + type = "custom", from = from, to = to, + duration = duration, easing = easing or "outQuad", + setter = function(tex, t) + if horizontal then + tex:SetWidth(math.max(baseW * t, 0.01)) + tex:SetHeight(baseH) + if reversed then + tex:SetTexCoord(1 - t, 1, 0, 1) + else + tex:SetTexCoord(0, t, 0, 1) + end + else + tex:SetWidth(baseW) + tex:SetHeight(math.max(baseH * t, 0.01)) + if reversed then + tex:SetTexCoord(0, 1, 1 - t, 1) + else + tex:SetTexCoord(0, 1, 0, t) + end + end + end, + onFinish = function() + target:SetTexCoord(0, 1, 0, 1) + target:ClearAllPoints() + target:SetAllPoints(parent) + if o.onFinish then o.onFinish() end + end, + onCancel = o.onCancel, delay = o.delay, defer = o.defer, + }) +end + +function lib:Reveal(target, direction, duration, easing, o) + target:Show() + return clipTween(target, direction, duration, easing, 0, 1, o) +end + +function lib:Conceal(target, direction, duration, easing, o) + o = opts(o) + local onFinish = o.onFinish + return clipTween(target, direction, duration, easing, 1, 0, { + delay = o.delay, onCancel = o.onCancel, defer = o.defer, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + }) +end + +-- WipeIn/WipeOut: texture slides in from a direction while being clipped. +-- Unlike Reveal/Conceal where the image is stationary and a window opens, +-- here the image physically moves into view (like sliding out from under a cover). +-- WipeIn/WipeOut: the image slides into/out of view while being clipped. +-- The texture stays full-size (no stretching) -- we clip by shrinking the +-- visible region and offsetting tex coords so the revealed portion matches +-- the correct part of the image. +-- +-- For "LEFT" (image slides in from the left edge): +-- anchor at TOPLEFT, width = baseW * t, texCoord left = 1-t +-- → at t=0 nothing visible; at t=1 full image +local wipeAnchors = { + LEFT = "TOPLEFT", + RIGHT = "TOPRIGHT", + UP = "TOPLEFT", + DOWN = "BOTTOMLEFT", +} + +local function wipeTween(target, direction, duration, easing, from, to, o) + o = opts(o) + direction = string.upper(direction or "LEFT") + duration = duration or 0.3 + + local parent = target:GetParent() + local baseW = target._clipBaseW or parent:GetWidth() + local baseH = target._clipBaseH or parent:GetHeight() + target._clipBaseW = baseW + target._clipBaseH = baseH + + local horizontal = (direction == "LEFT" or direction == "RIGHT") + local anchor = wipeAnchors[direction] + + target:ClearAllPoints() + target:SetPoint(anchor, parent, anchor, 0, 0) + + return lib:Tween(target, { + type = "custom", from = from, to = to, + duration = duration, easing = easing or "outQuad", + setter = function(tex, t) + if horizontal then + tex:SetWidth(math.max(baseW * t, 0.01)) + tex:SetHeight(baseH) + if direction == "LEFT" then + tex:SetTexCoord(1 - t, 1, 0, 1) + else + tex:SetTexCoord(0, t, 0, 1) + end + else + tex:SetWidth(baseW) + tex:SetHeight(math.max(baseH * t, 0.01)) + if direction == "DOWN" then + tex:SetTexCoord(0, 1, 1 - t, 1) + else + tex:SetTexCoord(0, 1, 0, t) + end + end + end, + onFinish = function() + target:SetTexCoord(0, 1, 0, 1) + target:ClearAllPoints() + target:SetAllPoints(parent) + if o.onFinish then o.onFinish() end + end, + onCancel = o.onCancel, delay = o.delay, defer = o.defer, + }) +end + +local wipeInFlip = { LEFT="RIGHT", RIGHT="LEFT", UP="DOWN", DOWN="UP" } + +function lib:WipeIn(target, direction, duration, easing, o) + target:Show() + local dir = wipeInFlip[string.upper(direction or "LEFT")] or direction + return wipeTween(target, dir, duration, easing, 0, 1, o) +end + +function lib:WipeOut(target, direction, duration, easing, o) + o = opts(o) + local onFinish = o.onFinish + return wipeTween(target, direction, duration, easing, 1, 0, { + delay = o.delay, onCancel = o.onCancel, defer = o.defer, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + }) +end + +-- ============================================================ +-- Meta / Utility +-- ============================================================ + +function lib:Stagger(targets, animFn, delay, o) + o = opts(o) + delay = delay or 0.08 + local children = {} + for i = 1, table.getn(targets) do + local anim = animFn(targets[i], i) + anim._autoplay = false + if i > 1 then + table.insert(children, lib:Sequence({ lib:Delay(delay * (i - 1)), anim }, { defer = true })) + else + table.insert(children, anim) + end + end + return lib:Group(children, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:Delay(duration) + -- A no-op tween that just waits. Uses a dummy target-less approach. + local dummy = lib._frame + return lib:Tween(dummy, { + type = "alpha", + from = dummy:GetAlpha(), + to = dummy:GetAlpha(), + duration = duration or 1, + defer = true, + }) +end + +function lib:Run(fn, o) + o = opts(o) + return lib:Tween(nil, { + type = "custom", from = 0, to = 0, duration = 0, + onStart = fn, delay = o.delay, defer = o.defer, + }) +end + +function lib:Stop(target) + for anim in pairs(lib._active) do + if anim._target == target then + anim:Cancel() + elseif anim._children then + lib:_stopInChildren(anim, anim, target) + end + end + -- Also cancel pending (not yet started) animations for this target + for i = 1, lib._pendingN do + local anim = lib._pending[i] + if anim then + if anim._target == target then + anim._autoplay = false + elseif anim._children then + if lib:_hasTargetInChildren(anim, target) then + anim._autoplay = false + end + end + end + end +end + +function lib:_hasTargetInChildren(parent, target) + for i = 1, table.getn(parent._children) do + local child = parent._children[i] + if child._target == target then return true end + if child._children and lib:_hasTargetInChildren(child, target) then return true end + end + return false +end + +function lib:_stopInChildren(root, parent, target) + for i = 1, table.getn(parent._children) do + local child = parent._children[i] + if child._target == target then + root:Cancel() + return + elseif child._children then + lib:_stopInChildren(root, child, target) + if not root._active then return end + end + end +end diff --git a/api/core-scan-beasttraining.lua b/api/core-scan-beasttraining.lua index 4dd0316..11586a6 100644 --- a/api/core-scan-beasttraining.lua +++ b/api/core-scan-beasttraining.lua @@ -432,6 +432,9 @@ local function MTH_PT_ProcessLearnSystemMessage(rawMessage) if changed then MTH_PT_ApplyScan("learn-msg") end + if changed and type(MTH_CelebratePetAbility) == "function" then + MTH_CelebratePetAbility(label) + end if type(MTH_PSP_RequestScan) == "function" then MTH_PSP_RequestScan("train-learn-msg") end diff --git a/api/core.lua b/api/core.lua index dc1d2da..672b481 100644 --- a/api/core.lua +++ b/api/core.lua @@ -325,6 +325,69 @@ function SlashCmdList.MTH(msg, editbox) MTH_CommandBook() elseif lowerMsg == "peers" or lowerMsg == "who" then MTH_CommandPeers() + elseif lowerCmd == "trainscan" or lowerCmd == "petscan" then + if type(MTH_CommandTrainScan) == "function" then + MTH_CommandTrainScan() + else + MTH:Print("Train scan is not available.") + end + elseif lowerCmd == "fx" or lowerCmd == "fireworks" or lowerCmd == "celebrate" then + -- TEMP: preview the pet-ability celebration effect. + if type(MTH_Celebrate) == "function" then + local label = msg + local _, _, rest = string.find(msg, "^%S+%s+(.-)%s*$") + if rest and rest ~= "" then + MTH_Celebrate("New Pet Ability!", rest) + else + MTH_Celebrate("New Pet Ability!", "Growl (Rank 3)") + end + else + -- Self-diagnosing: report which piece failed to load so we can tell + -- a stale UI from a genuine load error. + MTH:Print("Celebration effect is not available. Diagnostics:") + MTH:Print(" PizzaSauce global: " .. type(PizzaSauce)) + MTH:Print(" MTH_Celebrate: " .. type(MTH_Celebrate)) + MTH:Print(" MTH_FX_IsEnabled: " .. type(MTH_FX_IsEnabled)) + MTH:Print(" MTH_SetupAnimationsOptions: " .. type(MTH_SetupAnimationsOptions)) + MTH:Print(" -> If these are 'nil', do /reload (your UI is running code from before the feature was added).") + end + elseif lowerCmd == "fxdebug" then + -- TEMP: dump the animation load-path tracers so we can see exactly where + -- fx-celebrate.lua / options-animations.lua aborted at load time. + MTH:Print("=== MetaHunt animation load debug ===") + MTH:Print("Globals: PizzaSauce=" .. type(PizzaSauce) + .. " CreateFrame=" .. type(CreateFrame) + .. " MTH=" .. type(MTH)) + if type(MTH_FX_DBG) == "table" then + MTH:Print("fx-celebrate: step=" .. tostring(MTH_FX_DBG.step) + .. " sauceType=" .. tostring(MTH_FX_DBG.sauceType) + .. " registerOk=" .. tostring(MTH_FX_DBG.registerOk) + .. " registerSkipped=" .. tostring(MTH_FX_DBG.registerSkipped)) + if MTH_FX_DBG.registerErr ~= nil then + MTH:Print("fx-celebrate: registerErr=" .. tostring(MTH_FX_DBG.registerErr)) + end + MTH:Print("fx-celebrate: hasCelebrate=" .. tostring(MTH_FX_DBG.hasCelebrate) + .. " hasIsEnabled=" .. tostring(MTH_FX_DBG.hasIsEnabled) + .. " hasAnimEnabled=" .. tostring(MTH_FX_DBG.hasAnimEnabled)) + else + MTH:Print("fx-celebrate: MTH_FX_DBG is " .. type(MTH_FX_DBG) .. " (file did not reach its first line)") + end + if type(MTH_ANIM_DBG) == "table" then + MTH:Print("options-animations: step=" .. tostring(MTH_ANIM_DBG.step) + .. " ready=" .. tostring(MTH_ANIM_DBG.ready) + .. " hasSetup=" .. tostring(MTH_ANIM_DBG.hasSetup)) + MTH:Print("options-animations: req=" .. tostring(MTH_ANIM_DBG.reqType) + .. " getFrame=" .. tostring(MTH_ANIM_DBG.getFrameType) + .. " clear=" .. tostring(MTH_ANIM_DBG.clearType) + .. " checkbox=" .. tostring(MTH_ANIM_DBG.checkboxType)) + MTH:Print("options-animations: setupCalled=" .. tostring(MTH_ANIM_DBG.setupCalled) + .. " setupResult=" .. tostring(MTH_ANIM_DBG.setupResult) + .. " containerType=" .. tostring(MTH_ANIM_DBG.containerType) + .. " fxEnabledType=" .. tostring(MTH_ANIM_DBG.fxEnabledType)) + else + MTH:Print("options-animations: MTH_ANIM_DBG is " .. type(MTH_ANIM_DBG) .. " (file did not reach its first line)") + end + MTH:Print("=== end animation load debug ===") elseif lowerMsg == "zoneinfo" then -- Dumps all available zone data for the current zone, for adding to ds-zones / ds-minimap-sizes pcall(SetMapToCurrentZone) diff --git a/api/fx-celebrate.lua b/api/fx-celebrate.lua new file mode 100644 index 0000000..9cda041 --- /dev/null +++ b/api/fx-celebrate.lua @@ -0,0 +1,297 @@ +-- MetaHunt: center-screen celebration effect (message + firework burst). +-- Uses the embedded PizzaSauce animation library. Degrades to a silent no-op +-- when PizzaSauce or the WoW frame API are unavailable (e.g. offline tests). + +-- Capture our own PizzaSauce instance now, before another addon that ships its +-- own copy can overwrite the global. This is the pattern the library requires. +local Sauce = PizzaSauce + +-- Localization helper: resolve a key through MetaHunt's loc system, falling back +-- to the inline English default. Every user-facing string is keyed so the enUS +-- strings can be harvested later; the inline default is the working English. +local function MTH_FX_L(key, default) + if MTH and type(MTH.GetLocalization) == "function" then + return MTH:GetLocalization(key, default) + end + return default or key +end + +-- --------------------------------------------------------------------------- +-- DEBUG: load-path tracer. Global so /mth fxdebug can dump it. Records how far +-- this file got at load time and any error from the one executable file-scope +-- block (RegisterType). TEMP diagnostic. +-- --------------------------------------------------------------------------- +MTH_FX_DBG = { + sauceType = type(PizzaSauce), + createFrameType = type(CreateFrame), + step = "1-top", +} + +-- --------------------------------------------------------------------------- +-- Animation settings (shared by the effect code and the options "Animations" tab) +-- --------------------------------------------------------------------------- +-- Sections shown, in order, in the options panel. `titleKey` localizes `title`. +MTH_FX_ANIM_SECTIONS = { + { id = "pet", title = "Pet", titleKey = "ANIM_SECTION_PET" }, + { id = "book", title = "Book", titleKey = "ANIM_SECTION_BOOK" }, + { id = "ui", title = "Interface", titleKey = "ANIM_SECTION_UI" }, +} + +-- Every individual animation the addon can play. Each has a stable `key`, the +-- section it belongs to, a checkbox label, an optional tooltip, and a default. +-- labelKey/tooltipKey localize label/tooltip (resolved at render time). +-- Adding a new animation = one entry here + one MTH_FX_IsEnabled("key") guard. +MTH_FX_ANIM_DEFS = { + { key = "pet.celebrate", section = "pet", default = true, + label = "Celebrate new pet abilities", labelKey = "ANIM_PET_CELEBRATE", + tooltip = "Play a firework and a banner in the middle of the screen when your pet learns a new ability or a new rank.", + tooltipKey = "ANIM_PET_CELEBRATE_TIP" }, + { key = "book.open", section = "book", default = true, + label = "Fade in the Hunter's Book", labelKey = "ANIM_BOOK_OPEN", + tooltip = "Fade the Hunter's Book window in when you open it.", + tooltipKey = "ANIM_BOOK_OPEN_TIP" }, + { key = "book.tabs", section = "book", default = true, + label = "Fade Hunter's Book tab changes", labelKey = "ANIM_BOOK_TABS", + tooltip = "Fade the results list back in when you switch tabs inside the Hunter's Book.", + tooltipKey = "ANIM_BOOK_TABS_TIP" }, + { key = "ui.options", section = "ui", default = true, + label = "Slide the options window open", labelKey = "ANIM_UI_OPTIONS", + tooltip = "Slide and fade the MetaHunt options window in when you open it.", + tooltipKey = "ANIM_UI_OPTIONS_TIP" }, +} + +local function MTH_FX_Store() + if MTH and type(MTH.GetModuleSavedVariables) == "function" then + return MTH:GetModuleSavedVariables("animations") + end + return nil +end + +local function MTH_FX_DefaultFor(key) + local i = 1 + while i <= table.getn(MTH_FX_ANIM_DEFS) do + if MTH_FX_ANIM_DEFS[i].key == key then + return MTH_FX_ANIM_DEFS[i].default and true or false + end + i = i + 1 + end + return true +end + +function MTH_FX_AnimationsEnabled() + local store = MTH_FX_Store() + if not store or store.enabled == nil then + return true + end + return store.enabled and true or false +end + +function MTH_FX_SetAnimationsEnabled(value) + local store = MTH_FX_Store() + if not store then return end + store.enabled = value and true or false +end + +function MTH_FX_GetFlag(key) + local store = MTH_FX_Store() + if store and store.flags and store.flags[key] ~= nil then + return store.flags[key] and true or false + end + return MTH_FX_DefaultFor(key) +end + +function MTH_FX_SetFlag(key, value) + local store = MTH_FX_Store() + if not store then return end + store.flags = store.flags or {} + store.flags[key] = value and true or false +end + +-- Master gate every animation call site checks: false when animations are +-- globally disabled, or when this specific animation is turned off. +function MTH_FX_IsEnabled(key) + if not MTH_FX_AnimationsEnabled() then + return false + end + if key == nil then + return true + end + return MTH_FX_GetFlag(key) +end + +-- Register a one-shot "ballistic" tween type: a spark that flies out from the +-- center along a velocity vector while gravity pulls it back down. +MTH_FX_DBG.step = "2-before-registertype" +if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasBallistic then + local ok, err = pcall(function() + Sauce.MTH_HasBallistic = true + Sauce:RegisterType("mth_ballistic", { + init = function(target, from) + return from or { 0, 0 } + end, + apply = function(target, from, to, t, tween) + local dur = tween.dur or 1 + local elapsed = t * dur + local vx = tween.vx or 0 + local vy = tween.vy or 0 + local gravity = tween.gravity or 0 + local x = from[1] + vx * elapsed + local y = from[2] + vy * elapsed - 0.5 * gravity * elapsed * elapsed + local anchor = tween.anchor or UIParent + target:ClearAllPoints() + target:SetPoint("CENTER", anchor, "CENTER", x, y) + end, + }) + end) + MTH_FX_DBG.registerOk = ok + MTH_FX_DBG.registerErr = err +else + MTH_FX_DBG.registerSkipped = true +end +MTH_FX_DBG.step = "3-after-registertype" + +local MTH_FX_NUM_SPARKS = 22 +local MTH_FX_Container +local MTH_FX_MsgFrame +local MTH_FX_TitleFS +local MTH_FX_SubFS +local MTH_FX_Sparks = {} + +-- Palette for the firework sparks (r, g, b), warm celebratory tones. +local MTH_FX_Colors = { + { 1.00, 0.82, 0.00 }, + { 1.00, 0.45, 0.10 }, + { 1.00, 0.20, 0.30 }, + { 0.35, 0.70, 1.00 }, + { 0.55, 1.00, 0.45 }, + { 0.85, 0.50, 1.00 }, +} + +local function MTH_FX_EnsureFrames() + if type(CreateFrame) ~= "function" then + return false + end + if MTH_FX_Container then + return true + end + + MTH_FX_Container = CreateFrame("Frame", "MTH_FX_Container", UIParent) + MTH_FX_Container:SetWidth(1) + MTH_FX_Container:SetHeight(1) + MTH_FX_Container:SetPoint("CENTER", UIParent, "CENTER", 0, 40) + MTH_FX_Container:SetFrameStrata("FULLSCREEN_DIALOG") + MTH_FX_Container:Hide() + + MTH_FX_MsgFrame = CreateFrame("Frame", "MTH_FX_Message", UIParent) + MTH_FX_MsgFrame:SetWidth(512) + MTH_FX_MsgFrame:SetHeight(90) + MTH_FX_MsgFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 80) + MTH_FX_MsgFrame:SetFrameStrata("FULLSCREEN_DIALOG") + + MTH_FX_TitleFS = MTH_FX_MsgFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge") + MTH_FX_TitleFS:SetPoint("CENTER", MTH_FX_MsgFrame, "CENTER", 0, 14) + MTH_FX_TitleFS:SetTextColor(1, 0.82, 0) + + MTH_FX_SubFS = MTH_FX_MsgFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") + MTH_FX_SubFS:SetPoint("TOP", MTH_FX_TitleFS, "BOTTOM", 0, -8) + MTH_FX_SubFS:SetTextColor(1, 1, 1) + + MTH_FX_MsgFrame:Hide() + + local i = 1 + while i <= MTH_FX_NUM_SPARKS do + local spark = MTH_FX_Container:CreateTexture(nil, "OVERLAY") + spark:SetTexture("Interface\\Cooldown\\star4") + spark:SetWidth(22) + spark:SetHeight(22) + spark:SetBlendMode("ADD") + spark:SetPoint("CENTER", MTH_FX_Container, "CENTER", 0, 0) + spark:Hide() + MTH_FX_Sparks[i] = spark + i = i + 1 + end + + return true +end + +-- Public entry point: play a celebratory message + firework burst in the middle +-- of the screen. `title` and `subtitle` are optional strings. +function MTH_Celebrate(title, subtitle) + if not Sauce then + return + end + if not MTH_FX_EnsureFrames() then + return + end + + MTH_FX_TitleFS:SetText(title or "") + MTH_FX_SubFS:SetText(subtitle or "") + + Sauce:Stop(MTH_FX_MsgFrame) + MTH_FX_MsgFrame:SetAlpha(0) + MTH_FX_MsgFrame:Show() + Sauce:Sequence({ + Sauce:Group({ + Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.25), + Sauce:Pulse(MTH_FX_MsgFrame, 1.22, 0.5), + }), + Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = 2.2 }), + }) + + MTH_FX_Container:Show() + local colorCount = table.getn(MTH_FX_Colors) + local colorIdx = 0 + local i = 1 + while i <= MTH_FX_NUM_SPARKS do + local spark = MTH_FX_Sparks[i] + local angle = (i / MTH_FX_NUM_SPARKS) * 2 * math.pi + (math.random() - 0.5) * 0.4 + local speed = 150 + math.random() * 130 + local vx = math.cos(angle) * speed + local vy = math.sin(angle) * speed + 70 + local dur = 0.9 + math.random() * 0.4 + + colorIdx = colorIdx + 1 + if colorIdx > colorCount then colorIdx = 1 end + local color = MTH_FX_Colors[colorIdx] + spark:SetVertexColor(color[1], color[2], color[3]) + spark:SetAlpha(1) + spark:Show() + + Sauce:Stop(spark) + Sauce:Group({ + Sauce:Tween(spark, { + type = "mth_ballistic", + from = { 0, 0 }, + duration = dur, + easing = "linear", + anchor = MTH_FX_Container, + dur = dur, + vx = vx, + vy = vy, + gravity = 300, + }), + Sauce:FadeTo(spark, 0, 0.45, "inQuad", { + delay = dur - 0.45, + onFinish = function() + spark:Hide() + end, + }), + }) + i = i + 1 + end +end + +-- Convenience wrapper used when the hunter's pet learns a new ability/rank. +function MTH_CelebratePetAbility(label) + if not MTH_FX_IsEnabled("pet.celebrate") then + return + end + MTH_Celebrate(MTH_FX_L("ANIM_PET_LEARNED_TITLE", "New Pet Ability!"), label) +end + +-- DEBUG: mark that the whole file finished loading and record which globals it +-- successfully defined. TEMP diagnostic. +MTH_FX_DBG.step = "4-complete" +MTH_FX_DBG.hasCelebrate = type(MTH_Celebrate) +MTH_FX_DBG.hasIsEnabled = type(MTH_FX_IsEnabled) +MTH_FX_DBG.hasAnimEnabled = type(MTH_FX_AnimationsEnabled) diff --git a/api/hunterbook.lua b/api/hunterbook.lua index 2f1808a..f531778 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -1,5 +1,34 @@ -- MetaHunt standalone Hunter Book UI (XML + Lua) +-- Capture our PizzaSauce instance for opening animations (may be nil if the +-- library is unavailable; all uses are guarded). +local Sauce = PizzaSauce + +-- Fade the book window in on open, unless the player disabled it in options. +local function MTH_BOOK_FadeInWindow(frame) + if not Sauce or not frame then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("book.open") then return end + -- Hide the window's alpha THIS frame: Show() already ran, so without this the + -- book renders fully opaque for one frame before the tween's onStart snaps it + -- to 0 -- a flash that makes the fade look like it never happened. + if type(frame.SetAlpha) == "function" then frame:SetAlpha(0) end + -- Slide the whole window up into place while fading in. Position and alpha are + -- the only safe properties on a complex frame: the built-in "scale" tween + -- resizes width/height, which the child widgets do NOT follow (they clip), so + -- a slide + fade is the strongest effect that keeps the layout intact. + Sauce:SlideIn(frame, "UP", 60, 0.4, "outCubic") +end + +-- Fade the results list back in when switching Hunter's Book tabs, unless disabled. +local function MTH_BOOK_AnimateTabSwitch() + if not Sauce then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("book.tabs") then return end + local list = getglobal("MTH_BOOK_ListBackdrop") + if not list then return end + if type(list.SetAlpha) == "function" then list:SetAlpha(0) end + Sauce:FadeIn(list, 0.25) +end + MTH_HUNTERBOOK_LOADED = true MTH_HUNTERBOOK_TABS = MTH_HUNTERBOOK_TABS or {} @@ -4929,6 +4958,7 @@ local function MTH_BOOK_SetMode(mode) MTH_BOOK_UpdateModeLabels() MTH_BOOK_UpdateSectionTabs() MTH_BOOK_RefreshFilter() + MTH_BOOK_AnimateTabSwitch() end function MTH_BOOK_JumpToBeastById(beastId) @@ -5712,6 +5742,7 @@ function MTH_OpenHunterBook() local frame = MTH_BOOK_EnsureWindow() if not frame then return end frame:Show() + MTH_BOOK_FadeInWindow(frame) if MTH_BOOK_UpdateRealmGateOverlay(frame) then return end MTH_BOOK_RefreshFilter() end @@ -5724,6 +5755,7 @@ function MTH_ToggleHunterBook() frame:Hide() else frame:Show() + MTH_BOOK_FadeInWindow(frame) if MTH_BOOK_UpdateRealmGateOverlay(frame) then return end MTH_BOOK_RefreshFilter() end diff --git a/api/options-animations.lua b/api/options-animations.lua new file mode 100644 index 0000000..f36fd77 --- /dev/null +++ b/api/options-animations.lua @@ -0,0 +1,173 @@ +-- MetaHunt: "Animations" options panel. +-- Global enable/disable plus a per-animation checkbox for each effect, grouped +-- into sections (Pet, Book, ...). The animation registry lives in fx-celebrate.lua +-- (MTH_FX_ANIM_SECTIONS / MTH_FX_ANIM_DEFS) so both the effects and this panel +-- stay in sync. + +local MTH_ANIM_READY = MTH_OptionsRequire and MTH_OptionsRequire("options-animations", { + "MTH_GetFrame", + "MTH_ClearContainer", + "MTH_CreateCheckbox", +}) + +-- DEBUG: load-path tracer, global so /mth fxdebug can dump it. TEMP diagnostic. +MTH_ANIM_DBG = { + ready = tostring(MTH_ANIM_READY), + reqType = type(MTH_OptionsRequire), + getFrameType = type(MTH_GetFrame), + clearType = type(MTH_ClearContainer), + checkboxType = type(MTH_CreateCheckbox), + step = "1-top", +} + +local MTH_ANIM_LINK = "https://codeberg.org/Pizzahawaii/PizzaSauce" +local MTH_ANIM_CTRL = {} + +local function MTH_ANIM_L(key, default) + if MTH and MTH.GetLocalization then + return MTH:GetLocalization(key, default) + end + return default or key +end + +local function MTH_ANIM_InsertLink(url) + local link = tostring(url or "") + if link == "" then return end + if type(MTH_InsertLinkToChat) == "function" and MTH_InsertLinkToChat(link) then + if MTH and MTH.Print then + MTH:Print(MTH_ANIM_L("ANIM_LINK_INSERTED", "Link inserted in chat: ") .. link) + end + return + end + if MTH and MTH.Print then + MTH:Print(MTH_ANIM_L("ANIM_LINK_FALLBACK", "Link: ") .. link) + end +end + +local function MTH_ANIM_UpdateChildStates() + local master = MTH_FX_AnimationsEnabled() + local i = 1 + while i <= table.getn(MTH_ANIM_CTRL) do + local cb = MTH_ANIM_CTRL[i] + if cb then + cb:SetChecked(MTH_FX_GetFlag(cb.mthKey)) + if master then + cb:Enable() + else + cb:Disable() + end + end + i = i + 1 + end +end + +function MTH_SetupAnimationsOptions() + MTH_ANIM_DBG.setupCalled = true + MTH_ANIM_DBG.fxEnabledType = type(MTH_FX_AnimationsEnabled) + if not MTH_ANIM_READY then + MTH_ANIM_DBG.setupResult = "abort-not-ready" + return + end + if type(MTH_FX_AnimationsEnabled) ~= "function" then + MTH_ANIM_DBG.setupResult = "abort-no-fx-globals" + return + end + + local container = MTH_GetFrame("MetaHuntOptionsAnimations") + MTH_ANIM_DBG.containerType = type(container) + if not container then + MTH_ANIM_DBG.setupResult = "abort-no-container" + return + end + + MTH_ClearContainer(container) + MTH_ANIM_CTRL = {} + + local title = container:CreateFontString("MetaHuntOptionsAnimationsTitle", "ARTWORK", "GameFontNormal") + title:SetPoint("TOPLEFT", container, "TOPLEFT", 10, -10) + title:SetText(MTH_ANIM_L("ANIM_TITLE", "Animations")) + title:SetTextColor(1.00, 0.82, 0.00) + + local credit = container:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + credit:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8) + credit:SetWidth(460) + credit:SetJustifyH("LEFT") + credit:SetText(MTH_ANIM_L("ANIM_CREDIT", + "Animations are powered by PizzaSauce, a WoW 1.12 animation library kindly made and shared by Pizzahawaii. Thank you!")) + credit:SetTextColor(0.93, 0.93, 0.93) + + local link = CreateFrame("Button", nil, container) + link:SetPoint("TOPLEFT", credit, "BOTTOMLEFT", 0, -4) + link:SetHeight(16) + link:SetWidth(460) + local linkText = link:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + linkText:SetPoint("LEFT", link, "LEFT", 0, 0) + linkText:SetJustifyH("LEFT") + linkText:SetText(MTH_ANIM_LINK) + linkText:SetTextColor(0.40, 0.75, 1.00) + link:SetScript("OnClick", function() MTH_ANIM_InsertLink(MTH_ANIM_LINK) end) + link:SetScript("OnEnter", function() linkText:SetTextColor(0.60, 0.90, 1.00) end) + link:SetScript("OnLeave", function() linkText:SetTextColor(0.40, 0.75, 1.00) end) + + -- Global master toggle. + local master = MTH_CreateCheckbox(container, "MetaHuntOptionsAnimMaster", + MTH_ANIM_L("ANIM_ENABLE_ALL", "Enable animations"), -70) + if master then + master:SetChecked(MTH_FX_AnimationsEnabled()) + master:SetScript("OnClick", function() + MTH_FX_SetAnimationsEnabled(master:GetChecked()) + MTH_ANIM_UpdateChildStates() + end) + end + + -- Per-animation checkboxes, grouped by section. + local y = -104 + local rowStep = 26 + local sectionGap = 12 + local s = 1 + while MTH_FX_ANIM_SECTIONS and s <= table.getn(MTH_FX_ANIM_SECTIONS) do + local sec = MTH_FX_ANIM_SECTIONS[s] + local header = container:CreateFontString(nil, "ARTWORK", "GameFontNormal") + header:SetPoint("TOPLEFT", container, "TOPLEFT", 15, y) + header:SetText(MTH_ANIM_L(sec.titleKey, sec.title)) + header:SetTextColor(1.00, 0.82, 0.00) + y = y - 22 + + local d = 1 + while MTH_FX_ANIM_DEFS and d <= table.getn(MTH_FX_ANIM_DEFS) do + local def = MTH_FX_ANIM_DEFS[d] + if def.section == sec.id then + local cbName = "MetaHuntOptionsAnim_" .. string.gsub(def.key, "%.", "_") + local cb = MTH_CreateCheckbox(container, cbName, MTH_ANIM_L(def.labelKey, def.label), y, 28) + if cb then + cb.mthKey = def.key + cb:SetChecked(MTH_FX_GetFlag(def.key)) + cb:SetScript("OnClick", function() + MTH_FX_SetFlag(cb.mthKey, cb:GetChecked()) + end) + if def.tooltip and def.tooltip ~= "" then + cb.mthTooltip = MTH_ANIM_L(def.tooltipKey, def.tooltip) + cb:SetScript("OnEnter", function() + GameTooltip:SetOwner(cb, "ANCHOR_RIGHT") + GameTooltip:SetText(cb.mthTooltip, 1, 1, 1, 1, true) + GameTooltip:Show() + end) + cb:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + table.insert(MTH_ANIM_CTRL, cb) + y = y - rowStep + end + end + d = d + 1 + end + y = y - sectionGap + s = s + 1 + end + + MTH_ANIM_UpdateChildStates() + MTH_ANIM_DBG.setupResult = "ok" +end + +-- DEBUG: mark that the whole file finished loading. TEMP diagnostic. +MTH_ANIM_DBG.step = "2-complete" +MTH_ANIM_DBG.hasSetup = type(MTH_SetupAnimationsOptions) diff --git a/api/options-shell.lua b/api/options-shell.lua index 87bbeb7..1a12831 100644 --- a/api/options-shell.lua +++ b/api/options-shell.lua @@ -20,6 +20,16 @@ local function MTH_OPT_L(key, default) return default or key end +-- Capture our PizzaSauce instance for the options-window open animation (guarded). +local MTH_OPT_Sauce = PizzaSauce +local function MTH_OPT_AnimateOpen(frame) + if not MTH_OPT_Sauce or not frame then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.options") then return end + -- Hide alpha this frame so there is no one-frame opaque flash before onStart. + if type(frame.SetAlpha) == "function" then frame:SetAlpha(0) end + MTH_OPT_Sauce:SlideIn(frame, "UP", 60, 0.4, "outCubic") +end + local function MTH_RegisterEscClose(frameName) if not (frameName and UISpecialFrames) then return @@ -232,6 +242,13 @@ function MTH_SelectOptionsTab(tabKey) elseif MTH and MTH.Print then MTH:Print("ICU options setup function is not loaded", "error") end + elseif tabKey == "Animations" then + if not MTH_OPTIONS_SETUP["Animations"] then + if type(MTH_SetupAnimationsOptions) == "function" then + MTH_SetupAnimationsOptions() + MTH_OPTIONS_SETUP["Animations"] = true + end + end elseif tabKey == "Credits" then local setupCredits = _G and _G["MTH_SetupCreditsOptions"] if type(setupCredits) == "function" then @@ -324,6 +341,7 @@ function MTH_OpenOptions(tabKey) return end optionsFrame:Show() + MTH_OPT_AnimateOpen(optionsFrame) if MTH_OPTIONS_UpdateRealmGateOverlay(optionsFrame) then return end MTH_SelectOptionsTab(tabKey or "General") end diff --git a/api/options-tree.lua b/api/options-tree.lua index 1bb92b8..15243da 100644 --- a/api/options-tree.lua +++ b/api/options-tree.lua @@ -52,6 +52,7 @@ MTH_OPTIONS_TABS = MTH_OPTIONS_TABS or { { key = "ChronometerClassSpells", label = "Hunter Spells", frame = "MetaHuntOptionsChronometer" }, { key = "ChronometerClassEvents", label = "Hunter Events", frame = "MetaHuntOptionsChronometer" }, { key = "ChronometerRacial", label = "Racial", frame = "MetaHuntOptionsChronometer" }, + { key = "Animations", label = "Animations", frame = "MetaHuntOptionsAnimations" }, { key = "Credits", label = "Credits", frame = "MetaHuntOptionsCredits" }, } @@ -92,6 +93,7 @@ MTH_OPTIONS_TREE = MTH_OPTIONS_TREE or { { label = "Racial", key = "ChronometerRacial" }, }, }, + { label = "Animations", key = "Animations" }, { label = "Credits", key = "Credits" }, } diff --git a/api/options.xml b/api/options.xml index 041aa9d..e3df266 100644 --- a/api/options.xml +++ b/api/options.xml @@ -85,6 +85,7 @@ + diff --git a/init/api.xml b/init/api.xml index c964985..9387315 100644 --- a/init/api.xml +++ b/init/api.xml @@ -1,7 +1,9 @@ + + @@ -14,6 +16,7 @@ + From d2fb04f7c514c1e04f54ccfc34da8435fb538d15 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 16:05:36 +0200 Subject: [PATCH 11/42] Animations: add option-tab fade, minimap pulse, zButton expand fade, map-pin drop-in; wire BeastTraining debug log to /mth err --- CHANGELOG.md | 2 +- api/core-scan-beasttraining.lua | 15 ++++++++++++++- api/fx-celebrate.lua | 16 ++++++++++++++++ api/map-markers.lua | 10 ++++++++++ api/minimap-button.lua | 14 ++++++++++++++ api/options-shell.lua | 18 ++++++++++++++++++ modules/zhunter/ZSpellButtonTemplate.lua | 18 ++++++++++++++++++ 7 files changed, 91 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03852b7..949e830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Resurrecting on Octowow. ### Added -- **Animations**: MetaHunt now plays optional animations that make the interface feel alive. A firework-and-banner celebration bursts on screen when your pet learns a new ability or a new rank, the Great Book slides up and fades in when you open it and gently fades its list when you switch tabs, and the options window slides in when opened. Everything is optional and off-switchable: a new **Animations** tab (Options → Animations) lets you toggle each effect on its own — or disable them all at once. Powered by the PizzaSauce animation library. +- **Animations**: MetaHunt now plays optional animations that make the interface feel alive. A firework-and-banner celebration bursts on screen when your pet learns a new ability or a new rank, the Great Book slides up and fades in when you open it and gently fades its list when you switch tabs, the options window slides in when opened and its tabs fade as you switch them, the minimap button pulses when clicked, collapsible zButton bars fade their buttons in as they expand, and world-map markers fade in when you locate a beast, vendor or master. Everything is optional and off-switchable: a new **Animations** tab (Options → Animations) lets you toggle each effect on its own — or disable them all at once. Powered by the PizzaSauce animation library. - **zTrack — Find Fish**: Added new spell Find Fish to the zTrack list of trackings. diff --git a/api/core-scan-beasttraining.lua b/api/core-scan-beasttraining.lua index 11586a6..7fb930f 100644 --- a/api/core-scan-beasttraining.lua +++ b/api/core-scan-beasttraining.lua @@ -168,7 +168,10 @@ function MTH_PT_HasBeastTrainingRows() end local function MTH_PT_DebugLog(line) - return + if line == nil then return end + if MTH_DebugFrame and type(MTH_DebugFrame.AddInfo) == "function" then + MTH_DebugFrame:AddInfo("[BeastTraining] " .. tostring(line)) + end end local function MTH_PT_IsScanMessageEnabled() @@ -381,6 +384,10 @@ local function MTH_PT_ProcessLearnSystemMessage(rawMessage) _, _, abilityName, rankText = string.find(message, "^[Yy]ou have learned a new spell:%s*(.-)%s*[Rr]ank%s*(%d+)%.?$") end if not abilityName or not rankText then + -- Only trace messages that look like a learn line, to avoid flooding the log. + if MTH_PT_DebugLog and string.find(message, "[Ll]earned") then + MTH_PT_DebugLog("learn-msg parse FAILED: '" .. tostring(message) .. "'") + end return false end @@ -412,6 +419,12 @@ local function MTH_PT_ProcessLearnSystemMessage(rawMessage) petRecorded = MTH_PETS_RecordCurrentPetLearnedAbility(abilityName, rankNumber, "beasttraining:learn-msg") and true or false end + if MTH_PT_DebugLog then + MTH_PT_DebugLog("learn-msg matched: ability='" .. tostring(abilityName) .. "' rank=" .. tostring(rankNumber) + .. " token='" .. tostring(token) .. "' alreadyKnown=" .. tostring(alreadyKnown) + .. " changed=" .. tostring(changed) .. " petRecorded=" .. tostring(petRecorded)) + end + if changed or petRecorded then if MTH_PT_DebugLog then MTH_PT_DebugLog("learn-msg captured: source='you have learned a new spell' ability='" .. tostring(abilityName) .. "' rank=" .. tostring(rankNumber) .. " token='" .. tostring(token) .. "' changed=" .. tostring(changed) .. " petRecorded=" .. tostring(petRecorded)) diff --git a/api/fx-celebrate.lua b/api/fx-celebrate.lua index 9cda041..ee6af0f 100644 --- a/api/fx-celebrate.lua +++ b/api/fx-celebrate.lua @@ -58,6 +58,22 @@ MTH_FX_ANIM_DEFS = { label = "Slide the options window open", labelKey = "ANIM_UI_OPTIONS", tooltip = "Slide and fade the MetaHunt options window in when you open it.", tooltipKey = "ANIM_UI_OPTIONS_TIP" }, + { key = "ui.optiontabs", section = "ui", default = true, + label = "Fade options tab changes", labelKey = "ANIM_UI_OPTIONTABS", + tooltip = "Fade the settings panel when you switch tabs inside the options window.", + tooltipKey = "ANIM_UI_OPTIONTABS_TIP" }, + { key = "ui.minimap", section = "ui", default = true, + label = "Pulse the minimap button on click", labelKey = "ANIM_UI_MINIMAP", + tooltip = "Give the minimap button a quick pulse when you click it.", + tooltipKey = "ANIM_UI_MINIMAP_TIP" }, + { key = "ui.zbuttons", section = "ui", default = true, + label = "Fade zButton bars open", labelKey = "ANIM_UI_ZBUTTONS", + tooltip = "Fade the child buttons in when a collapsible zButton bar (pet, tracking, aspects...) expands.", + tooltipKey = "ANIM_UI_ZBUTTONS_TIP" }, + { key = "ui.mappins", section = "ui", default = true, + label = "Fade map markers in", labelKey = "ANIM_UI_MAPPINS", + tooltip = "Fade the world map markers in when you locate a beast, vendor or master.", + tooltipKey = "ANIM_UI_MAPPINS_TIP" }, } local function MTH_FX_Store() diff --git a/api/map-markers.lua b/api/map-markers.lua index 965069e..9d51619 100644 --- a/api/map-markers.lua +++ b/api/map-markers.lua @@ -29,6 +29,15 @@ MTH_Map = { minimapTickIdle = 0.75, } +-- Capture our PizzaSauce instance for the world map pin drop-in (guarded; may be nil). +local MTH_MAP_Sauce = PizzaSauce +local function MTH_Map_AnimatePin(pin) + if not MTH_MAP_Sauce or not pin then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.mappins") then return end + if type(pin.SetAlpha) == "function" then pin:SetAlpha(0) end + MTH_MAP_Sauce:FadeIn(pin, 0.3) +end + local MTH_MAP_MINIMAP_ZOOM = { [0] = { [0] = 300, [1] = 240, [2] = 180, [3] = 120, [4] = 80, [5] = 50 }, [1] = { [0] = 466 + 2/3, [1] = 400, [2] = 333 + 1/3, [3] = 266 + 2/6, [4] = 200, [5] = 133 + 1/3 }, @@ -1041,6 +1050,7 @@ function MTH_Map:UpdateWorldMap() pin:ClearAllPoints() pin:SetPoint("CENTER", worldButton, "TOPLEFT", x, -y) pin:Show() + MTH_Map_AnimatePin(pin) end end diff --git a/api/minimap-button.lua b/api/minimap-button.lua index 26eed87..9ecee50 100644 --- a/api/minimap-button.lua +++ b/api/minimap-button.lua @@ -14,6 +14,19 @@ if not MTH_MinimapButton then MTH_MinimapButton = {} end +-- Capture our PizzaSauce instance for the click pulse (guarded; may be nil). +local MTH_MB_Sauce = PizzaSauce +local function MTH_MB_AnimateClick() + if not MTH_MB_Sauce then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.minimap") then return end + local icon = MTH_MinimapButton and MTH_MinimapButton.icon + if not icon or type(icon.SetWidth) ~= "function" then return end + -- Reset to base size first so rapid clicks can't make the icon drift in size. + icon:SetWidth(20) + icon:SetHeight(20) + MTH_MB_Sauce:Pulse(icon, 1.35, 0.28) +end + local function MTH_MB_L(key, default) if MTH and MTH.GetLocalization then return MTH:GetLocalization(key, default) @@ -164,6 +177,7 @@ function MTH_MinimapButton:Initialize() button:SetScript("OnClick", function() local mouseButton = arg1 + MTH_MB_AnimateClick() if mouseButton == "RightButton" then if type(MTH_OpenOptions) == "function" then MTH_OpenOptions("General") diff --git a/api/options-shell.lua b/api/options-shell.lua index 1a12831..1b27bbb 100644 --- a/api/options-shell.lua +++ b/api/options-shell.lua @@ -30,6 +30,23 @@ local function MTH_OPT_AnimateOpen(frame) MTH_OPT_Sauce:SlideIn(frame, "UP", 60, 0.4, "outCubic") end +-- Fade the active settings panel when switching options tabs, unless disabled. +local function MTH_OPT_AnimateTabContent(tabKey) + if not MTH_OPT_Sauce then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.optiontabs") then return end + if type(MTH_OPTIONS_TABS) ~= "table" then return end + for i = 1, table.getn(MTH_OPTIONS_TABS) do + if MTH_OPTIONS_TABS[i].key == tabKey then + local frame = MTH_GetFrame(MTH_OPTIONS_TABS[i].frame) + if frame then + if type(frame.SetAlpha) == "function" then frame:SetAlpha(0) end + MTH_OPT_Sauce:FadeIn(frame, 0.22) + end + return + end + end +end + local function MTH_RegisterEscClose(frameName) if not (frameName and UISpecialFrames) then return @@ -277,6 +294,7 @@ function MTH_SelectOptionsTab(tabKey) end end end + MTH_OPT_AnimateTabContent(tabKey) end function MTH_ResetAndSelectOptionsTab(tabKey) diff --git a/modules/zhunter/ZSpellButtonTemplate.lua b/modules/zhunter/ZSpellButtonTemplate.lua index e5b5ca0..4072ff5 100644 --- a/modules/zhunter/ZSpellButtonTemplate.lua +++ b/modules/zhunter/ZSpellButtonTemplate.lua @@ -1,5 +1,15 @@ -- MAX_SPELLS = 1024 +-- Capture our PizzaSauce instance for the expand animation (guarded; may be nil). +local ZSB_Sauce = PizzaSauce +local function ZSpellButton_AnimateExpand(parent) + if not ZSB_Sauce or not parent or not parent.children then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.zbuttons") then return end + -- Hide alpha this frame so there is no one-frame opaque flash before onStart. + if type(parent.children.SetAlpha) == "function" then parent.children:SetAlpha(0) end + ZSB_Sauce:FadeIn(parent.children, 0.2) +end + -- Key binding prefix mapping (button name → Bindings.xml binding prefix) local ZHUNTER_BINDING_PREFIX = { zButtonAspect = "ZAspect", @@ -378,6 +388,7 @@ function ZSpellButton_SetChildrenExpanded(parent, expanded) end if expanded then saved["children"]["expanded"] = 1 + local wasCollapsed = parent.children and not parent.children:IsShown() if parent.children then parent.children:Show() end @@ -394,6 +405,11 @@ function ZSpellButton_SetChildrenExpanded(parent, expanded) end end ZSpellButton_StartFadeTimer(parent) + -- Animate only on a genuine collapsed->expanded toggle, and never on the + -- very first apply at login (guarded by _zbAnimReady, set in ApplyChildrenExpanded). + if wasCollapsed and parent._zbAnimReady then + ZSpellButton_AnimateExpand(parent) + end else saved["children"]["expanded"] = 0 if parent.children then @@ -417,6 +433,8 @@ function ZSpellButton_ApplyChildrenExpanded(parent) end ZSpellButton_SetChildrenExpanded(parent, ZSpellButton_GetChildrenExpanded(parent)) ZSpellButton_UpdateKeybindingText(parent) + -- Mark ready so future user toggles animate, but the initial login apply above does not. + parent._zbAnimReady = true end -- ========== Fade Timer ========== From 66ff1ae7115948fd6dddbaf45171612623a21cba Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 16:38:14 +0200 Subject: [PATCH 12/42] Animations: wire remaining effects + 2-column options panel - Shared FX toolkit in fx-celebrate.lua: mth_shake tween type, MTH_FX_Flash glow helper, MTH_CelebrateRunaway red shaking banner, gold title reset. - pet.runaway, pet.happiness, pet.loyaltyup, pet.feed - combat.ammoswap, combat.rangedswap, combat.mmprocs - ui.zbar, ui.stable, ui.minimapping, ui.optionsclose - book.model, book.search - Options > Animations panel now uses a balanced two-column layout. - CHANGELOG Animations entry expanded (no version bump). --- CHANGELOG.md | 2 +- api/core-feed-tracking.lua | 7 ++ api/core-scan-stablemaster.lua | 3 + api/fx-celebrate.lua | 144 ++++++++++++++++++++++++++++++++ api/hunterbook-model-viewer.lua | 14 ++++ api/hunterbook-tab-stable.lua | 11 +++ api/hunterbook.lua | 33 +++++++- api/map-markers.lua | 19 +++++ api/options-animations.lua | 26 ++++-- api/options-shell.lua | 20 ++++- modules/expammo/engine.lua | 9 ++ modules/smartammo/engine.lua | 15 ++++ modules/zhunter/zBar.lua | 23 +++++ modules/zhunter/zButtonPet.lua | 28 +++++++ 14 files changed, 343 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 949e830..6c2cce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Resurrecting on Octowow. ### Added -- **Animations**: MetaHunt now plays optional animations that make the interface feel alive. A firework-and-banner celebration bursts on screen when your pet learns a new ability or a new rank, the Great Book slides up and fades in when you open it and gently fades its list when you switch tabs, the options window slides in when opened and its tabs fade as you switch them, the minimap button pulses when clicked, collapsible zButton bars fade their buttons in as they expand, and world-map markers fade in when you locate a beast, vendor or master. Everything is optional and off-switchable: a new **Animations** tab (Options → Animations) lets you toggle each effect on its own — or disable them all at once. Powered by the PizzaSauce animation library. +- **Animations**: MetaHunt now plays optional animations that make the interface feel alive. A firework-and-banner celebration bursts on screen when your pet learns a new ability or a new rank, and a red shaking banner warns you when a pet runs away for good. Your pet button flashes when its happiness changes, it gains a loyalty level, or you feed it; the ammo button flashes when Smart Ammo swaps your bullets or auto-equips after a weapon swap; and the MM Widget pulses on a Lock & Load proc. The Great Book slides up and fades in when you open it, gently fades its list when you switch tabs or run a search, fades beast models into view, and fades out when closed; stable slots flash when a pet is deposited, withdrawn or swapped. The options window slides in when opened, fades its tabs as you switch them, and fades out when closed. The minimap button pulses when clicked, minimap markers ping as beasts come into range, collapsible zButton bars fade their buttons in as they expand, the zBar fades in when enabled, and world-map markers fade in when you locate a beast, vendor or master. Everything is optional and off-switchable: a new **Animations** tab (Options → Animations) lets you toggle each effect on its own — or disable them all at once. Powered by the PizzaSauce animation library. - **zTrack — Find Fish**: Added new spell Find Fish to the zTrack list of trackings. diff --git a/api/core-feed-tracking.lua b/api/core-feed-tracking.lua index 565831e..95871df 100644 --- a/api/core-feed-tracking.lua +++ b/api/core-feed-tracking.lua @@ -1385,6 +1385,13 @@ function MTH_FEED_FinalizeAttempt(attemptId, payload) if petKey ~= "" then MTH_FEED_Runtime.sessionFeedsByPetId[petKey] = (tonumber(MTH_FEED_Runtime.sessionFeedsByPetId[petKey]) or 0) + 1 end + if type(MTH_FX_Flash) == "function" and type(MTH_FX_IsEnabled) == "function" + and MTH_FX_IsEnabled("pet.feed") then + local target = getglobal("zButtonPet") + if target then + MTH_FX_Flash(target, 0.35, 1.00, 0.35, 0.5) + end + end else petRow.totals.rejected = petRow.totals.rejected + 1 if reason == "no-buff" then diff --git a/api/core-scan-stablemaster.lua b/api/core-scan-stablemaster.lua index 45b4bf1..efd50aa 100644 --- a/api/core-scan-stablemaster.lua +++ b/api/core-scan-stablemaster.lua @@ -2791,6 +2791,9 @@ function MTH_PETS_RecordPetRunaway(source, rawMessage) if MTH:IsMessageEnabled("petRanAway", true) then MTH:Print("YOUR PET " .. tostring(displayName) .. " HAS RUNAWAY FOREVER :-( Try to feed it better next time.") end + if type(MTH_CelebrateRunaway) == "function" then + MTH_CelebrateRunaway(displayName) + end end return true diff --git a/api/fx-celebrate.lua b/api/fx-celebrate.lua index ee6af0f..3649c85 100644 --- a/api/fx-celebrate.lua +++ b/api/fx-celebrate.lua @@ -33,6 +33,7 @@ MTH_FX_DBG = { -- Sections shown, in order, in the options panel. `titleKey` localizes `title`. MTH_FX_ANIM_SECTIONS = { { id = "pet", title = "Pet", titleKey = "ANIM_SECTION_PET" }, + { id = "combat", title = "Combat", titleKey = "ANIM_SECTION_COMBAT" }, { id = "book", title = "Book", titleKey = "ANIM_SECTION_BOOK" }, { id = "ui", title = "Interface", titleKey = "ANIM_SECTION_UI" }, } @@ -46,6 +47,34 @@ MTH_FX_ANIM_DEFS = { label = "Celebrate new pet abilities", labelKey = "ANIM_PET_CELEBRATE", tooltip = "Play a firework and a banner in the middle of the screen when your pet learns a new ability or a new rank.", tooltipKey = "ANIM_PET_CELEBRATE_TIP" }, + { key = "pet.runaway", section = "pet", default = true, + label = "Warn when a pet runs away", labelKey = "ANIM_PET_RUNAWAY", + tooltip = "Flash a red, shaking banner in the middle of the screen when one of your pets runs away for good.", + tooltipKey = "ANIM_PET_RUNAWAY_TIP" }, + { key = "pet.happiness", section = "pet", default = true, + label = "Flash on happiness change", labelKey = "ANIM_PET_HAPPINESS", + tooltip = "Briefly flash the pet happiness area green when your pet cheers up, or red when it gets unhappy.", + tooltipKey = "ANIM_PET_HAPPINESS_TIP" }, + { key = "pet.loyaltyup", section = "pet", default = true, + label = "Glow on loyalty gain", labelKey = "ANIM_PET_LOYALTYUP", + tooltip = "Give a short golden glow when your pet gains a loyalty level.", + tooltipKey = "ANIM_PET_LOYALTYUP_TIP" }, + { key = "pet.feed", section = "pet", default = true, + label = "Flash when feeding your pet", labelKey = "ANIM_PET_FEED", + tooltip = "Give a short green flash when Feed-O-Matic feeds your pet.", + tooltipKey = "ANIM_PET_FEED_TIP" }, + { key = "combat.ammoswap", section = "combat", default = true, + label = "Flash on Smart Ammo swap", labelKey = "ANIM_COMBAT_AMMOSWAP", + tooltip = "Briefly highlight the zAmmo button when Smart Ammo swaps your ammo.", + tooltipKey = "ANIM_COMBAT_AMMOSWAP_TIP" }, + { key = "combat.rangedswap", section = "combat", default = true, + label = "Flash on ranged weapon swap", labelKey = "ANIM_COMBAT_RANGEDSWAP", + tooltip = "Briefly highlight the zRanged button when you hot-swap a ranged weapon.", + tooltipKey = "ANIM_COMBAT_RANGEDSWAP_TIP" }, + { key = "combat.mmprocs", section = "combat", default = true, + label = "Pulse MM Widget procs", labelKey = "ANIM_COMBAT_MMPROCS", + tooltip = "Pulse an MM Widget cell when a Lock and Load or Experimental ammo proc lights up.", + tooltipKey = "ANIM_COMBAT_MMPROCS_TIP" }, { key = "book.open", section = "book", default = true, label = "Fade in the Hunter's Book", labelKey = "ANIM_BOOK_OPEN", tooltip = "Fade the Hunter's Book window in when you open it.", @@ -54,6 +83,14 @@ MTH_FX_ANIM_DEFS = { label = "Fade Hunter's Book tab changes", labelKey = "ANIM_BOOK_TABS", tooltip = "Fade the results list back in when you switch tabs inside the Hunter's Book.", tooltipKey = "ANIM_BOOK_TABS_TIP" }, + { key = "book.model", section = "book", default = true, + label = "Fade in beast models", labelKey = "ANIM_BOOK_MODEL", + tooltip = "Fade the beast model viewer in when it loads a new model.", + tooltipKey = "ANIM_BOOK_MODEL_TIP" }, + { key = "book.search", section = "book", default = true, + label = "Fade search results", labelKey = "ANIM_BOOK_SEARCH", + tooltip = "Fade the results list when your search filter changes.", + tooltipKey = "ANIM_BOOK_SEARCH_TIP" }, { key = "ui.options", section = "ui", default = true, label = "Slide the options window open", labelKey = "ANIM_UI_OPTIONS", tooltip = "Slide and fade the MetaHunt options window in when you open it.", @@ -62,14 +99,30 @@ MTH_FX_ANIM_DEFS = { label = "Fade options tab changes", labelKey = "ANIM_UI_OPTIONTABS", tooltip = "Fade the settings panel when you switch tabs inside the options window.", tooltipKey = "ANIM_UI_OPTIONTABS_TIP" }, + { key = "ui.optionsclose", section = "ui", default = true, + label = "Fade windows closed", labelKey = "ANIM_UI_OPTIONSCLOSE", + tooltip = "Fade the options and Hunter's Book windows out when you close them.", + tooltipKey = "ANIM_UI_OPTIONSCLOSE_TIP" }, { key = "ui.minimap", section = "ui", default = true, label = "Pulse the minimap button on click", labelKey = "ANIM_UI_MINIMAP", tooltip = "Give the minimap button a quick pulse when you click it.", tooltipKey = "ANIM_UI_MINIMAP_TIP" }, + { key = "ui.minimapping", section = "ui", default = true, + label = "Ping the minimap on locate", labelKey = "ANIM_UI_MINIMAPPING", + tooltip = "Send a short ping ripple over the minimap when you locate a tracked NPC.", + tooltipKey = "ANIM_UI_MINIMAPPING_TIP" }, { key = "ui.zbuttons", section = "ui", default = true, label = "Fade zButton bars open", labelKey = "ANIM_UI_ZBUTTONS", tooltip = "Fade the child buttons in when a collapsible zButton bar (pet, tracking, aspects...) expands.", tooltipKey = "ANIM_UI_ZBUTTONS_TIP" }, + { key = "ui.zbar", section = "ui", default = true, + label = "Fade the zBar in", labelKey = "ANIM_UI_ZBAR", + tooltip = "Fade the unified zBar buttons in when the bar becomes active.", + tooltipKey = "ANIM_UI_ZBAR_TIP" }, + { key = "ui.stable", section = "ui", default = true, + label = "Flash stable slots on change", labelKey = "ANIM_UI_STABLE", + tooltip = "Flash a stable slot in the Hunter's Book when a pet is deposited or withdrawn.", + tooltipKey = "ANIM_UI_STABLE_TIP" }, { key = "ui.mappins", section = "ui", default = true, label = "Fade map markers in", labelKey = "ANIM_UI_MAPPINS", tooltip = "Fade the world map markers in when you locate a beast, vendor or master.", @@ -166,6 +219,63 @@ else end MTH_FX_DBG.step = "3-after-registertype" +-- Register a decaying "shake" tween type: oscillates the frame around its own +-- current anchor point, with the amplitude fading to zero over the tween so it +-- always lands back exactly where it started. Used for negative events. +if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasShake then + local ok, err = pcall(function() + Sauce.MTH_HasShake = true + Sauce:RegisterType("mth_shake", { + init = function(target) + local p, rel, rp, x, y = target:GetPoint(1) + return { p or "CENTER", rel, rp or p or "CENTER", x or 0, y or 0 } + end, + apply = function(target, from, to, t, tween) + local mag = tween.magnitude or 8 + local freq = tween.freq or 34 + local decay = 1 - t + local offset = math.sin(t * freq) * mag * decay + target:ClearAllPoints() + target:SetPoint(from[1], from[2], from[3], (from[4] or 0) + offset, from[5] or 0) + end, + }) + end) + MTH_FX_DBG.shakeOk = ok + MTH_FX_DBG.shakeErr = err +end + +-- Reusable flash: overlays a short additive colour glow on any frame/button and +-- fades it out. Safe on any frame that supports CreateTexture; silent no-op +-- otherwise. The overlay texture is cached on the frame for reuse. +function MTH_FX_Flash(frame, r, g, b, duration) + if not Sauce or not frame then + return + end + if type(frame.CreateTexture) ~= "function" then + return + end + local tex = frame.mthFxFlash + if not tex then + tex = frame:CreateTexture(nil, "OVERLAY") + tex:SetTexture("Interface\\Buttons\\CheckButtonHilight") + tex:SetBlendMode("ADD") + tex:SetAllPoints(frame) + frame.mthFxFlash = tex + end + tex:SetVertexColor(r or 1, g or 1, b or 1) + tex:SetAlpha(0) + tex:Show() + local dur = duration or 0.45 + Sauce:Stop(tex) + Sauce:Sequence({ + Sauce:FadeTo(tex, 0.85, dur * 0.35), + Sauce:FadeTo(tex, 0, dur * 0.65, "inQuad", { + onFinish = function() tex:Hide() end, + }), + }) +end + + local MTH_FX_NUM_SPARKS = 22 local MTH_FX_Container local MTH_FX_MsgFrame @@ -240,6 +350,7 @@ function MTH_Celebrate(title, subtitle) return end + MTH_FX_TitleFS:SetTextColor(1, 0.82, 0) MTH_FX_TitleFS:SetText(title or "") MTH_FX_SubFS:SetText(subtitle or "") @@ -305,6 +416,39 @@ function MTH_CelebratePetAbility(label) MTH_Celebrate(MTH_FX_L("ANIM_PET_LEARNED_TITLE", "New Pet Ability!"), label) end +-- Dramatic negative banner: a red, shaking message shown when a pet runs away +-- for good. Gated on pet.runaway. Reuses the shared message frame. +function MTH_CelebrateRunaway(petName) + if not MTH_FX_IsEnabled("pet.runaway") then + return + end + if not Sauce then + return + end + if not MTH_FX_EnsureFrames() then + return + end + + MTH_FX_TitleFS:SetTextColor(1, 0.15, 0.15) + MTH_FX_TitleFS:SetText(MTH_FX_L("ANIM_PET_RUNAWAY_TITLE", "Your pet ran away!")) + MTH_FX_SubFS:SetText(petName or "") + + Sauce:Stop(MTH_FX_MsgFrame) + MTH_FX_MsgFrame:SetAlpha(0) + MTH_FX_MsgFrame:Show() + Sauce:Sequence({ + Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.18), + Sauce:Tween(MTH_FX_MsgFrame, { + type = "mth_shake", + duration = 0.55, + easing = "linear", + magnitude = 11, + freq = 42, + }), + Sauce:FadeOut(MTH_FX_MsgFrame, 0.7, "inQuad", { delay = 1.6 }), + }) +end + -- DEBUG: mark that the whole file finished loading and record which globals it -- successfully defined. TEMP diagnostic. MTH_FX_DBG.step = "4-complete" diff --git a/api/hunterbook-model-viewer.lua b/api/hunterbook-model-viewer.lua index ef0ace4..644f6d9 100644 --- a/api/hunterbook-model-viewer.lua +++ b/api/hunterbook-model-viewer.lua @@ -6,6 +6,18 @@ -- Uses MTH_DS_BeastSkins[familyName] generated by .tools/generators/gen_beast_skins.py -- Skin rendering: PlayerModel:SetUnit(CreatureDisplayInfo.ID) +local MTH_MV_Sauce = PizzaSauce + +-- Fade a model frame in when a new skin is displayed, unless disabled. +local function MTH_MV_AnimateModel(model) + if not MTH_MV_Sauce or not model then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("book.model") then return end + if type(model.SetAlpha) ~= "function" then return end + if not model:IsShown() then return end + model:SetAlpha(0) + MTH_MV_Sauce:FadeTo(model, 1, 0.30, "outQuad") +end + local MTH_MV = { inited = false, family = nil, @@ -51,6 +63,8 @@ local function MTH_MV_DisplayCurrent() MTH_MV_ApplyModel(MTH_MV.previewModel, skin.id) MTH_MV_ApplyModel(MTH_MV.popupModel, skin.id) + MTH_MV_AnimateModel(MTH_MV.previewModel) + MTH_MV_AnimateModel(MTH_MV.popupModel) local indexStr = tostring(MTH_MV.index) .. " / " .. tostring(n) if MTH_MV.popupSkinLabel then diff --git a/api/hunterbook-tab-stable.lua b/api/hunterbook-tab-stable.lua index 7ba3618..2a9284e 100644 --- a/api/hunterbook-tab-stable.lua +++ b/api/hunterbook-tab-stable.lua @@ -1245,6 +1245,17 @@ function MTH_BOOKTAB_RenderStableCards() local row = slotEntry and slotEntry.row or nil MTH_BOOK_DebugTrace("Render card#" .. tostring(i) .. " petId=" .. tostring(petId) .. " hasRow=" .. tostring(type(row) == "table")) + -- Animations: flash the slot when its occupant changes (deposit / + -- withdraw / swap), but never on the first render of this card. + if card._stableInit and card._lastStablePetId ~= petId then + if type(MTH_FX_Flash) == "function" and type(MTH_FX_IsEnabled) == "function" + and MTH_FX_IsEnabled("ui.stable") then + MTH_FX_Flash(card, 1.00, 0.82, 0.20, 0.55) + end + end + card._lastStablePetId = petId + card._stableInit = true + card:ClearAllPoints() card:SetPoint("TOPLEFT", ui.frame, "TOPLEFT", 8, y) card:SetPoint("TOPRIGHT", ui.frame, "TOPRIGHT", -8, y) diff --git a/api/hunterbook.lua b/api/hunterbook.lua index f531778..7b713fd 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -29,6 +29,34 @@ local function MTH_BOOK_AnimateTabSwitch() Sauce:FadeIn(list, 0.25) end +-- Fade the book window out before hiding it, unless disabled (shares the +-- "fade windows closed" toggle with the options window). +local function MTH_BOOK_FadeOutWindow(frame) + if not frame then return end + if not Sauce + or (type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.optionsclose")) then + if type(frame.SetAlpha) == "function" then frame:SetAlpha(1) end + frame:Hide() + return + end + Sauce:FadeOut(frame, 0.22, "inQuad", { + onFinish = function() + frame:Hide() + if type(frame.SetAlpha) == "function" then frame:SetAlpha(1) end + end, + }) +end + +-- Fade the results list back in after a search/filter is applied, unless disabled. +local function MTH_BOOK_AnimateSearchResults() + if not Sauce then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("book.search") then return end + local list = getglobal("MTH_BOOK_ListBackdrop") + if not list then return end + if type(list.SetAlpha) == "function" then list:SetAlpha(0) end + Sauce:FadeIn(list, 0.22) +end + MTH_HUNTERBOOK_LOADED = true MTH_HUNTERBOOK_TABS = MTH_HUNTERBOOK_TABS or {} @@ -4719,6 +4747,7 @@ local function MTH_BOOK_ApplyInputs() MTH_BOOK_STATE.selectedPetRankEntry = nil MTH_BOOK_STATE.petRankRows = {} MTH_BOOK_RefreshFilter() + MTH_BOOK_AnimateSearchResults() end local function MTH_BOOK_SetQuickFilter(index) @@ -5301,7 +5330,7 @@ local function MTH_BOOK_WireUI(frame) closeButton:SetWidth(22) closeButton:SetHeight(22) closeButton:SetText("X") - closeButton:SetScript("OnClick", function() this:GetParent():Hide() end) + closeButton:SetScript("OnClick", function() MTH_BOOK_FadeOutWindow(this:GetParent()) end) end if applyButton then applyButton:SetScript("OnClick", MTH_BOOK_ApplyInputs) end @@ -5752,7 +5781,7 @@ function MTH_ToggleHunterBook() local frame = MTH_BOOK_EnsureWindow() if not frame then return end if frame:IsShown() then - frame:Hide() + MTH_BOOK_FadeOutWindow(frame) else frame:Show() MTH_BOOK_FadeInWindow(frame) diff --git a/api/map-markers.lua b/api/map-markers.lua index 9d51619..778a5cd 100644 --- a/api/map-markers.lua +++ b/api/map-markers.lua @@ -38,6 +38,15 @@ local function MTH_Map_AnimatePin(pin) MTH_MAP_Sauce:FadeIn(pin, 0.3) end +-- Ping a minimap pin that just came into range. Only called for genuinely new +-- nodes (see UpdateMinimap), never every tick, to avoid strobing. +local function MTH_Map_PingMinimapPin(pin) + if not MTH_MAP_Sauce or not pin then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.minimapping") then return end + if type(pin.SetAlpha) == "function" then pin:SetAlpha(0) end + MTH_MAP_Sauce:FadeIn(pin, 0.35) +end + local MTH_MAP_MINIMAP_ZOOM = { [0] = { [0] = 300, [1] = 240, [2] = 180, [3] = 120, [4] = 80, [5] = 50 }, [1] = { [0] = 466 + 2/3, [1] = 400, [2] = 333 + 1/3, [3] = 266 + 2/6, [4] = 200, [5] = 133 + 1/3 }, @@ -948,6 +957,7 @@ function MTH_Map:HideMinimapPins() for i = 1, table.getn(self.minimapPins) do if self.minimapPins[i] then self.minimapPins[i]:Hide() end end + self._miniShownSet = nil end function MTH_Map:HideAllPins() @@ -1161,6 +1171,8 @@ function MTH_Map:UpdateMinimap() local xDraw = Minimap:GetWidth() / xScale / 100 local yDraw = Minimap:GetHeight() / yScale / 100 local shown = 0 + local prevShown = self._miniShownSet or {} + local nowShown = {} for i = 1, table.getn(nodes) do local node = nodes[i] @@ -1182,9 +1194,16 @@ function MTH_Map:UpdateMinimap() pin:ClearAllPoints() pin:SetPoint("CENTER", Minimap, "CENTER", xPos, -yPos) pin:Show() + + local nodeKey = tostring(node.name or "") .. "@" .. tostring(node.x) .. "," .. tostring(node.y) + nowShown[nodeKey] = true + if not prevShown[nodeKey] then + MTH_Map_PingMinimapPin(pin) + end end end end + self._miniShownSet = nowShown for i = shown + 1, table.getn(self.minimapPins) do if self.minimapPins[i] then self.minimapPins[i]:Hide() end diff --git a/api/options-animations.lua b/api/options-animations.lua index f36fd77..9513d7a 100644 --- a/api/options-animations.lua +++ b/api/options-animations.lua @@ -120,25 +120,37 @@ function MTH_SetupAnimationsOptions() end) end - -- Per-animation checkboxes, grouped by section. - local y = -104 + -- Per-animation checkboxes, grouped by section, laid out in two columns. + -- Each whole section is placed into whichever column is currently shorter + -- (largest remaining Y), so the two columns stay balanced as defs grow. local rowStep = 26 local sectionGap = 12 + local columns = { + { headerX = 15, cbX = 28, y = -104 }, + { headerX = 300, cbX = 313, y = -104 }, + } local s = 1 while MTH_FX_ANIM_SECTIONS and s <= table.getn(MTH_FX_ANIM_SECTIONS) do local sec = MTH_FX_ANIM_SECTIONS[s] + + -- Pick the shorter column (the one whose cursor is nearest the top). + local col = columns[1] + if columns[2].y > columns[1].y then + col = columns[2] + end + local header = container:CreateFontString(nil, "ARTWORK", "GameFontNormal") - header:SetPoint("TOPLEFT", container, "TOPLEFT", 15, y) + header:SetPoint("TOPLEFT", container, "TOPLEFT", col.headerX, col.y) header:SetText(MTH_ANIM_L(sec.titleKey, sec.title)) header:SetTextColor(1.00, 0.82, 0.00) - y = y - 22 + col.y = col.y - 22 local d = 1 while MTH_FX_ANIM_DEFS and d <= table.getn(MTH_FX_ANIM_DEFS) do local def = MTH_FX_ANIM_DEFS[d] if def.section == sec.id then local cbName = "MetaHuntOptionsAnim_" .. string.gsub(def.key, "%.", "_") - local cb = MTH_CreateCheckbox(container, cbName, MTH_ANIM_L(def.labelKey, def.label), y, 28) + local cb = MTH_CreateCheckbox(container, cbName, MTH_ANIM_L(def.labelKey, def.label), col.y, col.cbX) if cb then cb.mthKey = def.key cb:SetChecked(MTH_FX_GetFlag(def.key)) @@ -155,12 +167,12 @@ function MTH_SetupAnimationsOptions() cb:SetScript("OnLeave", function() GameTooltip:Hide() end) end table.insert(MTH_ANIM_CTRL, cb) - y = y - rowStep + col.y = col.y - rowStep end end d = d + 1 end - y = y - sectionGap + col.y = col.y - sectionGap s = s + 1 end diff --git a/api/options-shell.lua b/api/options-shell.lua index 1b27bbb..ee2d9e8 100644 --- a/api/options-shell.lua +++ b/api/options-shell.lua @@ -47,6 +47,24 @@ local function MTH_OPT_AnimateTabContent(tabKey) end end +-- Fade the options window out before hiding it, unless disabled. +local function MTH_OPT_AnimateClose() + local frame = MetaHuntOptions + if not frame then return end + if not MTH_OPT_Sauce + or (type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.optionsclose")) then + frame:SetAlpha(1) + frame:Hide() + return + end + MTH_OPT_Sauce:FadeOut(frame, 0.22, "inQuad", { + onFinish = function() + frame:Hide() + frame:SetAlpha(1) + end, + }) +end + local function MTH_RegisterEscClose(frameName) if not (frameName and UISpecialFrames) then return @@ -103,7 +121,7 @@ local function MTH_EnsureOptionsFrame() closeBtn:SetWidth(MTH_OPTIONS_CONST.CLOSE_BTN_SIZE) closeBtn:SetHeight(MTH_OPTIONS_CONST.CLOSE_BTN_SIZE) closeBtn:SetText(MTH_OPT_L("COMMON_CLOSE_SHORT", "X")) - closeBtn:SetScript("OnClick", function() MetaHuntOptions:Hide() end) + closeBtn:SetScript("OnClick", function() MTH_OPT_AnimateClose() end) end MTH_BuildOptionsTree() diff --git a/modules/expammo/engine.lua b/modules/expammo/engine.lua index 777cf60..1ea62ee 100644 --- a/modules/expammo/engine.lua +++ b/modules/expammo/engine.lua @@ -403,6 +403,7 @@ local lnl = { expiresAt = 0, DURATION = 10, -- Lock and Load real duration (tooltip: "Lasts 10 sec or until Aimed Shot is cast") } +local MTH_EA_LnLWasActive = false -- for detecting the proc rising edge (animations) -- ── Confirmed icon paths (captured in-game 2026-03-21) ──────── -- Action-button spells (spellbook scan) @@ -700,10 +701,18 @@ MTH_EA_UpdateUI = function() cellTop.icon:SetVertexColor(1.00, 0.82, 0.00, 1) cellTop:SetBackdropBorderColor(1.00, 0.82, 0.00, 1) cellTopCD:SetText("|cffffffff" .. secs .. "|r") + if not MTH_EA_LnLWasActive then + if type(MTH_FX_Flash) == "function" and type(MTH_FX_IsEnabled) == "function" + and MTH_FX_IsEnabled("combat.mmprocs") then + MTH_FX_Flash(cellTop, 1.00, 0.82, 0.20, 0.55) + end + MTH_EA_LnLWasActive = true + end else cellTop.icon:SetVertexColor(0.28, 0.28, 0.28, 0.40) cellTop:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.35) cellTopCD:SetText("") + MTH_EA_LnLWasActive = false end end diff --git a/modules/smartammo/engine.lua b/modules/smartammo/engine.lua index 6d6f14f..bdb506a 100644 --- a/modules/smartammo/engine.lua +++ b/modules/smartammo/engine.lua @@ -221,6 +221,14 @@ local function MTHSmartAmmo_ShowFallbackWarning(previousAmmo, newAmmo) elseif UIErrorsFrame and UIErrorsFrame.AddMessage then UIErrorsFrame:AddMessage(warning, 1.0, 0.15, 0.15, 1.0) end + + if type(MTH_FX_Flash) == "function" and type(MTH_FX_IsEnabled) == "function" + and MTH_FX_IsEnabled("combat.ammoswap") then + local target = getglobal("zButtonAmmo") + if target then + MTH_FX_Flash(target, 1.00, 0.75, 0.20, 0.5) + end + end end local MTHSmartAmmo_FallbackCheckFrame = nil @@ -371,6 +379,13 @@ local function MTHSmartAmmo_HandleWeaponSwapAutoEquip(reason) if MTHSmartAmmo_EquipAmmoFromSlot(bestAmmo.bag, bestAmmo.slot) then MTH_SA_LastKnownEquippedName = bestAmmo.name MTHSmartAmmo_RefreshAmmoButtonDisplay() + if type(MTH_FX_Flash) == "function" and type(MTH_FX_IsEnabled) == "function" + and MTH_FX_IsEnabled("combat.rangedswap") then + local target = getglobal("zButtonAmmo") + if target then + MTH_FX_Flash(target, 0.30, 0.70, 1.00, 0.5) + end + end end end diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index 2579d7a..c39f5fe 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -5,6 +5,8 @@ local ZBAR_FRAME_NAME = "MTH_ZBar_Anchor" +local ZBar_Sauce = PizzaSauce + local ZBAR_ALL_BUTTONS = { "zButtonAspect", "zButtonAmmo", @@ -282,6 +284,26 @@ function MTH_ZBar_Release() end end +-- Animations: gentle staggered fade-in of the bar's buttons when it activates. +function MTH_ZBar_AnimateActivate() + if not ZBar_Sauce then return end + if type(MTH_FX_IsEnabled) ~= "function" or not MTH_FX_IsEnabled("ui.zbar") then + return + end + local s = MTH_ZBar_GetSaved() + local order = s.order or {} + for i = 1, table.getn(order) do + local buttonName = order[i] + if type(buttonName) == "string" then + local btn = getglobal(buttonName) + if btn and btn:IsShown() then + btn:SetAlpha(0) + ZBar_Sauce:FadeTo(btn, 1, 0.30, "outQuad", { delay = (i - 1) * 0.05 }) + end + end + end +end + -- Called when the user toggles the Enable checkbox in the options. function MTH_ZBar_SetEnabled(enabled) local s = MTH_ZBar_GetSaved() @@ -289,6 +311,7 @@ function MTH_ZBar_SetEnabled(enabled) if enabled then MTH_ZBar_EnsureAnchor() MTH_ZBar_ApplyLayout() + MTH_ZBar_AnimateActivate() else MTH_ZBar_Release() end diff --git a/modules/zhunter/zButtonPet.lua b/modules/zhunter/zButtonPet.lua index e70f24a..88d6a37 100644 --- a/modules/zhunter/zButtonPet.lua +++ b/modules/zhunter/zButtonPet.lua @@ -312,6 +312,8 @@ local function zButtonPet_GetCorePetInfo() return nil end +local zButtonPet_LastLoyalty = nil + local function zButtonPet_RefreshSavedPetState(saved, corePetInfo) local hasLivePet = false if type(UnitExists) == "function" then @@ -343,8 +345,34 @@ local function zButtonPet_RefreshSavedPetState(saved, corePetInfo) if happiness == nil and type(GetPetHappiness) == "function" then happiness = GetPetHappiness() end + local prevHappiness = saved["pet"]["happiness"] saved["pet"]["happiness"] = happiness + -- Animations: flash the pet button when happiness or loyalty changes. + if type(MTH_FX_Flash) == "function" and type(MTH_FX_IsEnabled) == "function" then + if MTH_FX_IsEnabled("pet.happiness") + and prevHappiness ~= nil and happiness ~= nil and happiness ~= prevHappiness then + local target = getglobal("zButtonPet") + if target then + if happiness > prevHappiness then + MTH_FX_Flash(target, 0.30, 1.00, 0.35, 0.5) + else + MTH_FX_Flash(target, 1.00, 0.30, 0.30, 0.5) + end + end + end + local loyalty = corePetInfo and tonumber(corePetInfo.loyaltyLevel) or nil + if MTH_FX_IsEnabled("pet.loyaltyup") and loyalty ~= nil then + if zButtonPet_LastLoyalty ~= nil and loyalty > zButtonPet_LastLoyalty then + local target = getglobal("zButtonPet") + if target then + MTH_FX_Flash(target, 1.00, 0.82, 0.20, 0.6) + end + end + zButtonPet_LastLoyalty = loyalty + end + end + if isDead then saved["pet"]["status"] = 1 elseif currentHealth and currentHealthMax and currentHealthMax > 0 then From 3233ebff7fe8ad9aeffba25ecf1ed49b07615e81 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 17:21:10 +0200 Subject: [PATCH 13/42] Strengthen subtle animations: pin scale-pops, window slide-in/out with overshoot, panel slides, zButton cascade, zBar springy stagger --- api/hunterbook.lua | 21 ++++++++++++++++----- api/map-markers.lua | 13 +++++++++++-- api/options-shell.lua | 19 +++++++++++++++---- modules/zhunter/ZSpellButtonTemplate.lua | 19 ++++++++++++++++--- modules/zhunter/zBar.lua | 4 +++- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/api/hunterbook.lua b/api/hunterbook.lua index 7b713fd..0576fff 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -15,8 +15,9 @@ local function MTH_BOOK_FadeInWindow(frame) -- Slide the whole window up into place while fading in. Position and alpha are -- the only safe properties on a complex frame: the built-in "scale" tween -- resizes width/height, which the child widgets do NOT follow (they clip), so - -- a slide + fade is the strongest effect that keeps the layout intact. - Sauce:SlideIn(frame, "UP", 60, 0.4, "outCubic") + -- a slide + fade is the strongest effect that keeps the layout intact. A + -- bigger travel + springy "outBack" overshoot makes the entrance pop. + Sauce:SlideIn(frame, "UP", 110, 0.5, "outBack") end -- Fade the results list back in when switching Hunter's Book tabs, unless disabled. @@ -26,7 +27,9 @@ local function MTH_BOOK_AnimateTabSwitch() local list = getglobal("MTH_BOOK_ListBackdrop") if not list then return end if type(list.SetAlpha) == "function" then list:SetAlpha(0) end - Sauce:FadeIn(list, 0.25) + -- Slide the list in from the right while fading so the tab change reads as + -- motion rather than a faint brightness bump. + Sauce:SlideIn(list, "RIGHT", 55, 0.3, "outCubic") end -- Fade the book window out before hiding it, unless disabled (shares the @@ -39,10 +42,17 @@ local function MTH_BOOK_FadeOutWindow(frame) frame:Hide() return end - Sauce:FadeOut(frame, 0.22, "inQuad", { + -- Capture the resting anchor so SlideOut's downward displacement can be + -- restored once the window is hidden (otherwise the next open is offset). + local p, rel, rp, x, y = frame:GetPoint(1) + Sauce:SlideOut(frame, "DOWN", 90, 0.26, "inCubic", { onFinish = function() frame:Hide() if type(frame.SetAlpha) == "function" then frame:SetAlpha(1) end + if p then + frame:ClearAllPoints() + frame:SetPoint(p, rel, rp, x, y) + end end, }) end @@ -54,7 +64,8 @@ local function MTH_BOOK_AnimateSearchResults() local list = getglobal("MTH_BOOK_ListBackdrop") if not list then return end if type(list.SetAlpha) == "function" then list:SetAlpha(0) end - Sauce:FadeIn(list, 0.22) + -- Slide the refreshed list in from the right so a search visibly refreshes. + Sauce:SlideIn(list, "RIGHT", 45, 0.26, "outCubic") end MTH_HUNTERBOOK_LOADED = true diff --git a/api/map-markers.lua b/api/map-markers.lua index 778a5cd..a8444d6 100644 --- a/api/map-markers.lua +++ b/api/map-markers.lua @@ -35,7 +35,12 @@ local function MTH_Map_AnimatePin(pin) if not MTH_MAP_Sauce or not pin then return end if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.mappins") then return end if type(pin.SetAlpha) == "function" then pin:SetAlpha(0) end - MTH_MAP_Sauce:FadeIn(pin, 0.3) + -- Bouncy scale "pop": the pin overshoots its size and springs back while + -- fading in. Far more eye-catching than a plain alpha fade. + MTH_MAP_Sauce:Group({ + MTH_MAP_Sauce:FadeIn(pin, 0.20, "outQuad", { defer = true }), + MTH_MAP_Sauce:Rubber(pin, 0.35, 0.55, { defer = true }), + }) end -- Ping a minimap pin that just came into range. Only called for genuinely new @@ -44,7 +49,11 @@ local function MTH_Map_PingMinimapPin(pin) if not MTH_MAP_Sauce or not pin then return end if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.minimapping") then return end if type(pin.SetAlpha) == "function" then pin:SetAlpha(0) end - MTH_MAP_Sauce:FadeIn(pin, 0.35) + -- Springy grow-in so a freshly-discovered node clearly "pops" onto the minimap. + MTH_MAP_Sauce:Group({ + MTH_MAP_Sauce:FadeIn(pin, 0.20, "outQuad", { defer = true }), + MTH_MAP_Sauce:Rubber(pin, 0.30, 0.55, { defer = true }), + }) end local MTH_MAP_MINIMAP_ZOOM = { diff --git a/api/options-shell.lua b/api/options-shell.lua index ee2d9e8..c73f4c0 100644 --- a/api/options-shell.lua +++ b/api/options-shell.lua @@ -27,7 +27,9 @@ local function MTH_OPT_AnimateOpen(frame) if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.options") then return end -- Hide alpha this frame so there is no one-frame opaque flash before onStart. if type(frame.SetAlpha) == "function" then frame:SetAlpha(0) end - MTH_OPT_Sauce:SlideIn(frame, "UP", 60, 0.4, "outCubic") + -- Bigger travel + a springy "outBack" overshoot so the window clearly + -- pops up into place instead of a subtle nudge. + MTH_OPT_Sauce:SlideIn(frame, "UP", 110, 0.5, "outBack") end -- Fade the active settings panel when switching options tabs, unless disabled. @@ -40,14 +42,16 @@ local function MTH_OPT_AnimateTabContent(tabKey) local frame = MTH_GetFrame(MTH_OPTIONS_TABS[i].frame) if frame then if type(frame.SetAlpha) == "function" then frame:SetAlpha(0) end - MTH_OPT_Sauce:FadeIn(frame, 0.22) + -- Slide the panel in from the right while fading so the content + -- visibly moves, not just brightens. + MTH_OPT_Sauce:SlideIn(frame, "RIGHT", 55, 0.3, "outCubic") end return end end end --- Fade the options window out before hiding it, unless disabled. +-- Slide the options window down and out before hiding it, unless disabled. local function MTH_OPT_AnimateClose() local frame = MetaHuntOptions if not frame then return end @@ -57,10 +61,17 @@ local function MTH_OPT_AnimateClose() frame:Hide() return end - MTH_OPT_Sauce:FadeOut(frame, 0.22, "inQuad", { + -- Capture the resting anchor so SlideOut's downward displacement can be + -- restored once the window is hidden (otherwise the next open is offset). + local p, rel, rp, x, y = frame:GetPoint(1) + MTH_OPT_Sauce:SlideOut(frame, "DOWN", 90, 0.26, "inCubic", { onFinish = function() frame:Hide() frame:SetAlpha(1) + if p then + frame:ClearAllPoints() + frame:SetPoint(p, rel, rp, x, y) + end end, }) end diff --git a/modules/zhunter/ZSpellButtonTemplate.lua b/modules/zhunter/ZSpellButtonTemplate.lua index 4072ff5..1b682b0 100644 --- a/modules/zhunter/ZSpellButtonTemplate.lua +++ b/modules/zhunter/ZSpellButtonTemplate.lua @@ -5,9 +5,22 @@ local ZSB_Sauce = PizzaSauce local function ZSpellButton_AnimateExpand(parent) if not ZSB_Sauce or not parent or not parent.children then return end if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.zbuttons") then return end - -- Hide alpha this frame so there is no one-frame opaque flash before onStart. - if type(parent.children.SetAlpha) == "function" then parent.children:SetAlpha(0) end - ZSB_Sauce:FadeIn(parent.children, 0.2) + -- The child cluster is anchored to the main button, not to the container, so + -- sliding the container has no visible effect. Instead reveal the children in + -- a quick staggered cascade so the fly-out clearly reads as motion. Keep the + -- container fully visible and drive each child's own alpha. + local container = parent.children + if type(container.SetAlpha) == "function" then container:SetAlpha(1) end + local count = parent.count or 0 + local shown = 0 + for i = 1, count do + local btn = getglobal((parent.name or "") .. i) + if btn and btn:IsShown() then + if type(btn.SetAlpha) == "function" then btn:SetAlpha(0) end + ZSB_Sauce:FadeTo(btn, 1, 0.20, "outQuad", { delay = shown * 0.05 }) + shown = shown + 1 + end + end end -- Key binding prefix mapping (button name → Bindings.xml binding prefix) diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index c39f5fe..1475807 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -298,7 +298,9 @@ function MTH_ZBar_AnimateActivate() local btn = getglobal(buttonName) if btn and btn:IsShown() then btn:SetAlpha(0) - ZBar_Sauce:FadeTo(btn, 1, 0.30, "outQuad", { delay = (i - 1) * 0.05 }) + -- Staggered springy slide-up so the bar visibly assembles button + -- by button instead of a faint collective fade. + ZBar_Sauce:SlideIn(btn, "UP", 26, 0.32, "outBack", { delay = (i - 1) * 0.06 }) end end end From c5a42d8d2a127221fce43c20bc8406a8bbfcb9a6 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 17:42:07 +0200 Subject: [PATCH 14/42] zBar: cap stagger window + shorter slide so a full bar assembles quickly --- modules/zhunter/zBar.lua | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index 1475807..580078f 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -292,18 +292,35 @@ function MTH_ZBar_AnimateActivate() end local s = MTH_ZBar_GetSaved() local order = s.order or {} + -- Collect only the buttons that will actually animate so the stagger is based + -- on the visible count, not gaps in the saved order. + local shownButtons = {} for i = 1, table.getn(order) do local buttonName = order[i] if type(buttonName) == "string" then local btn = getglobal(buttonName) if btn and btn:IsShown() then - btn:SetAlpha(0) - -- Staggered springy slide-up so the bar visibly assembles button - -- by button instead of a faint collective fade. - ZBar_Sauce:SlideIn(btn, "UP", 26, 0.32, "outBack", { delay = (i - 1) * 0.06 }) + table.insert(shownButtons, btn) end end end + local count = table.getn(shownButtons) + -- Cap the total stagger window so a full bar assembles just as quickly as a + -- short one: the per-button delay shrinks as the button count grows. + local step = 0.05 + if count > 1 then + local maxWindow = 0.28 + if step * (count - 1) > maxWindow then + step = maxWindow / (count - 1) + end + end + for i = 1, count do + local btn = shownButtons[i] + btn:SetAlpha(0) + -- Staggered springy slide-up so the bar visibly assembles button by + -- button instead of a faint collective fade. + ZBar_Sauce:SlideIn(btn, "UP", 26, 0.26, "outBack", { delay = (i - 1) * step }) + end end -- Called when the user toggles the Enable checkbox in the options. From e7cf99facbca2e76f5c0742c2d1fa7fac0738602 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 17:43:04 +0200 Subject: [PATCH 15/42] zBar: speed up assemble animation --- modules/zhunter/zBar.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index 580078f..c274b33 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -307,9 +307,9 @@ function MTH_ZBar_AnimateActivate() local count = table.getn(shownButtons) -- Cap the total stagger window so a full bar assembles just as quickly as a -- short one: the per-button delay shrinks as the button count grows. - local step = 0.05 + local step = 0.035 if count > 1 then - local maxWindow = 0.28 + local maxWindow = 0.18 if step * (count - 1) > maxWindow then step = maxWindow / (count - 1) end @@ -319,7 +319,7 @@ function MTH_ZBar_AnimateActivate() btn:SetAlpha(0) -- Staggered springy slide-up so the bar visibly assembles button by -- button instead of a faint collective fade. - ZBar_Sauce:SlideIn(btn, "UP", 26, 0.26, "outBack", { delay = (i - 1) * step }) + ZBar_Sauce:SlideIn(btn, "UP", 26, 0.20, "outBack", { delay = (i - 1) * step }) end end From 0609d487a7da0e3adf5ed3586a05365f8aaf211b Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 17:44:18 +0200 Subject: [PATCH 16/42] zBar: speed up assemble animation further --- modules/zhunter/zBar.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index c274b33..a9526cc 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -307,9 +307,9 @@ function MTH_ZBar_AnimateActivate() local count = table.getn(shownButtons) -- Cap the total stagger window so a full bar assembles just as quickly as a -- short one: the per-button delay shrinks as the button count grows. - local step = 0.035 + local step = 0.02 if count > 1 then - local maxWindow = 0.18 + local maxWindow = 0.10 if step * (count - 1) > maxWindow then step = maxWindow / (count - 1) end @@ -319,7 +319,7 @@ function MTH_ZBar_AnimateActivate() btn:SetAlpha(0) -- Staggered springy slide-up so the bar visibly assembles button by -- button instead of a faint collective fade. - ZBar_Sauce:SlideIn(btn, "UP", 26, 0.20, "outBack", { delay = (i - 1) * step }) + ZBar_Sauce:SlideIn(btn, "UP", 26, 0.14, "outBack", { delay = (i - 1) * step }) end end From 2863adff1cb659bb6f2231c8c068fb0a1853dafd Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 17:45:33 +0200 Subject: [PATCH 17/42] zBar: play assemble animation on init/reload, not only on checkbox toggle --- modules/zhunter/zBar.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index a9526cc..ed8dda2 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -371,5 +371,6 @@ function MTH_ZBar_Init() if s.enabled then MTH_ZBar_EnsureAnchor() MTH_ZBar_ApplyLayout() + MTH_ZBar_AnimateActivate() end end From 64e449481f2d807a60d543ca42c52573f0e3253a Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 17:48:46 +0200 Subject: [PATCH 18/42] zBar: revert to plain staggered fade + no init animation (SlideIn caused client micro-freezes on reload) --- modules/zhunter/zBar.lua | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index ed8dda2..0477972 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -285,6 +285,10 @@ function MTH_ZBar_Release() end -- Animations: gentle staggered fade-in of the bar's buttons when it activates. +-- NOTE: keep this a plain alpha fade only. A SlideIn (position tween) here caused +-- client micro-freezes on reload because it re-anchors every chained button each +-- frame, forcing repeated layout recalcs. Use position/scale tweens on the zBar +-- with extreme caution. function MTH_ZBar_AnimateActivate() if not ZBar_Sauce then return end if type(MTH_FX_IsEnabled) ~= "function" or not MTH_FX_IsEnabled("ui.zbar") then @@ -292,35 +296,16 @@ function MTH_ZBar_AnimateActivate() end local s = MTH_ZBar_GetSaved() local order = s.order or {} - -- Collect only the buttons that will actually animate so the stagger is based - -- on the visible count, not gaps in the saved order. - local shownButtons = {} for i = 1, table.getn(order) do local buttonName = order[i] if type(buttonName) == "string" then local btn = getglobal(buttonName) if btn and btn:IsShown() then - table.insert(shownButtons, btn) + btn:SetAlpha(0) + ZBar_Sauce:FadeTo(btn, 1, 0.30, "outQuad", { delay = (i - 1) * 0.05 }) end end end - local count = table.getn(shownButtons) - -- Cap the total stagger window so a full bar assembles just as quickly as a - -- short one: the per-button delay shrinks as the button count grows. - local step = 0.02 - if count > 1 then - local maxWindow = 0.10 - if step * (count - 1) > maxWindow then - step = maxWindow / (count - 1) - end - end - for i = 1, count do - local btn = shownButtons[i] - btn:SetAlpha(0) - -- Staggered springy slide-up so the bar visibly assembles button by - -- button instead of a faint collective fade. - ZBar_Sauce:SlideIn(btn, "UP", 26, 0.14, "outBack", { delay = (i - 1) * step }) - end end -- Called when the user toggles the Enable checkbox in the options. @@ -371,6 +356,5 @@ function MTH_ZBar_Init() if s.enabled then MTH_ZBar_EnsureAnchor() MTH_ZBar_ApplyLayout() - MTH_ZBar_AnimateActivate() end end From adb21c336c91083654e16f7b15783d6306f48594 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 21:20:56 +0200 Subject: [PATCH 19/42] Add Growl pet auto-teach feature and smooth zButton expand animation - New Pet Auto-Teach: on tame, offer to teach Growl at the best rank via the Beast Training window (Craft API DoCraft), with a General options checkbox and /mth growl on|off|status toggle (on by default). - De-stagger zButton flyout bars and zBar so buttons fade in together. - Update CHANGELOG. --- CHANGELOG.md | 4 + api/core-scan-stablemaster.lua | 6 + api/core.lua | 13 + api/options-zbuttons.lua | 19 +- api/pet-growl-teach.lua | 333 +++++++++++++++++++++++ init/api.xml | 1 + modules/zhunter/ZSpellButtonTemplate.lua | 8 +- modules/zhunter/zBar.lua | 13 +- 8 files changed, 384 insertions(+), 13 deletions(-) create mode 100644 api/pet-growl-teach.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2cce7..b94b21a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Resurrecting on Octowow. ### Added +- **Pet Auto-Teach — Growl**: When you tame a new beast, MetaHunt now offers to teach it Growl at the best rank for the pet's level. Click **Yes** and it opens the Beast Training window, teaches the ability and closes the window for you — no manual trip to the pet trainer needed. Toggle it in **Options → General → Pet Auto-Teach** ("Activate Auto-Teach of Growl (Max rank)", on by default) or with `/mth growl on|off`. + - **Animations**: MetaHunt now plays optional animations that make the interface feel alive. A firework-and-banner celebration bursts on screen when your pet learns a new ability or a new rank, and a red shaking banner warns you when a pet runs away for good. Your pet button flashes when its happiness changes, it gains a loyalty level, or you feed it; the ammo button flashes when Smart Ammo swaps your bullets or auto-equips after a weapon swap; and the MM Widget pulses on a Lock & Load proc. The Great Book slides up and fades in when you open it, gently fades its list when you switch tabs or run a search, fades beast models into view, and fades out when closed; stable slots flash when a pet is deposited, withdrawn or swapped. The options window slides in when opened, fades its tabs as you switch them, and fades out when closed. The minimap button pulses when clicked, minimap markers ping as beasts come into range, collapsible zButton bars fade their buttons in as they expand, the zBar fades in when enabled, and world-map markers fade in when you locate a beast, vendor or master. Everything is optional and off-switchable: a new **Animations** tab (Options → Animations) lets you toggle each effect on its own — or disable them all at once. Powered by the PizzaSauce animation library. - **zTrack — Find Fish**: Added new spell Find Fish to the zTrack list of trackings. @@ -17,6 +19,8 @@ Resurrecting on Octowow. ### Changed +- **zButton bars — expand animation**: Collapsible zButton flyout bars and the zBar now fade in together when they open, instead of stacking their buttons one after another, so expanding them feels smoother. + - **Complete revamp of SavedVariables**: All your settings now live in one tidy place instead of being scattered around. A migration happens the first time you log in, and you shouldn't lose any settings. diff --git a/api/core-scan-stablemaster.lua b/api/core-scan-stablemaster.lua index efd50aa..766710e 100644 --- a/api/core-scan-stablemaster.lua +++ b/api/core-scan-stablemaster.lua @@ -3104,6 +3104,9 @@ function MTH_PETS_RefreshCurrentPet() at = now, context = eventContext, }) + if eventType == "pet-tamed" and type(MTH_Growl_OfferForTamedPet) == "function" then + MTH_Growl_OfferForTamedPet(snapshot.name, snapshot.level) + end end end @@ -3733,6 +3736,9 @@ local function MTH_PETS_HandleUnitPetTransition(source) context = MTH_PETS_CaptureContext(), }) end + if eventType == "pet-tamed" and type(MTH_Growl_OfferForTamedPet) == "function" then + MTH_Growl_OfferForTamedPet(snapshot.name, snapshot.level) + end end if not hadPet and hasPendingTame then MTH_PETS_LogTame("Acquire completed; clearing pending tame attempt") diff --git a/api/core.lua b/api/core.lua index 672b481..f1f06b5 100644 --- a/api/core.lua +++ b/api/core.lua @@ -495,6 +495,19 @@ function SlashCmdList.MTH(msg, editbox) else MTH:Print("Dev commands: /mth dev petmigrate") end + elseif lowerCmd == "growl" then + if type(MTH_Growl_SetEnabled) ~= "function" or type(MTH_Growl_IsEnabled) ~= "function" then + MTH:Print("Growl auto-teach is not available.") + elseif lowerArg == "on" or lowerArg == "enable" then + MTH_Growl_SetEnabled(true) + MTH:Print("Growl auto-teach: |cff00ff00ON|r (offers Growl to newly tamed pets).") + elseif lowerArg == "off" or lowerArg == "disable" then + MTH_Growl_SetEnabled(false) + MTH:Print("Growl auto-teach: |cffff0000OFF|r.") + else + local state = MTH_Growl_IsEnabled() and "|cff00ff00ON|r" or "|cffff0000OFF|r" + MTH:Print("Growl auto-teach is " .. state .. ". Usage: /mth growl on | off") + end elseif lowerCmd == "food" then local sub = lowerArg local function MTH_FoodItemLabel(itemId) diff --git a/api/options-zbuttons.lua b/api/options-zbuttons.lua index 60930ec..f5e02d0 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -1540,7 +1540,7 @@ function MTH_SetupGeneralOptions() ensureHelpText(smartAmmoSection, "MetaHuntGeneralSmartAmmoWeaponSwapHelp", "When you swap between gun and bow/crossbow, instantly equips the best matching ammo from your bags.", -208) end - local stripSection = ensureSection("MetaHuntGeneralAutoStripBox", "Auto-Strip", topY - 270, 122, "left") + local stripSection = ensureSection("MetaHuntGeneralAutoStripBox", "Auto-Strip", topY - 270, 100, "left") if stripSection then local autoStripToggle = ensureCheckbox(stripSection, "MetaHuntGeneralAutoStripToggle", "Enable Auto-Strip on Combat Exit", -10, stripSaved["autostrip"] and true or false) if autoStripToggle then @@ -1563,7 +1563,7 @@ function MTH_SetupGeneralOptions() ensureHelpText(stripSection, "MetaHuntGeneralAutoStripHelp", "Auto-strip unequips items when combat ends. Requires at least one empty bag slot.", -64) end - local antiSection = ensureSection("MetaHuntGeneralAntiDazeBox", "Anti-Daze", topY - 406, 96, "left") + local antiSection = ensureSection("MetaHuntGeneralAntiDazeBox", "Anti-Daze", topY - 384, 76, "left") if antiSection then local antiDazeEnabled = (AntiDaze_GetEnabled and AntiDaze_GetEnabled()) or false local antiToggle = ensureCheckbox(antiSection, "MetaHuntGeneralAntiDazeToggle", "Enable Anti-Daze", -10, antiDazeEnabled) @@ -1586,6 +1586,21 @@ function MTH_SetupGeneralOptions() ensureHelpText(antiSection, "MetaHuntGeneralAntiDazeHelp", "Cancels Cheetah/Pack when dazed.", -40) end + local petTeachSection = ensureSection("MetaHuntGeneralPetTeachBox", "Pet Auto-Teach", topY - 474, 76, "left") + if petTeachSection then + local growlEnabled = (type(MTH_Growl_IsEnabled) == "function" and MTH_Growl_IsEnabled()) or false + local growlToggle = ensureCheckbox(petTeachSection, "MetaHuntGeneralPetTeachGrowlToggle", "Activate Auto-Teach of Growl (Max rank)", -10, growlEnabled) + if growlToggle then + growlToggle:SetScript("OnClick", function() + if type(MTH_Growl_SetEnabled) == "function" then + MTH_Growl_SetEnabled(MTH_ZB_IsChecked(this)) + end + end) + end + + ensureHelpText(petTeachSection, "MetaHuntGeneralPetTeachGrowlHelp", "When you tame a beast, offers to teach it the best Growl rank for its level.", -40) + end + local tooltipsSection = ensureSection("MetaHuntGeneralTooltipsBox", "Tooltips", topY, 356, "right") if tooltipsSection then local tooltipsStore = MTH and MTH.GetModuleCharSavedVariables and MTH:GetModuleCharSavedVariables("tooltips") diff --git a/api/pet-growl-teach.lua b/api/pet-growl-teach.lua new file mode 100644 index 0000000..bb271a7 --- /dev/null +++ b/api/pet-growl-teach.lua @@ -0,0 +1,333 @@ +-- MetaHunt: auto-train Growl to a freshly tamed pet. +-- +-- Growl is a "trainer" pet ability (Growl / Great Stamina / Natural Armor / +-- Great Resistance) that the hunter learns from a Pet Trainer NPC and then +-- teaches to the active pet through the always-available BEAST TRAINING window +-- (a spellbook-like hunter spell, no NPC required). Growl specifically costs +-- 0 training points, so it can always be taught to a brand-new pet. +-- +-- The Beast Training window is driven by the vanilla Craft API: +-- GetNumCrafts() -> number of rows +-- GetCraftInfo(i) -> name, rankSubText, type, ... +-- DoCraft(i) -> trains that row (the "Select + Train" action) +-- CloseCraft() -> closes the window +-- +-- The correct Growl rank is a function of the pet's level: +-- rank = floor(level / 10) + 1 (ranks 1..7, at pet levels 1/10/.../60) +-- +-- Flow on a confirmed fresh tame (when enabled): compute the target rank, open +-- the Beast Training window, find the Growl row at that rank once the craft list +-- populates, DoCraft() it silently, then close the window if we opened it. + +local MTH_GROWL_MAX_RANK = 7 + +-- --------------------------------------------------------------------------- +-- Saved-variable backed toggle (per character), default ON. +-- --------------------------------------------------------------------------- +local function MTH_Growl_GetStore() + if type(MTH_CharSavedVariables) ~= "table" then + MTH_CharSavedVariables = {} + end + if type(MTH_CharSavedVariables.growlTeach) ~= "table" then + MTH_CharSavedVariables.growlTeach = {} + end + local store = MTH_CharSavedVariables.growlTeach + if store.enabled == nil then + store.enabled = true + end + return store +end + +function MTH_Growl_IsEnabled() + return MTH_Growl_GetStore().enabled and true or false +end + +function MTH_Growl_SetEnabled(enabled) + MTH_Growl_GetStore().enabled = enabled and true or false +end + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- +local function MTH_Growl_DebugLog(line) + if type(MTH_DebugFrame) == "table" and type(MTH_DebugFrame.AddInfo) == "function" then + MTH_DebugFrame:AddInfo("[Growl] " .. tostring(line)) + end +end + +local function MTH_Growl_GetGlobal(name) + if type(getglobal) == "function" then + return getglobal(name) + end + if _G then + return _G[name] + end + return nil +end + +-- Rank appropriate for a pet of the given level. +local function MTH_Growl_RankForLevel(level) + local lvl = tonumber(level) or 0 + if lvl < 1 then lvl = 1 end + local rank = math.floor(lvl / 10) + 1 + if rank < 1 then rank = 1 end + if rank > MTH_GROWL_MAX_RANK then rank = MTH_GROWL_MAX_RANK end + return rank +end + +-- Parse a rank number out of a craft row's name/subtext (e.g. "Growl", "Rank 5"). +local function MTH_Growl_ParseRank(name, subText) + local sub = tostring(subText or "") + local _, _, subRank = string.find(sub, "(%d+)") + if subRank then return tonumber(subRank) end + local nm = tostring(name or "") + local _, _, nameRank = string.find(nm, "(%d+)") + if nameRank then return tonumber(nameRank) end + return nil +end + +-- Find the hunter spellbook slot for a spell by name (e.g. "Beast Training"). +local function MTH_Growl_FindSpellSlot(spellName) + if not spellName or spellName == "" then return nil end + local getSpellName = MTH_Growl_GetGlobal("GetSpellName") + if type(getSpellName) ~= "function" then return nil end + + local getNumSpellTabs = MTH_Growl_GetGlobal("GetNumSpellTabs") + local getSpellTabInfo = MTH_Growl_GetGlobal("GetSpellTabInfo") + if type(getNumSpellTabs) == "function" and type(getSpellTabInfo) == "function" then + local numTabs = getNumSpellTabs() or 0 + for tabIndex = 1, numTabs do + local _, _, offset, numSpells = getSpellTabInfo(tabIndex) + offset = tonumber(offset) or 0 + numSpells = tonumber(numSpells) or 0 + for spellIndex = (offset + 1), (offset + numSpells) do + if getSpellName(spellIndex, "spell") == spellName then + return spellIndex + end + end + end + end + + -- Fallback: linear scan of the spellbook. + local nilStreak = 0 + for spellIndex = 1, 1024 do + local nameAtIndex = getSpellName(spellIndex, "spell") + if nameAtIndex then + nilStreak = 0 + if nameAtIndex == spellName then + return spellIndex + end + else + nilStreak = nilStreak + 1 + if nilStreak >= 30 then break end + end + end + return nil +end + +-- Open the Beast Training window by casting the hunter "Beast Training" spell. +local function MTH_Growl_OpenTrainingWindow() + local spellName = MTH_Growl_GetGlobal("ZHUNTER_PET_TRAINING") or "Beast Training" + local slot = MTH_Growl_FindSpellSlot(spellName) + local castSpell = MTH_Growl_GetGlobal("CastSpell") + if slot and type(castSpell) == "function" then + local getSpellName = MTH_Growl_GetGlobal("GetSpellName") + local atSlot = (type(getSpellName) == "function") and getSpellName(slot, "spell") or "?" + castSpell(slot, "spell") + MTH_Growl_DebugLog("Opened Beast Training via CastSpell(slot=" .. tostring(slot) + .. " name='" .. tostring(atSlot) .. "')") + return true + end + local castSpellByName = MTH_Growl_GetGlobal("CastSpellByName") + if type(castSpellByName) == "function" then + castSpellByName(spellName) + MTH_Growl_DebugLog("Opened Beast Training via CastSpellByName('" .. tostring(spellName) .. "') (slot not found)") + return true + end + MTH_Growl_DebugLog("Could not open Beast Training (no spell slot, no CastSpellByName)") + return false +end + +-- Is the Beast Training / craft window currently visible? +local function MTH_Growl_IsTrainingWindowVisible() + local frameNames = { "BeastTrainingFrame", "PetTrainingFrame", "CraftFrame" } + for i = 1, table.getn(frameNames) do + local frame = MTH_Growl_GetGlobal(frameNames[i]) + if frame then + if frame.IsVisible and frame:IsVisible() then return true end + if frame.IsShown and frame:IsShown() then return true end + end + end + return false +end + +-- --------------------------------------------------------------------------- +-- Pending auto-train request. Set when a fresh tame is confirmed; cleared once +-- we train (or time out). { rank, name, elapsed, closeAfter, done } +-- --------------------------------------------------------------------------- +local MTH_Growl_Pending = nil + +-- The offer currently awaiting the player's Yes/No answer. { rank, name, level } +local MTH_Growl_Offer = nil +local function MTH_Growl_TryTrainNow() + if not MTH_Growl_Pending or MTH_Growl_Pending.done then + return false + end + + local getNumCrafts = MTH_Growl_GetGlobal("GetNumCrafts") + local getCraftInfo = MTH_Growl_GetGlobal("GetCraftInfo") + local doCraft = MTH_Growl_GetGlobal("DoCraft") + if type(getNumCrafts) ~= "function" or type(getCraftInfo) ~= "function" or type(doCraft) ~= "function" then + return false + end + + local total = tonumber(getNumCrafts()) or 0 + if total <= 0 then + return false -- window not populated yet + end + + local target = MTH_Growl_Pending.rank + local bestIndex, bestRank = nil, 0 + for i = 1, total do + local craftName, craftSub, craftType = getCraftInfo(i) + local lname = string.lower(tostring(craftName or "")) + if string.find(lname, "growl", 1, true) and tostring(craftType) ~= "header" then + local rank = MTH_Growl_ParseRank(craftName, craftSub) + if rank then + if rank == target then + bestIndex, bestRank = i, rank + break + elseif rank < target and rank > bestRank then + -- Highest rank we could give if the exact target isn't offered. + bestIndex, bestRank = i, rank + end + end + end + end + + if not bestIndex then + return false -- no Growl row found yet + end + + doCraft(bestIndex) + MTH_Growl_DebugLog("Trained Growl Rank " .. tostring(bestRank) + .. " (target " .. tostring(target) .. ") for '" .. tostring(MTH_Growl_Pending.name) + .. "' via DoCraft(" .. tostring(bestIndex) .. ")") + + MTH_Growl_Pending.done = true + if MTH_Growl_Pending.closeAfter then + local closeCraft = MTH_Growl_GetGlobal("CloseCraft") + if type(closeCraft) == "function" then + closeCraft() + end + end + MTH_Growl_Pending = nil + return true +end + +-- Driver frame: react to the craft window opening/updating, with an OnUpdate +-- poll as backup and a timeout so we never leave a stale pending request. +local MTH_Growl_Frame = CreateFrame("Frame", "MTH_GrowlTeachFrame") +MTH_Growl_Frame:RegisterEvent("CRAFT_SHOW") +MTH_Growl_Frame:RegisterEvent("CRAFT_UPDATE") +MTH_Growl_Frame:RegisterEvent("PET_TRAINING_SHOW") +MTH_Growl_Frame:SetScript("OnEvent", function() + MTH_Growl_TryTrainNow() +end) +MTH_Growl_Frame:SetScript("OnUpdate", function() + if not MTH_Growl_Pending then return end + local p = MTH_Growl_Pending + p.elapsed = (p.elapsed or 0) + (arg1 or 0) + + -- Train as soon as the window is populated. + if MTH_Growl_TryTrainNow() then return end + + if p.elapsed > 8 then + local getNumCrafts = MTH_Growl_GetGlobal("GetNumCrafts") + local crafts = (type(getNumCrafts) == "function" and tonumber(getNumCrafts())) or -1 + MTH_Growl_DebugLog("Gave up training Growl Rank " .. tostring(p.rank) + .. " for '" .. tostring(p.name) .. "' (timeout: windowVisible=" + .. tostring(MTH_Growl_IsTrainingWindowVisible()) .. " numCrafts=" .. tostring(crafts) + .. " openAttempts=" .. tostring(p.openAttempts) .. ")") + MTH_Growl_Pending = nil + return + end + + -- Window not up yet: re-attempt opening every second (the pet may still be + -- settling right after the tame, so the first cast can be ignored). + if not MTH_Growl_IsTrainingWindowVisible() then + p.sinceOpen = (p.sinceOpen or 0) + (arg1 or 0) + if p.sinceOpen >= 1.0 then + p.sinceOpen = 0 + p.openAttempts = (p.openAttempts or 0) + 1 + if p.openAttempts <= 6 then + MTH_Growl_OpenTrainingWindow() + end + end + end +end) + +-- --------------------------------------------------------------------------- +-- Start training the confirmed offer. Called from the popup's Yes button (a +-- hardware event), which makes opening the Beast Training window reliable. +-- --------------------------------------------------------------------------- +local function MTH_Growl_StartTraining(rank, petName) + local alreadyOpen = MTH_Growl_IsTrainingWindowVisible() + MTH_Growl_Pending = { + rank = rank, + name = petName, + elapsed = 0, + sinceOpen = 0, + openAttempts = 0, + closeAfter = not alreadyOpen, + done = false, + } + MTH_Growl_DebugLog("Confirmed: train Growl Rank " .. tostring(rank) .. " for '" + .. tostring(petName) .. "' (windowOpen=" .. tostring(alreadyOpen) .. ")") + if alreadyOpen then + MTH_Growl_TryTrainNow() + else + MTH_Growl_Pending.openAttempts = 1 + MTH_Growl_OpenTrainingWindow() + end +end + +-- --------------------------------------------------------------------------- +-- Confirmation popup. Yes = hardware event -> open Beast Training + DoCraft. +-- --------------------------------------------------------------------------- +StaticPopupDialogs["MTH_GROWL_TEACH"] = { + text = "Teach %s to your newly tamed %s?", + button1 = YES or "Yes", + button2 = NO or "No", + OnAccept = function() + local offer = MTH_Growl_Offer + MTH_Growl_Offer = nil + if not offer then return end + MTH_Growl_StartTraining(offer.rank, offer.name) + end, + OnCancel = function() + MTH_Growl_Offer = nil + end, + timeout = 0, + whileDead = 1, + hideOnEscape = 1, +} + +-- --------------------------------------------------------------------------- +-- Public entry point: called when a fresh tame is confirmed. +-- --------------------------------------------------------------------------- +function MTH_Growl_OfferForTamedPet(petName, petLevel) + if not MTH_Growl_IsEnabled() then + return + end + if type(StaticPopup_Show) ~= "function" then + return + end + + local rank = MTH_Growl_RankForLevel(petLevel) + MTH_Growl_Offer = { rank = rank, name = petName, level = petLevel } + local spellLabel = "Growl (Rank " .. tostring(rank) .. ")" + local name = tostring(petName or "your pet") + MTH_Growl_DebugLog("Offer pet='" .. name .. "' level=" .. tostring(petLevel) .. " -> " .. spellLabel) + StaticPopup_Show("MTH_GROWL_TEACH", spellLabel, name) +end diff --git a/init/api.xml b/init/api.xml index 9387315..e04672e 100644 --- a/init/api.xml +++ b/init/api.xml @@ -45,6 +45,7 @@ + diff --git a/modules/zhunter/ZSpellButtonTemplate.lua b/modules/zhunter/ZSpellButtonTemplate.lua index 1b682b0..6197c96 100644 --- a/modules/zhunter/ZSpellButtonTemplate.lua +++ b/modules/zhunter/ZSpellButtonTemplate.lua @@ -418,11 +418,9 @@ function ZSpellButton_SetChildrenExpanded(parent, expanded) end end ZSpellButton_StartFadeTimer(parent) - -- Animate only on a genuine collapsed->expanded toggle, and never on the - -- very first apply at login (guarded by _zbAnimReady, set in ApplyChildrenExpanded). - if wasCollapsed and parent._zbAnimReady then - ZSpellButton_AnimateExpand(parent) - end + -- Flyout reveal is instant: no stagger/cascade animation. The staggered + -- "stacking one after the other" fly-out was slow and unwanted, so the + -- children just appear immediately when expanded. else saved["children"]["expanded"] = 0 if parent.children then diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index 0477972..b9d7bdc 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -284,11 +284,9 @@ function MTH_ZBar_Release() end end --- Animations: gentle staggered fade-in of the bar's buttons when it activates. --- NOTE: keep this a plain alpha fade only. A SlideIn (position tween) here caused --- client micro-freezes on reload because it re-anchors every chained button each --- frame, forcing repeated layout recalcs. Use position/scale tweens on the zBar --- with extreme caution. +-- Animations: a single, quick, simultaneous fade-in of the bar's buttons when +-- it activates. NO stagger (no per-button delay) — every button fades in at the +-- same time so it reads as one clean fade instead of a slow "stacking" cascade. function MTH_ZBar_AnimateActivate() if not ZBar_Sauce then return end if type(MTH_FX_IsEnabled) ~= "function" or not MTH_FX_IsEnabled("ui.zbar") then @@ -302,7 +300,7 @@ function MTH_ZBar_AnimateActivate() local btn = getglobal(buttonName) if btn and btn:IsShown() then btn:SetAlpha(0) - ZBar_Sauce:FadeTo(btn, 1, 0.30, "outQuad", { delay = (i - 1) * 0.05 }) + ZBar_Sauce:FadeTo(btn, 1, 0.20, "outQuad") end end end @@ -356,5 +354,8 @@ function MTH_ZBar_Init() if s.enabled then MTH_ZBar_EnsureAnchor() MTH_ZBar_ApplyLayout() + -- Play the gentle fade on login/reload too, not just on checkbox toggle. + -- Safe here because AnimateActivate is a plain alpha fade (no re-anchoring). + MTH_ZBar_AnimateActivate() end end From ccf9d8b0b6728503000b21f04f17aebcad193de3 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 23:59:37 +0200 Subject: [PATCH 20/42] Unify all option panels through MTH_Layout engine Add reusable MTH_Layout box/column layout engine (api/options-layout.lua) and route every option panel through it (shell, general, zButtons per-button config, zBar, chronometer, auto buy, profiles, credits, etc.) for consistent, auto-sizing layout. Restore realm gate. README note. --- CHANGELOG.md | 6 +- api/options-animations.lua | 125 ++-- api/options-autobuy.lua | 165 ++--- api/options-autoquest.lua | 220 ++----- api/options-chronometer.lua | 313 ++++----- api/options-credits.lua | 14 +- api/options-expammo.lua | 388 ++++------- api/options-feedomatic.lua | 232 +++---- api/options-icu.lua | 298 +++------ api/options-layout.lua | 608 +++++++++++++++++ api/options-profiles.lua | 9 +- api/options-shell.lua | 4 +- api/options-zbuttons.lua | 1226 ++++++++++++++--------------------- api/options.xml | 4 +- api/pet-growl-teach.lua | 18 - init/api.xml | 1 + 16 files changed, 1770 insertions(+), 1861 deletions(-) create mode 100644 api/options-layout.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index b94b21a..509e5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ Resurrecting on Octowow. ### Added -- **Pet Auto-Teach — Growl**: When you tame a new beast, MetaHunt now offers to teach it Growl at the best rank for the pet's level. Click **Yes** and it opens the Beast Training window, teaches the ability and closes the window for you — no manual trip to the pet trainer needed. Toggle it in **Options → General → Pet Auto-Teach** ("Activate Auto-Teach of Growl (Max rank)", on by default) or with `/mth growl on|off`. +- **Pet Auto-Teach — Growl**: When you tame a new beast, MetaHunt now offers to teach it Growl at the best rank for the pet's level. Click **Yes** and it opens the Beast Training window, teaches the ability and closes the window automatically. -- **Animations**: MetaHunt now plays optional animations that make the interface feel alive. A firework-and-banner celebration bursts on screen when your pet learns a new ability or a new rank, and a red shaking banner warns you when a pet runs away for good. Your pet button flashes when its happiness changes, it gains a loyalty level, or you feed it; the ammo button flashes when Smart Ammo swaps your bullets or auto-equips after a weapon swap; and the MM Widget pulses on a Lock & Load proc. The Great Book slides up and fades in when you open it, gently fades its list when you switch tabs or run a search, fades beast models into view, and fades out when closed; stable slots flash when a pet is deposited, withdrawn or swapped. The options window slides in when opened, fades its tabs as you switch them, and fades out when closed. The minimap button pulses when clicked, minimap markers ping as beasts come into range, collapsible zButton bars fade their buttons in as they expand, the zBar fades in when enabled, and world-map markers fade in when you locate a beast, vendor or master. Everything is optional and off-switchable: a new **Animations** tab (Options → Animations) lets you toggle each effect on its own — or disable them all at once. Powered by the PizzaSauce animation library. +- **Animations**: MetaHunt now plays optional animations that make the interface feel alive. Powered by the PizzaSauce library of [Pizzahawaii](https://codeberg.org/Pizzahawaii/) ! - **zTrack — Find Fish**: Added new spell Find Fish to the zTrack list of trackings. @@ -19,8 +19,6 @@ Resurrecting on Octowow. ### Changed -- **zButton bars — expand animation**: Collapsible zButton flyout bars and the zBar now fade in together when they open, instead of stacking their buttons one after another, so expanding them feels smoother. - - **Complete revamp of SavedVariables**: All your settings now live in one tidy place instead of being scattered around. A migration happens the first time you log in, and you shouldn't lose any settings. diff --git a/api/options-animations.lua b/api/options-animations.lua index 9513d7a..0bba53f 100644 --- a/api/options-animations.lua +++ b/api/options-animations.lua @@ -80,102 +80,71 @@ function MTH_SetupAnimationsOptions() return end - MTH_ClearContainer(container) + local panel = MTH_Layout.Panel(container) + panel:Title(MTH_ANIM_L("ANIM_TITLE", "Animations")) + panel:Note(MTH_ANIM_L("ANIM_CREDIT", + "Animations are powered by PizzaSauce, a WoW 1.12 animation library kindly made and shared by Pizzahawaii. Thank you!"), + { color = MTH_Layout.Theme.colText, lines = 2 }) + panel:Link("MetaHuntOptionsAnimLink", MTH_ANIM_LINK, function() MTH_ANIM_InsertLink(MTH_ANIM_LINK) end) + MTH_ANIM_CTRL = {} - local title = container:CreateFontString("MetaHuntOptionsAnimationsTitle", "ARTWORK", "GameFontNormal") - title:SetPoint("TOPLEFT", container, "TOPLEFT", 10, -10) - title:SetText(MTH_ANIM_L("ANIM_TITLE", "Animations")) - title:SetTextColor(1.00, 0.82, 0.00) - - local credit = container:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") - credit:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8) - credit:SetWidth(460) - credit:SetJustifyH("LEFT") - credit:SetText(MTH_ANIM_L("ANIM_CREDIT", - "Animations are powered by PizzaSauce, a WoW 1.12 animation library kindly made and shared by Pizzahawaii. Thank you!")) - credit:SetTextColor(0.93, 0.93, 0.93) - - local link = CreateFrame("Button", nil, container) - link:SetPoint("TOPLEFT", credit, "BOTTOMLEFT", 0, -4) - link:SetHeight(16) - link:SetWidth(460) - local linkText = link:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") - linkText:SetPoint("LEFT", link, "LEFT", 0, 0) - linkText:SetJustifyH("LEFT") - linkText:SetText(MTH_ANIM_LINK) - linkText:SetTextColor(0.40, 0.75, 1.00) - link:SetScript("OnClick", function() MTH_ANIM_InsertLink(MTH_ANIM_LINK) end) - link:SetScript("OnEnter", function() linkText:SetTextColor(0.60, 0.90, 1.00) end) - link:SetScript("OnLeave", function() linkText:SetTextColor(0.40, 0.75, 1.00) end) - - -- Global master toggle. - local master = MTH_CreateCheckbox(container, "MetaHuntOptionsAnimMaster", - MTH_ANIM_L("ANIM_ENABLE_ALL", "Enable animations"), -70) - if master then - master:SetChecked(MTH_FX_AnimationsEnabled()) - master:SetScript("OnClick", function() - MTH_FX_SetAnimationsEnabled(master:GetChecked()) + -- Global master toggle (full width, above the columns). + panel:TopCheckbox("MetaHuntOptionsAnimMaster", + MTH_ANIM_L("ANIM_ENABLE_ALL", "Enable animations"), { + checked = MTH_FX_AnimationsEnabled, + onClick = function() + local cb = getglobal("MetaHuntOptionsAnimMaster") + MTH_FX_SetAnimationsEnabled(cb:GetChecked()) MTH_ANIM_UpdateChildStates() - end) - end + end, + }) - -- Per-animation checkboxes, grouped by section, laid out in two columns. - -- Each whole section is placed into whichever column is currently shorter - -- (largest remaining Y), so the two columns stay balanced as defs grow. - local rowStep = 26 - local sectionGap = 12 - local columns = { - { headerX = 15, cbX = 28, y = -104 }, - { headerX = 300, cbX = 313, y = -104 }, - } + -- Per-animation checkboxes: each section is a self-sizing box, dropped into + -- whichever column currently has the most room so the two stay balanced. local s = 1 while MTH_FX_ANIM_SECTIONS and s <= table.getn(MTH_FX_ANIM_SECTIONS) do local sec = MTH_FX_ANIM_SECTIONS[s] - -- Pick the shorter column (the one whose cursor is nearest the top). - local col = columns[1] - if columns[2].y > columns[1].y then - col = columns[2] - end - - local header = container:CreateFontString(nil, "ARTWORK", "GameFontNormal") - header:SetPoint("TOPLEFT", container, "TOPLEFT", col.headerX, col.y) - header:SetText(MTH_ANIM_L(sec.titleKey, sec.title)) - header:SetTextColor(1.00, 0.82, 0.00) - col.y = col.y - 22 - + -- Collect this section's defs in order. + local secDefs = {} local d = 1 while MTH_FX_ANIM_DEFS and d <= table.getn(MTH_FX_ANIM_DEFS) do local def = MTH_FX_ANIM_DEFS[d] if def.section == sec.id then - local cbName = "MetaHuntOptionsAnim_" .. string.gsub(def.key, "%.", "_") - local cb = MTH_CreateCheckbox(container, cbName, MTH_ANIM_L(def.labelKey, def.label), col.y, col.cbX) - if cb then - cb.mthKey = def.key - cb:SetChecked(MTH_FX_GetFlag(def.key)) - cb:SetScript("OnClick", function() - MTH_FX_SetFlag(cb.mthKey, cb:GetChecked()) - end) - if def.tooltip and def.tooltip ~= "" then - cb.mthTooltip = MTH_ANIM_L(def.tooltipKey, def.tooltip) - cb:SetScript("OnEnter", function() - GameTooltip:SetOwner(cb, "ANCHOR_RIGHT") - GameTooltip:SetText(cb.mthTooltip, 1, 1, 1, 1, true) - GameTooltip:Show() - end) - cb:SetScript("OnLeave", function() GameTooltip:Hide() end) - end - table.insert(MTH_ANIM_CTRL, cb) - col.y = col.y - rowStep - end + table.insert(secDefs, def) end d = d + 1 end - col.y = col.y - sectionGap + local count = table.getn(secDefs) + + if count > 0 then + local box = panel:Box(panel:ShorterSide(), + "MetaHuntOptionsAnimBox_" .. tostring(sec.id), + MTH_ANIM_L(sec.titleKey, sec.title)) + + local di = 1 + while di <= count do + local def = secDefs[di] + local cbName = "MetaHuntOptionsAnim_" .. string.gsub(def.key, "%.", "_") + local cb = box:Checkbox(cbName, MTH_ANIM_L(def.labelKey, def.label), { + checked = function() return MTH_FX_GetFlag(def.key) end, + tooltip = (def.tooltip and def.tooltip ~= "") and MTH_ANIM_L(def.tooltipKey, def.tooltip) or nil, + }) + if cb then + cb.mthKey = def.key + cb:SetScript("OnClick", function() + MTH_FX_SetFlag(cb.mthKey, cb:GetChecked()) + end) + table.insert(MTH_ANIM_CTRL, cb) + end + di = di + 1 + end + end s = s + 1 end + panel:Finish() MTH_ANIM_UpdateChildStates() MTH_ANIM_DBG.setupResult = "ok" end diff --git a/api/options-autobuy.lua b/api/options-autobuy.lua index d95c5be..21ae22d 100644 --- a/api/options-autobuy.lua +++ b/api/options-autobuy.lua @@ -589,7 +589,9 @@ local function MTH_AB_RefreshOptionsUI() end end -local function MTH_AB_CreateRow(container, subtype, index, xBase, yOffset) +local function MTH_AB_CreateRow(box, subtype, index) + local f, y = box:Row(38) + local padX = box:PadX() local row = { subtype = subtype, index = index, @@ -597,14 +599,15 @@ local function MTH_AB_CreateRow(container, subtype, index, xBase, yOffset) } local nameBase = "MetaHuntOptionsAutoBuy_" .. subtype .. "_" .. tostring(index) - row.check = MTH_CreateCheckbox(container, nameBase .. "Check", "", yOffset, xBase + MTH_AB_LAYOUT.ROW_CHECK_X) - row.label = container:CreateFontString(nameBase .. "Label", "ARTWORK", "GameFontNormalSmall") - row.label:SetPoint("TOPLEFT", container, "TOPLEFT", xBase + MTH_AB_LAYOUT.ROW_CHECK_LABEL_X, yOffset - 5) + row.check = MTH_CreateCheckbox(f, nameBase .. "Check", "", y, padX) + row.label = getglobal(nameBase .. "Label") or f:CreateFontString(nameBase .. "Label", "ARTWORK", "GameFontNormalSmall") + row.label:ClearAllPoints() + row.label:SetPoint("TOPLEFT", f, "TOPLEFT", padX + 27, y - 5) row.label:SetText("On?") - row.dropdown = CreateFrame("Frame", nameBase .. "Drop", container, "UIDropDownMenuTemplate") + row.dropdown = getglobal(nameBase .. "Drop") or CreateFrame("Frame", nameBase .. "Drop", f, "UIDropDownMenuTemplate") row.dropdown:ClearAllPoints() - row.dropdown:SetPoint("TOPLEFT", container, "TOPLEFT", xBase + MTH_AB_LAYOUT.ROW_DROPDOWN_X, yOffset + 3) + row.dropdown:SetPoint("TOPLEFT", f, "TOPLEFT", padX + 33, y + 3) if UIDropDownMenu_SetWidth then UIDropDownMenu_SetWidth(MTH_AB_LAYOUT.ROW_DROPDOWN_WIDTH, row.dropdown) end @@ -640,7 +643,8 @@ local function MTH_AB_CreateRow(container, subtype, index, xBase, yOffset) end) end - row.qty = CreateFrame("EditBox", nameBase .. "Qty", container, "InputBoxTemplate") + row.qty = getglobal(nameBase .. "Qty") or CreateFrame("EditBox", nameBase .. "Qty", f, "InputBoxTemplate") + row.qty:ClearAllPoints() local dropButton = getglobal(nameBase .. "DropButton") if dropButton then row.qty:SetPoint("LEFT", dropButton, "RIGHT", MTH_AB_LAYOUT.ROW_QTY_GAP, -1) @@ -652,7 +656,8 @@ local function MTH_AB_CreateRow(container, subtype, index, xBase, yOffset) row.qty:SetNumeric(true) row.qty:SetAutoFocus(false) - row.suffix = container:CreateFontString(nameBase .. "Suffix", "ARTWORK", "GameFontNormalSmall") + row.suffix = getglobal(nameBase .. "Suffix") or f:CreateFontString(nameBase .. "Suffix", "ARTWORK", "GameFontNormalSmall") + row.suffix:ClearAllPoints() row.suffix:SetPoint("LEFT", row.qty, "RIGHT", MTH_AB_LAYOUT.ROW_SUFFIX_GAP, 0) row.suffix:SetText("Stacks") @@ -687,32 +692,18 @@ local function MTH_AB_BuildUI(container) return end - MTH_ClearContainer(container) + local panel = MTH_Layout.Panel(container) MTH_AB_STATE.container = container MTH_AB_STATE.built = true local controls = MTH_AB_STATE.controls - controls.title = container:CreateFontString("MetaHuntOptionsAutoBuyTitle", "ARTWORK", "GameFontHighlight") - if controls.title then - if controls.title.SetPoint then controls.title:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -10) end - if controls.title.SetText then controls.title:SetText("Auto Buy") end - end + panel:Title("Auto Buy") + panel:Note("Configure automatic buying rules that trigger when you open a vendor. Quantity is how many STACKS of the item you want in your bags after purchase.", + { lines = 2, color = MTH_Layout.Theme.colText }) - controls.body = container:CreateFontString("MetaHuntOptionsAutoBuyBody", "ARTWORK", "GameFontNormalSmall") - if controls.body then - if controls.body.SetPoint then controls.body:SetPoint("TOPLEFT", container, "TOPLEFT", 176, -30) end - if controls.body.SetWidth then controls.body:SetWidth(400) end - if controls.body.SetJustifyH then controls.body:SetJustifyH("LEFT") end - if controls.body.SetTextColor then controls.body:SetTextColor(1, 1, 1) end - if controls.body.SetText then - controls.body:SetText("Configure automatic buying rules that triggers when you open a vendor.\nThe quantity refers to how many STACKS of the item you want to have in your bags after your purchase") - end - end - - controls.moduleEnabled = MTH_CreateCheckbox(container, "MetaHuntOptionsAutoBuyModuleEnabled", "Enable Auto Buy module", -30) - if controls.moduleEnabled then - controls.moduleEnabled:SetScript("OnClick", function() + controls.moduleEnabled = panel:TopCheckbox("MetaHuntOptionsAutoBuyModuleEnabled", "Enable Auto Buy module", { + onClick = function() if not this or MTH_AB_STATE.syncing then return end if MTH and MTH.SetModuleEnabled then local ok, err = MTH:SetModuleEnabled("autobuy", MTH_AB_IsChecked(this)) @@ -721,70 +712,44 @@ local function MTH_AB_BuildUI(container) end end MTH_AB_RefreshOptionsUI() - end) - end + end, + }) - controls.projectilesEnabled = MTH_CreateCheckbox(container, "MetaHuntOptionsAutoBuyProjectilesEnabled", "Enable for Projectiles", -74) - if controls.projectilesEnabled then - controls.projectilesEnabled:SetScript("OnClick", function() + controls.projectilesEnabled = panel:TopCheckbox("MetaHuntOptionsAutoBuyProjectilesEnabled", "Enable for Projectiles (arrows & bullets)", { + onClick = function() if not this or MTH_AB_STATE.syncing then return end local store = MTH_AB_EnsureConfig() store.projectiles.enabled = MTH_AB_IsChecked(this) MTH_AB_RefreshOptionsUI() - end) - end - - controls.sectionArrowTitle = container:CreateFontString("MetaHuntOptionsAutoBuyArrowsTitle", "ARTWORK", "GameFontHighlight") - if controls.sectionArrowTitle then - if controls.sectionArrowTitle.SetPoint then - controls.sectionArrowTitle:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_AB_LAYOUT.LEFT_SECTION_X, MTH_AB_LAYOUT.SECTION_TITLE_Y) - end - if controls.sectionArrowTitle.SetText then controls.sectionArrowTitle:SetText("Arrows") end - end - - controls.sectionBulletTitle = container:CreateFontString("MetaHuntOptionsAutoBuyBulletsTitle", "ARTWORK", "GameFontHighlight") - if controls.sectionBulletTitle then - if controls.sectionBulletTitle.SetPoint then - controls.sectionBulletTitle:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_AB_LAYOUT.RIGHT_SECTION_X, MTH_AB_LAYOUT.SECTION_TITLE_Y) - end - if controls.sectionBulletTitle.SetText then controls.sectionBulletTitle:SetText("Bullets") end - end + end, + }) + -- ===== LEFT: Arrows / RIGHT: Bullets (3 rules each) ===== + local arrowBox = panel:Box("left", "MetaHuntOptionsAutoBuyArrowsBox", "Arrows") for i = 1, 3 do - local rowY = MTH_AB_LAYOUT.SECTION_ROWS_Y0 - ((i - 1) * MTH_AB_LAYOUT.ROW_STEP) - controls.rows.arrow[i] = MTH_AB_CreateRow(container, "arrow", i, MTH_AB_LAYOUT.LEFT_SECTION_X, rowY) - controls.rows.bullet[i] = MTH_AB_CreateRow(container, "bullet", i, MTH_AB_LAYOUT.RIGHT_SECTION_X, rowY) + controls.rows.arrow[i] = MTH_AB_CreateRow(arrowBox, "arrow", i) end - controls.petFoodTitle = container:CreateFontString("MetaHuntOptionsAutoBuyPetFoodTitle", "ARTWORK", "GameFontHighlight") - if controls.petFoodTitle then - controls.petFoodTitle:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_AB_LAYOUT.LEFT_SECTION_X, -300) - controls.petFoodTitle:SetText("Pet Food") + local bulletBox = panel:Box("right", "MetaHuntOptionsAutoBuyBulletsBox", "Bullets") + for i = 1, 3 do + controls.rows.bullet[i] = MTH_AB_CreateRow(bulletBox, "bullet", i) end - controls.petFoodEnabled = MTH_CreateCheckbox(container, "MetaHuntOptionsAutoBuyPetFoodEnabled", "Enable for Pet Food", -322, MTH_AB_LAYOUT.LEFT_SECTION_X) - if controls.petFoodEnabled then - controls.petFoodEnabled:SetScript("OnClick", function() + -- ===== Pet Food ===== + local petBox = panel:Box(panel:ShorterSide(), "MetaHuntOptionsAutoBuyPetFoodBox", "Pet Food") + + controls.petFoodEnabled = petBox:Checkbox("MetaHuntOptionsAutoBuyPetFoodEnabled", "Enable for Pet Food", { + onClick = function() if not this or MTH_AB_STATE.syncing then return end local store = MTH_AB_EnsureConfig() store.petFood.enabled = MTH_AB_IsChecked(this) MTH_AB_RefreshOptionsUI() - end) - end + end, + }) - controls.petFoodQtyLabel = container:CreateFontString("MetaHuntOptionsAutoBuyPetFoodQtyLabel", "ARTWORK", "GameFontNormalSmall") - if controls.petFoodQtyLabel then - controls.petFoodQtyLabel:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_AB_LAYOUT.LEFT_SECTION_X + 28, -350) - controls.petFoodQtyLabel:SetText("Quantity :") - end - - controls.petFoodQty = CreateFrame("EditBox", "MetaHuntOptionsAutoBuyPetFoodQty", container, "InputBoxTemplate") + controls.petFoodQty = petBox:EditRow("MetaHuntOptionsAutoBuyPetFoodQtyLabel", "Quantity:", + "MetaHuntOptionsAutoBuyPetFoodQty", 40, { numeric = true, height = 20 }) if controls.petFoodQty then - controls.petFoodQty:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_AB_LAYOUT.LEFT_SECTION_X + 95, -347) - controls.petFoodQty:SetWidth(40) - controls.petFoodQty:SetHeight(20) - controls.petFoodQty:SetNumeric(true) - controls.petFoodQty:SetAutoFocus(false) controls.petFoodQty:SetScript("OnEnterPressed", function() if not this then return end local store = MTH_AB_EnsureConfig() @@ -798,43 +763,52 @@ local function MTH_AB_BuildUI(container) MTH_AB_SetPetFoodStacks(store, this:GetText()) MTH_AB_RefreshOptionsUI() end) + local suffix = getglobal("MetaHuntOptionsAutoBuyPetFoodQtySuffix") + if not suffix then + suffix = petBox:Frame():CreateFontString("MetaHuntOptionsAutoBuyPetFoodQtySuffix", "ARTWORK", "GameFontNormalSmall") + end + suffix:ClearAllPoints() + suffix:SetPoint("LEFT", controls.petFoodQty, "RIGHT", 8, 0) + suffix:SetText("stacks") + controls.petFoodQtySuffix = suffix end - controls.petFoodQtySuffix = container:CreateFontString("MetaHuntOptionsAutoBuyPetFoodQtySuffix", "ARTWORK", "GameFontNormalSmall") - if controls.petFoodQtySuffix then - controls.petFoodQtySuffix:SetPoint("LEFT", controls.petFoodQty, "RIGHT", 8, 0) - controls.petFoodQtySuffix:SetText("stacks") - end - - controls.petFoodScopeCurrent = MTH_CreateCheckbox(container, "MetaHuntOptionsAutoBuyPetFoodScopeCurrent", "Buy only for my current pet", -376, MTH_AB_LAYOUT.LEFT_SECTION_X + 28) - if controls.petFoodScopeCurrent then - controls.petFoodScopeCurrent:SetScript("OnClick", function() + controls.petFoodScopeCurrent = petBox:Checkbox("MetaHuntOptionsAutoBuyPetFoodScopeCurrent", "Buy only for my current pet", { + onClick = function() if not this or MTH_AB_STATE.syncing then return end local store = MTH_AB_EnsureConfig() store.petFood.scope = "current" MTH_AB_RefreshOptionsUI() - end) - end + end, + }) - controls.petFoodScopeAll = MTH_CreateCheckbox(container, "MetaHuntOptionsAutoBuyPetFoodScopeAll", "Buy for all my pets", -402, MTH_AB_LAYOUT.LEFT_SECTION_X + 28) - if controls.petFoodScopeAll then - controls.petFoodScopeAll:SetScript("OnClick", function() + controls.petFoodScopeAll = petBox:Checkbox("MetaHuntOptionsAutoBuyPetFoodScopeAll", "Buy for all my pets", { + onClick = function() if not this or MTH_AB_STATE.syncing then return end local store = MTH_AB_EnsureConfig() store.petFood.scope = "all" MTH_AB_RefreshOptionsUI() - end) - end + end, + }) controls.petFoodPetLines = {} for i = 1, 5 do - local rowY = -434 - ((i - 1) * 22) - local icon = container:CreateTexture("MetaHuntOptionsAutoBuyPetFoodPetIcon" .. tostring(i), "ARTWORK") + local f, y = petBox:Row(22) + local padX = petBox:PadX() + local icon = getglobal("MetaHuntOptionsAutoBuyPetFoodPetIcon" .. tostring(i)) + if not icon then + icon = f:CreateTexture("MetaHuntOptionsAutoBuyPetFoodPetIcon" .. tostring(i), "ARTWORK") + end icon:SetWidth(16) icon:SetHeight(16) - icon:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_AB_LAYOUT.LEFT_SECTION_X + 28, rowY) + icon:ClearAllPoints() + icon:SetPoint("TOPLEFT", f, "TOPLEFT", padX + 4, y) icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") - local text = container:CreateFontString("MetaHuntOptionsAutoBuyPetFoodPetText" .. tostring(i), "ARTWORK", "GameFontNormalSmall") + local text = getglobal("MetaHuntOptionsAutoBuyPetFoodPetText" .. tostring(i)) + if not text then + text = f:CreateFontString("MetaHuntOptionsAutoBuyPetFoodPetText" .. tostring(i), "ARTWORK", "GameFontNormalSmall") + end + text:ClearAllPoints() text:SetPoint("LEFT", icon, "RIGHT", 6, 0) text:SetJustifyH("LEFT") text:SetTextColor(1, 1, 1) @@ -842,6 +816,7 @@ local function MTH_AB_BuildUI(container) controls.petFoodPetLines[i] = { icon = icon, text = text } end + panel:Finish() end function MTH_SetupAutoBuyOptions() diff --git a/api/options-autoquest.lua b/api/options-autoquest.lua index 13c7a5f..21f4288 100644 --- a/api/options-autoquest.lua +++ b/api/options-autoquest.lua @@ -48,19 +48,10 @@ function MTH_SetupAutoQuestOptions() local container = MTH_GetFrame("MetaHuntOptionsAutoQuest") if not container then return end - MTH_ClearContainer(container) - - local title = container:CreateFontString("MetaHuntOptionsAutoQuestTitle", "ARTWORK", "GameFontHighlight") - title:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -10) - title:SetText("Auto Quest") - - local body = container:CreateFontString("MetaHuntOptionsAutoQuestBody", "ARTWORK", "GameFontNormalSmall") - body:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -30) - body:SetWidth(560) - body:SetJustifyH("LEFT") - body:SetJustifyV("TOP") - body:SetTextColor(0.45, 0.75, 1) - body:SetText("Automation for specific quest interactions.") + local store = MTH_AQ_EnsureStore() + if store.scorpokTooltip == nil then + store.scorpokTooltip = false + end local moduleEnabled = true if MTH and MTH.GetModule then @@ -73,10 +64,14 @@ function MTH_SetupAutoQuestOptions() elseif MTH and MTH.IsModuleEnabled then moduleEnabled = MTH:IsModuleEnabled("autoquest", false) and true or false end - local moduleToggle = MTH_CreateCheckbox(container, "MetaHuntOptionsAutoQuestModuleToggle", "Enable Auto Quest module", -70, 20) - if moduleToggle then - moduleToggle:SetChecked(moduleEnabled and true or false) - moduleToggle:SetScript("OnClick", function() + + local panel = MTH_Layout.Panel(container) + panel:Title("Auto Quest") + panel:Note("Automation for specific quest interactions.") + + panel:TopCheckbox("MetaHuntOptionsAutoQuestModuleToggle", "Enable Auto Quest module", { + checked = moduleEnabled, + onClick = function() if not this then return end local enabled = MTH_AQ_IsChecked(this) if MTH and MTH.SetModuleEnabled then @@ -88,145 +83,66 @@ function MTH_SetupAutoQuestOptions() local module = MTH and MTH.GetModule and MTH:GetModule("autoquest") or nil local actual = (type(module) == "table" and module.enabled ~= nil) and (module.enabled and true or false) or enabled this:SetChecked(actual and true or false) - end) - end + end, + }) - local store = MTH_AQ_EnsureStore() - if store.scorpokTooltip == nil then - store.scorpokTooltip = false - end - - local sectionWidth = (container:GetWidth() or 600) - 40 - if sectionWidth < 360 then - sectionWidth = 360 - end - - local function ensureSection(name, sectionTitle, yOffset, height) - local section = getglobal(name) - if not section then - section = CreateFrame("Frame", name, container, "OptionFrameBoxTemplate") - end - if not section then - return nil - end - section:SetParent(container) - section:ClearAllPoints() - section:SetPoint("TOPLEFT", container, "TOPLEFT", 20, yOffset) - section:SetWidth(sectionWidth) - section:SetHeight(height) - section:Show() - - local titleFrame = getglobal(name .. "Title") - if titleFrame then - titleFrame:SetText(sectionTitle) - end - return section - end - - local function ensureCheckbox(section, name, label, yOffset, checked) - local check = getglobal(name) - if not check then - check = CreateFrame("CheckButton", name, section, "OptionsCheckButtonTemplate") - end - if not check then - return nil - end - check:SetParent(section) - check:ClearAllPoints() - check:SetPoint("TOPLEFT", section, "TOPLEFT", 12, yOffset) - check:SetChecked(checked and true or false) - check:Show() - - local text = getglobal(name .. "Text") - if text then - text:SetText(label) - end - return check - end - - local scorpokSection = ensureSection("MetaHuntAutoQuestScorpokBox", "Salt of the Scorpok", -130, 112) - if scorpokSection then - local scorpokToggle = ensureCheckbox( - scorpokSection, - "MetaHuntOptionsAutoQuestScorpokToggle", - "Enable SHIFT-rightclick for Bloodmage Drazial", - -10, - store.scorpokDrazial and true or false - ) - if scorpokToggle then - scorpokToggle:SetScript("OnClick", function() - local enabled = MTH_AQ_IsChecked(this) - store.scorpokDrazial = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.scorpokDrazial = store.scorpokDrazial and true or false - end + -- Salt of the Scorpok (left column) + local scorpok = panel:Box("left", "MetaHuntAutoQuestScorpokBox", "Salt of the Scorpok") + scorpok:Checkbox("MetaHuntOptionsAutoQuestScorpokToggle", + "Enable SHIFT-rightclick for Bloodmage Drazial", { + checked = store.scorpokDrazial and true or false, + onClick = function() + local enabled = MTH_AQ_IsChecked(this) + store.scorpokDrazial = enabled and true or false + if MTH and MTH.GetModuleCharSavedVariables then + local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") + if type(moduleStore) == "table" then + moduleStore.scorpokDrazial = store.scorpokDrazial and true or false end - local module = MTH and MTH.GetModule and MTH:GetModule("autoquest") - if module and module.SetScorpokDrazialEnabled then - module:SetScorpokDrazialEnabled(enabled) + end + local module = MTH and MTH.GetModule and MTH:GetModule("autoquest") + if module and module.SetScorpokDrazialEnabled then + module:SetScorpokDrazialEnabled(enabled) + end + end, + }) + scorpok:Checkbox("MetaHuntOptionsAutoQuestScorpokTooltipToggle", + "Enhance Tooltip on Drazial and related beasts (requires Tooltips module enabled)", { + checked = store.scorpokTooltip and true or false, + onClick = function() + local enabled = MTH_AQ_IsChecked(this) + store.scorpokTooltip = enabled and true or false + if MTH and MTH.GetModuleCharSavedVariables then + local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") + if type(moduleStore) == "table" then + moduleStore.scorpokTooltip = store.scorpokTooltip and true or false end - end) - end + end + end, + }) - local tooltipToggle = ensureCheckbox( - scorpokSection, - "MetaHuntOptionsAutoQuestScorpokTooltipToggle", - "Enhance Tooltip on Drazial and related beasts (requires Tooltips module enabled)", - -42, - store.scorpokTooltip and true or false - ) - if tooltipToggle then - tooltipToggle:SetScript("OnClick", function() - local enabled = MTH_AQ_IsChecked(this) - store.scorpokTooltip = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.scorpokTooltip = store.scorpokTooltip and true or false - end + -- Arrows Are For Sissies (right column) + local sissies = panel:Box("right", "MetaHuntAutoQuestSissiesBox", "Arrows Are For Sissies") + sissies:Note("MetaHuntOptionsAutoQuestSissiesNote", + "Note that you should not activate this if you are using LazyPig", { lines = 2 }) + sissies:Checkbox("MetaHuntOptionsAutoQuestSissiesToggle", + "Enable SHIFT-rightclick for Artilleryman Sheldonore", { + checked = store.arrowsForSissies and true or false, + onClick = function() + local enabled = MTH_AQ_IsChecked(this) + store.arrowsForSissies = enabled and true or false + if MTH and MTH.GetModuleCharSavedVariables then + local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") + if type(moduleStore) == "table" then + moduleStore.arrowsForSissies = store.arrowsForSissies and true or false end - end) - end - end + end + local module = MTH and MTH.GetModule and MTH:GetModule("autoquest") + if module and module.SetArrowsForSissiesEnabled then + module:SetArrowsForSissiesEnabled(enabled) + end + end, + }) - local sissiesSection = ensureSection("MetaHuntAutoQuestSissiesBox", "Arrows Are For Sissies", -255, 108) - if sissiesSection then - local sissiesNote = getglobal("MetaHuntOptionsAutoQuestSissiesNote") - if not sissiesNote then - sissiesNote = sissiesSection:CreateFontString("MetaHuntOptionsAutoQuestSissiesNote", "ARTWORK", "GameFontNormalSmall") - end - sissiesNote:ClearAllPoints() - sissiesNote:SetPoint("TOPLEFT", sissiesSection, "TOPLEFT", 14, -10) - sissiesNote:SetWidth(sectionWidth - 28) - sissiesNote:SetJustifyH("LEFT") - sissiesNote:SetTextColor(0.45, 0.75, 1) - sissiesNote:SetText("Note that you should not activate this if you are using LazyPig") - sissiesNote:Show() - - local sissiesToggle = ensureCheckbox( - sissiesSection, - "MetaHuntOptionsAutoQuestSissiesToggle", - "Enable SHIFT-rightclick for Artilleryman Sheldonore", - -40, - store.arrowsForSissies and true or false - ) - if sissiesToggle then - sissiesToggle:SetScript("OnClick", function() - local enabled = MTH_AQ_IsChecked(this) - store.arrowsForSissies = enabled and true or false - if MTH and MTH.GetModuleCharSavedVariables then - local moduleStore = MTH:GetModuleCharSavedVariables("autoquest") - if type(moduleStore) == "table" then - moduleStore.arrowsForSissies = store.arrowsForSissies and true or false - end - end - local module = MTH and MTH.GetModule and MTH:GetModule("autoquest") - if module and module.SetArrowsForSissiesEnabled then - module:SetArrowsForSissiesEnabled(enabled) - end - end) - end - end + panel:Finish() end diff --git a/api/options-chronometer.lua b/api/options-chronometer.lua index 446c3cf..2a5c097 100644 --- a/api/options-chronometer.lua +++ b/api/options-chronometer.lua @@ -358,25 +358,14 @@ local function MTH_CHRON_NormalizeSection(tabKey) return "general" end -local function MTH_CHRON_RenderTimerSection(container, profile, title, items, bucket, opts) - opts = opts or {} - local timerX = opts.x or 20 - local timerY = opts.y or -84 - local prefix = tostring(opts.prefix or "Main") - - MTH_CHRON_MakeTitle(container, "MetaHuntOptionsChronometerTimersHeader" .. prefix, title, timerX, timerY) - timerY = timerY - 24 - +local function MTH_CHRON_RenderTimerBox(box, profile, items, bucket, prefix) + prefix = tostring(prefix or "Main") if table.getn(items) == 0 then - MTH_CHRON_MakeSmall(container, "MetaHuntOptionsChronometerSectionEmpty" .. prefix, "(none)", timerX + 8, timerY) + box:Text("MetaHuntOptionsChronometerSectionEmpty" .. prefix, "(none)") return end for i = 1, table.getn(items) do - if timerY < -560 then - MTH_CHRON_MakeSmall(container, "MetaHuntOptionsChronometerOverflow" .. prefix, "...more", timerX + 8, timerY) - break - end local row = items[i] local timerName = row local label = row @@ -387,11 +376,9 @@ local function MTH_CHRON_RenderTimerSection(container, profile, title, items, bu timerName = tostring(timerName or "") label = tostring(label or timerName) local controlName = "MetaHuntOptionsChronometerTimer" .. prefix .. tostring(bucket) .. tostring(i) - local check = MTH_CreateCheckbox(container, controlName, label, timerY, timerX) - if check then - check:SetChecked(profile.disabledSpells[bucket][timerName] == nil and true or false) - check:SetScript("OnClick", function() - if not this then return end + box:Checkbox(controlName, label, { + checked = profile.disabledSpells[bucket][timerName] == nil, + onClick = function() local currentlyDisabled = profile.disabledSpells[bucket][timerName] ~= nil if currentlyDisabled then profile.disabledSpells[bucket][timerName] = nil @@ -402,9 +389,8 @@ local function MTH_CHRON_RenderTimerSection(container, profile, title, items, bu end MTH_CHRON_ApplyLiveProfile(profile) - end) - end - timerY = timerY - 20 + end, + }) end end @@ -461,210 +447,185 @@ function MTH_SetupChronometerOptions(tabKey) local container = MTH_GetFrame("MetaHuntOptionsChronometer") if not container then return end - MTH_ClearContainer(container) MTH_CHRON_STATE.controls = {} local profile = MTH_CHRON_GetProfile() local _, class = UnitClass("player") class = class or "HUNTER" - MTH_CHRON_MakeTitle(container, "MetaHuntOptionsChronometerTitle", "Chronometer", 20, -10) + local panel = MTH_Layout.Panel(container) + panel:Title("Chronometer") - local moduleCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerEnabled", "Enable Chronometer module", -54) - if moduleCheck then + local moduleEnabled = false + do local module = MTH and MTH:GetModule("chronometer") - moduleCheck:SetChecked(module and module.enabled and true or false) - moduleCheck:SetScript("OnClick", function() - if not this then return end - local enabled = MTH_CHRON_IsChecked(this) + moduleEnabled = module and module.enabled and true or false + end + panel:TopCheckbox("MetaHuntOptionsChronometerEnabled", "Enable Chronometer module", { + checked = moduleEnabled, + onClick = function() + local enabled = this:GetChecked() == 1 if MTH and MTH.SetModuleEnabled then MTH:SetModuleEnabled("chronometer", enabled) end - end) - end + end, + }) if section == "general" then - MTH_CHRON_MakeTitle(container, "MetaHuntOptionsChronometerGeneralHeader", "General", 20, -84) - - local ghostSlider = MTH_CreateSlider(container, "MetaHuntOptionsChronometerGhost", "Ghost Duration", 0, 30, 1, -112) - if ghostSlider then - ghostSlider:SetValue(tonumber(profile.ghost) or 0) - ghostSlider.onChange = function(value) + local genBox = panel:Box("left", "MetaHuntOptionsChronometerGeneralBox", "General") + genBox:Slider("MetaHuntOptionsChronometerGhost", { + label = "Ghost Duration", min = 0, max = 30, step = 1, + value = tonumber(profile.ghost) or 0, + onChange = function(value) profile.ghost = tonumber(value) or 0 MTH_CHRON_ApplyLiveProfile(profile) - end - end - - local killCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerFadeOnKill", "Fade on kill", -154) - if killCheck then - killCheck:SetChecked(profile.fadeonkill and true or false) - killCheck:SetScript("OnClick", function() - if not this then return end - profile.fadeonkill = MTH_CHRON_IsChecked(this) - end) - end - - local fadeCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerFadeOnFade", "Fade on aura fade", -179) - if fadeCheck then - fadeCheck:SetChecked(profile.fadeonfade and true or false) - fadeCheck:SetScript("OnClick", function() - if not this then return end - profile.fadeonfade = MTH_CHRON_IsChecked(this) - end) - end - - local selfCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerSelfBars", "Show self bars", -204) - if selfCheck then - selfCheck:SetChecked(profile.selfbars and true or false) - selfCheck:SetScript("OnClick", function() - if not this then return end - profile.selfbars = MTH_CHRON_IsChecked(this) - end) - end - - local onlySelfCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerOnlySelf", "Only self target", -229) - if onlySelfCheck then - onlySelfCheck:SetChecked(profile.onlyself and true or false) - onlySelfCheck:SetScript("OnClick", function() - if not this then return end - profile.onlyself = MTH_CHRON_IsChecked(this) - end) - end + end, + }) + genBox:Checkbox("MetaHuntOptionsChronometerFadeOnKill", "Fade on kill", { + checked = profile.fadeonkill and true or false, + onClick = function() profile.fadeonkill = this:GetChecked() == 1 end, + }) + genBox:Checkbox("MetaHuntOptionsChronometerFadeOnFade", "Fade on aura fade", { + checked = profile.fadeonfade and true or false, + onClick = function() profile.fadeonfade = this:GetChecked() == 1 end, + }) + genBox:Checkbox("MetaHuntOptionsChronometerSelfBars", "Show self bars", { + checked = profile.selfbars and true or false, + onClick = function() profile.selfbars = this:GetChecked() == 1 end, + }) + genBox:Checkbox("MetaHuntOptionsChronometerOnlySelf", "Only self target", { + checked = profile.onlyself and true or false, + onClick = function() profile.onlyself = this:GetChecked() == 1 end, + }) elseif section == "bar" then - MTH_CHRON_MakeTitle(container, "MetaHuntOptionsChronometerBarHeader", "Bars", 20, -84) - - local runTestButton = MTH_CreateActionButton(container, "MetaHuntOptionsChronometerRunTest", "Run Test", 20, -112, 130, 22, function() - local engine = MTH_ChronometerHunter - if engine and engine.RunTest then - engine:RunTest() - end - end) - local anchorButton = MTH_CreateActionButton(container, "MetaHuntOptionsChronometerAnchor", "Toggle Anchor", 165, -112, 130, 22, function() - local engine = MTH_ChronometerHunter - if engine and engine.ToggleAnchor then - engine:ToggleAnchor() - end - end) - if runTestButton then runTestButton:Show() end - if anchorButton then anchorButton:Show() end - + local ctrlBox = panel:Box("left", "MetaHuntOptionsChronometerControlsBox", "Controls") + ctrlBox:Buttons({ + { name = "MetaHuntOptionsChronometerRunTest", label = "Run Test", width = 120, + onClick = function() + local engine = MTH_ChronometerHunter + if engine and engine.RunTest then engine:RunTest() end + end }, + { name = "MetaHuntOptionsChronometerAnchor", label = "Toggle Anchor", width = 120, + onClick = function() + local engine = MTH_ChronometerHunter + if engine and engine.ToggleAnchor then engine:ToggleAnchor() end + end }, + }) local anchorX = tonumber(profile.barposition and profile.barposition.x) or 0 local anchorY = tonumber(profile.barposition and profile.barposition.y) or 0 - MTH_CHRON_MakeSmall(container, "MetaHuntOptionsChronometerAnchorPos", "Anchor: X " .. tostring(anchorX) .. " Y " .. tostring(anchorY), 20, -142) + ctrlBox:Note("MetaHuntOptionsChronometerAnchorPos", + "Anchor: X " .. tostring(anchorX) .. " Y " .. tostring(anchorY)) - local widthSlider = MTH_CreateSlider(container, "MetaHuntOptionsChronometerBarWidth", "Bar Width", 80, 320, 1, -180) - if widthSlider then - widthSlider:SetValue(tonumber(profile.barwidth) or 220) - widthSlider.onChange = function(value) + local dimBox = panel:Box("left", "MetaHuntOptionsChronometerDimBox", "Dimensions") + dimBox:Slider("MetaHuntOptionsChronometerBarWidth", { + label = "Bar Width", min = 80, max = 320, step = 1, + value = tonumber(profile.barwidth) or 220, + onChange = function(value) profile.barwidth = tonumber(value) or 220 MTH_CHRON_ApplyLiveProfile(profile) - end - end - - local scaleSlider = MTH_CreateSlider(container, "MetaHuntOptionsChronometerBarScale", "Bar Scale", 0.5, 1.5, 0.1, -222) - if scaleSlider then - scaleSlider:SetValue(tonumber(profile.barscale) or 1) - scaleSlider.onChange = function(value) + end, + }) + dimBox:Slider("MetaHuntOptionsChronometerBarScale", { + label = "Bar Scale", min = 0.5, max = 1.5, step = 0.1, + value = tonumber(profile.barscale) or 1, + format = function(v) return string.format("%.1f", v) end, + onChange = function(value) profile.barscale = tonumber(value) or 1 MTH_CHRON_ApplyLiveProfile(profile) - end - end - - local heightSlider = MTH_CreateSlider(container, "MetaHuntOptionsChronometerBarHeight", "Bar Height", 8, 30, 1, -264) - if heightSlider then - heightSlider:SetValue(tonumber(profile.barheight) or 16) - heightSlider.onChange = function(value) + end, + }) + dimBox:Slider("MetaHuntOptionsChronometerBarHeight", { + label = "Bar Height", min = 8, max = 30, step = 1, + value = tonumber(profile.barheight) or 16, + onChange = function(value) profile.barheight = tonumber(value) or 16 MTH_CHRON_ApplyLiveProfile(profile) - end - end - - local spacingSlider = MTH_CreateSlider(container, "MetaHuntOptionsChronometerBarSpacing", "Bar Spacing", 0, 15, 1, -306) - if spacingSlider then - spacingSlider:SetValue(tonumber(profile.spacing) or 0) - spacingSlider.onChange = function(value) + end, + }) + dimBox:Slider("MetaHuntOptionsChronometerBarSpacing", { + label = "Bar Spacing", min = 0, max = 15, step = 1, + value = tonumber(profile.spacing) or 0, + onChange = function(value) profile.spacing = tonumber(value) or 0 MTH_CHRON_ApplyLiveProfile(profile) - end - end - - local textSizeSlider = MTH_CreateSlider(container, "MetaHuntOptionsChronometerTextSize", "Text Size", 8, 20, 1, -348) - if textSizeSlider then - textSizeSlider:SetValue(tonumber(profile.textsize) or 10) - textSizeSlider.onChange = function(value) + end, + }) + dimBox:Slider("MetaHuntOptionsChronometerTextSize", { + label = "Text Size", min = 8, max = 20, step = 1, + value = tonumber(profile.textsize) or 10, + onChange = function(value) profile.textsize = tonumber(value) or 10 MTH_CHRON_ApplyLiveProfile(profile) - end - end + end, + }) - MTH_CHRON_MakeSmall(container, "MetaHuntOptionsChronometerTextPatternLabel", "Bar Text ($s spell, $t target)", 20, -390) - MTH_CHRON_MakeInput(container, "MetaHuntOptionsChronometerTextPattern", 20, -408, 220, 20, profile.text or "$t", function(newText) - if not newText or newText == "" then - newText = "$t" - end - profile.text = tostring(newText) - MTH_CHRON_ApplyLiveProfile(profile) - end) - - local growCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerGrowUp", "Grow bars upward", -438) - if growCheck then - growCheck:SetChecked(profile.growup and true or false) - growCheck:SetScript("OnClick", function() - if not this then return end - profile.growup = MTH_CHRON_IsChecked(this) + local textBox = panel:Box("right", "MetaHuntOptionsChronometerTextBox", "Text & Growth") + textBox:Note("MetaHuntOptionsChronometerTextPatternHint", + "Bar text tokens: $s = spell, $t = target", { lines = 2 }) + local patternEdit = textBox:EditRow("MetaHuntOptionsChronometerTextPatternLabel", "Bar Text", + "MetaHuntOptionsChronometerTextPattern", 150, { height = 20 }) + if patternEdit then + patternEdit:SetText(tostring(profile.text or "$t")) + patternEdit:SetScript("OnEnterPressed", function() + local newText = this:GetText() or "" + if newText == "" then newText = "$t" end + profile.text = tostring(newText) MTH_CHRON_ApplyLiveProfile(profile) + this:ClearFocus() end) end - - local reverseCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerReverse", "Reverse bars", -463) - if reverseCheck then - reverseCheck:SetChecked(profile.reverse and true or false) - reverseCheck:SetScript("OnClick", function() - if not this then return end - profile.reverse = MTH_CHRON_IsChecked(this) + textBox:Checkbox("MetaHuntOptionsChronometerGrowUp", "Grow bars upward", { + checked = profile.growup and true or false, + onClick = function() + profile.growup = this:GetChecked() == 1 MTH_CHRON_ApplyLiveProfile(profile) - end) - end + end, + }) + textBox:Checkbox("MetaHuntOptionsChronometerReverse", "Reverse bars", { + checked = profile.reverse and true or false, + onClick = function() + profile.reverse = this:GetChecked() == 1 + MTH_CHRON_ApplyLiveProfile(profile) + end, + }) - MTH_CHRON_MakeSmall(container, "MetaHuntOptionsChronometerIconPosLabel", "Icon Position", 20, -494) - local iconLeft = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerIconLeft", "Left", -514) - local iconRight = MTH_CreateCheckbox(container, "MetaHuntOptionsChronometerIconRight", "Right", -538) - if iconLeft and iconRight then - iconLeft:SetChecked(profile.iconposition ~= "RIGHT") - iconRight:SetChecked(profile.iconposition == "RIGHT") - iconLeft:SetScript("OnClick", function() + local iconBox = panel:Box("right", "MetaHuntOptionsChronometerIconBox", "Icon Position") + local iconLeft, iconRight + iconLeft = iconBox:Checkbox("MetaHuntOptionsChronometerIconLeft", "Left", { + checked = profile.iconposition ~= "RIGHT", + onClick = function() profile.iconposition = "LEFT" iconLeft:SetChecked(true) iconRight:SetChecked(nil) MTH_CHRON_ApplyLiveProfile(profile) - end) - iconRight:SetScript("OnClick", function() + end, + }) + iconRight = iconBox:Checkbox("MetaHuntOptionsChronometerIconRight", "Right", { + checked = profile.iconposition == "RIGHT", + onClick = function() profile.iconposition = "RIGHT" iconRight:SetChecked(true) iconLeft:SetChecked(nil) MTH_CHRON_ApplyLiveProfile(profile) - end) - end - + end, + }) else local classSpells, classEvents, racial = MTH_CHRON_BuildTimerLists() if section == "classspells" then - MTH_CHRON_RenderTimerSection(container, profile, "Hunter Spells", classSpells, class, { - x = 20, - y = -84, - prefix = "ClassSpells", - }) - MTH_CHRON_RenderTimerSection(container, profile, "Hunter Trinkets", MTH_CHRON_BuildHunterTrinketItems(classEvents), class, { - x = 300, - y = -84, - prefix = "ClassTrinkets", - }) + local spellsBox = panel:Box("left", "MetaHuntOptionsChronometerSpellsBox", "Hunter Spells") + MTH_CHRON_RenderTimerBox(spellsBox, profile, classSpells, class, "ClassSpells") + local trinketBox = panel:Box("right", "MetaHuntOptionsChronometerTrinketsBox", "Hunter Trinkets") + MTH_CHRON_RenderTimerBox(trinketBox, profile, MTH_CHRON_BuildHunterTrinketItems(classEvents), class, "ClassTrinkets") elseif section == "classevents" then - MTH_CHRON_RenderTimerSection(container, profile, "Hunter Events", classEvents, class, { prefix = "ClassEvents" }) + local eventsBox = panel:Box("left", "MetaHuntOptionsChronometerEventsBox", "Hunter Events") + MTH_CHRON_RenderTimerBox(eventsBox, profile, classEvents, class, "ClassEvents") elseif section == "racial" then - MTH_CHRON_RenderTimerSection(container, profile, "Racial", racial, "RACIAL", { prefix = "Racial" }) + local racialBox = panel:Box("left", "MetaHuntOptionsChronometerRacialBox", "Racial") + MTH_CHRON_RenderTimerBox(racialBox, profile, racial, "RACIAL", "Racial") end end + + panel:Finish() end if _G then diff --git a/api/options-credits.lua b/api/options-credits.lua index 25603ac..f729e2d 100644 --- a/api/options-credits.lua +++ b/api/options-credits.lua @@ -96,12 +96,8 @@ function MTH_SetupCreditsOptions() local container = MTH_GetFrame("MetaHuntOptionsCredits") if not container then return end - MTH_ClearContainer(container) - - local title = container:CreateFontString("MetaHuntOptionsCreditsTitle", "ARTWORK", "GameFontNormal") - title:SetPoint("TOPLEFT", container, "TOPLEFT", 10, -10) - title:SetText(MTH_CR_L("CREDITS_TITLE", "Credits")) - title:SetTextColor(1.00, 0.82, 0.00) + local panel = MTH_Layout.Panel(container) + local title = panel:Title(MTH_CR_L("CREDITS_TITLE", "Credits")) local scrollFrame = MTH_GetFrame("MetaHuntOptionsCreditsScroll") if not scrollFrame then @@ -124,12 +120,12 @@ function MTH_SetupCreditsOptions() cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_ABOUT_CODE_INTRO", "This addon is, in others, a compilation of old vanilla addons that I loved and played with during years. But most of them had become very buggy with TwoW as new contain was added to the server. \n I fixed, reworked, enhanced, and melted them within a modern modular framework that I created.\n" - .. "I want to make it crystal clear about what work is mine, and what is not and respectfully credit the original authors for the great stuff I've taken from them:"), + .. "I want to make it clear about what work is mine, and what is not and respectfully credit the original authors for the great stuff I've taken from them:"), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, MTH_CR_L("CREDITS_ABOUT_ZHUNTER", - "- zBars, Antidaze and Autostrip were core functionalities of Vanilla zHunterMod addon. And I kept the \"z\" naming of bars/buttons to always remember it. On top of this I have created myself zBars, zAmmo, zCompanions, zMount, zToys, zCraft.\nAlso the SmartAmmo feature idea is coming from Zhuntermod: there was a file in it with that functionality, but was like a work-in-progress, totally unfunctional and even \"dangerous\" in its current state, and was not active in the addon. Since it was a fucking great idea, I recoded it mostly from scratch and made it work reliably and safely."), + "- zBars, Antidaze and Autostrip were core functionalities of Vanilla zHunterMod addon. And I kept the \"z\" naming of bars/buttons to always remember it. I improve it and added zBar, zAmmo, zCompanions, zMount, zToys, zCraft.\nAlso the SmartAmmo feature idea is coming from Zhuntermod: there was a file in it with that functionality, but was like a work-in-progress, totally unfunctional and even \"dangerous\" in its current state, and was not active in the addon. Since it was a fucking great idea, I recoded it mostly from scratch and made it work reliably and safely."), "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, @@ -189,7 +185,7 @@ function MTH_SetupCreditsOptions() "GameFontNormalSmall", -10) cursor = MTH_CreditsCreateText(content, cursor, - MTH_CR_L("CREDITS_CLOSING", "With that being said, go back hunting fellas! Preferably on Octowow."), + MTH_CR_L("CREDITS_CLOSING", ""), "GameFontNormalSmall", -10, 1.00, 0.82, 0.00) cursor = MTH_CreditsCreateText(content, cursor, diff --git a/api/options-expammo.lua b/api/options-expammo.lua index 69b0651..75fc179 100644 --- a/api/options-expammo.lua +++ b/api/options-expammo.lua @@ -15,16 +15,7 @@ local function MTH_EA_OPT_GetCfg() return MTH_SavedVariables.modules["expammo"] or {} end -local MTH_EA_OPT_built = false local MTH_EA_OPT_STATE = { bindStatus = nil, bindButton = nil, captureFrame = nil } - --- ── Layout constants ────────────────────────────────────────── --- Two equal columns separated by a gutter. Values are offsets from the --- container TOPLEFT corner (positive X right, negative Y down). -local OPT_L = 20 -- left column X -local OPT_R = 300 -- right column X -local OPT_CW = 255 -- column width (used for text wrapping and sliders) - local function MTH_EA_OPT_GetBindText() if not GetBindingKey then return "Unbound" end local k1, k2 = GetBindingKey("EXPAMMO ACTION") @@ -119,271 +110,168 @@ local function MTH_EA_OPT_StartCapture() if bb then bb:SetText("Press key...") end end --- Helper: checkbox anchored to a given X column -local function MTH_EA_OPT_CB(container, name, label, yOff, xOff, isChecked, onClick) - local cb = MTH_CreateCheckbox(container, name, label, yOff, xOff or OPT_L) - if cb then - cb:SetChecked(isChecked and 1 or 0) - cb:SetScript("OnClick", function() - local v = this:GetChecked() - onClick(v == 1 or v == true) - end) - end - return cb -end - --- Helper: section header label -local function MTH_EA_OPT_Header(container, text, yOff, xOff) - local fs = container:CreateFontString(nil, "ARTWORK", "GameFontNormal") - fs:SetPoint("TOPLEFT", container, "TOPLEFT", xOff or OPT_L, yOff) - fs:SetText("|cffff9900" .. text .. "|r") - return fs -end - --- Helper: small descriptive text -local function MTH_EA_OPT_Tip(container, text, yOff, xOff, r, g, b) - local fs = container:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") - fs:SetPoint("TOPLEFT", container, "TOPLEFT", xOff or OPT_L, yOff) - fs:SetWidth(OPT_CW) - fs:SetJustifyH("LEFT") - fs:SetTextColor(r or 0.6, g or 0.6, b or 0.6) - fs:SetText(text) - return fs -end - --- Enable or disable a list of widgets (supports Frames/Buttons and FontStrings) -local function MTH_EA_OPT_SetGroupEnabled(widgets, enabled) - for _, w in ipairs(widgets) do - if w then - if w.Enable and w.Disable then - -- Frame / Button / CheckButton - if enabled then w:Enable() else w:Disable() end - end - -- Dim all (including FontStrings and frames) via alpha - if w.SetAlpha then - w:SetAlpha(enabled and 1.0 or 0.35) - end - end - end -end - local function MTH_EA_OPT_BuildUI(container) - if MTH_EA_OPT_built then return end - MTH_EA_OPT_built = true - local cfg = MTH_EA_OPT_GetCfg() local showHint = cfg.showHint ~= false - --------------------------------------------------------------------------- - -- Title - --------------------------------------------------------------------------- - local title = container:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge") - title:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L, -16) - title:SetText("MM Widget — Experimental Ammunition Tracker") + local panel = MTH_Layout.Panel(container) + panel:Title("MM Widget — Experimental Ammunition Tracker") - --------------------------------------------------------------------------- - -- LEFT COLUMN - --------------------------------------------------------------------------- - - -- ── Module ────────────────────────────────────────────── - MTH_EA_OPT_Header(container, "Module", -46) + -- Right-column gating: dim the box frames and disable their interactive + -- controls when the "Show action hint cell" checkbox is off. + local rightBoxes = {} + local rightControls = {} + local function setRightEnabled(enabled) + local i + for i = 1, table.getn(rightBoxes) do + local f = rightBoxes[i] + if f and f.SetAlpha then f:SetAlpha(enabled and 1.0 or 0.35) end + end + for i = 1, table.getn(rightControls) do + local c = rightControls[i] + if c and c.Enable and c.Disable then + if enabled then c:Enable() else c:Disable() end + end + end + end + -- ===== LEFT: Module ===== + local moduleBox = panel:Box("left", "MTH_ExpAmmoModuleBox", "Module") local moduleEnabled = true if MTH and MTH.IsModuleEnabled then moduleEnabled = MTH:IsModuleEnabled("expammo", true) and true or false end - MTH_EA_OPT_CB(container, "MTH_ExpAmmoModuleCB", - "Enable MM Widget module", - -66, OPT_L, moduleEnabled, - function(v) + moduleBox:Checkbox("MTH_ExpAmmoModuleCB", "Enable MM Widget module", { + checked = moduleEnabled, + onClick = function() + local v = this:GetChecked() == 1 if MTH and MTH.SetModuleEnabled then MTH:SetModuleEnabled("expammo", v) end if MTH_ExpAmmo then MTH_ExpAmmo.SetEnabled(v) end - end) + end, + }) - -- ── Tracker Visibility ─────────────────────────────────── - MTH_EA_OPT_Header(container, "Tracker Visibility", -100) - - MTH_EA_OPT_CB(container, "MTH_ExpAmmoShowLnLCB", - "Show Lock and Load cell (top)", - -120, OPT_L, cfg.showLnL ~= false, - function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetShowLnL(v) end end) - - -- "Show action hint cell" also gates the right column - local rightGroup = {} -- filled below; toggled when this CB changes - local showHintCB = MTH_EA_OPT_CB(container, "MTH_ExpAmmoShowHintCB", - "Show action hint cell (bottom)", - -144, OPT_L, showHint, - function(v) + -- ===== LEFT: Tracker Visibility ===== + local visBox = panel:Box("left", "MTH_ExpAmmoVisBox", "Tracker Visibility") + visBox:Checkbox("MTH_ExpAmmoShowLnLCB", "Show Lock and Load cell (top)", { + checked = cfg.showLnL ~= false, + onClick = function() if MTH_ExpAmmo then MTH_ExpAmmo.SetShowLnL(this:GetChecked() == 1) end end, + }) + visBox:Checkbox("MTH_ExpAmmoShowHintCB", "Show action hint cell (bottom)", { + checked = showHint, + onClick = function() + local v = this:GetChecked() == 1 if MTH_ExpAmmo then MTH_ExpAmmo.SetShowHint(v) end - MTH_EA_OPT_SetGroupEnabled(rightGroup, v) - end) + setRightEnabled(v) + end, + }) + visBox:Checkbox("MTH_ExpAmmoHideOOCCB", "Hide widget when out of combat", { + checked = cfg.hideOOC == true, + onClick = function() if MTH_ExpAmmo then MTH_ExpAmmo.SetHideOOC(this:GetChecked() == 1) end end, + }) - MTH_EA_OPT_CB(container, "MTH_ExpAmmoHideOOCCB", - "Hide widget when out of combat", - -168, OPT_L, cfg.hideOOC == true, - function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetHideOOC(v) end end) + -- ===== LEFT: Appearance ===== + local appBox = panel:Box("left", "MTH_ExpAmmoAppBox", "Appearance") + appBox:Slider("MTH_ExpAmmoCellSizeSlider", { + label = "Cell Size (px)", min = 20, max = 80, step = 4, + value = math.max(20, math.min(80, cfg.cellSize or 40)), + onChange = function(val) if MTH_ExpAmmo then MTH_ExpAmmo.SetCellSize(val) end end, + }) + appBox:Slider("MTH_ExpAmmoCellGapSlider", { + label = "Cell Spacing (px)", min = 0, max = 20, step = 1, + value = math.max(0, math.min(20, cfg.cellGap or 2)), + onChange = function(val) if MTH_ExpAmmo then MTH_ExpAmmo.SetCellGap(val) end end, + }) + appBox:Checkbox("MTH_ExpAmmoBigCDCB", "Large cooldown numbers (centered in cell)", { + checked = cfg.bigCD == true, + onClick = function() if MTH_ExpAmmo then MTH_ExpAmmo.SetBigCD(this:GetChecked() == 1) end end, + }) - -- ── Appearance ─────────────────────────────────────────── - MTH_EA_OPT_Header(container, "Appearance", -202) - - local sizeSlider = MTH_CreateSlider(container, "MTH_ExpAmmoCellSizeSlider", - "Cell Size (px)", 20, 80, 4, -222) - if sizeSlider then - sizeSlider:ClearAllPoints() - sizeSlider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L + 5, -222) - sizeSlider:SetWidth(OPT_CW - 10) - sizeSlider:SetValue(math.max(20, math.min(80, cfg.cellSize or 40))) - sizeSlider.onChange = function(val) - if MTH_ExpAmmo then MTH_ExpAmmo.SetCellSize(val) end - end - end - - local gapSlider = MTH_CreateSlider(container, "MTH_ExpAmmoCellGapSlider", - "Cell Spacing (px)", 0, 20, 1, -286) - if gapSlider then - gapSlider:ClearAllPoints() - gapSlider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_L + 5, -286) - gapSlider:SetWidth(OPT_CW - 10) - gapSlider:SetValue(math.max(0, math.min(20, cfg.cellGap or 2))) - gapSlider.onChange = function(val) - if MTH_ExpAmmo then MTH_ExpAmmo.SetCellGap(val) end - end - end - - MTH_EA_OPT_CB(container, "MTH_ExpAmmoBigCDCB", - "Large cooldown numbers (centered in cell)", - -350, OPT_L, cfg.bigCD == true, - function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetBigCD(v) end end) - - -- ── Position ───────────────────────────────────────────── - MTH_EA_OPT_Header(container, "Position", -390) - - local anchorButton = MTH_CreateActionButton(container, "MTH_ExpAmmoToggleAnchor", - "Toggle Anchor", OPT_L, -410, 130, 22, function() - if MTH_ExpAmmo then MTH_ExpAmmo.ToggleAnchor() end - end) - if anchorButton then anchorButton:Show() end - - MTH_EA_OPT_Tip(container, + -- ===== LEFT: Position ===== + local posBox = panel:Box("left", "MTH_ExpAmmoPosBox", "Position") + posBox:Button("MTH_ExpAmmoToggleAnchor", "Toggle Anchor", { + width = 130, + onClick = function() if MTH_ExpAmmo then MTH_ExpAmmo.ToggleAnchor() end end, + }) + posBox:Note("MTH_ExpAmmoPosNote", "Shows 4 drag handles around the widget (top / bottom / left / right). Drag any one to reposition it.", - -436, OPT_L) + { color = MTH_Layout.Theme.colText, lines = 3 }) - --------------------------------------------------------------------------- - -- RIGHT COLUMN — Action Hint Cell (keybind + casting) - -- All widgets here are greyed out when the bottom cell is hidden. - --------------------------------------------------------------------------- - - -- Vertical divider line - local divider = container:CreateTexture(nil, "BACKGROUND") - divider:SetTexture(0.3, 0.3, 0.3, 0.6) - divider:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R - 10, -46) - divider:SetPoint("BOTTOMLEFT", container, "TOPLEFT", OPT_R - 10, -530) - divider:SetWidth(1) - - -- Column title - local rTitle = container:CreateFontString(nil, "ARTWORK", "GameFontHighlight") - rTitle:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, -46) - rTitle:SetText("|cffff9900" .. "Action Hint Cell" .. "|r") - table.insert(rightGroup, rTitle) - - - local rSubtitle = MTH_EA_OPT_Tip(container, - "Options below apply to the bottom cell only.\n Disable \"Show action hint cell\" to hide the cell and these controls.", - -64, OPT_R, 1.00, 1.00, 1.00) - if rSubtitle then rSubtitle:SetWidth(290) end - table.insert(rightGroup, rSubtitle) - - local rNamPowerTip = MTH_EA_OPT_Tip(container, + -- ===== RIGHT: Smart Action Cell (helper + keybind + casting) ===== + -- One feature = one box. The intro notes explain the bottom cell, and the + -- Keybind / Casting controls that drive it live underneath as sub-sections. + local cellBox = panel:Box("right", "MTH_ExpAmmoCellBox", "Smart Action Cell") + table.insert(rightBoxes, cellBox:Frame()) + cellBox:Text("MTH_ExpAmmoHintSub", + "Options below apply to the bottom cell only. Disable \"Show action hint cell\" to hide the cell and these controls.", + { color = { 1, 1, 1 }, lines = 3 }) + cellBox:Note("MTH_ExpAmmoHintNam", "Notes:\n - You need NAMPOWER for this to work reliably.\n - Aimed/Steady/Multi/Arcane Shots SHOULD BE on your action bars.", - -100, OPT_R, 0.35, 0.65, 1.00) - if rNamPowerTip then rNamPowerTip:SetWidth(290) end - table.insert(rightGroup, rNamPowerTip) + { lines = 3 }) - -- ── Keybind ────────────────────────────────────────────── - table.insert(rightGroup, MTH_EA_OPT_Header(container, "Keybind", -182, OPT_R)) + -- Keybind sub-section + cellBox:Gap(4) + cellBox:Text("MTH_ExpAmmoKeyHead", "Keybind", + { font = MTH_Layout.Theme.fontBoxTitle, color = MTH_Layout.Theme.colPanelTitle }) + cellBox:Note("MTH_ExpAmmoBindTip", + "One key to rule them all.\n Aimed Shot when ready, Steady Shot while Aimed is on cooldown, consume-spell (Multi / Serpent / Arcane) when proc is up.", + { lines = 4 }) + MTH_EA_OPT_STATE.bindStatus = cellBox:Text("MTH_ExpAmmoBindStatus", + "Key: " .. MTH_EA_OPT_GetBindText()) + local keyButtons = cellBox:Buttons({ + { name = "MTH_ExpAmmoBindButton", label = "Set Key", width = 90, + onClick = function() + if MTH_EA_OPT_STATE.captureFrame and MTH_EA_OPT_STATE.captureFrame:IsShown() then + MTH_EA_OPT_StopCapture() + else + MTH_EA_OPT_StartCapture() + end + end }, + { name = "MTH_ExpAmmoBindClear", label = "Clear", width = 70, + onClick = function() MTH_EA_OPT_SaveBinding(nil) end }, + }) + MTH_EA_OPT_STATE.bindButton = keyButtons[1] + table.insert(rightControls, keyButtons[1]) + table.insert(rightControls, keyButtons[2]) - local bindTip = MTH_EA_OPT_Tip(container, - "One key to rule them all.\n Aimed Shot when ready, Steady Shot while Aimed is on cooldown,\n consume-spell (Multi / Serpent / Arcane) when proc is up.", - -200, OPT_R, 0.65, 0.65, 0.65) - if bindTip then bindTip:SetWidth(290) end - table.insert(rightGroup, bindTip) + -- Casting sub-section + cellBox:Gap(4) + cellBox:Text("MTH_ExpAmmoCastHead", "Casting", + { font = MTH_Layout.Theme.fontBoxTitle, color = MTH_Layout.Theme.colPanelTitle }) + local quiverCB = cellBox:Checkbox("MTH_ExpAmmoQuiverNoClipCB", "Use Quiver no-clip casting", { + checked = cfg.useQuiverNoClip == true, + onClick = function() if MTH_ExpAmmo then MTH_ExpAmmo.SetUseQuiverNoClip(this:GetChecked() == 1) end end, + }) + table.insert(rightControls, quiverCB) + cellBox:Note("MTH_ExpAmmoQuiverTip", + "Requires the Quiver addon. Routes Aimed/Steady/Multi cast through Quiver.CastNoClip to avoid clipping your auto-shot swing timer.", + { lines = 3 }) - local bindStatus = container:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") - bindStatus:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, -248) - bindStatus:SetText("Key: " .. MTH_EA_OPT_GetBindText()) - MTH_EA_OPT_STATE.bindStatus = bindStatus - table.insert(rightGroup, bindStatus) + -- ===== RIGHT: Rotation ===== + local rotBox = panel:Box("right", "MTH_ExpAmmoRotBox", "Rotation") + table.insert(rightBoxes, rotBox:Frame()) + rotBox:Note("MTH_ExpAmmoRotTip", + "Uncheck a shot to permanently skip it during its proc window. The keybind will cast Steady/Aimed instead.", + { lines = 2 }) + local multiCB = rotBox:Checkbox("MTH_ExpAmmoBlockMultiCB", "Multi-Shot (Explosive)", { + checked = cfg.blockExplosive ~= true, + onClick = function() if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockExplosive(not (this:GetChecked() == 1)) end end, + }) + table.insert(rightControls, multiCB) + local serpentCB = rotBox:Checkbox("MTH_ExpAmmoBlockSerpentCB", "Serpent Sting (Poisonous)", { + checked = cfg.blockPoisonous ~= true, + onClick = function() if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockPoisonous(not (this:GetChecked() == 1)) end end, + }) + table.insert(rightControls, serpentCB) + local arcaneCB = rotBox:Checkbox("MTH_ExpAmmoBlockArcaneCB", "Arcane Shot (Enchanted)", { + checked = cfg.blockEnchanted ~= true, + onClick = function() if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockEnchanted(not (this:GetChecked() == 1)) end end, + }) + table.insert(rightControls, arcaneCB) - local bindButton = CreateFrame("Button", "MTH_ExpAmmoBindButton", container, "UIPanelButtonTemplate") - bindButton:SetPoint("TOPLEFT", container, "TOPLEFT", OPT_R, -264) - bindButton:SetWidth(90) - bindButton:SetHeight(22) - bindButton:SetText("Set Key") - bindButton:EnableKeyboard(false) - bindButton:SetScript("OnClick", function() - if MTH_EA_OPT_STATE.captureFrame and MTH_EA_OPT_STATE.captureFrame:IsShown() then - MTH_EA_OPT_StopCapture() - else - MTH_EA_OPT_StartCapture() - end - end) - MTH_EA_OPT_STATE.bindButton = bindButton - table.insert(rightGroup, bindButton) + panel:Finish() - local clearButton = CreateFrame("Button", "MTH_ExpAmmoBindClear", container, "UIPanelButtonTemplate") - clearButton:SetPoint("LEFT", bindButton, "RIGHT", 6, 0) - clearButton:SetWidth(70) - clearButton:SetHeight(22) - clearButton:SetText("Clear") - clearButton:SetScript("OnClick", function() - MTH_EA_OPT_SaveBinding(nil) - end) - table.insert(rightGroup, clearButton) - - -- ── Casting ────────────────────────────────────────────── - table.insert(rightGroup, MTH_EA_OPT_Header(container, "Casting", -304, OPT_R)) - - local quiverCB = MTH_EA_OPT_CB(container, "MTH_ExpAmmoQuiverNoClipCB", - "Use Quiver no-clip casting", - -324, OPT_R, cfg.useQuiverNoClip == true, - function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetUseQuiverNoClip(v) end end) - table.insert(rightGroup, quiverCB) - - local quiverTip = MTH_EA_OPT_Tip(container, - "|cff4d9cffRequires the Quiver addon.|r \n Routes Aimed/Steady/Multi cast through Quiver.CastNoClip \n to avoid clipping your auto-shot swing timer", - -350, OPT_R, 1, 1, 1) - if quiverTip then quiverTip:SetWidth(290) end - table.insert(rightGroup, quiverTip) - - -- ── Rotation ───────────────────────────────────────────── - table.insert(rightGroup, MTH_EA_OPT_Header(container, "Rotation", -400, OPT_R)) - - local rotTip = MTH_EA_OPT_Tip(container, - "Uncheck a shot to permanently skip it during its proc window.\n The keybind will cast Steady/Aimed instead.", - -418, OPT_R, 0.65, 0.65, 0.65) - if rotTip then rotTip:SetWidth(290) end - table.insert(rightGroup, rotTip) - - local multiCB = MTH_EA_OPT_CB(container, "MTH_ExpAmmoBlockMultiCB", - "Multi-Shot (Explosive)", - -452, OPT_R, cfg.blockExplosive ~= true, - function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockExplosive(not v) end end) - table.insert(rightGroup, multiCB) - - local serpentCB = MTH_EA_OPT_CB(container, "MTH_ExpAmmoBlockSerpentCB", - "Serpent Sting (Poisonous)", - -476, OPT_R, cfg.blockPoisonous ~= true, - function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockPoisonous(not v) end end) - table.insert(rightGroup, serpentCB) - - local arcaneCB = MTH_EA_OPT_CB(container, "MTH_ExpAmmoBlockArcaneCB", - "Arcane Shot (Enchanted)", - -500, OPT_R, cfg.blockEnchanted ~= true, - function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetBlockEnchanted(not v) end end) - table.insert(rightGroup, arcaneCB) - - -- Apply initial enabled state - MTH_EA_OPT_SetGroupEnabled(rightGroup, showHint) + -- Apply initial gating state. + setRightEnabled(showHint) end function MTH_SetupExpAmmoOptions() diff --git a/api/options-feedomatic.lua b/api/options-feedomatic.lua index 6c0c79c..a24bb2c 100644 --- a/api/options-feedomatic.lua +++ b/api/options-feedomatic.lua @@ -242,31 +242,25 @@ function MTH_RefreshFeedOMaticOptions() if MTH_FOM_STATE.ctrl.SaveCookNone then MTH_FOM_STATE.ctrl.SaveCookNone:SetChecked(FOM_Config.SaveForCookingLevel and FOM_Config.SaveForCookingLevel >= 4 and 1 or nil) end end +-- Small label helper: prefer Feed-O-Matic's own button text, else localized. +local function MTH_FOM_Label(key, lkey, fallback) + return (FOM_OptionsButtonText and FOM_OptionsButtonText[key]) or MTH_FOM_L(lkey, fallback) +end + function MTH_SetupFeedOMaticOptions() if not MTH_FOM_READY then return end local container = MTH_GetFrame("MetaHuntOptionsFeedOMatic") if not container then return end - local rightColumnX = 300 - local yAdjust = -64 - MTH_ClearContainer(container) MTH_FOM_STATE.ctrl = {} - local title = container:CreateFontString("MetaHuntOptionsFeedOMaticTitle", "ARTWORK", "GameFontHighlight") - title:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -10) - title:SetText(MTH_FOM_L("FOM_TITLE", "Feed-O-Matic")) + local panel = MTH_Layout.Panel(container) + panel:Title(MTH_FOM_L("FOM_TITLE", "Feed-O-Matic")) + panel:Note(MTH_FOM_L("FOM_STATUS_NOTICE", "Feed-O-Matic, created by the great Fizzwidget, is not yet entirely ready for Twow because I am missing \na reliable list of all foods, mostly impossible to fetch from the DB.\n It still works much better in this version, but all stuff related to Food buff and Cooking isnt fully functional."), { lines = 4 }) - local statusNotice = container:CreateFontString("MetaHuntOptionsFeedOMaticStatusNotice", "ARTWORK", "GameFontNormalSmall") - statusNotice:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -34) - statusNotice:SetWidth(600) - statusNotice:SetJustifyH("LEFT") - statusNotice:SetJustifyV("TOP") - statusNotice:SetTextColor(0.35, 0.65, 1) - statusNotice:SetText(MTH_FOM_L("FOM_STATUS_NOTICE", "Feed-O-Matic, created by the great Fizzwidget, is not yet entirely ready for Twow because I am missing \na reliable list of all foods, mostly impossible to fetch from the DB.\n It still works much better in this version, but all stuff related to Food buff and Cooking isnt fully functional.")) - - local moduleCheck = MTH_CreateCheckbox(container, "MetaHuntOptionsFeedOMaticEnabled", MTH_FOM_L("FOM_ENABLE_MODULE", "Enable FeedOMatic module"), -32 + yAdjust) - if moduleCheck then - moduleCheck:SetScript("OnClick", function() + MTH_FOM_STATE.enableCheck = panel:TopCheckbox("MetaHuntOptionsFeedOMaticEnabled", + MTH_FOM_L("FOM_ENABLE_MODULE", "Enable FeedOMatic module"), { + onClick = function() if not this then return end local enabled = this:GetChecked() == 1 if MTH and MTH.SetModuleEnabled then @@ -276,125 +270,113 @@ function MTH_SetupFeedOMaticOptions() end end MTH_RefreshFeedOMaticOptions() - end) - MTH_FOM_STATE.enableCheck = moduleCheck - end + end, + }) - local leftHeader = container:CreateFontString("MetaHuntOptionsFeedOMaticGeneralHeader", "ARTWORK", "GameFontHighlight") - leftHeader:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -62 + yAdjust) - leftHeader:SetText(MTH_FOM_L("FOM_HEADER_GENERAL", "General")) + -- ===== General (left column) ===== + local general = panel:Box("left", "MetaHuntOptionsFOMGeneralBox", MTH_FOM_L("FOM_HEADER_GENERAL", "General")) - local bindStatus = container:CreateFontString("MetaHuntOptionsFOMBindStatus", "ARTWORK", "GameFontNormalSmall") - bindStatus:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -80 + yAdjust) - bindStatus:SetText(string.format(MTH_FOM_L("FOM_BIND_STATUS_VALUE", "Feed key: %s"), MTH_FOM_GetBindingDisplayText())) - MTH_FOM_STATE.bindStatus = bindStatus + MTH_FOM_STATE.bindStatus = general:Text("MetaHuntOptionsFOMBindStatus", + string.format(MTH_FOM_L("FOM_BIND_STATUS_VALUE", "Feed key: %s"), MTH_FOM_GetBindingDisplayText())) - local bindButton = CreateFrame("Button", "MetaHuntOptionsFOMBindButton", container, "UIPanelButtonTemplate") - bindButton:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -100 + yAdjust) - bindButton:SetWidth(90) - bindButton:SetHeight(22) - bindButton:SetText(MTH_FOM_L("FOM_BIND_SET_KEY", "Set Key")) - bindButton:SetScript("OnClick", function() - if MTH_FOM_STATE.captureFrame and MTH_FOM_STATE.captureFrame:IsShown() then - MTH_FOM_StopBindingCapture() - else - MTH_FOM_StartBindingCapture() - end - end) - MTH_FOM_STATE.bindButton = bindButton + local bindButtons = general:Buttons({ + { name = "MetaHuntOptionsFOMBindButton", label = MTH_FOM_L("FOM_BIND_SET_KEY", "Set Key"), width = 90, + onClick = function() + if MTH_FOM_STATE.captureFrame and MTH_FOM_STATE.captureFrame:IsShown() then + MTH_FOM_StopBindingCapture() + else + MTH_FOM_StartBindingCapture() + end + end }, + { name = "MetaHuntOptionsFOMBindClearButton", label = MTH_FOM_L("FOM_BIND_CLEAR", "Clear"), width = 70, + onClick = function() MTH_FOM_SaveBinding(nil) end }, + }) + MTH_FOM_STATE.bindButton = bindButtons[1] + MTH_FOM_STATE.bindClearButton = bindButtons[2] - local bindClearButton = CreateFrame("Button", "MetaHuntOptionsFOMBindClearButton", container, "UIPanelButtonTemplate") - bindClearButton:SetPoint("LEFT", bindButton, "RIGHT", 8, 0) - bindClearButton:SetWidth(70) - bindClearButton:SetHeight(22) - bindClearButton:SetText(MTH_FOM_L("FOM_BIND_CLEAR", "Clear")) - bindClearButton:SetScript("OnClick", function() - MTH_FOM_SaveBinding(nil) - end) - MTH_FOM_STATE.bindClearButton = bindClearButton + general:Note("MetaHuntOptionsFOMBindHint", + MTH_FOM_L("FOM_BIND_HINT", "You should assign a key bind for feed-o-matic,\nin order to feed your pet automatically.\nThe key \"P\" is an excellent candidate !"), { lines = 3 }) - local bindHint = container:CreateFontString("MetaHuntOptionsFOMBindHint", "ARTWORK", "GameFontNormalSmall") - bindHint:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -128 + yAdjust) - bindHint:SetWidth(360) - bindHint:SetJustifyH("LEFT") - bindHint:SetJustifyV("TOP") - bindHint:SetTextColor(0.35, 0.65, 1) - bindHint:SetText(MTH_FOM_L("FOM_BIND_HINT", "You should assign a key bind for feed-o-matic,\nin order to feed your pet automatically.\nThe key \"P\" is an excellent candidate !")) + MTH_FOM_STATE.ctrl.AvoidQuestFood = general:Checkbox("MetaHuntOptionsFOMAvoidQuest", + MTH_FOM_Label("AvoidQuestFood", "FOM_LABEL_AVOID_QUEST_FOODS", "Avoid quest foods"), + { onClick = function() if not this then return end FOM_Config.AvoidQuestFood = this:GetChecked() == 1 end }) + MTH_FOM_STATE.ctrl.AvoidBonusFood = general:Checkbox("MetaHuntOptionsFOMAvoidBonus", + MTH_FOM_Label("AvoidBonusFood", "FOM_LABEL_AVOID_BONUS_FOODS", "Avoid bonus foods"), + { onClick = function() if not this then return end FOM_Config.AvoidBonusFood = this:GetChecked() == 1 end }) + MTH_FOM_STATE.ctrl.PreferHigherQuality = general:Checkbox("MetaHuntOptionsFOMPreferQuality", + MTH_FOM_Label("PreferHigherQuality", "FOM_LABEL_PREFER_HIGHER_QUALITY_FOOD", "Prefer higher quality food"), + { onClick = function() if not this then return end FOM_Config.PreferHigherQuality = this:GetChecked() == 1 end }) + MTH_FOM_STATE.ctrl.Fallback = general:Checkbox("MetaHuntOptionsFOMFallback", + MTH_FOM_Label("Fallback", "FOM_LABEL_FALLBACK_TO_AVOIDED_FOODS", "Fallback to avoided foods"), + { onClick = function() if not this then return end FOM_Config.Fallback = this:GetChecked() == 1 end }) - MTH_FOM_STATE.ctrl.AvoidQuestFood = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMAvoidQuest", FOM_OptionsButtonText and FOM_OptionsButtonText["AvoidQuestFood"] or MTH_FOM_L("FOM_LABEL_AVOID_QUEST_FOODS", "Avoid quest foods"), -170 + yAdjust) - MTH_FOM_STATE.ctrl.AvoidBonusFood = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMAvoidBonus", FOM_OptionsButtonText and FOM_OptionsButtonText["AvoidBonusFood"] or MTH_FOM_L("FOM_LABEL_AVOID_BONUS_FOODS", "Avoid bonus foods"), -195 + yAdjust) - MTH_FOM_STATE.ctrl.PreferHigherQuality = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMPreferQuality", FOM_OptionsButtonText and FOM_OptionsButtonText["PreferHigherQuality"] or MTH_FOM_L("FOM_LABEL_PREFER_HIGHER_QUALITY_FOOD", "Prefer higher quality food"), -220 + yAdjust) - MTH_FOM_STATE.ctrl.Fallback = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMFallback", FOM_OptionsButtonText and FOM_OptionsButtonText["Fallback"] or MTH_FOM_L("FOM_LABEL_FALLBACK_TO_AVOIDED_FOODS", "Fallback to avoided foods"), -245 + yAdjust) + MTH_FOM_STATE.keepOpenEdit = general:EditRow("MetaHuntOptionsFOMKeepOpenLabel", + MTH_FOM_L("FOM_KEEP_OPEN_BAG_SLOTS", "Keep open bag slots:"), + "MetaHuntOptionsFOMKeepOpen", 40, { + numeric = true, + onChange = function() + if not this then return end + FOM_Config.KeepOpenSlots = tonumber(this:GetText()) or 0 + end, + }) - if MTH_FOM_STATE.ctrl.AvoidQuestFood then MTH_FOM_STATE.ctrl.AvoidQuestFood:SetScript("OnClick", function() if not this then return end FOM_Config.AvoidQuestFood = this:GetChecked() == 1 end) end - if MTH_FOM_STATE.ctrl.AvoidBonusFood then MTH_FOM_STATE.ctrl.AvoidBonusFood:SetScript("OnClick", function() if not this then return end FOM_Config.AvoidBonusFood = this:GetChecked() == 1 end) end - if MTH_FOM_STATE.ctrl.PreferHigherQuality then MTH_FOM_STATE.ctrl.PreferHigherQuality:SetScript("OnClick", function() if not this then return end FOM_Config.PreferHigherQuality = this:GetChecked() == 1 end) end - if MTH_FOM_STATE.ctrl.Fallback then MTH_FOM_STATE.ctrl.Fallback:SetScript("OnClick", function() if not this then return end FOM_Config.Fallback = this:GetChecked() == 1 end) end + -- ===== Notify when feeding (left column) ===== + local notify = panel:Box("left", "MetaHuntOptionsFOMNotifyBox", MTH_FOM_L("FOM_HEADER_NOTIFY_WHEN_FEEDING", "Notify when feeding")) + MTH_FOM_STATE.ctrl.AlertEmote = notify:Checkbox("MetaHuntOptionsFOMAlertEmote", + MTH_FOM_Label("AlertEmote", "FOM_LABEL_NOTIFY_VIA_EMOTE", "Via emote"), + { onClick = function() MTH_FOM_SetAlert("emote"); MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.AlertChat = notify:Checkbox("MetaHuntOptionsFOMAlertChat", + MTH_FOM_Label("AlertChat", "FOM_LABEL_NOTIFY_IN_CHAT", "In chat"), + { onClick = function() MTH_FOM_SetAlert("chat"); MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.AlertNone = notify:Checkbox("MetaHuntOptionsFOMAlertNone", + MTH_FOM_Label("AlertNone", "FOM_LABEL_NOTIFY_NONE", "Don't notify"), + { onClick = function() MTH_FOM_SetAlert("none"); MTH_RefreshFeedOMaticOptions() end }) - local keepOpenLabel = container:CreateFontString("MetaHuntOptionsFOMKeepOpenLabel", "ARTWORK", "GameFontNormalSmall") - keepOpenLabel:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -302 + yAdjust) - keepOpenLabel:SetText(MTH_FOM_L("FOM_KEEP_OPEN_BAG_SLOTS", "Keep open bag slots:")) + -- ===== Avoid foods used in cooking (right column) ===== + local avoid = panel:Box("right", "MetaHuntOptionsFOMAvoidBox", MTH_FOM_L("FOM_HEADER_AVOID_FOODS_USED_IN_COOKING", "Avoid foods used in cooking")) + MTH_FOM_STATE.ctrl.SaveCookOrange = avoid:Checkbox("MetaHuntOptionsFOMSaveCookOrange", + MTH_FOM_Label("SaveForCook_Orange", "FOM_LABEL_ONLY_DIFFICULT_RECIPES", "Only difficult recipes"), + { onClick = function() MTH_FOM_SetCookingLevel(3); MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.SaveCookYellow = avoid:Checkbox("MetaHuntOptionsFOMSaveCookYellow", + MTH_FOM_Label("SaveForCook_Yellow", "FOM_LABEL_MEDIUM_OR_BETTER", "Medium or better"), + { onClick = function() MTH_FOM_SetCookingLevel(2); MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.SaveCookGreen = avoid:Checkbox("MetaHuntOptionsFOMSaveCookGreen", + MTH_FOM_Label("SaveForCook_Green", "FOM_LABEL_EASY_OR_BETTER", "Easy or better"), + { onClick = function() MTH_FOM_SetCookingLevel(1); MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.SaveCookAll = avoid:Checkbox("MetaHuntOptionsFOMSaveCookAll", + MTH_FOM_Label("SaveForCook_All", "FOM_LABEL_ALL_FOODS", "All foods"), + { onClick = function() MTH_FOM_SetCookingLevel(0); MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.SaveCookNone = avoid:Checkbox("MetaHuntOptionsFOMSaveCookNone", + MTH_FOM_Label("SaveForCook_None", "FOM_LABEL_DO_NOT_SAVE_COOKING_FOODS", "Do not save cooking foods"), + { onClick = function() MTH_FOM_SetCookingLevel(4); MTH_RefreshFeedOMaticOptions() end }) - local keepOpen = CreateFrame("EditBox", "MetaHuntOptionsFOMKeepOpen", container, "InputBoxTemplate") - keepOpen:SetPoint("LEFT", keepOpenLabel, "RIGHT", 8, 0) - keepOpen:SetWidth(40) - keepOpen:SetHeight(20) - keepOpen:SetNumeric(true) - keepOpen:SetAutoFocus(false) - keepOpen:SetScript("OnTextChanged", function() - if not this then return end - local value = tonumber(this:GetText()) or 0 - FOM_Config.KeepOpenSlots = value - end) - keepOpen:SetScript("OnEnterPressed", function() if this then this:ClearFocus() end end) - keepOpen:SetScript("OnEscapePressed", function() if this then this:ClearFocus() end end) - MTH_FOM_STATE.keepOpenEdit = keepOpen + -- ===== Warn when pet needs feeding (right column) ===== + local warn = panel:Box("right", "MetaHuntOptionsFOMWarnBox", MTH_FOM_L("FOM_HEADER_WARN_WHEN_PET_NEEDS_FEEDING", "Warn when pet needs feeding")) + MTH_FOM_STATE.ctrl.LevelContent = warn:Checkbox("MetaHuntOptionsFOMLevelContent", + MTH_FOM_Label("LevelContent", "FOM_LABEL_WARN_WHEN_CONTENT", "When content"), + { onClick = function() MTH_FOM_SetLevel("content"); MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.LevelUnhappy = warn:Checkbox("MetaHuntOptionsFOMLevelUnhappy", + MTH_FOM_Label("LevelUnhappy", "FOM_LABEL_WARN_WHEN_UNHAPPY", "When unhappy"), + { onClick = function() MTH_FOM_SetLevel("unhappy"); MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.LevelOff = warn:Checkbox("MetaHuntOptionsFOMLevelOff", + MTH_FOM_Label("LevelOff", "FOM_LABEL_DONT_WARN", "Don't warn"), + { onClick = function() MTH_FOM_SetLevel("off"); MTH_RefreshFeedOMaticOptions() end }) - local notifyHeader = container:CreateFontString("MetaHuntOptionsFOMNotifyHeader", "ARTWORK", "GameFontHighlight") - notifyHeader:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -336 + yAdjust) - notifyHeader:SetText(MTH_FOM_L("FOM_HEADER_NOTIFY_WHEN_FEEDING", "Notify when feeding")) + warn:Gap(6) - MTH_FOM_STATE.ctrl.AlertEmote = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMAlertEmote", FOM_OptionsButtonText and FOM_OptionsButtonText["AlertEmote"] or MTH_FOM_L("FOM_LABEL_NOTIFY_VIA_EMOTE", "Via emote"), -360 + yAdjust) - MTH_FOM_STATE.ctrl.AlertChat = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMAlertChat", FOM_OptionsButtonText and FOM_OptionsButtonText["AlertChat"] or MTH_FOM_L("FOM_LABEL_NOTIFY_IN_CHAT", "In chat"), -385 + yAdjust) - MTH_FOM_STATE.ctrl.AlertNone = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMAlertNone", FOM_OptionsButtonText and FOM_OptionsButtonText["AlertNone"] or MTH_FOM_L("FOM_LABEL_NOTIFY_NONE", "Don't notify"), -410 + yAdjust) - if MTH_FOM_STATE.ctrl.AlertEmote then MTH_FOM_STATE.ctrl.AlertEmote:SetScript("OnClick", function() MTH_FOM_SetAlert("emote"); MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.AlertChat then MTH_FOM_STATE.ctrl.AlertChat:SetScript("OnClick", function() MTH_FOM_SetAlert("chat"); MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.AlertNone then MTH_FOM_STATE.ctrl.AlertNone:SetScript("OnClick", function() MTH_FOM_SetAlert("none"); MTH_RefreshFeedOMaticOptions() end) end - - local warnHeader = container:CreateFontString("MetaHuntOptionsFOMWarnHeader", "ARTWORK", "GameFontHighlight") - warnHeader:SetPoint("TOPLEFT", container, "TOPLEFT", rightColumnX, -276 + yAdjust) - warnHeader:SetText(MTH_FOM_L("FOM_HEADER_WARN_WHEN_PET_NEEDS_FEEDING", "Warn when pet needs feeding")) - - MTH_FOM_STATE.ctrl.LevelContent = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMLevelContent", FOM_OptionsButtonText and FOM_OptionsButtonText["LevelContent"] or MTH_FOM_L("FOM_LABEL_WARN_WHEN_CONTENT", "When content"), -300 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.LevelUnhappy = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMLevelUnhappy", FOM_OptionsButtonText and FOM_OptionsButtonText["LevelUnhappy"] or MTH_FOM_L("FOM_LABEL_WARN_WHEN_UNHAPPY", "When unhappy"), -325 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.LevelOff = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMLevelOff", FOM_OptionsButtonText and FOM_OptionsButtonText["LevelOff"] or MTH_FOM_L("FOM_LABEL_DONT_WARN", "Don't warn"), -350 + yAdjust, rightColumnX) - if MTH_FOM_STATE.ctrl.LevelContent then MTH_FOM_STATE.ctrl.LevelContent:SetScript("OnClick", function() MTH_FOM_SetLevel("content"); MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.LevelUnhappy then MTH_FOM_STATE.ctrl.LevelUnhappy:SetScript("OnClick", function() MTH_FOM_SetLevel("unhappy"); MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.LevelOff then MTH_FOM_STATE.ctrl.LevelOff:SetScript("OnClick", function() MTH_FOM_SetLevel("off"); MTH_RefreshFeedOMaticOptions() end) end - - MTH_FOM_STATE.ctrl.AudioWarning = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMAudioWarning", FOM_OptionsButtonText and FOM_OptionsButtonText["AudioWarning"] or MTH_FOM_L("FOM_LABEL_PLAY_SOUND", "Play sound"), -378 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.AudioWarningBell = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMAudioWarningBell", FOM_OptionsButtonText and FOM_OptionsButtonText["AudioWarningBell"] or MTH_FOM_L("FOM_LABEL_USE_BELL_SOUND", "Use bell sound"), -403 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.TextWarning = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMTextWarning", FOM_OptionsButtonText and FOM_OptionsButtonText["TextWarning"] or MTH_FOM_L("FOM_LABEL_SHOW_TEXT", "Show text"), -428 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.IconWarning = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMIconWarning", FOM_OptionsButtonText and FOM_OptionsButtonText["IconWarning"] or MTH_FOM_L("FOM_LABEL_FLASH_ICON", "Flash icon"), -453 + yAdjust, rightColumnX) - if MTH_FOM_STATE.ctrl.AudioWarning then MTH_FOM_STATE.ctrl.AudioWarning:SetScript("OnClick", function() if not this then return end if this:GetChecked() == 1 then if FOM_Config.AudioWarning ~= "bell" then FOM_Config.AudioWarning = true end else FOM_Config.AudioWarning = nil end MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.AudioWarningBell then MTH_FOM_STATE.ctrl.AudioWarningBell:SetScript("OnClick", function() if not this then return end if this:GetChecked() == 1 then FOM_Config.AudioWarning = "bell" else FOM_Config.AudioWarning = true end MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.TextWarning then MTH_FOM_STATE.ctrl.TextWarning:SetScript("OnClick", function() if not this then return end FOM_Config.TextWarning = this:GetChecked() == 1 end) end - if MTH_FOM_STATE.ctrl.IconWarning then MTH_FOM_STATE.ctrl.IconWarning:SetScript("OnClick", function() if not this then return end FOM_Config.IconWarning = this:GetChecked() == 1 end) end - - local cookHeader = container:CreateFontString("MetaHuntOptionsFOMCookingHeader", "ARTWORK", "GameFontHighlight") - cookHeader:SetPoint("TOPLEFT", container, "TOPLEFT", rightColumnX, -62 + yAdjust) - cookHeader:SetText(MTH_FOM_L("FOM_HEADER_AVOID_FOODS_USED_IN_COOKING", "Avoid foods used in cooking")) - - MTH_FOM_STATE.ctrl.SaveCookOrange = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMSaveCookOrange", FOM_OptionsButtonText and FOM_OptionsButtonText["SaveForCook_Orange"] or MTH_FOM_L("FOM_LABEL_ONLY_DIFFICULT_RECIPES", "Only difficult recipes"), -86 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.SaveCookYellow = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMSaveCookYellow", FOM_OptionsButtonText and FOM_OptionsButtonText["SaveForCook_Yellow"] or MTH_FOM_L("FOM_LABEL_MEDIUM_OR_BETTER", "Medium or better"), -111 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.SaveCookGreen = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMSaveCookGreen", FOM_OptionsButtonText and FOM_OptionsButtonText["SaveForCook_Green"] or MTH_FOM_L("FOM_LABEL_EASY_OR_BETTER", "Easy or better"), -136 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.SaveCookAll = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMSaveCookAll", FOM_OptionsButtonText and FOM_OptionsButtonText["SaveForCook_All"] or MTH_FOM_L("FOM_LABEL_ALL_FOODS", "All foods"), -161 + yAdjust, rightColumnX) - MTH_FOM_STATE.ctrl.SaveCookNone = MTH_CreateCheckbox(container, "MetaHuntOptionsFOMSaveCookNone", FOM_OptionsButtonText and FOM_OptionsButtonText["SaveForCook_None"] or MTH_FOM_L("FOM_LABEL_DO_NOT_SAVE_COOKING_FOODS", "Do not save cooking foods"), -186 + yAdjust, rightColumnX) - if MTH_FOM_STATE.ctrl.SaveCookOrange then MTH_FOM_STATE.ctrl.SaveCookOrange:SetScript("OnClick", function() MTH_FOM_SetCookingLevel(3); MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.SaveCookYellow then MTH_FOM_STATE.ctrl.SaveCookYellow:SetScript("OnClick", function() MTH_FOM_SetCookingLevel(2); MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.SaveCookGreen then MTH_FOM_STATE.ctrl.SaveCookGreen:SetScript("OnClick", function() MTH_FOM_SetCookingLevel(1); MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.SaveCookAll then MTH_FOM_STATE.ctrl.SaveCookAll:SetScript("OnClick", function() MTH_FOM_SetCookingLevel(0); MTH_RefreshFeedOMaticOptions() end) end - if MTH_FOM_STATE.ctrl.SaveCookNone then MTH_FOM_STATE.ctrl.SaveCookNone:SetScript("OnClick", function() MTH_FOM_SetCookingLevel(4); MTH_RefreshFeedOMaticOptions() end) end + MTH_FOM_STATE.ctrl.AudioWarning = warn:Checkbox("MetaHuntOptionsFOMAudioWarning", + MTH_FOM_Label("AudioWarning", "FOM_LABEL_PLAY_SOUND", "Play sound"), + { onClick = function() if not this then return end if this:GetChecked() == 1 then if FOM_Config.AudioWarning ~= "bell" then FOM_Config.AudioWarning = true end else FOM_Config.AudioWarning = nil end MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.AudioWarningBell = warn:Checkbox("MetaHuntOptionsFOMAudioWarningBell", + MTH_FOM_Label("AudioWarningBell", "FOM_LABEL_USE_BELL_SOUND", "Use bell sound"), + { onClick = function() if not this then return end if this:GetChecked() == 1 then FOM_Config.AudioWarning = "bell" else FOM_Config.AudioWarning = true end MTH_RefreshFeedOMaticOptions() end }) + MTH_FOM_STATE.ctrl.TextWarning = warn:Checkbox("MetaHuntOptionsFOMTextWarning", + MTH_FOM_Label("TextWarning", "FOM_LABEL_SHOW_TEXT", "Show text"), + { onClick = function() if not this then return end FOM_Config.TextWarning = this:GetChecked() == 1 end }) + MTH_FOM_STATE.ctrl.IconWarning = warn:Checkbox("MetaHuntOptionsFOMIconWarning", + MTH_FOM_Label("IconWarning", "FOM_LABEL_FLASH_ICON", "Flash icon"), + { onClick = function() if not this then return end FOM_Config.IconWarning = this:GetChecked() == 1 end }) + panel:Finish() MTH_RefreshFeedOMaticOptions() end diff --git a/api/options-icu.lua b/api/options-icu.lua index aab7f9e..72c2278 100644 --- a/api/options-icu.lua +++ b/api/options-icu.lua @@ -197,31 +197,25 @@ local function MTH_ICU_OpenColorPicker(colorKey) ColorPickerFrame:Show() end -local function MTH_ICU_CreateOptionRow(container, rowName, label, key, y, labelX, dropdownX) - labelX = labelX or MTH_ICU_LAYOUT.LEFT_X - dropdownX = dropdownX or MTH_ICU_LAYOUT.LEFT_DROPDOWN_X +local function MTH_ICU_BuildUI(container) + MTH_ICU_OPT_STATE.controls = {} + MTH_ICU_OPT_STATE.container = container + MTH_ICU_OPT_STATE.built = true - local labelFs = container:CreateFontString(rowName .. "Label", "ARTWORK", "GameFontNormalSmall") - labelFs:SetPoint("TOPLEFT", container, "TOPLEFT", labelX, y) - labelFs:SetText(label) + local controls = MTH_ICU_OPT_STATE.controls - local dropdown = CreateFrame("Frame", rowName .. "Drop", container, "UIDropDownMenuTemplate") - dropdown:ClearAllPoints() - dropdown:SetPoint("TOPLEFT", container, "TOPLEFT", dropdownX, y + 12) - if UIDropDownMenu_SetWidth then - UIDropDownMenu_SetWidth(MTH_ICU_LAYOUT.DROPDOWN_WIDTH, dropdown) - end - if UIDropDownMenu_JustifyText then - UIDropDownMenu_JustifyText("LEFT", dropdown) - end + local panel = MTH_Layout.Panel(container) + panel:Title("MTH ICU") + panel:Note("ICU is a minimap targeting utility module for your Trackings. It provides enhanced target information and click actions for players and NPCs on the minimap.", + { lines = 2 }) - if UIDropDownMenu_Initialize then - UIDropDownMenu_Initialize(dropdown, function() + -- Standard UIDropDownMenu initialize factory for a stored option key. + local function makeInit(key) + return function() local values = MTH_ICU_GetOptionValues(key) local store = MTH_ICU_GetStore() local current = tostring(store[key] or "") local infoFactory = UIDropDownMenu_CreateInfo - for i = 1, table.getn(values) do local optionValue = tostring(values[i]) local info = type(infoFactory) == "function" and infoFactory() or {} @@ -229,76 +223,23 @@ local function MTH_ICU_CreateOptionRow(container, rowName, label, key, y, labelX info.checked = optionValue == current and 1 or nil info.func = function() MTH_ICU_SetValue(key, optionValue) - MTH_RefreshICUOptions() + -- ANCHOR toggles whether the Toggle Anchor / Expand Up + -- controls exist, so rebuild instead of a plain refresh. + if key == "ANCHOR" then + MTH_SetupICUOptions() + else + MTH_RefreshICUOptions() + end end if UIDropDownMenu_AddButton then UIDropDownMenu_AddButton(info) end end - end) + end end - MTH_ICU_OPT_STATE.controls[key] = { - label = labelFs, - dropdown = dropdown, - } -end - -local function MTH_ICU_CreateColorRow(container, rowName, label, colorKey, y, labelX, buttonX) - labelX = labelX or MTH_ICU_LAYOUT.RIGHT_X - buttonX = buttonX or MTH_ICU_LAYOUT.COLOR_BUTTON_X - - local labelFs = container:CreateFontString(rowName .. "Label", "ARTWORK", "GameFontNormalSmall") - labelFs:SetPoint("TOPLEFT", container, "TOPLEFT", labelX, y) - labelFs:SetText(label) - - local button = CreateFrame("Button", rowName .. "Button", container, "UIPanelButtonTemplate") - button:SetPoint("TOPLEFT", container, "TOPLEFT", buttonX, y + 8) - button:SetWidth(MTH_ICU_LAYOUT.COLOR_BUTTON_WIDTH) - button:SetHeight(22) - button:SetText("Pick") - button:SetScript("OnClick", function() - MTH_ICU_OpenColorPicker(colorKey) - end) - - local swatch = button:CreateTexture(rowName .. "Swatch", "ARTWORK") - swatch:SetTexture("Interface\\Buttons\\WHITE8X8") - swatch:SetPoint("LEFT", button, "LEFT", 6, 0) - swatch:SetWidth(14) - swatch:SetHeight(14) - - MTH_ICU_OPT_STATE.controls[colorKey] = { - label = labelFs, - button = button, - swatch = swatch, - isColor = true, - } -end - -local function MTH_ICU_BuildUI(container) - if MTH_ICU_OPT_STATE.built and MTH_ICU_OPT_STATE.container == container then - return - end - - MTH_ClearContainer(container) - MTH_ICU_OPT_STATE.controls = {} - MTH_ICU_OPT_STATE.container = container - MTH_ICU_OPT_STATE.built = true - - local title = container:CreateFontString("MetaHuntOptionsICUTitle", "ARTWORK", "GameFontHighlight") - title:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -10) - title:SetText("MTH ICU") - - local note = container:CreateFontString("MetaHuntOptionsICUNote", "ARTWORK", "GameFontNormalSmall") - note:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -32) - note:SetWidth(560) - note:SetJustifyH("LEFT") - note:SetJustifyV("TOP") - note:SetTextColor(0.35, 0.65, 1) - note:SetText("ICU is a minimap targeting utility module for your Trackings.\nIt provides enhanced target information and click actions for players and NPCs on the minimap.") - - local moduleEnabled = MTH_CreateCheckbox(container, "MetaHuntOptionsICUModuleEnabled", "Enable ICU module", -72, MTH_ICU_LAYOUT.LEFT_X) - if moduleEnabled then - moduleEnabled:SetScript("OnClick", function() - if not this then return end + -- ===== LEFT: Module ===== + local moduleBox = panel:Box("left", "MTH_ICUModuleBox", "Module") + controls.moduleEnabled = moduleBox:Checkbox("MetaHuntOptionsICUModuleEnabled", "Enable ICU module", { + onClick = function() if MTH and MTH.SetModuleEnabled then local ok, err = MTH:SetModuleEnabled("icu", this:GetChecked() == 1) if not ok and MTH and MTH.Print then @@ -306,130 +247,97 @@ local function MTH_ICU_BuildUI(container) end end MTH_RefreshICUOptions() - end) - MTH_ICU_OPT_STATE.controls.moduleEnabled = moduleEnabled - end - - local mouseOver = MTH_CreateCheckbox(container, "MetaHuntOptionsICUMouseOver", "Enable CTRL mouseover scan", -98, MTH_ICU_LAYOUT.LEFT_X) - if mouseOver then - mouseOver:SetScript("OnClick", function() - if not this then return end + end, + }) + controls.mouseOver = moduleBox:Checkbox("MetaHuntOptionsICUMouseOver", "Enable CTRL mouseover scan", { + onClick = function() local store = MTH_ICU_GetStore() store.mouseOver = this:GetChecked() == 1 and true or false - end) - MTH_ICU_OPT_STATE.controls.mouseOver = mouseOver + end, + }) + + -- ===== LEFT: General Settings ===== + local genBox = panel:Box("left", "MTH_ICUGeneralBox", "General Settings") + controls.ALERT = { dropdown = genBox:Dropdown("MetaHuntOptionsICUAlertDrop", + { label = "ALERT PvP", width = 130, initialize = makeInit("ALERT") }) } + controls.ANNOUNCE = { dropdown = genBox:Dropdown("MetaHuntOptionsICUAnnounceDrop", + { label = "ANNOUNCE on click", width = 130, initialize = makeInit("ANNOUNCE") }) } + controls.HEALTH_TEXT_MODE = { dropdown = genBox:Dropdown("MetaHuntOptionsICUHealthModeDrop", + { label = "Health text", width = 130, initialize = makeInit("HEALTH_TEXT_MODE") }) } + controls.POPUP_HIDE_DELAY = { dropdown = genBox:Dropdown("MetaHuntOptionsICUHideDelayDrop", + { label = "List hide delay", width = 130, initialize = makeInit("POPUP_HIDE_DELAY") }) } + controls.ANCHOR = { dropdown = genBox:Dropdown("MetaHuntOptionsICUAnchorDrop", + { label = "ANCHOR", width = 130, initialize = makeInit("ANCHOR") }) } + -- The manual-anchor controls only apply to a CUSTOM anchor. Build them + -- only in that case so the box never reserves empty space for them. + if tostring(MTH_ICU_GetStore().ANCHOR or "") == "CUSTOM" then + controls.customAnchorButton = genBox:Button("MetaHuntOptionsICUToggleAnchorButton", "Toggle Anchor", { + width = 116, + onClick = function() if type(ICU_ToggleAnchor) == "function" then ICU_ToggleAnchor() end end, + }) + controls.expandUp = genBox:Checkbox("MetaHuntOptionsICUExpandUp", "Expand Up", { + onClick = function() + local store = MTH_ICU_GetStore() + store.EXPAND_UP = this:GetChecked() == 1 and true or false + if type(ICU_SetPoints) == "function" then ICU_SetPoints() end + end, + }) end - local sectionHeader = container:CreateFontString("MetaHuntOptionsICUSectionHeader", "ARTWORK", "GameFontHighlight") - sectionHeader:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_ICU_LAYOUT.LEFT_X, -146) - sectionHeader:SetText("General Settings") - - MTH_ICU_CreateOptionRow(container, "MetaHuntOptionsICUAlert", "ALERT PVP", "ALERT", -172, MTH_ICU_LAYOUT.LEFT_X, MTH_ICU_LAYOUT.LEFT_DROPDOWN_X) - MTH_ICU_CreateOptionRow(container, "MetaHuntOptionsICUAnnounce", "ANNOUNCE on click", "ANNOUNCE", -198, MTH_ICU_LAYOUT.LEFT_X, MTH_ICU_LAYOUT.LEFT_DROPDOWN_X) - MTH_ICU_CreateOptionRow(container, "MetaHuntOptionsICUHealthMode", "Health text", "HEALTH_TEXT_MODE", -224, MTH_ICU_LAYOUT.LEFT_X, MTH_ICU_LAYOUT.LEFT_DROPDOWN_X) - MTH_ICU_CreateOptionRow(container, "MetaHuntOptionsICUHideDelay", "List hide delay", "POPUP_HIDE_DELAY", -250, MTH_ICU_LAYOUT.LEFT_X, MTH_ICU_LAYOUT.LEFT_DROPDOWN_X) - MTH_ICU_CreateOptionRow(container, "MetaHuntOptionsICUAnchor", "ANCHOR", "ANCHOR", -276, MTH_ICU_LAYOUT.LEFT_X, MTH_ICU_LAYOUT.LEFT_DROPDOWN_X) - - local toggleAnchorButton = CreateFrame("Button", "MetaHuntOptionsICUToggleAnchorButton", container, "UIPanelButtonTemplate") - toggleAnchorButton:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_ICU_LAYOUT.LEFT_X, -302) - toggleAnchorButton:SetWidth(116) - toggleAnchorButton:SetHeight(22) - toggleAnchorButton:SetText("Toggle Anchor") - toggleAnchorButton:SetScript("OnClick", function() - if type(ICU_ToggleAnchor) == "function" then - ICU_ToggleAnchor() - end - end) - MTH_ICU_OPT_STATE.controls.customAnchorButton = toggleAnchorButton - - local expandUp = MTH_CreateCheckbox(container, "MetaHuntOptionsICUExpandUp", "Expand Up", -301, MTH_ICU_LAYOUT.LEFT_X + 132) - if expandUp then - expandUp:SetScript("OnClick", function() - if not this then return end - local store = MTH_ICU_GetStore() - store.EXPAND_UP = this:GetChecked() == 1 and true or false - if type(ICU_SetPoints) == "function" then - ICU_SetPoints() - end - end) - MTH_ICU_OPT_STATE.controls.expandUp = expandUp - end - - local showGuild = MTH_CreateCheckbox(container, "MetaHuntOptionsICUShowGuild", "Show player guild names", -344, MTH_ICU_LAYOUT.LEFT_X) - if showGuild then - showGuild:SetScript("OnClick", function() - if not this then return end - local store = MTH_ICU_GetStore() - store.SHOW_GUILD_NAME = this:GetChecked() == 1 and true or false - end) - MTH_ICU_OPT_STATE.controls.showGuildName = showGuild - end - - local showClass = MTH_CreateCheckbox(container, "MetaHuntOptionsICUShowClass", "Show player class", -368, MTH_ICU_LAYOUT.LEFT_X) - if showClass then - showClass:SetScript("OnClick", function() - if not this then return end - local store = MTH_ICU_GetStore() - store.SHOW_PLAYER_CLASS = this:GetChecked() == 1 and true or false - end) - MTH_ICU_OPT_STATE.controls.showPlayerClass = showClass - end - - local showRace = MTH_CreateCheckbox(container, "MetaHuntOptionsICUShowRace", "Show player race", -392, MTH_ICU_LAYOUT.LEFT_X) - if showRace then - showRace:SetScript("OnClick", function() - if not this then return end - local store = MTH_ICU_GetStore() - store.SHOW_PLAYER_RACE = this:GetChecked() == 1 and true or false - end) - MTH_ICU_OPT_STATE.controls.showPlayerRace = showRace - end - - local showTitles = MTH_CreateCheckbox(container, "MetaHuntOptionsICUShowTitles", "Show custom titles in alerts", -416, MTH_ICU_LAYOUT.LEFT_X) - if showTitles then - showTitles:SetScript("OnClick", function() - if not this then return end - local store = MTH_ICU_GetStore() - store.SHOW_CUSTOM_TITLES = this:GetChecked() == 1 and true or false - end) - MTH_ICU_OPT_STATE.controls.showCustomTitles = showTitles - end - - local rightHeader = container:CreateFontString("MetaHuntOptionsICURightHeader", "ARTWORK", "GameFontHighlight") - rightHeader:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_ICU_LAYOUT.RIGHT_X, -146) - rightHeader:SetText("Background Colors") - - MTH_ICU_CreateOptionRow(container, "MetaHuntOptionsICUPlayerColorMode", "Player colors", "PLAYER_COLOR_MODE", -172, MTH_ICU_LAYOUT.RIGHT_X, MTH_ICU_LAYOUT.RIGHT_DROPDOWN_X) - - local reactionOnlyPvp = MTH_CreateCheckbox(container, "MetaHuntOptionsICUReactionOnlyPvp", "Reaction only when PvP-flagged", -200, MTH_ICU_LAYOUT.RIGHT_X) - if reactionOnlyPvp then - reactionOnlyPvp:SetScript("OnClick", function() - if not this then return end + -- ===== RIGHT: Background Colors ===== + local bgBox = panel:Box("right", "MTH_ICUBgBox", "Background Colors") + controls.PLAYER_COLOR_MODE = { dropdown = bgBox:Dropdown("MetaHuntOptionsICUPlayerColorModeDrop", + { label = "Player colors", width = 130, initialize = makeInit("PLAYER_COLOR_MODE") }) } + controls.reactionOnlyPvp = bgBox:Checkbox("MetaHuntOptionsICUReactionOnlyPvp", "Reaction only when PvP-flagged", { + onClick = function() local store = MTH_ICU_GetStore() store.REACTION_ONLY_PVP_PLAYERS = this:GetChecked() == 1 and true or false - end) - MTH_ICU_OPT_STATE.controls.reactionOnlyPvp = reactionOnlyPvp - end + end, + }) - local colorHeader = container:CreateFontString("MetaHuntOptionsICUColorsHeader", "ARTWORK", "GameFontHighlight") - colorHeader:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_ICU_LAYOUT.RIGHT_X, -224) - - local colorScope = container:CreateFontString("MetaHuntOptionsICUColorsScope", "ARTWORK", "GameFontNormalSmall") - colorScope:SetPoint("TOPLEFT", container, "TOPLEFT", MTH_ICU_LAYOUT.RIGHT_X, -242) - colorScope:SetWidth(250) - colorScope:SetJustifyH("LEFT") - colorScope:SetJustifyV("TOP") - colorScope:SetTextColor(0.35, 0.65, 1) - colorScope:SetText("Player pickers: REACTION / FACTION / CUSTOM\nNPC pickers: always") - - local colorY = -284 + -- ===== RIGHT: Color Pickers ===== + local colorBox = panel:Box("right", "MTH_ICUColorBox", "Color Pickers") + colorBox:Note("MetaHuntOptionsICUColorsScope", + "Player pickers: REACTION / FACTION / CUSTOM\nNPC pickers: always", { lines = 2 }) for i = 1, table.getn(MTH_ICU_COLOR_ROWS) do local row = MTH_ICU_COLOR_ROWS[i] - MTH_ICU_CreateColorRow(container, "MetaHuntOptionsICUColor" .. tostring(i), row.label, row.key, colorY, MTH_ICU_LAYOUT.RIGHT_X, MTH_ICU_LAYOUT.COLOR_BUTTON_X) - colorY = colorY - MTH_ICU_LAYOUT.COLOR_ROW_STEP + local btn, swatch = colorBox:ColorRow("MetaHuntOptionsICUColor" .. tostring(i), { + label = row.label, + buttonText = "Pick", + buttonWidth = 78, + onClick = function() MTH_ICU_OpenColorPicker(row.key) end, + }) + controls[row.key] = { button = btn, swatch = swatch, isColor = true } end + -- ===== Player Info (placed on whichever column has the most room) ===== + local infoBox = panel:Box(panel:ShorterSide(), "MTH_ICUInfoBox", "Player Info") + controls.showGuildName = infoBox:Checkbox("MetaHuntOptionsICUShowGuild", "Show player guild names", { + onClick = function() + local store = MTH_ICU_GetStore() + store.SHOW_GUILD_NAME = this:GetChecked() == 1 and true or false + end, + }) + controls.showPlayerClass = infoBox:Checkbox("MetaHuntOptionsICUShowClass", "Show player class", { + onClick = function() + local store = MTH_ICU_GetStore() + store.SHOW_PLAYER_CLASS = this:GetChecked() == 1 and true or false + end, + }) + controls.showPlayerRace = infoBox:Checkbox("MetaHuntOptionsICUShowRace", "Show player race", { + onClick = function() + local store = MTH_ICU_GetStore() + store.SHOW_PLAYER_RACE = this:GetChecked() == 1 and true or false + end, + }) + controls.showCustomTitles = infoBox:Checkbox("MetaHuntOptionsICUShowTitles", "Show custom titles in alerts", { + onClick = function() + local store = MTH_ICU_GetStore() + store.SHOW_CUSTOM_TITLES = this:GetChecked() == 1 and true or false + end, + }) + panel:Finish() end function MTH_RefreshICUOptions() diff --git a/api/options-layout.lua b/api/options-layout.lua new file mode 100644 index 0000000..d8b8f66 --- /dev/null +++ b/api/options-layout.lua @@ -0,0 +1,608 @@ +-- ===================================================================== +-- MetaHunt Option Panel Layout Engine +-- --------------------------------------------------------------------- +-- One reusable builder that every option panel uses. It removes ALL +-- hand-typed pixel offsets and box heights from the panels: you declare +-- rows, the engine stacks them and auto-sizes each box. +-- +-- Think of it as CSS + flexbox for WoW 1.12: +-- * MTH_Layout.Theme -> the "CSS variables" (spacing / fonts / colors). +-- Change a value here to restyle every panel. +-- * panel / box methods -> the "flexbox": append rows, boxes grow to fit. +-- +-- Design rules baked in (so the classic bugs are impossible): +-- * Each box TITLE lives INSIDE its box -> titles can never collide. +-- * Box HEIGHT is computed from its content -> never cramped/too short. +-- * Box CONTENT is parented to the box frame -> correct z-order. +-- * Everything is reuse-by-name (getglobal) -> no duplicates on /rl. +-- +-- Typical usage: +-- local panel = MTH_Layout.Panel(container) +-- panel:Title("Feed-O-Matic") +-- panel:Note("blue helper paragraph", { lines = 3 }) +-- panel:TopCheckbox("MyEnable", "Enable module", { onClick = fn, checked = fn }) +-- local box = panel:Box("left", "MyBoxName", "General") +-- box:Text("MyStatus", "Feed key: P") +-- box:Buttons({ { name="A", label="Set Key", width=90, onClick=fn }, +-- { name="B", label="Clear", width=70, onClick=fn } }) +-- box:Note("MyHint", "blue helper text", { lines = 3 }) +-- local cb = box:Checkbox("MyOpt", "Some option", { onClick = fn, checked = true }) +-- box:EditRow("MyLbl", "Keep open bag slots:", "MyEdit", 40, { numeric = true, onChange = fn }) +-- panel:Finish() +-- ===================================================================== + +MTH_Layout = MTH_Layout or {} +local L = MTH_Layout + +-- --------------------------------------------------------------------- +-- THEME ("CSS variables": tweak these to restyle the whole UI) +-- --------------------------------------------------------------------- +L.Theme = { + -- Spacing (pixels) + panelPad = 12, -- outer gutter from the panel edges + colGap = 12, -- horizontal gap between the two columns + boxGap = 16, -- vertical gap between stacked boxes + colTopGap = 8, -- gap between the header block and the first boxes + + boxPadX = 10, -- content inset from a box's left edge + boxPadTop = 28, -- first row offset from a box's top (clears inside title) + boxPadBottom = 12, -- padding beneath the last row inside a box + cbInset = 8, -- checkbox inset from a box's left edge + + rowCheckbox = 24, -- vertical step consumed by a checkbox / edit row + rowButton = 30, -- vertical step consumed by a button row + rowSlider = 46, -- vertical step consumed by a slider (label+track+value) + rowDropdown = 50, -- vertical step consumed by a dropdown (label+menu) + textLine = 14, -- height of one wrapped text line + textGap = 6, -- extra gap after a text/note block + + titleInsetX = 10, -- box title inset X (inside the box) + titleInsetY = -12, -- box title inset Y (inside the box) + + headTop = -12, -- Y where the panel header starts + headTitleStep = 24, -- vertical step after the panel title + headNoteGap = 10, -- extra gap after a header note + headRowGap = 8, -- extra gap after a header checkbox + headLinkStep = 18, -- vertical step after a header link + + -- Fonts + fontPanelTitle = "GameFontNormal", -- panel heading + fontBoxTitle = "GameFontNormal", -- box title (recolored) + fontLabel = "GameFontNormalSmall", -- checkbox / edit labels + fontText = "GameFontNormalSmall", -- plain text + fontNote = "GameFontNormalSmall", -- helper paragraphs + fontLink = "GameFontHighlightSmall", + + -- Colors { r, g, b } + colPanelTitle = { 1.00, 0.82, 0.00 }, -- gold + colBoxTitle = { 1.00, 1.00, 1.00 }, -- white + colNote = { 0.35, 0.65, 1.00 }, -- blue helper text + colText = { 0.93, 0.93, 0.93 }, -- normal text + colLink = { 0.40, 0.75, 1.00 }, -- link idle + colLinkHover = { 0.60, 0.90, 1.00 }, -- link hover + + -- Templates + boxTemplate = "OptionFrameBoxTemplate", + checkTemplate = "UICheckButtonTemplate", + buttonTemplate = "UIPanelButtonTemplate", + editTemplate = "InputBoxTemplate", +} + +-- --------------------------------------------------------------------- +-- Small shared helpers +-- --------------------------------------------------------------------- +local function applyColor(region, c) + if region and c then region:SetTextColor(c[1], c[2], c[3]) end +end + +local function ensureFontString(parent, name, font) + local fs = getglobal(name) + if not fs then + fs = parent:CreateFontString(name, "ARTWORK", font) + end + fs:Show() + return fs +end + +local function ensureFrame(kind, name, parent, template) + local f = getglobal(name) + if not f then + f = CreateFrame(kind, name, parent, template) + end + f:SetParent(parent) + return f +end + +-- Attach (or refresh) a checkbox's label, tooltip, checked state and click. +-- labelWidth (optional) enables word-wrap; the wrapped text height is returned +-- so the caller can grow the row to fit multi-line labels. +local function configureCheckbox(cb, label, opts, T, labelWidth) + local txt = getglobal(cb:GetName() .. "Text") + if not txt then + txt = cb:CreateFontString(cb:GetName() .. "Text", "ARTWORK", T.fontLabel) + end + -- Anchor the label to the checkbox's top-right and let it grow downward so + -- wrapped lines never collide with the row above. Centre a single line + -- vertically against the checkbox. + local cbH = (cb.GetHeight and cb:GetHeight()) or 24 + local topOff = -((cbH - T.textLine) / 2) + if topOff > 0 then topOff = 0 end + txt:ClearAllPoints() + txt:SetPoint("TOPLEFT", cb, "TOPRIGHT", 4, topOff) + if labelWidth and labelWidth > 0 then + txt:SetWidth(labelWidth) + else + txt:SetWidth(0) + end + txt:SetJustifyH("LEFT") + txt:SetJustifyV("TOP") + txt:SetText(label or "") + txt:Show() + + if opts.checked ~= nil then + if type(opts.checked) == "function" then + cb:SetChecked(opts.checked()) + else + cb:SetChecked(opts.checked) + end + end + + if opts.onClick then + cb:SetScript("OnClick", opts.onClick) + end + + if opts.tooltip and opts.tooltip ~= "" then + cb.mthTooltip = opts.tooltip + cb:SetScript("OnEnter", function() + GameTooltip:SetOwner(cb, "ANCHOR_RIGHT") + GameTooltip:SetText(cb.mthTooltip, 1, 1, 1, 1, true) + GameTooltip:Show() + end) + cb:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + + local h = 0 + if txt.GetStringHeight then h = txt:GetStringHeight() end + if (not h or h <= 0) and txt.GetHeight then h = txt:GetHeight() end + return h or 0 +end + +-- --------------------------------------------------------------------- +-- Box object (returned by panel:Box). Appends rows top-to-bottom and +-- keeps the box height in sync with its content. +-- --------------------------------------------------------------------- +local function makeBox(container, box, boxWidth, state, T) + local self = {} + + local function apply() + state.height = -state.cursor + T.boxPadBottom + box:SetHeight(state.height) + end + + function self:Checkbox(name, label, opts) + opts = opts or {} + local cb = ensureFrame("CheckButton", name, box, T.checkTemplate) + cb:ClearAllPoints() + cb:SetPoint("TOPLEFT", box, "TOPLEFT", T.cbInset, state.cursor) + cb:Show() + -- Wrap width = box content width minus the checkbox frame, the 4px label + -- gap, and a 2px right cushion so wrapped text never touches the border. + local cbW = (cb.GetWidth and cb:GetWidth()) or 26 + local labelWidth = boxWidth - T.boxPadX - (T.cbInset + cbW + 4) - 2 + local txtH = configureCheckbox(cb, label, opts, T, labelWidth) + -- A single line keeps the compact row height; a wrapped label grows the + -- row to clear its top-centre offset plus a gap before the next row. + local rowH = T.rowCheckbox + local needed = txtH + 8 + if txtH > (T.textLine + 4) then needed = txtH + 14 end + if needed > rowH then rowH = needed end + state.cursor = state.cursor - rowH + apply() + return cb + end + + function self:Text(name, text, opts) + opts = opts or {} + local fs = ensureFontString(box, name, opts.font or T.fontText) + fs:ClearAllPoints() + fs:SetPoint("TOPLEFT", box, "TOPLEFT", T.boxPadX, state.cursor) + fs:SetWidth(opts.width or (boxWidth - T.boxPadX * 2)) + fs:SetJustifyH("LEFT") + fs:SetJustifyV("TOP") + fs:SetText(text or "") + applyColor(fs, opts.color or T.colText) + -- Reserve vertical space: honour an explicit line count, otherwise + -- measure the wrapped height so callers don't have to guess. + local reserve + if opts.lines then + reserve = opts.lines * T.textLine + else + local h = (fs.GetStringHeight and fs:GetStringHeight()) or 0 + if (not h or h <= 0) and fs.GetHeight then h = fs:GetHeight() end + if not h or h <= 0 then h = T.textLine end + reserve = h + end + state.cursor = state.cursor - reserve - T.textGap + apply() + return fs + end + + -- Blue helper paragraph (same as MM Widget style), usable inside a box. + function self:Note(name, text, opts) + opts = opts or {} + opts.color = opts.color or T.colNote + opts.font = opts.font or T.fontNote + return self:Text(name, text, opts) + end + + -- One row of buttons placed left-to-right. + -- list = { { name, label, width, height, onClick, gap, template }, ... } + function self:Buttons(list) + local out = {} + local x = T.boxPadX + local i = 1 + while list and i <= table.getn(list) do + local spec = list[i] + local b = ensureFrame("Button", spec.name, box, spec.template or T.buttonTemplate) + b:ClearAllPoints() + b:SetPoint("TOPLEFT", box, "TOPLEFT", x, state.cursor) + b:SetWidth(spec.width or 90) + b:SetHeight(spec.height or 22) + b:SetText(spec.label or "") + b:Show() + if spec.onClick then b:SetScript("OnClick", spec.onClick) end + table.insert(out, b) + x = x + (spec.width or 90) + (spec.gap or 8) + i = i + 1 + end + state.cursor = state.cursor - T.rowButton + apply() + return out + end + + -- Label + edit box on one row (e.g. "Keep open bag slots: [__]"). + function self:EditRow(labelName, labelText, editName, editWidth, opts) + opts = opts or {} + local lbl = ensureFontString(box, labelName, T.fontLabel) + lbl:ClearAllPoints() + lbl:SetPoint("TOPLEFT", box, "TOPLEFT", T.boxPadX, state.cursor) + lbl:SetText(labelText or "") + applyColor(lbl, T.colText) + + local edit = ensureFrame("EditBox", editName, box, T.editTemplate) + edit:ClearAllPoints() + edit:SetPoint("LEFT", lbl, "RIGHT", 8, 0) + edit:SetWidth(editWidth or 40) + edit:SetHeight(opts.height or 20) + if opts.numeric then edit:SetNumeric(true) end + edit:SetAutoFocus(false) + edit:Show() + if opts.onChange then edit:SetScript("OnTextChanged", opts.onChange) end + edit:SetScript("OnEnterPressed", function() if this then this:ClearFocus() end end) + edit:SetScript("OnEscapePressed", function() if this then this:ClearFocus() end end) + + state.cursor = state.cursor - T.rowCheckbox + apply() + return edit, lbl + end + + -- Slider with a left label and a right-aligned live value readout. + -- opts = { label, min, max, step, value, width, onChange, format } + -- format(value) -> string (defaults to a rounded integer). + function self:Slider(name, opts) + opts = opts or {} + local lbl = ensureFontString(box, name .. "Caption", T.fontLabel) + lbl:ClearAllPoints() + lbl:SetPoint("TOPLEFT", box, "TOPLEFT", T.boxPadX, state.cursor) + lbl:SetText(opts.label or "") + applyColor(lbl, T.colText) + + local valueFs = ensureFontString(box, name .. "Value", T.fontLabel) + valueFs:ClearAllPoints() + valueFs:SetPoint("TOPRIGHT", box, "TOPRIGHT", -T.boxPadX, state.cursor) + valueFs:SetJustifyH("RIGHT") + applyColor(valueFs, T.colPanelTitle) + + local slider = ensureFrame("Slider", name, box, "OptionsSliderTemplate") + slider:ClearAllPoints() + slider:SetPoint("TOPLEFT", box, "TOPLEFT", T.boxPadX + 4, state.cursor - 18) + slider:SetWidth(opts.width or (boxWidth - T.boxPadX * 2 - 8)) + slider:SetMinMaxValues(opts.min or 0, opts.max or 1) + slider:SetValueStep(opts.step or 1) + slider:Show() + + -- OptionsSliderTemplate ships Low/High/Text children; silence them. + local lowFs = getglobal(name .. "Low"); if lowFs then lowFs:SetText(""); lowFs:Hide() end + local highFs = getglobal(name .. "High"); if highFs then highFs:SetText(""); highFs:Hide() end + local midFs = getglobal(name .. "Text"); if midFs then midFs:SetText("") end + + slider.mthValueFs = valueFs + slider.mthFormat = opts.format + slider.mthOnChange = opts.onChange + slider:SetScript("OnValueChanged", function() + local v = tonumber(arg1) + if not v and this and this.GetValue then v = this:GetValue() end + if not v then v = 0 end + if this.mthValueFs then + if this.mthFormat then + this.mthValueFs:SetText(this.mthFormat(v)) + else + this.mthValueFs:SetText(tostring(math.floor(v + 0.5))) + end + end + if this.mthOnChange then this.mthOnChange(v) end + end) + slider:SetValue(opts.value or opts.min or 0) + + state.cursor = state.cursor - T.rowSlider + apply() + return slider + end + + -- Dropdown menu with a left label. The caller supplies the standard + -- UIDropDownMenu initialize function and (optionally) the current text. + -- opts = { label, width, initialize, text } + function self:Dropdown(name, opts) + opts = opts or {} + local lbl = ensureFontString(box, name .. "Caption", T.fontLabel) + lbl:ClearAllPoints() + lbl:SetPoint("TOPLEFT", box, "TOPLEFT", T.boxPadX, state.cursor) + lbl:SetText(opts.label or "") + applyColor(lbl, T.colText) + + local dd = ensureFrame("Frame", name, box, "UIDropDownMenuTemplate") + dd:ClearAllPoints() + -- UIDropDownMenu textures carry ~15px of built-in left inset. + dd:SetPoint("TOPLEFT", box, "TOPLEFT", T.boxPadX - 15, state.cursor - 16) + dd:Show() + if UIDropDownMenu_SetWidth then + UIDropDownMenu_SetWidth(opts.width or (boxWidth - T.boxPadX * 2 - 20), dd) + end + if UIDropDownMenu_JustifyText then UIDropDownMenu_JustifyText("LEFT", dd) end + if opts.initialize and UIDropDownMenu_Initialize then + UIDropDownMenu_Initialize(dd, opts.initialize) + end + if opts.text and UIDropDownMenu_SetText then + UIDropDownMenu_SetText(opts.text, dd) + end + + state.cursor = state.cursor - T.rowDropdown + apply() + return dd + end + + -- Single button occupying one button row (thin wrapper over :Buttons). + function self:Button(name, label, opts) + opts = opts or {} + local out = self:Buttons({ { + name = name, label = label, + width = opts.width, height = opts.height, + onClick = opts.onClick, template = opts.template, + } }) + return out[1] + end + + -- Label on the left, a colour swatch + "Pick" button on the right. + -- opts = { label, color = {r,g,b}, buttonText, buttonWidth, onClick } + function self:ColorRow(name, opts) + opts = opts or {} + local lbl = ensureFontString(box, name .. "Caption", T.fontLabel) + lbl:ClearAllPoints() + lbl:SetPoint("TOPLEFT", box, "TOPLEFT", T.boxPadX, state.cursor) + lbl:SetText(opts.label or "") + applyColor(lbl, T.colText) + + local btn = ensureFrame("Button", name .. "Button", box, T.buttonTemplate) + btn:ClearAllPoints() + btn:SetPoint("TOPRIGHT", box, "TOPRIGHT", -T.boxPadX, state.cursor + 2) + btn:SetWidth(opts.buttonWidth or 60) + btn:SetHeight(20) + btn:SetText(opts.buttonText or "Pick") + btn:Show() + if opts.onClick then btn:SetScript("OnClick", opts.onClick) end + + local swatch = getglobal(name .. "Swatch") + if not swatch then + swatch = btn:CreateTexture(name .. "Swatch", "OVERLAY") + swatch:SetTexture("Interface\\Buttons\\WHITE8X8") + swatch:SetWidth(12) + swatch:SetHeight(12) + swatch:SetPoint("LEFT", btn, "LEFT", 5, 0) + end + if opts.color then + swatch:SetVertexColor(opts.color[1], opts.color[2], opts.color[3]) + end + + state.cursor = state.cursor - T.rowCheckbox + apply() + return btn, swatch + end + + -- Manual vertical spacer. + function self:Gap(px) + state.cursor = state.cursor - (px or 12) + apply() + end + + -- Reserve a custom row of the given pixel height. Returns the box frame and + -- the TOPLEFT y-offset (within the box) where custom widgets can anchor. + -- Use for composite rows the primitives don't cover (e.g. checkbox + + -- dropdown + edit box on one line). x anchoring is left to the caller. + function self:Row(height) + local y = state.cursor + state.cursor = state.cursor - (height or T.rowCheckbox) + apply() + return box, y + end + + -- Left content inset (px) for callers positioning custom row widgets. + function self:PadX() return T.boxPadX end + + function self:Frame() return box end + + return self +end + +-- --------------------------------------------------------------------- +-- Panel object (returned by MTH_Layout.Panel). +-- --------------------------------------------------------------------- +function L.Panel(container) + if not container then return nil end + local T = L.Theme + MTH_ClearContainer(container) + + local cname = (container.GetName and container:GetName()) or "MTHPanel" + local width = (container.GetWidth and container:GetWidth()) or 0 + if not width or width < 460 then width = 580 end + + local colWidth = math.floor((width - T.panelPad * 2 - T.colGap) / 2) + if colWidth < 220 then colWidth = 220 end + local leftX = T.panelPad + local rightX = T.panelPad + colWidth + T.colGap + local fullWidth = width - T.panelPad * 2 + + local headY = T.headTop + local started = false + local col = { + left = { x = leftX, y = 0, open = nil }, + right = { x = rightX, y = 0, open = nil }, + } + local noteId = 0 + + local panel = {} + + local function startColumns() + if started then return end + started = true + local top = headY - T.colTopGap + col.left.y = top + col.right.y = top + end + + local function closeSide(side) + local c = col[side] + if c and c.open then + c.y = c.y - c.open.height - T.boxGap + c.open = nil + end + end + + -- ---- Header elements (full width, above the columns) ---- + function panel:Title(text) + local fs = ensureFontString(container, cname .. "_Title", T.fontPanelTitle) + fs:ClearAllPoints() + fs:SetPoint("TOPLEFT", container, "TOPLEFT", T.panelPad, headY) + fs:SetText(text or "") + applyColor(fs, T.colPanelTitle) + headY = headY - T.headTitleStep + return fs + end + + function panel:Note(text, opts) + opts = opts or {} + noteId = noteId + 1 + local fs = ensureFontString(container, cname .. "_Note" .. noteId, T.fontNote) + fs:ClearAllPoints() + fs:SetPoint("TOPLEFT", container, "TOPLEFT", T.panelPad, headY) + fs:SetWidth(opts.width or fullWidth) + fs:SetJustifyH("LEFT") + fs:SetJustifyV("TOP") + fs:SetText(text or "") + applyColor(fs, opts.color or T.colNote) + local lines = opts.lines or 1 + headY = headY - (lines * T.textLine) - T.headNoteGap + return fs + end + + function panel:Link(name, text, onClick) + local btn = ensureFrame("Button", name, container, nil) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", container, "TOPLEFT", T.panelPad, headY) + btn:SetHeight(16) + btn:SetWidth(fullWidth) + local fs = getglobal(name .. "Text") + if not fs then + fs = btn:CreateFontString(name .. "Text", "ARTWORK", T.fontLink) + fs:SetPoint("LEFT", btn, "LEFT", 0, 0) + fs:SetJustifyH("LEFT") + end + fs:SetText(text or "") + applyColor(fs, T.colLink) + btn:Show() + btn:SetScript("OnEnter", function() applyColor(fs, T.colLinkHover) end) + btn:SetScript("OnLeave", function() applyColor(fs, T.colLink) end) + if onClick then btn:SetScript("OnClick", onClick) end + headY = headY - T.headLinkStep + return btn + end + + function panel:TopCheckbox(name, label, opts) + opts = opts or {} + local cb = ensureFrame("CheckButton", name, container, T.checkTemplate) + cb:ClearAllPoints() + cb:SetPoint("TOPLEFT", container, "TOPLEFT", T.panelPad, headY) + cb:Show() + local txtH = configureCheckbox(cb, label, opts, T, fullWidth - 26) + local rowH = T.rowCheckbox + if (txtH + 8) > rowH then rowH = txtH + 8 end + headY = headY - rowH - T.headRowGap + return cb + end + + -- ---- Boxes (columns) ---- + function panel:Box(side, name, title) + startColumns() + side = (side == "right") and "right" or "left" + closeSide(side) + local c = col[side] + + local box = ensureFrame("Frame", name, container, T.boxTemplate) + box:ClearAllPoints() + box:SetPoint("TOPLEFT", container, "TOPLEFT", c.x, c.y) + box:SetWidth(colWidth) + box:Show() + + local titleFS = getglobal(name .. "Title") + if titleFS then + titleFS:SetText(title or "") + applyColor(titleFS, T.colBoxTitle) + titleFS:ClearAllPoints() + titleFS:SetPoint("TOPLEFT", box, "TOPLEFT", T.titleInsetX, T.titleInsetY) + end + + local state = { + cursor = -T.boxPadTop, + height = T.boxPadTop + T.boxPadBottom, + } + box:SetHeight(state.height) + c.open = state + + return makeBox(container, box, colWidth, state, T) + end + + -- Returns "left" or "right": whichever column currently has more room + -- left (i.e. its content bottom is higher). Lets callers auto-balance + -- a variable number of boxes without hand-picking columns. + function panel:ShorterSide() + startColumns() + local function bottom(side) + local c = col[side] + local y = c.y + if c.open then y = y - c.open.height - T.boxGap end + return y + end + if bottom("right") > bottom("left") then return "right" end + return "left" + end + + function panel:Finish() + closeSide("left") + closeSide("right") + end + + -- Expose geometry for callers that need it (rare). + panel.colWidth = colWidth + panel.fullWidth = fullWidth + + return panel +end diff --git a/api/options-profiles.lua b/api/options-profiles.lua index d11aad4..c8272b3 100644 --- a/api/options-profiles.lua +++ b/api/options-profiles.lua @@ -300,11 +300,10 @@ function MTH_SetupProfilesOptions() if not container then return end if not gSetupDone then - -- Title - local title = container:CreateFontString(nil, "ARTWORK", "GameFontNormal") - title:SetPoint("TOPLEFT", container, "TOPLEFT", 6, -6) - title:SetText("Profiles") - title:SetTextColor(1.00, 0.82, 0.00) + local panel = MTH_Layout.Panel(container) + + -- Title (engine header, consistent gold styling) + local title = panel:Title("Profiles") -- Active profile indicator gActiveLabel = container:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") diff --git a/api/options-shell.lua b/api/options-shell.lua index c73f4c0..610e61c 100644 --- a/api/options-shell.lua +++ b/api/options-shell.lua @@ -3,8 +3,8 @@ local MTH_OPTIONS_SETUP = {} -- Track which frames have been set up MTH_OPTIONS_CONST = MTH_OPTIONS_CONST or {} MTH_OPTIONS_CONST.NAV_DEFAULT_WIDTH = MTH_OPTIONS_CONST.NAV_DEFAULT_WIDTH or 120 MTH_OPTIONS_CONST.WINDOW_PADDING = MTH_OPTIONS_CONST.WINDOW_PADDING or 12 -MTH_OPTIONS_CONST.CONTENT_TOP_OFFSET = MTH_OPTIONS_CONST.CONTENT_TOP_OFFSET or -58 -MTH_OPTIONS_CONST.CONTENT_HEIGHT = MTH_OPTIONS_CONST.CONTENT_HEIGHT or 566 +MTH_OPTIONS_CONST.CONTENT_TOP_OFFSET = MTH_OPTIONS_CONST.CONTENT_TOP_OFFSET or -30 +MTH_OPTIONS_CONST.CONTENT_HEIGHT = MTH_OPTIONS_CONST.CONTENT_HEIGHT or 600 MTH_OPTIONS_CONST.CLOSE_BTN_SIZE = MTH_OPTIONS_CONST.CLOSE_BTN_SIZE or 22 MTH_OPTIONS_CONST.CLOSE_BTN_OFFSET_X = MTH_OPTIONS_CONST.CLOSE_BTN_OFFSET_X or -8 MTH_OPTIONS_CONST.CLOSE_BTN_OFFSET_Y = MTH_OPTIONS_CONST.CLOSE_BTN_OFFSET_Y or -8 diff --git a/api/options-zbuttons.lua b/api/options-zbuttons.lua index f5e02d0..016f1e1 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -338,315 +338,90 @@ local function MTH_SetupButtonOptions(containerName, buttonName, displayName, ma local rightX = leftWidth + 44 local rightWidth = containerWidth - rightX - 12 - local function MTH_ZB_EnsureSection(sectionName, titleText, yOffset, sectionHeight) - local section = CreateFrame("Frame", sectionName, container, "OptionFrameBoxTemplate") - if not section then return nil end - section:ClearAllPoints() - section:SetPoint("TOPLEFT", container, "TOPLEFT", 16, yOffset) - section:SetWidth(leftWidth) - section:SetHeight(sectionHeight) - section:Show() - - local title = getglobal(sectionName .. "Title") - if title then - title:SetText(titleText) - end - - return section - end + local panel = MTH_Layout.Panel(container) local enabledValue = saved["enabled"] and true or false local enableLabel = tostring(displayName or "") if string.len(enableLabel) > 0 then enableLabel = string.lower(string.sub(enableLabel, 1, 1)) .. string.sub(enableLabel, 2) end - - local enableButton = MTH_CreateCheckbox(container, containerName.."EnableButton", string.format(MTH_ZB_L("ZB_LABEL_ENABLE_FMT", "Enable %s"), enableLabel), -8) - if enableButton then - enableButton:SetChecked(enabledValue and true or false) - enableButton.buttonName = buttonName - enableButton.buttonObj = buttonObj - enableButton:SetScript("OnClick", function() + panel:TopCheckbox(containerName.."EnableButton", string.format(MTH_ZB_L("ZB_LABEL_ENABLE_FMT", "Enable %s"), enableLabel), { + checked = enabledValue, + onClick = function() if not this then return end - local checked = MTH_ZB_IsChecked(this) - MTH_SetButtonEnabledState(this.buttonName, this.buttonObj, checked) - end) - end + MTH_SetButtonEnabledState(buttonName, buttonObj, this:GetChecked() == 1) + end, + }) - local parentSectionHeight = 116 - if buttonName == "zButtonAspect" or buttonName == "zButtonTrack" or buttonName == "zButtonPet" then - parentSectionHeight = 200 - elseif buttonName == "zButtonMounts" then - parentSectionHeight = 225 - elseif buttonName == "zButtonCompanions" then - parentSectionHeight = 196 - end - local childrenSectionY = -52 - parentSectionHeight - 16 - local parentSection = MTH_ZB_EnsureSection(containerName.."ParentSection", MTH_ZB_L("ZB_SECTION_PARENT_BUTTON", "Parent Button"), -52, parentSectionHeight) - local childrenSection = MTH_ZB_EnsureSection(containerName.."ChildrenSection", MTH_ZB_L("ZB_SECTION_CHILDREN_BUTTONS", "Children Buttons"), childrenSectionY, 290) + local parentBox = panel:Box("left", containerName.."ParentSection", MTH_ZB_L("ZB_SECTION_PARENT_BUTTON", "Parent Button")) - local parentYOffset = -18 - local childrenYOffset = -18 - - local btnSize = MTH_CreateSlider(childrenSection or container, containerName.."ButtonSize", MTH_ZB_L("ZB_LABEL_BUTTON_SIZE", "Button Size"), 10, 100, 1, childrenYOffset) - if btnSize then - btnSize:SetWidth(leftWidth - 40) - btnSize:SetValue(saved["children"]["size"] or 36) - btnSize.buttonName = buttonName - btnSize.buttonObj = buttonObj - btnSize.itemList = MTH_GetButtonItemList(buttonName) - btnSize.maxButtons = maxButtons - btnSize.onChange = function(val, slider) - local frame = slider or btnSize - local btnName = frame and frame.buttonName - if not btnName or not (ZHunterMod_Saved and ZHunterMod_Saved[btnName]) then return end - if not ZHunterMod_Saved[btnName]["children"] then - ZHunterMod_Saved[btnName]["children"] = {} + parentBox:Slider(containerName.."MainButtonSize", { + label = MTH_ZB_L("ZB_LABEL_BUTTON_SIZE", "Button Size"), + min = 10, max = 100, step = 1, value = saved["parent"]["size"] or 36, + onChange = function(val) + if not (ZHunterMod_Saved and ZHunterMod_Saved[buttonName]) then return end + if not ZHunterMod_Saved[buttonName]["parent"] then + ZHunterMod_Saved[buttonName]["parent"] = {} end local size = math.floor((val or 0) + 0.5) - ZHunterMod_Saved[btnName]["children"]["size"] = size - MTH_RefreshButtonGeometry(btnName, frame.buttonObj) - end - end - childrenYOffset = childrenYOffset - 50 - - local rowCount = MTH_CreateSlider(childrenSection or container, containerName.."RowCount", MTH_ZB_L("ZB_LABEL_NUMBER_OF_ROWS", "Number of Rows"), 1, maxButtons, 1, childrenYOffset) - if rowCount then - rowCount:SetWidth(leftWidth - 40) - rowCount:SetValue(saved["rows"] or 1) - rowCount.buttonName = buttonName - rowCount.buttonObj = buttonObj - rowCount.itemList = MTH_GetButtonItemList(buttonName) - rowCount.maxButtons = maxButtons - rowCount.onChange = function(val, slider) - local frame = slider or rowCount - local btnName = frame and frame.buttonName - if not btnName or not (ZHunterMod_Saved and ZHunterMod_Saved[btnName]) then return end - ZHunterMod_Saved[btnName]["rows"] = math.floor((val or 1) + 0.5) - MTH_RefreshButtonGeometry(btnName, frame.buttonObj) - end - end - childrenYOffset = childrenYOffset - 40 - - local expandLeft = MTH_CreateCheckbox(childrenSection or container, containerName.."ExpandLeft", MTH_ZB_L("ZB_LABEL_EXPAND_LEFT", "Expand Left (opposite side)"), childrenYOffset) - if expandLeft then - expandLeft:SetChecked(saved["firstbutton"] == "LEFT") - expandLeft.buttonName = buttonName - expandLeft.buttonObj = buttonObj - expandLeft:SetScript("OnClick", function() - if not this then return end - local checked = MTH_ZB_IsChecked(this) - local btnName = this.buttonName - if not btnName or not (ZHunterMod_Saved and ZHunterMod_Saved[btnName]) then return end - ZHunterMod_Saved[btnName]["firstbutton"] = checked and "LEFT" or "RIGHT" - ZHunterMod_Saved[btnName]["horizontal"] = checked and 1 or nil - ZHunterMod_Saved[btnName]["vertical"] = checked and 1 or nil - MTH_RefreshButtonGeometry(btnName, this.buttonObj) - end) - end - childrenYOffset = childrenYOffset - 30 - - local hideClick = MTH_CreateCheckbox(childrenSection or container, containerName.."HideOnClick", MTH_ZB_L("ZB_LABEL_HIDE_BUTTONS_ON_CLICK", "Hide Buttons On Click"), childrenYOffset) - if hideClick then - hideClick:SetChecked(saved["children"]["hideonclick"]) - hideClick.buttonName = buttonName - hideClick.buttonObj = buttonObj - if hideClick.buttonObj then - hideClick.buttonObj.hideonclick = saved["children"]["hideonclick"] and true or false - end - hideClick:SetScript("OnClick", function() - if not this then return end - local checked = MTH_ZB_IsChecked(this) - ZHunterMod_Saved[this.buttonName]["children"]["hideonclick"] = checked - local btnObj = this.buttonObj or getglobal(this.buttonName) - if btnObj then btnObj.hideonclick = checked end - end) - end - childrenYOffset = childrenYOffset - 25 - - local expandOnHover = MTH_CreateCheckbox(childrenSection or container, containerName.."ExpandOnHover", MTH_ZB_L("ZB_LABEL_EXPAND_ON_HOVER", "Expand on Hover"), childrenYOffset) - if expandOnHover then - expandOnHover:SetChecked(saved["children"]["expandonhover"]) - expandOnHover.buttonName = buttonName - expandOnHover.buttonObj = buttonObj - expandOnHover:SetScript("OnClick", function() - if not this then return end - local checked = MTH_ZB_IsChecked(this) - local btnName = this.buttonName - if not btnName or not (ZHunterMod_Saved and ZHunterMod_Saved[btnName]) then return end - if not ZHunterMod_Saved[btnName]["children"] then - ZHunterMod_Saved[btnName]["children"] = {} + ZHunterMod_Saved[buttonName]["parent"]["size"] = size + if buttonObj then + buttonObj:SetWidth(size) + buttonObj:SetHeight(size) end - ZHunterMod_Saved[btnName]["children"]["expandonhover"] = checked - local btnObj = this.buttonObj or getglobal(btnName) - if btnObj then btnObj.expandonhover = checked end - end) - end - childrenYOffset = childrenYOffset - 40 + MTH_RefreshButtonGeometry(buttonName, buttonObj) + end, + }) - local fadeTimer = MTH_CreateSlider(childrenSection or container, containerName.."FadeTimer", MTH_ZB_L("ZB_LABEL_AUTO_HIDE_TIMER", "Auto-Hide Timer (seconds)"), 0, 10, 1, childrenYOffset) - if fadeTimer then - fadeTimer:SetWidth(leftWidth - 40) - fadeTimer:SetValue(saved["children"]["fadetimer"] or 0) - fadeTimer.buttonName = buttonName - fadeTimer.buttonObj = buttonObj - fadeTimer.onChange = function(val, slider) - local frame = slider or fadeTimer - local btnName = frame and frame.buttonName - if not btnName or not (ZHunterMod_Saved and ZHunterMod_Saved[btnName]) then return end - if not ZHunterMod_Saved[btnName]["children"] then - ZHunterMod_Saved[btnName]["children"] = {} - end - local secs = math.floor((val or 0) + 0.5) - ZHunterMod_Saved[btnName]["children"]["fadetimer"] = secs - local btnObj = frame.buttonObj or getglobal(btnName) - if btnObj then - btnObj.fadetimer = secs - if secs <= 0 then - ZSpellButton_StopFadeTimer(btnObj) - elseif btnObj.children and btnObj.children:IsVisible() then - ZSpellButton_StartFadeTimer(btnObj) - end - end - end - end - childrenYOffset = childrenYOffset - 40 - - local showTooltip = MTH_CreateCheckbox(childrenSection or container, containerName.."ShowTooltip", MTH_ZB_L("ZB_LABEL_SHOW_TOOLTIP", "Show Tooltip"), childrenYOffset) - if showTooltip then - showTooltip:SetChecked(saved["tooltip"]) - showTooltip.buttonName = buttonName - showTooltip.buttonObj = buttonObj - if showTooltip.buttonObj then - showTooltip.buttonObj.tooltip = saved["tooltip"] and true or false - end - showTooltip:SetScript("OnClick", function() - if not this then return end - local checked = MTH_ZB_IsChecked(this) - ZHunterMod_Saved[this.buttonName]["tooltip"] = checked - local btnObj = this.buttonObj or getglobal(this.buttonName) - if btnObj then btnObj.tooltip = checked end - end) - end - childrenYOffset = childrenYOffset - 20 - - if buttonName == "zButtonAmmo" then - local showAmmoName = MTH_CreateCheckbox(childrenSection or container, containerName.."ShowAmmoName", MTH_ZB_L("ZB_LABEL_SHOW_AMMO_NAME", "Show ammo name"), childrenYOffset) - if showAmmoName then - showAmmoName:SetChecked(saved["showammoname"]) - showAmmoName.buttonName = buttonName - showAmmoName.buttonObj = buttonObj - showAmmoName.maxButtons = maxButtons - showAmmoName:SetScript("OnClick", function() - if not this then return end - local checked = MTH_ZB_IsChecked(this) - local btnName = this.buttonName - ZHunterMod_Saved[btnName]["showammoname"] = checked - local btnObj = this.buttonObj or getglobal(btnName) - if btnObj then - for i = 1, (btnObj.count or 0) do - local child = getglobal(btnName..i) - if child and child.ammoname and not child.isspell then - zButtonAmmo_UpdateButton(child) - end - end - if btnObj.ammoname and not btnObj.isspell then - zButtonAmmo_UpdateButton(btnObj) - end - end - end) - end - childrenYOffset = childrenYOffset - 25 - else - childrenYOffset = childrenYOffset - 25 - end - - local mainSize = MTH_CreateSlider(parentSection or container, containerName.."MainButtonSize", MTH_ZB_L("ZB_LABEL_BUTTON_SIZE", "Button Size"), 10, 100, 1, parentYOffset) - if mainSize then - mainSize:SetWidth(leftWidth - 40) - mainSize:SetValue(saved["parent"]["size"] or 36) - mainSize.buttonName = buttonName - mainSize.buttonObj = buttonObj - mainSize.onChange = function(val, slider) - local frame = slider or mainSize - local btnName = frame and frame.buttonName - local btnObj = (frame and frame.buttonObj) or (btnName and getglobal(btnName)) - if not btnName or not (ZHunterMod_Saved and ZHunterMod_Saved[btnName]) then return end - if not ZHunterMod_Saved[btnName]["parent"] then - ZHunterMod_Saved[btnName]["parent"] = {} - end - local size = math.floor((val or 0) + 0.5) - ZHunterMod_Saved[btnName]["parent"]["size"] = size - if btnObj then - btnObj:SetWidth(size) - btnObj:SetHeight(size) - end - MTH_RefreshButtonGeometry(btnName, btnObj) - end - end - parentYOffset = parentYOffset - 35 - - local hideButton = MTH_CreateCheckbox(parentSection or container, containerName.."HideButton", MTH_ZB_L("ZB_LABEL_HIDE_BUTTON", "Hide Button"), parentYOffset) - if hideButton then - hideButton:SetChecked(saved["parent"]["hide"]) - hideButton.buttonName = buttonName - hideButton.buttonObj = buttonObj - hideButton:SetScript("OnClick", function() + parentBox:Checkbox(containerName.."HideButton", MTH_ZB_L("ZB_LABEL_HIDE_BUTTON", "Hide Button"), { + checked = saved["parent"]["hide"], + onClick = function() if not this then return end local checked = this:GetChecked() == 1 - ZHunterMod_Saved[this.buttonName]["parent"]["hide"] = checked - if this.buttonObj then - if checked then this.buttonObj:Hide() else this.buttonObj:Show() end + ZHunterMod_Saved[buttonName]["parent"]["hide"] = checked + if buttonObj then + if checked then buttonObj:Hide() else buttonObj:Show() end end - end) - end - parentYOffset = parentYOffset - 25 + end, + }) - local useCircle = MTH_CreateCheckbox(parentSection or container, containerName.."UseCircle", MTH_ZB_L("ZB_LABEL_USE_CIRCLE_BUTTON", "Use Circle Button"), parentYOffset) - if useCircle then - useCircle:SetChecked(saved["parent"]["circle"]) - useCircle.buttonName = buttonName - useCircle.buttonObj = buttonObj - useCircle:SetScript("OnClick", function() + parentBox:Checkbox(containerName.."UseCircle", MTH_ZB_L("ZB_LABEL_USE_CIRCLE_BUTTON", "Use Circle Button"), { + checked = saved["parent"]["circle"], + onClick = function() if not this then return end local checked = this:GetChecked() == 1 - ZHunterMod_Saved[this.buttonName]["parent"]["circle"] = checked - if this.buttonObj and this.buttonObj.circle then - if checked then this.buttonObj.circle:Show() else this.buttonObj.circle:Hide() end + ZHunterMod_Saved[buttonName]["parent"]["circle"] = checked + if buttonObj and buttonObj.circle then + if checked then buttonObj.circle:Show() else buttonObj.circle:Hide() end end - end) - end + end, + }) - -- Smart Parent: Aspect/Track/Pet if buttonName == "zButtonAspect" or buttonName == "zButtonTrack" or buttonName == "zButtonPet" then - parentYOffset = parentYOffset - 25 - local smartParent = MTH_CreateCheckbox(parentSection or container, containerName.."SmartParent", MTH_ZB_L("ZB_LABEL_SMART_PARENT", "Smart Parent"), parentYOffset) - if smartParent then - smartParent:SetChecked(saved["parent"]["smart"]) - smartParent.buttonName = buttonName - smartParent.buttonObj = buttonObj - smartParent:SetScript("OnClick", function() + parentBox:Checkbox(containerName.."SmartParent", MTH_ZB_L("ZB_LABEL_SMART_PARENT", "Smart Parent"), { + checked = saved["parent"]["smart"], + onClick = function() if not this then return end local checked = this:GetChecked() == 1 - ZHunterMod_Saved[this.buttonName]["parent"]["smart"] = checked - local btn = this.buttonObj or getglobal(this.buttonName) + ZHunterMod_Saved[buttonName]["parent"]["smart"] = checked + local btn = buttonObj or getglobal(buttonName) if btn then if not checked then - local firstChild = getglobal(this.buttonName .. "1") + local firstChild = getglobal(buttonName .. "1") if firstChild and firstChild.id then btn.id = firstChild.id ZSpellButton_UpdateButton(btn) ZSpellButton_UpdateCooldown(btn) end else - local setupFunc = getglobal(this.buttonName .. "_SetupSizeAndPosition") + local setupFunc = getglobal(buttonName .. "_SetupSizeAndPosition") if type(setupFunc) == "function" then setupFunc() end end end - end) - end + end, + }) local smartDescText if buttonName == "zButtonAspect" then smartDescText = "When ON, the parent toggles between your 1st and 2nd aspect. If the 1st is active, the parent shows the 2nd and vice-versa. Useful for quickly swapping between two aspects in combat. When OFF, the parent always shows the 1st aspect." @@ -657,56 +432,151 @@ local function MTH_SetupButtonOptions(containerName, buttonName, displayName, ma else smartDescText = "Dynamically swap the parent icon based on context." end - local smartDesc = (parentSection or container):CreateFontString(containerName.."SmartParentDesc", "ARTWORK", "GameFontNormalSmall") - smartDesc:SetPoint("TOPLEFT", parentSection or container, "TOPLEFT", 40, parentYOffset - 24) - smartDesc:SetWidth(leftWidth - 50) - smartDesc:SetJustifyH("LEFT") - smartDesc:SetText(smartDescText) - smartDesc:SetTextColor(0.5, 0.7, 1.0) + parentBox:Note(containerName.."SmartParentDesc", smartDescText) end - -- Random Parent: Mounts/Companions if buttonName == "zButtonMounts" or buttonName == "zButtonCompanions" then - parentYOffset = parentYOffset - 25 - local randomParent = MTH_CreateCheckbox(parentSection or container, containerName.."RandomParent", MTH_ZB_L("ZB_LABEL_RANDOM_PARENT", "Random Parent"), parentYOffset) - if randomParent then - randomParent:SetChecked(saved["parent"]["random"]) - randomParent.buttonName = buttonName - randomParent.buttonObj = buttonObj - randomParent:SetScript("OnClick", function() + parentBox:Checkbox(containerName.."RandomParent", MTH_ZB_L("ZB_LABEL_RANDOM_PARENT", "Random Parent"), { + checked = saved["parent"]["random"], + onClick = function() if not this then return end local checked = this:GetChecked() == 1 - ZHunterMod_Saved[this.buttonName]["parent"]["random"] = checked - end) - end - local randomDesc = (parentSection or container):CreateFontString(containerName.."RandomParentDesc", "ARTWORK", "GameFontNormalSmall") - randomDesc:SetPoint("TOPLEFT", parentSection or container, "TOPLEFT", 40, parentYOffset - 24) - randomDesc:SetWidth(leftWidth - 50) - randomDesc:SetJustifyH("LEFT") - randomDesc:SetText(MTH_ZB_L("ZB_DESC_RANDOM_PARENT", "Pick a random child as parent on each login and after each use.")) - randomDesc:SetTextColor(0.5, 0.7, 1.0) + ZHunterMod_Saved[buttonName]["parent"]["random"] = checked + end, + }) + parentBox:Note(containerName.."RandomParentDesc", MTH_ZB_L("ZB_DESC_RANDOM_PARENT", "Pick a random child as parent on each login and after each use.")) end - -- AQ Mount Filter: Mounts only if buttonName == "zButtonMounts" then - parentYOffset = parentYOffset - 45 - local aqFilter = MTH_CreateCheckbox(parentSection or container, containerName.."AQFilter", MTH_ZB_L("ZB_LABEL_AQ_FILTER", "AQ Mount Filter"), parentYOffset) - if aqFilter then - aqFilter:SetChecked(saved["parent"]["aqfilter"]) - aqFilter.buttonName = buttonName - aqFilter.buttonObj = buttonObj - aqFilter:SetScript("OnClick", function() + parentBox:Checkbox(containerName.."AQFilter", MTH_ZB_L("ZB_LABEL_AQ_FILTER", "AQ Mount Filter"), { + checked = saved["parent"]["aqfilter"], + onClick = function() if not this then return end local checked = this:GetChecked() == 1 - ZHunterMod_Saved[this.buttonName]["parent"]["aqfilter"] = checked - end) - end - local aqDesc = (parentSection or container):CreateFontString(containerName.."AQFilterDesc", "ARTWORK", "GameFontNormalSmall") - aqDesc:SetPoint("TOPLEFT", parentSection or container, "TOPLEFT", 40, parentYOffset - 24) - aqDesc:SetWidth(leftWidth - 50) - aqDesc:SetJustifyH("LEFT") - aqDesc:SetText(MTH_ZB_L("ZB_DESC_AQ_FILTER", "Hide Qiraji mounts outside AQ40. Show only Qiraji inside.")) - aqDesc:SetTextColor(0.5, 0.7, 1.0) + ZHunterMod_Saved[buttonName]["parent"]["aqfilter"] = checked + end, + }) + parentBox:Note(containerName.."AQFilterDesc", MTH_ZB_L("ZB_DESC_AQ_FILTER", "Hide Qiraji mounts outside AQ40. Show only Qiraji inside.")) + end + + local childrenBox = panel:Box("left", containerName.."ChildrenSection", MTH_ZB_L("ZB_SECTION_CHILDREN_BUTTONS", "Children Buttons")) + childrenBox:Slider(containerName.."ButtonSize", { + label = MTH_ZB_L("ZB_LABEL_BUTTON_SIZE", "Button Size"), + min = 10, max = 100, step = 1, value = saved["children"]["size"] or 36, + onChange = function(val) + if not (ZHunterMod_Saved and ZHunterMod_Saved[buttonName]) then return end + if not ZHunterMod_Saved[buttonName]["children"] then + ZHunterMod_Saved[buttonName]["children"] = {} + end + ZHunterMod_Saved[buttonName]["children"]["size"] = math.floor((val or 0) + 0.5) + MTH_RefreshButtonGeometry(buttonName, buttonObj) + end, + }) + + childrenBox:Slider(containerName.."RowCount", { + label = MTH_ZB_L("ZB_LABEL_NUMBER_OF_ROWS", "Number of Rows"), + min = 1, max = maxButtons, step = 1, value = saved["rows"] or 1, + onChange = function(val) + if not (ZHunterMod_Saved and ZHunterMod_Saved[buttonName]) then return end + ZHunterMod_Saved[buttonName]["rows"] = math.floor((val or 1) + 0.5) + MTH_RefreshButtonGeometry(buttonName, buttonObj) + end, + }) + + childrenBox:Checkbox(containerName.."ExpandLeft", MTH_ZB_L("ZB_LABEL_EXPAND_LEFT", "Expand Left (opposite side)"), { + checked = (saved["firstbutton"] == "LEFT"), + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + if not (ZHunterMod_Saved and ZHunterMod_Saved[buttonName]) then return end + ZHunterMod_Saved[buttonName]["firstbutton"] = checked and "LEFT" or "RIGHT" + ZHunterMod_Saved[buttonName]["horizontal"] = checked and 1 or nil + ZHunterMod_Saved[buttonName]["vertical"] = checked and 1 or nil + MTH_RefreshButtonGeometry(buttonName, buttonObj) + end, + }) + + childrenBox:Checkbox(containerName.."HideOnClick", MTH_ZB_L("ZB_LABEL_HIDE_BUTTONS_ON_CLICK", "Hide Buttons On Click"), { + checked = saved["children"]["hideonclick"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["children"]["hideonclick"] = checked + if buttonObj then buttonObj.hideonclick = checked end + end, + }) + if buttonObj then + buttonObj.hideonclick = saved["children"]["hideonclick"] and true or false + end + + childrenBox:Checkbox(containerName.."ExpandOnHover", MTH_ZB_L("ZB_LABEL_EXPAND_ON_HOVER", "Expand on Hover"), { + checked = saved["children"]["expandonhover"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + if not (ZHunterMod_Saved and ZHunterMod_Saved[buttonName]) then return end + if not ZHunterMod_Saved[buttonName]["children"] then + ZHunterMod_Saved[buttonName]["children"] = {} + end + ZHunterMod_Saved[buttonName]["children"]["expandonhover"] = checked + if buttonObj then buttonObj.expandonhover = checked end + end, + }) + + childrenBox:Slider(containerName.."FadeTimer", { + label = MTH_ZB_L("ZB_LABEL_AUTO_HIDE_TIMER", "Auto-Hide Timer (seconds)"), + min = 0, max = 10, step = 1, value = saved["children"]["fadetimer"] or 0, + onChange = function(val) + if not (ZHunterMod_Saved and ZHunterMod_Saved[buttonName]) then return end + if not ZHunterMod_Saved[buttonName]["children"] then + ZHunterMod_Saved[buttonName]["children"] = {} + end + local secs = math.floor((val or 0) + 0.5) + ZHunterMod_Saved[buttonName]["children"]["fadetimer"] = secs + if buttonObj then + buttonObj.fadetimer = secs + if secs <= 0 then + ZSpellButton_StopFadeTimer(buttonObj) + elseif buttonObj.children and buttonObj.children:IsVisible() then + ZSpellButton_StartFadeTimer(buttonObj) + end + end + end, + }) + + childrenBox:Checkbox(containerName.."ShowTooltip", MTH_ZB_L("ZB_LABEL_SHOW_TOOLTIP", "Show Tooltip"), { + checked = saved["tooltip"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["tooltip"] = checked + if buttonObj then buttonObj.tooltip = checked end + end, + }) + if buttonObj then + buttonObj.tooltip = saved["tooltip"] and true or false + end + + if buttonName == "zButtonAmmo" then + childrenBox:Checkbox(containerName.."ShowAmmoName", MTH_ZB_L("ZB_LABEL_SHOW_AMMO_NAME", "Show ammo name"), { + checked = saved["showammoname"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["showammoname"] = checked + if buttonObj then + for i = 1, (buttonObj.count or 0) do + local child = getglobal(buttonName..i) + if child and child.ammoname and not child.isspell then + zButtonAmmo_UpdateButton(child) + end + end + if buttonObj.ammoname and not buttonObj.isspell then + zButtonAmmo_UpdateButton(buttonObj) + end + end + end, + }) end MTH_SetButtonEnabledState(buttonName, buttonObj, saved["enabled"] and true or false) @@ -862,29 +732,24 @@ function MTH_SetupZBarOptions() local rightX = leftWidth + 44 local rightWidth = containerWidth - rightX - 12 - -- ===== Enable checkbox ===== - local enableBtn = MTH_CreateCheckbox(container, "MetaHuntOptionsZBarEnable", - "Enable zBar — group all zButtons into a single draggable bar", -8) - if enableBtn then - enableBtn:SetChecked(s.enabled and true or false) - enableBtn:SetScript("OnClick", function() + local panel = MTH_Layout.Panel(container) + + panel:TopCheckbox("MetaHuntOptionsZBarEnable", "Enable zBar", { + checked = s.enabled, + onClick = function() if type(MTH_ZBar_SetEnabled) == "function" then MTH_ZBar_SetEnabled(this:GetChecked() == 1) end - end) - end + end, + }) + panel:Note("Group all zButtons into a single draggable bar.", { width = 240 }) - -- ===== Direction header ===== - local dirLabel = container:CreateFontString(nil, "ARTWORK", "GameFontHighlight") - dirLabel:SetPoint("TOPLEFT", container, "TOPLEFT", 16, -46) - dirLabel:SetText("Bar Direction") + -- ===== Bar Direction box ===== + local dirBox = panel:Box("left", "MetaHuntOptionsZBarDirBox", "Bar Direction") - -- ===== Horizontal radio ===== - local horizBtn = MTH_CreateCheckbox(container, "MetaHuntOptionsZBarHoriz", - "Horizontal (side by side)", -64) - if horizBtn then - horizBtn:SetChecked(s.direction ~= "VERTICAL") - horizBtn:SetScript("OnClick", function() + dirBox:Checkbox("MetaHuntOptionsZBarHoriz", "Horizontal (side by side)", { + checked = (s.direction ~= "VERTICAL"), + onClick = function() if this:GetChecked() == 1 then local sv = MTH_ZBar_GetSaved() sv.direction = "HORIZONTAL" @@ -901,15 +766,12 @@ function MTH_SetupZBarOptions() else this:SetChecked(true) end - end) - end + end, + }) - -- ===== Vertical radio ===== - local vertBtn = MTH_CreateCheckbox(container, "MetaHuntOptionsZBarVert", - "Vertical (stacked)", -90) - if vertBtn then - vertBtn:SetChecked(s.direction == "VERTICAL") - vertBtn:SetScript("OnClick", function() + dirBox:Checkbox("MetaHuntOptionsZBarVert", "Vertical (stacked)", { + checked = (s.direction == "VERTICAL"), + onClick = function() if this:GetChecked() == 1 then local sv = MTH_ZBar_GetSaved() sv.direction = "VERTICAL" @@ -926,69 +788,59 @@ function MTH_SetupZBarOptions() else this:SetChecked(true) end - end) - end + end, + }) - -- ===== Children expand side (contextual to bar direction) ===== - local expandLbl = container:CreateFontString(nil, "ARTWORK", "GameFontHighlight") - expandLbl:SetPoint("TOPLEFT", container, "TOPLEFT", 16, -120) - if s.direction == "VERTICAL" then - expandLbl:SetText("Children expand side (Vertical bar):") - else - expandLbl:SetText("Children expand side (Horizontal bar):") - end + -- ===== Children box ===== + local childBox = panel:Box("left", "MetaHuntOptionsZBarChildBox", "Children") - local exp1Val, exp2Val, exp1Lbl, exp2Lbl + local exp1Val, exp2Val, exp1Lbl, exp2Lbl, expNote if s.direction == "VERTICAL" then exp1Val = "LEFT" ; exp1Lbl = "Left" exp2Val = "RIGHT" ; exp2Lbl = "Right" + expNote = "Expand side (Vertical bar):" else exp1Val = "TOP" ; exp1Lbl = "Top (above)" exp2Val = "BOTTOM" ; exp2Lbl = "Bottom (below)" + expNote = "Expand side (Horizontal bar):" end - local exp1Btn = MTH_CreateCheckbox(container, "MetaHuntOptionsZBarExp1", exp1Lbl, -138, 30) - if exp1Btn then - exp1Btn:SetChecked(s.childexpand == exp1Val) - exp1Btn.zbarVal = exp1Val - exp1Btn:SetScript("OnClick", function() + childBox:Note("MetaHuntOptionsZBarExpNote", expNote) + + childBox:Checkbox("MetaHuntOptionsZBarExp1", exp1Lbl, { + checked = (s.childexpand == exp1Val), + onClick = function() if this:GetChecked() == 1 then local sv = MTH_ZBar_GetSaved() - sv.childexpand = this.zbarVal + sv.childexpand = exp1Val if sv.enabled and type(MTH_ZBar_ApplyLayout) == "function" then MTH_ZBar_ApplyLayout() end MTH_ResetAndSelectOptionsTab("ZBar") else this:SetChecked(true) end - end) - end + end, + }) - local exp2Btn = MTH_CreateCheckbox(container, "MetaHuntOptionsZBarExp2", exp2Lbl, -164, 30) - if exp2Btn then - exp2Btn:SetChecked(s.childexpand == exp2Val) - exp2Btn.zbarVal = exp2Val - exp2Btn:SetScript("OnClick", function() + childBox:Checkbox("MetaHuntOptionsZBarExp2", exp2Lbl, { + checked = (s.childexpand == exp2Val), + onClick = function() if this:GetChecked() == 1 then local sv = MTH_ZBar_GetSaved() - sv.childexpand = this.zbarVal + sv.childexpand = exp2Val if sv.enabled and type(MTH_ZBar_ApplyLayout) == "function" then MTH_ZBar_ApplyLayout() end MTH_ResetAndSelectOptionsTab("ZBar") else this:SetChecked(true) end - end) - end + end, + }) - -- ===== Children layout (how children #2+ are arranged relative to #1) ===== - local arrLbl = container:CreateFontString(nil, "ARTWORK", "GameFontHighlight") - arrLbl:SetPoint("TOPLEFT", container, "TOPLEFT", 16, -196) - arrLbl:SetText("Children layout:") + childBox:Gap(6) + childBox:Note("MetaHuntOptionsZBarArrNote", "Layout:") - local arrVBtn = MTH_CreateCheckbox(container, "MetaHuntOptionsZBarArrV", - "Vertical (stacked column)", -214, 30) - if arrVBtn then - arrVBtn:SetChecked((s.childarrange or "VERTICAL") == "VERTICAL") - arrVBtn:SetScript("OnClick", function() + childBox:Checkbox("MetaHuntOptionsZBarArrV", "Vertical (stacked column)", { + checked = ((s.childarrange or "VERTICAL") == "VERTICAL"), + onClick = function() if this:GetChecked() == 1 then local sv = MTH_ZBar_GetSaved() sv.childarrange = "VERTICAL" @@ -997,14 +849,12 @@ function MTH_SetupZBarOptions() else this:SetChecked(true) end - end) - end + end, + }) - local arrHBtn = MTH_CreateCheckbox(container, "MetaHuntOptionsZBarArrH", - "Horizontal (side-by-side row)", -240, 30) - if arrHBtn then - arrHBtn:SetChecked((s.childarrange or "VERTICAL") == "HORIZONTAL") - arrHBtn:SetScript("OnClick", function() + childBox:Checkbox("MetaHuntOptionsZBarArrH", "Horizontal (side-by-side row)", { + checked = ((s.childarrange or "VERTICAL") == "HORIZONTAL"), + onClick = function() if this:GetChecked() == 1 then local sv = MTH_ZBar_GetSaved() sv.childarrange = "HORIZONTAL" @@ -1013,36 +863,33 @@ function MTH_SetupZBarOptions() else this:SetChecked(true) end - end) - end + end, + }) - -- ===== Spacing slider ===== - local spacingSlider = MTH_CreateSlider(container, "MetaHuntOptionsZBarSpacing", - "Spacing (px)", 0, 30, 1, -300) - if spacingSlider then - spacingSlider:SetWidth(leftWidth - 40) - spacingSlider:SetValue(tonumber(s.spacing) or 4) - spacingSlider.onChange = function(val) + -- ===== Sizing box ===== + local sizeBox = panel:Box("left", "MetaHuntOptionsZBarSizeBox", "Sizing") + + sizeBox:Slider("MetaHuntOptionsZBarSpacing", { + label = "Spacing (px)", + min = 0, max = 30, step = 1, value = tonumber(s.spacing) or 4, + onChange = function(val) local sv = MTH_ZBar_GetSaved() sv.spacing = math.floor((tonumber(val) or 4) + 0.5) if sv.enabled and type(MTH_ZBar_ApplyLayout) == "function" then MTH_ZBar_ApplyLayout() end - end - end + end, + }) - -- ===== Button size slider ===== - local sizeSlider = MTH_CreateSlider(container, "MetaHuntOptionsZBarSize", - "Button Size", 10, 100, 1, -360) - if sizeSlider then - sizeSlider:SetWidth(leftWidth - 40) - sizeSlider:SetValue(tonumber(s.size) or 36) - sizeSlider.onChange = function(val) + sizeBox:Slider("MetaHuntOptionsZBarSize", { + label = "Button Size", + min = 10, max = 100, step = 1, value = tonumber(s.size) or 36, + onChange = function(val) if type(MTH_ZBar_SetSize) == "function" then MTH_ZBar_SetSize(val) end - end - end + end, + }) -- ===== Right column: Bar Order ===== local orderHeader = container:CreateFontString(nil, "ARTWORK", "GameFontHighlight") @@ -1381,375 +1228,254 @@ function MTH_SetupGeneralOptions() return end - MTH_ClearContainer(parentFrame) - local stripSaved = AutoStrip_GetSaved and AutoStrip_GetSaved() or {} - local parentWidth = parentFrame:GetWidth() or 0 - if parentWidth < 460 then - parentWidth = 520 + local panel = MTH_Layout.Panel(parentFrame) + + -- ===== LEFT: Smart Ammo ===== + local smartBox = panel:Box("left", "MetaHuntGeneralSmartAmmoBox", "Smart Ammo") + local smartModuleEnabled = MTH and MTH.IsModuleEnabled and MTH:IsModuleEnabled("smartammo", true) + smartBox:Checkbox("MetaHuntGeneralSmartAmmoModuleToggle", "Enable module", { + checked = smartModuleEnabled, + onClick = function() + local enabled = this:GetChecked() == 1 + if MTH and MTH.SetModuleEnabled then + local ok = MTH:SetModuleEnabled("smartammo", enabled) + if not ok then + this:SetChecked(smartModuleEnabled and true or false) + return + end + smartModuleEnabled = enabled + end + MTH_SetupGeneralOptions() + end, + }) + + local smartEnabled = true + if type(MTHSmartAmmo_GetSmartEnabled) == "function" then + smartEnabled = MTHSmartAmmo_GetSmartEnabled() and true or false end + local smartToggle = smartBox:Checkbox("MetaHuntGeneralSmartAmmoEnabledToggle", "Enable Junk Shot Swaps", { + checked = smartEnabled, + onClick = function() + if type(MTHSmartAmmo_SetSmartEnabled) == "function" then + MTHSmartAmmo_SetSmartEnabled(this:GetChecked() == 1 and 1 or nil) + end + end, + }) + if not smartModuleEnabled and smartToggle and smartToggle.Disable then smartToggle:Disable() end + smartBox:Note("MetaHuntGeneralSmartAmmoEnabledHelp", "Swaps to low-tier ammo for shots that don't scale on weapon damage, then swaps back to your previous ammo.") - local gutter = 16 - local colGap = 16 - local colWidth = math.floor((parentWidth - (gutter * 2) - colGap) / 2) - if colWidth < 220 then - colWidth = 220 + local reloadEnabled = type(MTHSmartAmmo_GetReloadEnabled) == "function" and MTHSmartAmmo_GetReloadEnabled() and true or false + local reloadToggle = smartBox:Checkbox("MetaHuntGeneralSmartAmmoReloadToggle", "Enable Auto-Fallback", { + checked = reloadEnabled, + onClick = function() + if type(MTHSmartAmmo_SetReloadEnabled) == "function" then + MTHSmartAmmo_SetReloadEnabled(this:GetChecked() == 1 and 1 or nil) + end + end, + }) + if not smartModuleEnabled and reloadToggle and reloadToggle.Disable then reloadToggle:Disable() end + smartBox:Note("MetaHuntGeneralSmartAmmoReloadHelp", "Falls back to any available ammo when equipped ammo is out of stock.") + + local weaponSwapEnabled = type(MTHSmartAmmo_GetWeaponSwapEnabled) == "function" and MTHSmartAmmo_GetWeaponSwapEnabled() and true or false + local weaponSwapToggle = smartBox:Checkbox("MetaHuntGeneralSmartAmmoWeaponSwapToggle", "Enable Weapon-Swap Auto Ammo", { + checked = weaponSwapEnabled, + onClick = function() + if type(MTHSmartAmmo_SetWeaponSwapEnabled) == "function" then + MTHSmartAmmo_SetWeaponSwapEnabled(this:GetChecked() == 1 and 1 or nil) + end + end, + }) + if not smartModuleEnabled and weaponSwapToggle and weaponSwapToggle.Disable then weaponSwapToggle:Disable() end + smartBox:Note("MetaHuntGeneralSmartAmmoWeaponSwapHelp", "When you swap between gun and bow/crossbow, instantly equips the best matching ammo from your bags.") + + -- ===== LEFT: Auto-Strip ===== + local stripBox = panel:Box("left", "MetaHuntGeneralAutoStripBox", "Auto-Strip") + stripBox:Checkbox("MetaHuntGeneralAutoStripToggle", "Enable Auto-Strip on Combat Exit", { + checked = stripSaved["autostrip"] and true or false, + onClick = function() + if type(AutoStrip_SetAutoStripToggle) == "function" then + AutoStrip_SetAutoStripToggle(this:GetChecked() == 1) + end + end, + }) + stripBox:Checkbox("MetaHuntGeneralAutoStripDisplay", "Show Strip Button", { + checked = stripSaved["display"] and true or false, + onClick = function() + if type(AutoStrip_SetDisplayToggle) == "function" then + AutoStrip_SetDisplayToggle(this:GetChecked() == 1) + end + end, + }) + stripBox:Note("MetaHuntGeneralAutoStripHelp", "Auto-strip unequips items when combat ends. Requires at least one empty bag slot.") + + -- ===== LEFT: Anti-Daze ===== + local antiBox = panel:Box("left", "MetaHuntGeneralAntiDazeBox", "Anti-Daze") + local antiDazeEnabled = (AntiDaze_GetEnabled and AntiDaze_GetEnabled()) or false + antiBox:Checkbox("MetaHuntGeneralAntiDazeToggle", "Enable Anti-Daze", { + checked = antiDazeEnabled, + onClick = function() + local enabled = this:GetChecked() == 1 + if type(AntiDaze_SetEnabled) == "function" then + AntiDaze_SetEnabled(enabled, 1) + end + if MTH and MTH.Print then + MTH:Print("AntiDaze " .. (enabled and "Enabled." or "Disabled.")) + elseif DEFAULT_CHAT_FRAME then + DEFAULT_CHAT_FRAME:AddMessage("AntiDaze " .. (enabled and "Enabled." or "Disabled.")) + end + end, + }) + antiBox:Note("MetaHuntGeneralAntiDazeHelp", "Cancels Cheetah/Pack when dazed.") + + -- ===== LEFT: Pet Auto-Teach ===== + local petTeachBox = panel:Box("left", "MetaHuntGeneralPetTeachBox", "Pet Auto-Teach") + local growlEnabled = (type(MTH_Growl_IsEnabled) == "function" and MTH_Growl_IsEnabled()) or false + petTeachBox:Checkbox("MetaHuntGeneralPetTeachGrowlToggle", "Activate Auto-Teach of Growl (Max rank)", { + checked = growlEnabled, + onClick = function() + if type(MTH_Growl_SetEnabled) == "function" then + MTH_Growl_SetEnabled(this:GetChecked() == 1) + end + end, + }) + petTeachBox:Note("MetaHuntGeneralPetTeachGrowlHelp", "When you tame a beast, offers to teach it the best Growl rank for its level.") + + -- ===== RIGHT: Tooltips ===== + local tooltipsBox = panel:Box("right", "MetaHuntGeneralTooltipsBox", "Tooltips") + local tooltipsStore = MTH and MTH.GetModuleCharSavedVariables and MTH:GetModuleCharSavedVariables("tooltips") + if type(tooltipsStore) ~= "table" then + tooltipsStore = {} end - - local leftX = gutter - local rightX = gutter + colWidth + colGap - local topY = -8 - - local function ensureSection(name, title, yOffset, height, column) - local section = getglobal(name) - if not section then - section = CreateFrame("Frame", name, parentFrame, "OptionFrameBoxTemplate") + if MTH and MTH.GetModuleSavedVariables and next(tooltipsStore) == nil then + local accountStore = MTH:GetModuleSavedVariables("tooltips") + if type(accountStore) == "table" and next(accountStore) ~= nil then + for key, value in pairs(accountStore) do + if type(value) == "table" then + local copied = {} + for subKey, subValue in pairs(value) do + copied[subKey] = subValue + end + tooltipsStore[key] = copied + else + tooltipsStore[key] = value + end + end end - if not section then - return nil - end - - section:SetParent(parentFrame) - section:ClearAllPoints() - if column == "right" then - section:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", rightX, yOffset) + end + if tooltipsStore.beastTooltips == nil then + tooltipsStore.beastTooltips = true + end + if tooltipsStore.ammoVendorTooltips == nil then + tooltipsStore.ammoVendorTooltips = true + end + if tooltipsStore.foodItemTooltips == nil then + if type(FOM_Config) == "table" and FOM_Config.Tooltip ~= nil then + tooltipsStore.foodItemTooltips = FOM_Config.Tooltip and true or false else - section:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", leftX, yOffset) + tooltipsStore.foodItemTooltips = true end - section:SetWidth(colWidth) - section:SetHeight(height) - section:Show() - - local titleFrame = getglobal(name .. "Title") - if titleFrame then - titleFrame:SetText(title) - end - - return section + end + if tooltipsStore.ownPetTooltips == nil then + tooltipsStore.ownPetTooltips = false end - local function ensureCheckbox(section, name, label, yOffset, checked) - local check = getglobal(name) - if not check then - check = CreateFrame("CheckButton", name, section, "OptionsCheckButtonTemplate") - end - if not check then - return nil - end + local tooltipsModuleEnabled = MTH and MTH.IsModuleEnabled and MTH:IsModuleEnabled("tooltips", true) + local foodLabel = (MTH and MTH.GetLocalization and MTH:GetLocalization("TOOLTIPS_OPTION_FOOD", "Activate on food")) or "Activate on food" + local foodHelp = (MTH and MTH.GetLocalization and MTH:GetLocalization("TOOLTIPS_OPTION_FOOD_HELP", "Show your current pet's food preference directly in item tooltips.")) or "Show your current pet's food preference directly in item tooltips." + local ownPetLabel = (MTH and MTH.GetLocalization and MTH:GetLocalization("TOOLTIPS_OPTION_OWNPET", "Activate on my pet")) or "Activate on my pet" + local ownPetHelp = (MTH and MTH.GetLocalization and MTH:GetLocalization("TOOLTIPS_OPTION_OWNPET_HELP", "Show your own pet status details in tooltip when hovering your pet context.")) or "Show your own pet status details in tooltip when hovering your pet context." - check:SetParent(section) - check:ClearAllPoints() - check:SetPoint("TOPLEFT", section, "TOPLEFT", 12, yOffset) - check:SetChecked(checked and true or false) - check:Show() - - local text = getglobal(name .. "Text") - if text then - text:SetText(label) - else - local lbl = section:CreateFontString(name .. "Text", "ARTWORK", "GameFontNormal") - lbl:SetPoint("LEFT", check, "RIGHT", 5, 0) - lbl:SetText(label) - end - - return check - end - - local function ensureHelpText(section, name, text, yOffset) - local help = getglobal(name) - if not help then - help = section:CreateFontString(name, "ARTWORK") - help:SetFont("Fonts\\FRIZQT__.TTF", 10) - help:SetJustifyH("LEFT") - end - help:ClearAllPoints() - help:SetPoint("TOPLEFT", section, "TOPLEFT", 14, yOffset) - help:SetWidth(colWidth - 24) - help:SetText(text) - help:Show() - end - - local smartAmmoSection = ensureSection("MetaHuntGeneralSmartAmmoBox", "Smart Ammo", topY, 254, "left") - if smartAmmoSection then - local smartModuleEnabled = MTH and MTH.IsModuleEnabled and MTH:IsModuleEnabled("smartammo", true) - local smartModuleToggle = ensureCheckbox(smartAmmoSection, "MetaHuntGeneralSmartAmmoModuleToggle", "Enable module", -10, smartModuleEnabled) - if smartModuleToggle then - smartModuleToggle:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - if MTH and MTH.SetModuleEnabled then - local ok = MTH:SetModuleEnabled("smartammo", enabled) - if not ok then - this:SetChecked(smartModuleEnabled and true or false) - return - end - smartModuleEnabled = enabled + tooltipsBox:Checkbox("MetaHuntGeneralTooltipsModuleToggle", "Enable module", { + checked = tooltipsModuleEnabled, + onClick = function() + local enabled = this:GetChecked() == 1 + if MTH and MTH.SetModuleEnabled then + local ok = MTH:SetModuleEnabled("tooltips", enabled) + if not ok then + this:SetChecked(tooltipsModuleEnabled and true or false) + return end - MTH_SetupGeneralOptions() - end) - end - - local moduleHelp = getglobal("MetaHuntGeneralSmartAmmoModuleHelp") - if moduleHelp then - moduleHelp:Hide() - end - - local smartEnabled = true - if type(MTHSmartAmmo_GetSmartEnabled) == "function" then - smartEnabled = MTHSmartAmmo_GetSmartEnabled() and true or false - end - local smartToggle = ensureCheckbox(smartAmmoSection, "MetaHuntGeneralSmartAmmoEnabledToggle", "Enable Junk Shot Swaps", -58, smartEnabled) - if smartToggle then - if not smartModuleEnabled then - smartToggle:Disable() + tooltipsModuleEnabled = enabled end - smartToggle:SetScript("OnClick", function() - if type(MTHSmartAmmo_SetSmartEnabled) == "function" then - MTHSmartAmmo_SetSmartEnabled(MTH_ZB_IsChecked(this) and 1 or nil) - end - end) - end - ensureHelpText(smartAmmoSection, "MetaHuntGeneralSmartAmmoEnabledHelp", "Swaps to low-tier ammo for shots that don't scale on weapon damage, then swaps back to your previous ammo.", -86) + end, + }) + tooltipsBox:Note("MetaHuntGeneralTooltipsModuleHelp", "This module allows to enhance NPC tooltips with additional information useful for hunters.") - local reloadEnabled = type(MTHSmartAmmo_GetReloadEnabled) == "function" and MTHSmartAmmo_GetReloadEnabled() and true or false - local reloadToggle = ensureCheckbox(smartAmmoSection, "MetaHuntGeneralSmartAmmoReloadToggle", "Enable Auto-Fallback", -126, reloadEnabled) - if reloadToggle then - if not smartModuleEnabled then - reloadToggle:Disable() + tooltipsBox:Checkbox("MetaHuntGeneralTooltipsBeastToggle", "Activate on Beasts", { + checked = tooltipsStore.beastTooltips and true or false, + onClick = function() + local enabled = this:GetChecked() == 1 + tooltipsStore.beastTooltips = enabled + local tooltipsModule = MTH and MTH.GetModule and MTH:GetModule("tooltips") + if tooltipsModule and tooltipsModule.SetBeastTooltipsEnabled then + tooltipsModule:SetBeastTooltipsEnabled(enabled) end - reloadToggle:SetScript("OnClick", function() - if type(MTHSmartAmmo_SetReloadEnabled) == "function" then - MTHSmartAmmo_SetReloadEnabled(MTH_ZB_IsChecked(this) and 1 or nil) - end - end) - end - ensureHelpText(smartAmmoSection, "MetaHuntGeneralSmartAmmoReloadHelp", "Falls back to any available ammo when equipped ammo is out of stock.", -154) + end, + }) + tooltipsBox:Note("MetaHuntGeneralTooltipsBeastHelp", "When mouseover on a Beast, its tooltip will show if it can learn you any pet abilities. They appear Green if you already know them, and Red if you don't know them yet.") - local weaponSwapEnabled = type(MTHSmartAmmo_GetWeaponSwapEnabled) == "function" and MTHSmartAmmo_GetWeaponSwapEnabled() and true or false - local weaponSwapToggle = ensureCheckbox(smartAmmoSection, "MetaHuntGeneralSmartAmmoWeaponSwapToggle", "Enable Weapon-Swap Auto Ammo", -180, weaponSwapEnabled) - if weaponSwapToggle then - if not smartModuleEnabled then - weaponSwapToggle:Disable() + tooltipsBox:Checkbox("MetaHuntGeneralTooltipsOwnPetToggle", ownPetLabel, { + checked = tooltipsStore.ownPetTooltips and true or false, + onClick = function() + local enabled = this:GetChecked() == 1 + tooltipsStore.ownPetTooltips = enabled + local tooltipsModule = MTH and MTH.GetModule and MTH:GetModule("tooltips") + if tooltipsModule and tooltipsModule.SetOwnPetTooltipsEnabled then + tooltipsModule:SetOwnPetTooltipsEnabled(enabled) end - weaponSwapToggle:SetScript("OnClick", function() - if type(MTHSmartAmmo_SetWeaponSwapEnabled) == "function" then - MTHSmartAmmo_SetWeaponSwapEnabled(MTH_ZB_IsChecked(this) and 1 or nil) - end - end) - end - ensureHelpText(smartAmmoSection, "MetaHuntGeneralSmartAmmoWeaponSwapHelp", "When you swap between gun and bow/crossbow, instantly equips the best matching ammo from your bags.", -208) - end + end, + }) + tooltipsBox:Note("MetaHuntGeneralTooltipsOwnPetHelp", ownPetHelp) - local stripSection = ensureSection("MetaHuntGeneralAutoStripBox", "Auto-Strip", topY - 270, 100, "left") - if stripSection then - local autoStripToggle = ensureCheckbox(stripSection, "MetaHuntGeneralAutoStripToggle", "Enable Auto-Strip on Combat Exit", -10, stripSaved["autostrip"] and true or false) - if autoStripToggle then - autoStripToggle:SetScript("OnClick", function() - if type(AutoStrip_SetAutoStripToggle) == "function" then - AutoStrip_SetAutoStripToggle(MTH_ZB_IsChecked(this)) - end - end) - end - - local displayToggle = ensureCheckbox(stripSection, "MetaHuntGeneralAutoStripDisplay", "Show Strip Button", -36, stripSaved["display"] and true or false) - if displayToggle then - displayToggle:SetScript("OnClick", function() - if type(AutoStrip_SetDisplayToggle) == "function" then - AutoStrip_SetDisplayToggle(MTH_ZB_IsChecked(this)) - end - end) - end - - ensureHelpText(stripSection, "MetaHuntGeneralAutoStripHelp", "Auto-strip unequips items when combat ends. Requires at least one empty bag slot.", -64) - end - - local antiSection = ensureSection("MetaHuntGeneralAntiDazeBox", "Anti-Daze", topY - 384, 76, "left") - if antiSection then - local antiDazeEnabled = (AntiDaze_GetEnabled and AntiDaze_GetEnabled()) or false - local antiToggle = ensureCheckbox(antiSection, "MetaHuntGeneralAntiDazeToggle", "Enable Anti-Daze", -10, antiDazeEnabled) - if antiToggle then - antiToggle:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - if type(AntiDaze_SetEnabled) == "function" then - AntiDaze_SetEnabled(enabled, 1) - end - if DEFAULT_CHAT_FRAME then - if MTH and MTH.Print then - MTH:Print("AntiDaze " .. (enabled and "Enabled." or "Disabled.")) - else - DEFAULT_CHAT_FRAME:AddMessage("AntiDaze " .. (enabled and "Enabled." or "Disabled.")) - end - end - end) - end - - ensureHelpText(antiSection, "MetaHuntGeneralAntiDazeHelp", "Cancels Cheetah/Pack when dazed.", -40) - end - - local petTeachSection = ensureSection("MetaHuntGeneralPetTeachBox", "Pet Auto-Teach", topY - 474, 76, "left") - if petTeachSection then - local growlEnabled = (type(MTH_Growl_IsEnabled) == "function" and MTH_Growl_IsEnabled()) or false - local growlToggle = ensureCheckbox(petTeachSection, "MetaHuntGeneralPetTeachGrowlToggle", "Activate Auto-Teach of Growl (Max rank)", -10, growlEnabled) - if growlToggle then - growlToggle:SetScript("OnClick", function() - if type(MTH_Growl_SetEnabled) == "function" then - MTH_Growl_SetEnabled(MTH_ZB_IsChecked(this)) - end - end) - end - - ensureHelpText(petTeachSection, "MetaHuntGeneralPetTeachGrowlHelp", "When you tame a beast, offers to teach it the best Growl rank for its level.", -40) - end - - local tooltipsSection = ensureSection("MetaHuntGeneralTooltipsBox", "Tooltips", topY, 356, "right") - if tooltipsSection then - local tooltipsStore = MTH and MTH.GetModuleCharSavedVariables and MTH:GetModuleCharSavedVariables("tooltips") - if type(tooltipsStore) ~= "table" then - tooltipsStore = {} - end - if MTH and MTH.GetModuleSavedVariables and next(tooltipsStore) == nil then - local accountStore = MTH:GetModuleSavedVariables("tooltips") - if type(accountStore) == "table" and next(accountStore) ~= nil then - for key, value in pairs(accountStore) do - if type(value) == "table" then - local copied = {} - for subKey, subValue in pairs(value) do - copied[subKey] = subValue - end - tooltipsStore[key] = copied - else - tooltipsStore[key] = value - end - end + tooltipsBox:Checkbox("MetaHuntGeneralTooltipsAmmoVendorToggle", "Activate on Vendors", { + checked = tooltipsStore.ammoVendorTooltips and true or false, + onClick = function() + local enabled = this:GetChecked() == 1 + tooltipsStore.ammoVendorTooltips = enabled + local tooltipsModule = MTH and MTH.GetModule and MTH:GetModule("tooltips") + if tooltipsModule and tooltipsModule.SetAmmoVendorTooltipsEnabled then + tooltipsModule:SetAmmoVendorTooltipsEnabled(enabled) end - end - if tooltipsStore.beastTooltips == nil then - tooltipsStore.beastTooltips = true - end - if tooltipsStore.ammoVendorTooltips == nil then - tooltipsStore.ammoVendorTooltips = true - end - if tooltipsStore.foodItemTooltips == nil then - if type(FOM_Config) == "table" and FOM_Config.Tooltip ~= nil then - tooltipsStore.foodItemTooltips = FOM_Config.Tooltip and true or false - else - tooltipsStore.foodItemTooltips = true + end, + }) + tooltipsBox:Note("MetaHuntGeneralTooltipsVendorHelp", "When mouseover on a vendor, its tooltip will tell if it sells Arrows and/or Bullets.") + + tooltipsBox:Checkbox("MetaHuntGeneralTooltipsFoodToggle", foodLabel, { + checked = tooltipsStore.foodItemTooltips and true or false, + onClick = function() + local enabled = this:GetChecked() == 1 + tooltipsStore.foodItemTooltips = enabled + local tooltipsModule = MTH and MTH.GetModule and MTH:GetModule("tooltips") + if tooltipsModule and tooltipsModule.SetFoodItemTooltipsEnabled then + tooltipsModule:SetFoodItemTooltipsEnabled(enabled) end - end - if tooltipsStore.ownPetTooltips == nil then - tooltipsStore.ownPetTooltips = false - end + end, + }) + tooltipsBox:Note("MetaHuntGeneralTooltipsFoodHelp", foodHelp) - local moduleEnabled = MTH and MTH.IsModuleEnabled and MTH:IsModuleEnabled("tooltips", true) - local foodLabel = (MTH and MTH.GetLocalization and MTH:GetLocalization("TOOLTIPS_OPTION_FOOD", "Activate on food")) or "Activate on food on food" - local foodHelp = (MTH and MTH.GetLocalization and MTH:GetLocalization("TOOLTIPS_OPTION_FOOD_HELP", "Show your current pet's food preference directly in item tooltips.")) or "Show your current pet's food preference directly in item tooltips." - - local moduleToggle = ensureCheckbox(tooltipsSection, "MetaHuntGeneralTooltipsModuleToggle", "Enable module", -10, moduleEnabled) - if moduleToggle then - moduleToggle:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - if MTH and MTH.SetModuleEnabled then - local ok = MTH:SetModuleEnabled("tooltips", enabled) - if not ok then - this:SetChecked(moduleEnabled and true or false) - return - end - moduleEnabled = enabled - end - end) - end - - ensureHelpText(tooltipsSection, "MetaHuntGeneralTooltipsModuleHelp", "This module allows to enhance NPC tooltips with additional information useful for hunters.", -34) - - local beastToggle = ensureCheckbox(tooltipsSection, "MetaHuntGeneralTooltipsBeastToggle", "Activate on Beasts", -86, tooltipsStore.beastTooltips and true or false) - if beastToggle then - beastToggle:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - tooltipsStore.beastTooltips = enabled - local tooltipsModule = MTH and MTH.GetModule and MTH:GetModule("tooltips") - if tooltipsModule and tooltipsModule.SetBeastTooltipsEnabled then - tooltipsModule:SetBeastTooltipsEnabled(enabled) - end - end) - end - - ensureHelpText(tooltipsSection, "MetaHuntGeneralTooltipsBeastHelp", "When mouseover on a Beast, its tooltip will show if it can learn you any pet abilities. They appear Green if you already know them, and Red if you don't know them yet.", -110) - - local ownPetLabel = (MTH and MTH.GetLocalization and MTH:GetLocalization("TOOLTIPS_OPTION_OWNPET", "Activate on my pet")) or "Activate on my pet" - local ownPetHelp = (MTH and MTH.GetLocalization and MTH:GetLocalization("TOOLTIPS_OPTION_OWNPET_HELP", "Show your own pet status details in tooltip when hovering your pet context.")) or "Show your own pet status details in tooltip when hovering your pet context." - local ownPetToggle = ensureCheckbox(tooltipsSection, "MetaHuntGeneralTooltipsOwnPetToggle", ownPetLabel, -158, tooltipsStore.ownPetTooltips and true or false) - if ownPetToggle then - ownPetToggle:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - tooltipsStore.ownPetTooltips = enabled - local tooltipsModule = MTH and MTH.GetModule and MTH:GetModule("tooltips") - if tooltipsModule and tooltipsModule.SetOwnPetTooltipsEnabled then - tooltipsModule:SetOwnPetTooltipsEnabled(enabled) - end - end) - end - - ensureHelpText(tooltipsSection, "MetaHuntGeneralTooltipsOwnPetHelp", ownPetHelp, -182) - - local ammoToggle = ensureCheckbox(tooltipsSection, "MetaHuntGeneralTooltipsAmmoVendorToggle", "Activate on Vendors", -222, tooltipsStore.ammoVendorTooltips and true or false) - if ammoToggle then - ammoToggle:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - tooltipsStore.ammoVendorTooltips = enabled - local tooltipsModule = MTH and MTH.GetModule and MTH:GetModule("tooltips") - if tooltipsModule and tooltipsModule.SetAmmoVendorTooltipsEnabled then - tooltipsModule:SetAmmoVendorTooltipsEnabled(enabled) - end - end) - end - - ensureHelpText(tooltipsSection, "MetaHuntGeneralTooltipsVendorHelp", "When mouseover on a vendor, its tooltip will tell if it sells Arrows and/or Bullets.", -246) - - local foodToggle = ensureCheckbox(tooltipsSection, "MetaHuntGeneralTooltipsFoodToggle", foodLabel, -274, tooltipsStore.foodItemTooltips and true or false) - if foodToggle then - foodToggle:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - tooltipsStore.foodItemTooltips = enabled - local tooltipsModule = MTH and MTH.GetModule and MTH:GetModule("tooltips") - if tooltipsModule and tooltipsModule.SetFoodItemTooltipsEnabled then - tooltipsModule:SetFoodItemTooltipsEnabled(enabled) - end - end) - end - - ensureHelpText(tooltipsSection, "MetaHuntGeneralTooltipsFoodHelp", foodHelp, -298) - end - - local bagsSection = ensureSection("MetaHuntGeneralBagsBox", "Bag Ammo Labels", topY - 372, 168, "right") - if bagsSection then - local bagLabels = ensureCheckbox(bagsSection, "MetaHuntGeneralBagLabelsToggle", "Add text to ammunitions in bags", -10, - MTH_BagAmmoLabels_GetEnabled and MTH_BagAmmoLabels_GetEnabled() or false) - if bagLabels then - bagLabels:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - if MTH_BagAmmoLabels_SetEnabled then MTH_BagAmmoLabels_SetEnabled(enabled) end - end) - end - - local bankLabels = ensureCheckbox(bagsSection, "MetaHuntGeneralBankLabelsToggle", "Add text to ammunitions in bank", -36, - MTH_BankAmmoLabels_GetEnabled and MTH_BankAmmoLabels_GetEnabled() or false) - if bankLabels then - bankLabels:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - if MTH_BankAmmoLabels_SetEnabled then MTH_BankAmmoLabels_SetEnabled(enabled) end - end) - end - - local dmgLabels = ensureCheckbox(bagsSection, "MetaHuntGeneralBagDmgToggle", "Show ammo damage (heat-coloured)", -62, - MTH_BagAmmoDamage_GetEnabled and MTH_BagAmmoDamage_GetEnabled() or false) - if dmgLabels then - dmgLabels:SetScript("OnClick", function() - local enabled = MTH_ZB_IsChecked(this) - if MTH_BagAmmoDamage_SetEnabled then MTH_BagAmmoDamage_SetEnabled(enabled) end - end) - end - - ensureHelpText(bagsSection, "MetaHuntGeneralBagDmgHelp", "Damage is heat-coloured from red (7.5) to green (20.5).", -88) - end + -- ===== RIGHT: Bag Ammo Labels ===== + local bagsBox = panel:Box("right", "MetaHuntGeneralBagsBox", "Bag Ammo Labels") + bagsBox:Checkbox("MetaHuntGeneralBagLabelsToggle", "Add text to ammunitions in bags", { + checked = MTH_BagAmmoLabels_GetEnabled and MTH_BagAmmoLabels_GetEnabled() or false, + onClick = function() + if MTH_BagAmmoLabels_SetEnabled then MTH_BagAmmoLabels_SetEnabled(this:GetChecked() == 1) end + end, + }) + bagsBox:Checkbox("MetaHuntGeneralBankLabelsToggle", "Add text to ammunitions in bank", { + checked = MTH_BankAmmoLabels_GetEnabled and MTH_BankAmmoLabels_GetEnabled() or false, + onClick = function() + if MTH_BankAmmoLabels_SetEnabled then MTH_BankAmmoLabels_SetEnabled(this:GetChecked() == 1) end + end, + }) + bagsBox:Checkbox("MetaHuntGeneralBagDmgToggle", "Show ammo damage (heat-coloured)", { + checked = MTH_BagAmmoDamage_GetEnabled and MTH_BagAmmoDamage_GetEnabled() or false, + onClick = function() + if MTH_BagAmmoDamage_SetEnabled then MTH_BagAmmoDamage_SetEnabled(this:GetChecked() == 1) end + end, + }) + bagsBox:Note("MetaHuntGeneralBagDmgHelp", "Damage is heat-coloured from red (7.5) to green (20.5).") + panel:Finish() end diff --git a/api/options.xml b/api/options.xml index e3df266..56024b1 100644 --- a/api/options.xml +++ b/api/options.xml @@ -46,12 +46,12 @@ - + - + diff --git a/api/pet-growl-teach.lua b/api/pet-growl-teach.lua index bb271a7..b3769e5 100644 --- a/api/pet-growl-teach.lua +++ b/api/pet-growl-teach.lua @@ -1,23 +1,5 @@ -- MetaHunt: auto-train Growl to a freshly tamed pet. -- --- Growl is a "trainer" pet ability (Growl / Great Stamina / Natural Armor / --- Great Resistance) that the hunter learns from a Pet Trainer NPC and then --- teaches to the active pet through the always-available BEAST TRAINING window --- (a spellbook-like hunter spell, no NPC required). Growl specifically costs --- 0 training points, so it can always be taught to a brand-new pet. --- --- The Beast Training window is driven by the vanilla Craft API: --- GetNumCrafts() -> number of rows --- GetCraftInfo(i) -> name, rankSubText, type, ... --- DoCraft(i) -> trains that row (the "Select + Train" action) --- CloseCraft() -> closes the window --- --- The correct Growl rank is a function of the pet's level: --- rank = floor(level / 10) + 1 (ranks 1..7, at pet levels 1/10/.../60) --- --- Flow on a confirmed fresh tame (when enabled): compute the target rank, open --- the Beast Training window, find the Growl row at that rank once the craft list --- populates, DoCraft() it silently, then close the window if we opened it. local MTH_GROWL_MAX_RANK = 7 diff --git a/init/api.xml b/init/api.xml index e04672e..31d8377 100644 --- a/init/api.xml +++ b/init/api.xml @@ -14,6 +14,7 @@ + From aeaddd9558639e1317918769e28874d5072225fb Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Fri, 11 Sep 2026 00:05:38 +0200 Subject: [PATCH 21/42] Unify all option panels through MTH_Layout engine; restore realm gate --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 509e5b8..063c914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Resurrecting on Octowow. - **Animations**: MetaHunt now plays optional animations that make the interface feel alive. Powered by the PizzaSauce library of [Pizzahawaii](https://codeberg.org/Pizzahawaii/) ! -- **zTrack — Find Fish**: Added new spell Find Fish to the zTrack list of trackings. +- **zTrack — Find Fish**: Added new spell Find Fish to zTrack. - **DDOSers Realm Blocking**: The Addon will auto-disable itself on all Mafia's realms, we know who they are. @@ -21,6 +21,7 @@ Resurrecting on Octowow. - **Complete revamp of SavedVariables**: All your settings now live in one tidy place instead of being scattered around. A migration happens the first time you log in, and you shouldn't lose any settings. +- **Polished options**: The option panel had become messy overtime and it finally received some of attention to look cleaner and coherent with same styling everywhere and better arrangement. ### Fixed From 0fb52f67ff9ed4758dbd64fd45804b96c2c27c50 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Fri, 11 Sep 2026 13:44:41 +0200 Subject: [PATCH 22/42] Add trainer planning and level-up tracking --- CHANGELOG.md | 10 + MetaHunt.toc | 1 + README.MD | 8 +- api/core-framework.lua | 10 + api/core-level-tracking.lua | 323 ++++++ api/core-scan-trainer.lua | 251 +++++ api/core.lua | 41 +- api/fx-celebrate.lua | 343 ++++++- api/hunterbook-tab-trainers.lua | 225 +++++ api/hunterbook.lua | 394 +++++++- api/hunterbook.xml | 22 +- api/options-zbuttons.lua | 49 +- api/trainer-spell-planner.lua | 159 +++ data/ds-trainer-catalog.lua | 1646 +++++++++++++++++++++++++++++++ init/api.xml | 4 + 15 files changed, 3416 insertions(+), 70 deletions(-) create mode 100644 api/core-level-tracking.lua create mode 100644 api/core-scan-trainer.lua create mode 100644 api/hunterbook-tab-trainers.lua create mode 100644 api/trainer-spell-planner.lua create mode 100644 data/ds-trainer-catalog.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 063c914..4c9f68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ Resurrecting on Octowow. ### Added +- **Hunter's Book - Hunter and Pet spells**: Added catalog pages for all Hunter and Pet Trainer spells, with spell icons, required level, base/Honored prices, branch, known status, search, level filters, sorting, and a scrollable inspector. + +- **Trainer cost planning**: Hunter's Book now shows a training plan with the cost of spells available now, the next training level, and all remaining spells. Prices can be switched between standard and Honored costs. + +- **Live trainer status tracking**: Visiting a Hunter or Pet Trainer now records available, unavailable, and already-used services without guessing from filtered trainer lists. + +- **Level-up tracking**: Added Hunter, Pet, and Pet Loyalty level-up notifications. Hunter messages list catalog spells unlocked at that level with base and Honored costs; major Hunter milestones use a special celebration. + +- **Leveling animation and message options**: Added separate Animation and Message controls for Hunter training levels, major milestones, pet training levels, and pet loyalty gains. + - **Pet Auto-Teach — Growl**: When you tame a new beast, MetaHunt now offers to teach it Growl at the best rank for the pet's level. Click **Yes** and it opens the Beast Training window, teaches the ability and closes the window automatically. - **Animations**: MetaHunt now plays optional animations that make the interface feel alive. Powered by the PizzaSauce library of [Pizzahawaii](https://codeberg.org/Pizzahawaii/) ! diff --git a/MetaHunt.toc b/MetaHunt.toc index 50a0b74..069cdf0 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -36,6 +36,7 @@ data\ds-pet-spells.lua data\ds-pet-spells-trainer.lua data\ds-hunter-spells.lua data\ds-hunter-spells-dbc.lua +data\ds-trainer-catalog.lua data\ds-racial.lua data\init.lua diff --git a/README.MD b/README.MD index 8f68b75..ffd4f81 100644 --- a/README.MD +++ b/README.MD @@ -9,9 +9,9 @@ You play the best class, now you have the best companion for it (and its not you ### Important notes -- Altough Nampower isn't mandatory for most of the features, it is a must-have for some. -- If you were using old versions of Feed-O-Matic, ICU, zHunterMod and HunterHelper, you dont need those with MetaHunt and you should disable them to avoid conflicts. -- MetaHunt is fully compatible with QUIVER, and is not a replacement for it. +- Altough Nampower isn't mandatory for most of the features, it is a must-have for some, and certainly the MM Widget. +- If you were using old versions of Feed-O-Matic, ICU, zHunterMod and HunterHelper, you dont need those with MetaHunt and you SHOULD disable them to avoid conflicts. +- MetaHunt is fully compatible with QUIVER <3, and is not a replacement for it. - The addon purposely disable itself on all Capycraft/Ravencraft Realms, following the DDOS attacks they conducted on other servers. ## Twow Data @@ -36,7 +36,7 @@ Online bestiary is also available here https://DuvelCorp.github.io/MetaHunt-Web/ ## MM Widget - 1.18.1 weapon of mass-destruction. The widget is composed of 5 cells, tracking the 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock & Load procs, and a smart dynamic cell. The smart cell shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any, and it bypasses the proc spell if your current target is immune to the school. You can bind the smart cell to turn the widget into a very efficient one-button rotation. Lets make MM great again ! + 1.18.1 weapon of mass-destruction. NAMPOWER MANDATORY. The widget is composed of 5 cells, tracking the 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock & Load procs, and a smart dynamic cell. The smart cell shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any, and it bypasses the proc spell if your current target is immune to the school. You can bind the smart cell to turn the widget into a very efficient one-button rotation. Lets make MM great again ! ## Tooltips diff --git a/api/core-framework.lua b/api/core-framework.lua index cd17a90..698ce47 100644 --- a/api/core-framework.lua +++ b/api/core-framework.lua @@ -25,6 +25,11 @@ local MTH_MESSAGE_DEFAULTS = { petHungry = false, beastTrainingScan = true, spellbookScan = false, + trainerScan = true, + hunterLevelUp = true, + hunterLevelQuiet = false, + petLevelUp = true, + petLoyaltyUp = true, petRanAway = true, mapMarkers = true, stableScan = true, @@ -216,6 +221,11 @@ function MTH:IsRealmGateBlocked() end function MTH:CheckRealmGate() + -- TEMP TESTING: allow Ravencraft client access while HunterBook layout is reviewed. + if true then + return false + end + -- Realm name is only reliable once logged in; an empty result never blocks. local realm = self:GetCurrentRealmName() if type(realm) == "string" and realm ~= "" then diff --git a/api/core-level-tracking.lua b/api/core-level-tracking.lua new file mode 100644 index 0000000..d0fedaf --- /dev/null +++ b/api/core-level-tracking.lua @@ -0,0 +1,323 @@ +-- MetaHunt level tracker. All training facts come from MTH_DS_TrainerCatalog +-- through trainer-spell-planner.lua; this module only detects progression and +-- presents the resulting facts through chat and animations. + +local MTH_LT_Frame = nil +local MTH_LT_PendingPetCheck = false + +local MTH_LT_HunterLines = { + [1] = "You can now track beasts. Revolutionary: look for the things you intend to shoot.", + [2] = "The wilderness remains deeply unimpressed.", + [3] = "Still mostly leather, arrows, and misplaced confidence.", + [4] = "You have actual trainer lessons now. Try not to spend all your coin on repairs.", + [5] = "Your pet is doing its share. Please consider doing yours.", + [6] = "A hunter's mark: because shouting at the target was not precise enough.", + [7] = "Every pull is a plan until it becomes cardio.", + [8] = "Concussive Shot. Distance is now a tactical concept, not an accident.", + [9] = "Your arrows have begun their long argument with gravity.", + [10] = "You have a hawk aspect. The bird remains unavailable for comment.", + [11] = "A fine age for discovering that pets also require paperwork.", + [12] = "Mend Pet unlocked. Your pet has noticed this was overdue.", + [13] = "The wilderness has upgraded its complaints department.", + [14] = "Eagle Eye. Finally, a way to inspect trouble before walking into it.", + [15] = "You have survived another level through skill, luck, or a pet-shaped shield.", + [16] = "Traps and sharp things. Subtlety continues to be optional.", + [17] = "Your pet is still carrying the emotional weight of this partnership.", + [18] = "Multi-Shot. One target was apparently an unacceptable limitation.", + [19] = "Soon you may run faster. The enemies will take this personally.", + [20] = "Congratulations: escape now has a speed setting.", + [21] = "Fast enough to arrive at the next questionable decision sooner.", + [22] = "Your trainer has new bills and somehow they are all addressed to you.", + [23] = "The pet remains employed despite the working conditions.", + [24] = "Beast Lore. At last: research before violence. Briefly.", + [25] = "Halfway to better excuses, nowhere near fewer mistakes.", + [26] = "Rapid Fire. Your bow has entered its impatient phase.", + [27] = "Things are going well enough to become dangerous.", + [28] = "Your arrows now contain a measurable amount of intent.", + [29] = "Prepare for the hunter's most respected tactical maneuver.", + [30] = "Feign Death. Why win a fight when you can become administrative debris?", + [31] = "You are alive, which validates absolutely nothing.", + [32] = "Flare. Stealth has been downgraded to a suggestion.", + [33] = "The next trap is definitely part of the plan.", + [34] = "Explosive Trap. Negotiation has left the chat.", + [35] = "Your pet thinks this is all normal. That is worrying.", + [36] = "More damage, more maintenance, same heroic disregard for repair bills.", + [37] = "Keep going. The next disaster is probably already pathing toward you.", + [38] = "Your toolkit is becoming impressively hard to explain to civilized people.", + [39] = "One level until armor finally admits you have been taking hits.", + [40] = "Mail training. Leather got you this far; enemies kept putting holes in it.", + [41] = "You are now visibly better protected from consequences.", + [42] = "More Multi-Shot: because collateral damage needed a sequel.", + [43] = "Your pet has grown used to the sound of avoidable danger.", + [44] = "The trainer is delighted. Your coin pouch is less enthusiastic.", + [45] = "You are a seasoned hunter now, which means the mistakes are expensive.", + [46] = "Nature resistance: proof that the outdoors have been escalating.", + [47] = "Still alive. Still pointing the dangerous end away from yourself. Usually.", + [48] = "Your damage has matured into a fully funded public nuisance.", + [49] = "One more level until the trainer starts charging executive prices.", + [50] = "Your arrows have tenure now.", + [51] = "The wilderness would like a word. It has a queue.", + [52] = "Aimed Shot hits harder, which is your preferred form of diplomacy.", + [53] = "Your pet remains your most reliable party member. Make of that what you will.", + [54] = "Explosive Trap again. The floor continues to be a valid target.", + [55] = "Almost there. Try not to die to something with a tutorial tooltip.", + [56] = "Aspect of the Viper. Mana problems now have a bird-shaped solution.", + [57] = "The finish line is visible. So are several elite mobs. Choose carefully.", + [58] = "The final trainer bills are arriving with alarming confidence.", + [59] = "One last level. Your pet has begun drafting a victory speech.", + [60] = "You made it. The wildlife will remember this unfavorably.", +} + +local MTH_LT_Priority = { + ["Feign Death"] = 100, + ["Mail"] = 95, + ["Aspect of the Cheetah"] = 90, + ["Rapid Fire"] = 88, + ["Aspect of the Pack"] = 86, + ["Explosive Trap"] = 84, + ["Freezing Trap"] = 82, + ["Flare"] = 80, + ["Eagle Eye"] = 78, + ["Multi-Shot"] = 76, + ["Aimed Shot"] = 74, + ["Aspect of the Viper"] = 72, + ["Trueshot Aura"] = 70, + ["Volley"] = 68, + ["Track Hidden"] = 66, +} + +local MTH_LT_PetLines = { + "has leveled up. It is now statistically better than your last pull.", + "has leveled up. The food budget has noticed.", + "has leveled up. Please do not celebrate by pulling three extra mobs.", + "has leveled up. It remains committed to solving problems with teeth.", + "has leveled up. The wilderness has filed no objection.", + "has leveled up. Your dependable colleague is carrying this team again.", +} + +local MTH_LT_LoyaltyLines = { + "has decided you are less terrible than expected.", + "has upgraded its opinion of you. Keep the food coming.", + "is learning to trust your decision-making. This may be a mistake.", + "has accepted the arrangement. The arrangement includes snacks.", + "now trusts you more. Do not immediately test this with a cliff.", +} + +local function MTH_LT_Store() + if not MTH_CharSavedVariables then MTH_CharSavedVariables = {} end + if type(MTH_CharSavedVariables.levelTracker) ~= "table" then + MTH_CharSavedVariables.levelTracker = { petLine = 0, loyaltyLine = 0 } + end + return MTH_CharSavedVariables.levelTracker +end + +local function MTH_LT_MessageEnabled(key, defaultValue) + return MTH and type(MTH.IsMessageEnabled) == "function" and MTH:IsMessageEnabled(key, defaultValue) +end + +local function MTH_LT_FormatCost(copper) + local total = tonumber(copper) or 0 + local gold = math.floor(total / 10000) + local silver = math.floor((total - gold * 10000) / 100) + local bronze = total - gold * 10000 - silver * 100 + local parts = {} + if gold > 0 then table.insert(parts, "|cffffd100" .. gold .. "g|r") end + if silver > 0 or gold > 0 then table.insert(parts, "|cffc7c7cf" .. silver .. "s|r") end + table.insert(parts, "|cffeda55f" .. bronze .. "c|r") + return table.concat(parts, " ") +end + +local function MTH_LT_NextLine(store, key, lines) + store[key] = (tonumber(store[key]) or 0) + 1 + if store[key] > table.getn(lines) then store[key] = 1 end + return lines[store[key]] +end + +local function MTH_LT_GetHeadline(plan) + local best = nil + local bestPriority = -1 + for _, entry in ipairs(plan.rows or {}) do + local priority = MTH_LT_Priority[tostring(entry.row and entry.row.name or "")] or 0 + if priority > bestPriority then best, bestPriority = entry, priority end + end + return best +end + +local function MTH_LT_FormatRows(plan) + local labels = {} + for index, entry in ipairs(plan.rows or {}) do + local row = entry.row or {} + local label = tostring(row.name or "Unknown") + if row.rank and row.rank ~= "" then label = label .. " " .. tostring(row.rank) end + table.insert(labels, label) + end + return table.concat(labels, ", ") +end + +local function MTH_LT_PrintSpellList(plan) + local line = "" + local isFirstLine = true + for _, entry in ipairs(plan.rows or {}) do + local row = entry.row or {} + local label = tostring(row.name or "Unknown") + if row.rank and row.rank ~= "" then label = label .. " " .. tostring(row.rank) end + if line == "" then + line = label + elseif string.len(line) + string.len(label) + 2 > 92 then + MTH:Print((isFirstLine and "New spells available: " or "And also: ") .. line) + isFirstLine = false + line = label + else + line = line .. ", " .. label + end + end + if line ~= "" then MTH:Print((isFirstLine and "New spells available: " or "And also: ") .. line) end +end + +local function MTH_LT_AnnounceTraining(kind, level, eventKind, preview) + if type(MTH_TrainerPlan_BuildLevel) ~= "function" then return end + local base = MTH_TrainerPlan_BuildLevel(kind, level, { honored = false, includeKnown = preview }) + local honored = MTH_TrainerPlan_BuildLevel(kind, level, { honored = true, includeKnown = preview }) + if base.unlearned.count <= 0 then + if kind == "hunter" and MTH_LT_MessageEnabled("hunterLevelUp", true) then + MTH:Print("Congratz! You hit level " .. tostring(level) .. ".") + end + if preview then MTH:Print("Level test: no " .. kind .. " trainer spells in the 1.18.1 catalog at level " .. tostring(level) .. ".") end + return + end + local headline = MTH_LT_GetHeadline(base) + local headlineText = headline and headline.row and tostring(headline.row.name or "") or "New training" + if headline and headline.row and headline.row.rank and headline.row.rank ~= "" then headlineText = headlineText .. " " .. headline.row.rank end + local isLevelCap = kind == "hunter" and level == 60 + if isLevelCap then headlineText = "The hunt is yours" end + if MTH_LT_MessageEnabled(eventKind == "hunter" and "hunterLevelUp" or "petLevelUp", true) then + if kind == "hunter" then + MTH:Print("Congratz! You hit level " .. tostring(level) .. ".") + MTH:Print(MTH_LT_HunterLines[level] or "New training is available.") + MTH:Print("The spells you can learn will cost you " .. MTH_LT_FormatCost(base.unlearned.cost) + .. ", but only " .. MTH_LT_FormatCost(honored.unlearned.cost) .. " if you are Honored with the master.") + MTH_LT_PrintSpellList(base) + else + MTH:Print("" .. MTH_LT_FormatRows(base)) + MTH:Print("Trainer cost: Base " .. MTH_LT_FormatCost(base.unlearned.cost) .. " | Honored " .. MTH_LT_FormatCost(honored.unlearned.cost)) + end + end + local isMilestone = kind == "hunter" and (level == 10 or level == 20 or level == 30 or level == 40 or level == 50 or level == 60) + local flag = isMilestone and "level.huntermilestone" or (kind == "hunter" and "level.hunter" or "level.pet") + if type(MTH_FX_IsEnabled) == "function" and MTH_FX_IsEnabled(flag) and type(MTH_Celebrate) == "function" then + local subtitle = kind == "hunter" and (MTH_LT_HunterLines[level] or "New training is available.") or "New pet training is available. The trainer wants coin." + if isMilestone and type(MTH_CelebrateBlackHole) == "function" then + MTH_CelebrateBlackHole("Level " .. tostring(level) .. " - " .. headlineText, subtitle) + else + MTH_Celebrate("Level " .. tostring(level) .. " - " .. headlineText, subtitle, "fanfare", 4.8) + end + end +end + +local function MTH_LT_HandleHunterLevel(level) + local store = MTH_LT_Store() + local previous = tonumber(store.hunterLevel) + store.hunterLevel = level + if previous and level > previous then + MTH_LT_AnnounceTraining("hunter", level, "hunter") + end +end + +local function MTH_LT_GetPetState() + if type(UnitExists) ~= "function" or not UnitExists("pet") then return nil end + local level = type(UnitLevel) == "function" and tonumber(UnitLevel("pet")) or nil + local name = type(UnitName) == "function" and UnitName("pet") or "Your pet" + if not level or level <= 0 then return nil end + local loyalty = nil + local getPetLoyalty = (type(getglobal) == "function" and getglobal("GetPetLoyalty")) or (_G and _G["GetPetLoyalty"]) + if type(getPetLoyalty) == "function" then loyalty = tonumber(getPetLoyalty()) end + return { key = tostring(name or "pet"), name = tostring(name or "Your pet"), level = level, loyalty = loyalty } +end + +local function MTH_LT_HandlePetState() + MTH_LT_PendingPetCheck = false + local state = MTH_LT_GetPetState() + if not state then return end + local store = MTH_LT_Store() + local pet = store.pet + if type(pet) ~= "table" or pet.key ~= state.key then + store.pet = state + return + end + if state.level > (tonumber(pet.level) or 0) then + local line = MTH_LT_NextLine(store, "petLine", MTH_LT_PetLines) + if MTH_LT_MessageEnabled("petLevelUp", true) then MTH:Print(state.name .. " reached level " .. state.level .. ". " .. line) end + MTH_LT_AnnounceTraining("pet", state.level, "pet") + end + if state.loyalty and state.loyalty > (tonumber(pet.loyalty) or 0) then + local line = MTH_LT_NextLine(store, "loyaltyLine", MTH_LT_LoyaltyLines) + if state.loyalty == 6 then line = "is your Best Friend. Against all evidence, it trusts you completely." end + if MTH_LT_MessageEnabled("petLoyaltyUp", true) then MTH:Print(state.name .. " reached Loyalty Level " .. state.loyalty .. ". " .. line) end + if type(MTH_FX_IsEnabled) == "function" and MTH_FX_IsEnabled("level.loyalty") and type(MTH_Celebrate) == "function" then + MTH_Celebrate(state.name .. " - Loyalty " .. state.loyalty, line, state.loyalty == 6 and "victory" or "fanfare") + end + end + store.pet = state +end + +local function MTH_LT_RequestPetCheck() + if MTH_LT_PendingPetCheck or not MTH_LT_Frame then return end + MTH_LT_PendingPetCheck = true + MTH_LT_Frame.elapsed = 0 + MTH_LT_Frame:SetScript("OnUpdate", function() + this.elapsed = (this.elapsed or 0) + (arg1 or 0) + if this.elapsed >= 0.35 then + this:SetScript("OnUpdate", nil) + MTH_LT_HandlePetState() + end + end) +end + +function MTH_LevelTracker_Test(kind, level) + if kind == "hunter" or kind == "pet" then + local targetLevel = tonumber(level) or ((kind == "hunter") and (tonumber(UnitLevel("player")) or 1) or 1) + MTH:Print("Level test: previewing " .. kind .. " trainer spells at level " .. tostring(targetLevel) .. ".") + MTH_LT_AnnounceTraining(kind, targetLevel, kind, true) + return true + end + if kind == "loyalty" then + local state = MTH_LT_GetPetState() + local loyalty = tonumber(level) or (state and state.loyalty) or 1 + local petName = state and state.name or "Your pet" + local line = loyalty == 6 and "is your Best Friend. Against all evidence, it trusts you completely." + or MTH_LT_LoyaltyLines[1] + MTH:Print(petName .. " reached Loyalty Level " .. tostring(loyalty) .. ". " .. line) + if type(MTH_Celebrate) == "function" then + MTH_Celebrate(petName .. " - Loyalty " .. tostring(loyalty), line, loyalty == 6 and "victory" or "fanfare") + end + return true + end + return false +end + +function MTH_LevelTracker_Initialize() + if MTH_LT_Frame then return end + MTH_LT_Frame = CreateFrame("Frame", "MTH_LevelTracker") + MTH_LT_Frame:RegisterEvent("PLAYER_ENTERING_WORLD") + MTH_LT_Frame:RegisterEvent("PLAYER_LEVEL_UP") + MTH_LT_Frame:RegisterEvent("UNIT_LEVEL") + MTH_LT_Frame:RegisterEvent("UNIT_PET") + MTH_LT_Frame:RegisterEvent("PET_BAR_UPDATE") + MTH_LT_Frame:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + local level = type(UnitLevel) == "function" and tonumber(UnitLevel("player")) or nil + if level then MTH_LT_Store().hunterLevel = level end + MTH_LT_RequestPetCheck() + elseif event == "PLAYER_LEVEL_UP" then + MTH_LT_HandleHunterLevel(tonumber(arg1) or tonumber(UnitLevel("player")) or 1) + elseif event == "UNIT_LEVEL" and arg1 == "pet" then + MTH_LT_RequestPetCheck() + elseif event == "UNIT_PET" and arg1 == "player" then + MTH_LT_RequestPetCheck() + elseif event == "PET_BAR_UPDATE" then + MTH_LT_RequestPetCheck() + end + end) +end + +MTH_LevelTracker_Initialize() \ No newline at end of file diff --git a/api/core-scan-trainer.lua b/api/core-scan-trainer.lua new file mode 100644 index 0000000..68b8aab --- /dev/null +++ b/api/core-scan-trainer.lua @@ -0,0 +1,251 @@ +-- MetaHunt: one-time live trainer catalog collector. +-- Developer workflow: /mth trainerscan, then open a Hunter or Pet Trainer. + +local MTH_TS_Frame +local MTH_TS_Armed = false +local MTH_TS_AutoArmed = false + +local function MTH_TS_Log(text) + if MTH_DebugFrame and type(MTH_DebugFrame.AddInfo) == "function" then + MTH_DebugFrame:AddInfo("[TrainerScan] " .. tostring(text)) + end +end + +local function MTH_TS_Call(name, ...) + local fn = getglobal and getglobal(name) or (_G and _G[name]) + if type(fn) ~= "function" then return nil, "missing" end + local ok, a, b, c, d, e, f, g = pcall(fn, unpack(arg)) + if not ok then return nil, "error=" .. tostring(a) end + return { a, b, c, d, e, f, g }, nil +end + +local function MTH_TS_Values(values) + if not values then return "" end + local parts = {} + local i = 1 + while i <= table.getn(values) do + if values[i] ~= nil then + table.insert(parts, tostring(values[i])) + end + i = i + 1 + end + return table.concat(parts, " | ") +end + +local function MTH_TS_TargetName() + if type(UnitName) == "function" then + local name = UnitName("target") + if name and name ~= "" then return name end + end + local title = getglobal and getglobal("TrainerFrameTitleText") or nil + if title and title.GetText then return title:GetText() or "?" end + return "?" +end + +local function MTH_TS_GetLiveStatusStore() + if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end + if type(MTH_CharSavedVariables.trainerLiveStatus) ~= "table" then + MTH_CharSavedVariables.trainerLiveStatus = {} + end + return MTH_CharSavedVariables.trainerLiveStatus +end + +local function MTH_TS_GetCatalogKind(name, rank) + local catalog = MTH_DS_TrainerCatalog + if type(catalog) ~= "table" or type(catalog.byNameRank) ~= "table" then return nil end + local keySuffix = "\031" .. tostring(name or "") .. "\031" .. tostring(rank or "") + if catalog.byNameRank["hunter" .. keySuffix] then return "hunter" end + if catalog.byNameRank["pet" .. keySuffix] then return "pet" end + return nil +end + +function MTH_GetTrainerLiveStatus(kind, name, rank) + local store = type(MTH_CharSavedVariables) == "table" and MTH_CharSavedVariables.trainerLiveStatus or nil + if type(store) ~= "table" then return nil end + return store[tostring(kind or "") .. "\031" .. tostring(name or "") .. "\031" .. tostring(rank or "")] +end + +local function MTH_TS_CollectLiveStatuses() + local countValues = MTH_TS_Call("GetNumTrainerServices") + local total = countValues and tonumber(countValues[1]) or 0 + if total <= 0 then return false end + + local trainerName = MTH_TS_TargetName() + local store = MTH_TS_GetLiveStatusStore() + local observed = 0 + for index = 1, total do + local info = MTH_TS_Call("GetTrainerServiceInfo", index) + local name = info and info[1] or nil + local rank = info and info[2] or "" + local status = info and info[3] or nil + local kind = MTH_TS_GetCatalogKind(name, rank) + if kind and (status == "available" or status == "unavailable" or status == "used") then + local key = kind .. "\031" .. tostring(name) .. "\031" .. tostring(rank) + store[key] = { + status = status, + observedAt = time(), + trainer = trainerName, + } + observed = observed + 1 + end + end + + if observed > 0 then + MTH_TS_Log("Live trainer overlay: " .. tostring(observed) .. " observed catalog row(s) from " .. tostring(trainerName) .. ".") + if MTH and MTH.Print and (not MTH.IsMessageEnabled or MTH:IsMessageEnabled("trainerScan", true)) then + MTH:Print("Trainer scan: " .. tostring(observed) .. " Hunter/Pet spell status" .. (observed == 1 and "" or "es") .. " observed.") + end + return true + end + return false +end + +local function MTH_TS_Collect() + local countValues, countError = MTH_TS_Call("GetNumTrainerServices") + local total = countValues and tonumber(countValues[1]) or 0 + if not total or total <= 0 then + MTH_TS_Log("No trainer services yet. GetNumTrainerServices=" .. tostring(MTH_TS_Values(countValues)) + .. " " .. tostring(countError or "")) + return false + end + + local trainerName = MTH_TS_TargetName() + local capture = { + trainer = trainerName, + capturedAt = time(), + services = {}, + } + MTH_TRAINER_SCAN_LAST = capture + if type(MTH_CharSavedVariables) == "table" then + if type(MTH_CharSavedVariables.trainerCatalogScans) ~= "table" then + MTH_CharSavedVariables.trainerCatalogScans = {} + end + MTH_CharSavedVariables.trainerCatalogScans[trainerName] = capture + end + MTH_TS_Log("=== " .. tostring(capture.trainer) .. " | " .. tostring(total) .. " trainer services ===") + + local index = 1 + while index <= total do + local info, infoError = MTH_TS_Call("GetTrainerServiceInfo", index) + local icon, iconError = MTH_TS_Call("GetTrainerServiceIcon", index) + local cost, costError = MTH_TS_Call("GetTrainerServiceCost", index) + local level, levelError = MTH_TS_Call("GetTrainerServiceLevelReq", index) + local skill, skillError = MTH_TS_Call("GetTrainerServiceSkillReq", index) + local description, descriptionError = MTH_TS_Call("GetTrainerServiceDescription", index) + local reqCountValues = MTH_TS_Call("GetTrainerServiceNumAbilityReq", index) + local reqCount = reqCountValues and tonumber(reqCountValues[1]) or 0 + local requirements = {} + local reqIndex = 1 + while reqIndex <= reqCount do + local requirement, reqError = MTH_TS_Call("GetTrainerServiceAbilityReq", index, reqIndex) + table.insert(requirements, { + name = requirement and requirement[1] or nil, + flag = requirement and requirement[2] or nil, + error = reqError, + }) + reqIndex = reqIndex + 1 + end + + local row = { + index = index, + name = info and info[1] or nil, + rank = info and info[2] or nil, + status = info and info[3] or nil, + expanded = info and info[4] or nil, + icon = icon and icon[1] or nil, + cost = cost and cost[1] or nil, + requiredLevel = level and level[1] or nil, + skillRequirement = skill, + description = description and description[1] or nil, + abilityRequirements = requirements, + } + table.insert(capture.services, row) + + MTH_TS_Log(string.format("%02d name=%s | rank=%s | status=%s | expanded=%s | level=%s | cost=%s | icon=%s", + index, tostring(row.name), tostring(row.rank), tostring(row.status), tostring(row.expanded), + tostring(row.requiredLevel), tostring(row.cost), tostring(row.icon))) + if row.description and row.description ~= "" then + MTH_TS_Log(" description=" .. tostring(row.description)) + end + if table.getn(requirements) > 0 then + local requirementText = {} + local requirementIndex = 1 + while requirementIndex <= table.getn(requirements) do + local requirement = requirements[requirementIndex] + table.insert(requirementText, tostring(requirement.name) + .. " [flag=" .. tostring(requirement.flag) .. "]") + requirementIndex = requirementIndex + 1 + end + MTH_TS_Log(" abilityRequirements=" .. table.concat(requirementText, ", ")) + end + if skill and table.getn(skill) > 0 then + MTH_TS_Log(" skillRequirement=" .. MTH_TS_Values(skill)) + end + if infoError or iconError or costError or levelError or skillError or descriptionError then + MTH_TS_Log(" API=" .. tostring(infoError or "ok") .. ", " .. tostring(iconError or "ok") + .. ", " .. tostring(costError or "ok") .. ", " .. tostring(levelError or "ok") + .. ", " .. tostring(skillError or "ok") .. ", " .. tostring(descriptionError or "ok")) + end + index = index + 1 + end + + MTH_TS_Log("=== Capture complete. " .. tostring(total) + .. " rows retained in MTH_TRAINER_SCAN_LAST and trainerCatalogScans. ===") + return true +end + +local function MTH_TS_EnsureFrame() + if not MTH_TS_Frame then + MTH_TS_Frame = CreateFrame("Frame", "MTH_TrainerCatalogScanner") + MTH_TS_Frame:SetScript("OnEvent", function() + if event == "TRAINER_SHOW" then + MTH_TS_AutoArmed = true + this.elapsed = 0 + this:SetScript("OnUpdate", function() + this.elapsed = (this.elapsed or 0) + (arg1 or 0) + if this.elapsed < 0.25 then return end + this:SetScript("OnUpdate", nil) + MTH_TS_CollectLiveStatuses() + end) + return + end + if event ~= "TRAINER_LIST_UPDATE" or not (MTH_TS_Armed or MTH_TS_AutoArmed) then return end + this.elapsed = 0 + this:SetScript("OnUpdate", function() + this.elapsed = (this.elapsed or 0) + (arg1 or 0) + if this.elapsed < 0.25 then return end + this:SetScript("OnUpdate", nil) + local manualArmed = MTH_TS_Armed + MTH_TS_Armed = false + MTH_TS_AutoArmed = false + MTH_TS_CollectLiveStatuses() + if manualArmed and not MTH_TS_Collect() and MTH and MTH.Print then + MTH:Print("Trainer list was empty. Open the trainer again and run /mth trainerscan.") + end + end) + end) + MTH_TS_Frame:RegisterEvent("TRAINER_SHOW") + MTH_TS_Frame:RegisterEvent("TRAINER_LIST_UPDATE") + end +end + +function MTH_CommandTrainerScan() + if MTH_DebugFrame and type(MTH_DebugFrame.Show) == "function" then MTH_DebugFrame:Show() end + local currentRows = MTH_TS_Call("GetNumTrainerServices") + if currentRows and (tonumber(currentRows[1]) or 0) > 0 then + MTH_TS_Collect() + if MTH and MTH.Print then + MTH:Print("Trainer catalog captured from the open trainer window.") + end + return + end + + MTH_TS_EnsureFrame() + + MTH_TS_Armed = true + if MTH and MTH.Print then + MTH:Print("Trainer catalog scan armed. Open a Hunter Trainer or Pet Trainer; results will appear in the debug window.") + end +end + +MTH_TS_EnsureFrame() \ No newline at end of file diff --git a/api/core.lua b/api/core.lua index f1f06b5..89299e3 100644 --- a/api/core.lua +++ b/api/core.lua @@ -331,15 +331,40 @@ function SlashCmdList.MTH(msg, editbox) else MTH:Print("Train scan is not available.") end + elseif lowerCmd == "trainerscan" then + if type(MTH_CommandTrainerScan) == "function" then + MTH_CommandTrainerScan() + else + MTH:Print("Trainer catalog scanner is not available.") + end + elseif lowerCmd == "spellscan" then + if type(MTH_HT_RescanSpellbook) ~= "function" then + MTH:Print("Hunter spellbook scanner is not available.") + return + end + MTH_HT_RescanSpellbook() + local mail = type(MTH_HT_GetSpellInfo) == "function" and MTH_HT_GetSpellInfo("Mail") or nil + if mail then + MTH:Print("Hunter spellbook scan: Mail found (rank=" .. tostring(mail.rank or "") .. ", slot=" .. tostring(mail.slot or "?") .. ").") + else + MTH:Print("Hunter spellbook scan: Mail was not returned by the client spellbook API.") + end + elseif lowerCmd == "leveltest" then + local _, _, kind, levelText = string.find(lowerArg or "", "^(%S+)%s*(%d*)") + if type(MTH_LevelTracker_Test) ~= "function" then + MTH:Print("Level tracker is not available.") + elseif not MTH_LevelTracker_Test(kind, tonumber(levelText)) then + MTH:Print("Use /mth leveltest hunter [level], pet [level], or loyalty [level]") + end elseif lowerCmd == "fx" or lowerCmd == "fireworks" or lowerCmd == "celebrate" then - -- TEMP: preview the pet-ability celebration effect. - if type(MTH_Celebrate) == "function" then - local label = msg - local _, _, rest = string.find(msg, "^%S+%s+(.-)%s*$") - if rest and rest ~= "" then - MTH_Celebrate("New Pet Ability!", rest) - else - MTH_Celebrate("New Pet Ability!", "Growl (Rank 3)") + if type(MTH_CelebratePreview) == "function" then + local style = lowerArg or "" + if style == "" then style = "firework" end + if style == "help" or style == "list" then + MTH:Print("Celebrations: firework, blackhole, shockwave, shatter, all") + elseif not MTH_CelebratePreview(style) then + MTH:Print("Unknown celebration: " .. tostring(style)) + MTH:Print("Use /mth fx help for: firework, blackhole, shockwave, shatter, all") end else -- Self-diagnosing: report which piece failed to load so we can tell diff --git a/api/fx-celebrate.lua b/api/fx-celebrate.lua index 3649c85..2bb1fff 100644 --- a/api/fx-celebrate.lua +++ b/api/fx-celebrate.lua @@ -33,6 +33,7 @@ MTH_FX_DBG = { -- Sections shown, in order, in the options panel. `titleKey` localizes `title`. MTH_FX_ANIM_SECTIONS = { { id = "pet", title = "Pet", titleKey = "ANIM_SECTION_PET" }, + { id = "level", title = "Leveling", titleKey = "ANIM_SECTION_LEVEL" }, { id = "combat", title = "Combat", titleKey = "ANIM_SECTION_COMBAT" }, { id = "book", title = "Book", titleKey = "ANIM_SECTION_BOOK" }, { id = "ui", title = "Interface", titleKey = "ANIM_SECTION_UI" }, @@ -43,6 +44,18 @@ MTH_FX_ANIM_SECTIONS = { -- labelKey/tooltipKey localize label/tooltip (resolved at render time). -- Adding a new animation = one entry here + one MTH_FX_IsEnabled("key") guard. MTH_FX_ANIM_DEFS = { + { key = "level.hunter", section = "level", default = true, + label = "Celebrate Hunter training levels", + tooltip = "Show a banner and burst when a level unlocks Hunter trainer spells." }, + { key = "level.huntermilestone", section = "level", default = true, + label = "Celebrate Hunter milestones", + tooltip = "Use a large celebration at levels 10, 20, 30, 40, 50, and 60." }, + { key = "level.pet", section = "level", default = true, + label = "Celebrate pet training levels", + tooltip = "Show a banner when your current pet gains a level with Pet Trainer spells." }, + { key = "level.loyalty", section = "level", default = true, + label = "Celebrate pet loyalty gains", + tooltip = "Show a golden banner when your current pet gains a loyalty level." }, { key = "pet.celebrate", section = "pet", default = true, label = "Celebrate new pet abilities", labelKey = "ANIM_PET_CELEBRATE", tooltip = "Play a firework and a banner in the middle of the screen when your pet learns a new ability or a new rank.", @@ -244,6 +257,27 @@ if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasShake t MTH_FX_DBG.shakeErr = err end +if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasSpiral then + local ok, err = pcall(function() + Sauce.MTH_HasSpiral = true + Sauce:RegisterType("mth_spiral", { + init = function(target, from) + return from or 0 + end, + apply = function(target, from, to, t, tween) + local radius = (tween.radius or 0) * (1 - t) + local angle = (tween.startAngle or 0) + (tween.rotations or 1) * 2 * math.pi * t + target:ClearAllPoints() + target:SetPoint("CENTER", tween.anchor or UIParent, "CENTER", + (tween.cx or 0) + math.cos(angle) * radius, + (tween.cy or 0) + math.sin(angle) * radius) + end, + }) + end) + MTH_FX_DBG.spiralOk = ok + MTH_FX_DBG.spiralErr = err +end + -- Reusable flash: overlays a short additive colour glow on any frame/button and -- fades it out. Safe on any frame that supports CreateTexture; silent no-op -- otherwise. The overlay texture is cached on the frame for reuse. @@ -277,11 +311,15 @@ end local MTH_FX_NUM_SPARKS = 22 +local MTH_FX_NUM_CONFETTI = 48 +local MTH_FX_NUM_BLACKHOLE = 40 local MTH_FX_Container local MTH_FX_MsgFrame local MTH_FX_TitleFS local MTH_FX_SubFS local MTH_FX_Sparks = {} +local MTH_FX_Confetti = {} +local MTH_FX_BlackHole = {} -- Palette for the firework sparks (r, g, b), warm celebratory tones. local MTH_FX_Colors = { @@ -337,33 +375,75 @@ local function MTH_FX_EnsureFrames() i = i + 1 end + i = 1 + while i <= MTH_FX_NUM_CONFETTI do + local bit = MTH_FX_Container:CreateTexture(nil, "OVERLAY") + bit:SetTexture("Interface\\Buttons\\WHITE8X8") + bit:SetWidth(5) + bit:SetHeight(8) + bit:SetPoint("CENTER", MTH_FX_Container, "CENTER", 0, 0) + bit:Hide() + MTH_FX_Confetti[i] = bit + i = i + 1 + end + + i = 1 + while i <= MTH_FX_NUM_BLACKHOLE do + local particle = MTH_FX_Container:CreateTexture(nil, "OVERLAY") + particle:SetTexture("Interface\\Cooldown\\star4") + particle:SetWidth(10) + particle:SetHeight(10) + particle:SetBlendMode("ADD") + particle:SetPoint("CENTER", MTH_FX_Container, "CENTER", 0, 0) + particle:Hide() + MTH_FX_BlackHole[i] = particle + i = i + 1 + end + return true end --- Public entry point: play a celebratory message + firework burst in the middle --- of the screen. `title` and `subtitle` are optional strings. -function MTH_Celebrate(title, subtitle) +-- Public entry point: play a full-screen celebration. `style` defaults to the +-- original firework burst and may be firework, victory, starfall, fanfare, or comet. +function MTH_Celebrate(title, subtitle, style, displaySeconds) if not Sauce then return end if not MTH_FX_EnsureFrames() then return end + style = string.lower(tostring(style or "firework")) + local fadeDelay = tonumber(displaySeconds) or 2.1 + if fadeDelay < 0.5 then fadeDelay = 0.5 end - MTH_FX_TitleFS:SetTextColor(1, 0.82, 0) + if style == "starfall" then + MTH_FX_TitleFS:SetTextColor(0.45, 0.80, 1.00) + else + MTH_FX_TitleFS:SetTextColor(1, 0.82, 0) + end MTH_FX_TitleFS:SetText(title or "") MTH_FX_SubFS:SetText(subtitle or "") Sauce:Stop(MTH_FX_MsgFrame) MTH_FX_MsgFrame:SetAlpha(0) MTH_FX_MsgFrame:Show() - Sauce:Sequence({ - Sauce:Group({ - Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.25), - Sauce:Pulse(MTH_FX_MsgFrame, 1.22, 0.5), - }), - Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = 2.2 }), - }) + if style == "victory" or style == "fanfare" then + Sauce:Sequence({ + Sauce:Group({ + Sauce:SlideIn(MTH_FX_MsgFrame, "UP", 80, 0.36, "outBack", { defer = true }), + Sauce:Pulse(MTH_FX_MsgFrame, style == "victory" and 1.32 or 1.18, 0.52, { defer = true }), + }), + Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = fadeDelay }), + }) + else + Sauce:Sequence({ + Sauce:Group({ + Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.25), + Sauce:Pulse(MTH_FX_MsgFrame, 1.22, 0.5), + }), + Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = fadeDelay }), + }) + end MTH_FX_Container:Show() local colorCount = table.getn(MTH_FX_Colors) @@ -371,34 +451,70 @@ function MTH_Celebrate(title, subtitle) local i = 1 while i <= MTH_FX_NUM_SPARKS do local spark = MTH_FX_Sparks[i] + spark:SetTexture("Interface\\Cooldown\\star4") + spark:SetTexCoord(0, 1, 0, 1) + local startX, startY = 0, 0 + local delay, gravity = 0, 300 local angle = (i / MTH_FX_NUM_SPARKS) * 2 * math.pi + (math.random() - 0.5) * 0.4 local speed = 150 + math.random() * 130 local vx = math.cos(angle) * speed local vy = math.sin(angle) * speed + 70 local dur = 0.9 + math.random() * 0.4 + if style == "victory" then + startX = ((i - math.floor(i / 2) * 2) == 0) and -130 or 130 + speed = 100 + math.random() * 100 + vx = math.cos(angle) * speed + vy = math.sin(angle) * speed + 105 + elseif style == "starfall" then + startX = -230 + math.random() * 460 + startY = 200 + math.random() * 100 + vx = -35 + math.random() * 70 + vy = -10 + math.random() * 25 + gravity = 235 + dur = 1.1 + math.random() * 0.5 + elseif style == "fanfare" then + delay = ((i - 1) - math.floor((i - 1) / 3) * 3) * 0.18 + speed = 110 + math.random() * 90 + vx = math.cos(angle) * speed + vy = math.sin(angle) * speed + 90 + elseif style == "comet" then + if i == 1 then + startY = -180 + vx, vy, gravity, dur = 0, 430, 0, 0.52 + else + delay = 0.5 + startY = 45 + speed = 130 + math.random() * 120 + vx = math.cos(angle) * speed + vy = math.sin(angle) * speed + 70 + end + end + colorIdx = colorIdx + 1 if colorIdx > colorCount then colorIdx = 1 end local color = MTH_FX_Colors[colorIdx] spark:SetVertexColor(color[1], color[2], color[3]) - spark:SetAlpha(1) + spark:SetAlpha(delay > 0 and 0 or 1) spark:Show() Sauce:Stop(spark) Sauce:Group({ Sauce:Tween(spark, { type = "mth_ballistic", - from = { 0, 0 }, + from = { startX, startY }, duration = dur, easing = "linear", + delay = delay, anchor = MTH_FX_Container, dur = dur, vx = vx, vy = vy, - gravity = 300, + gravity = gravity, }), + Sauce:FadeTo(spark, 1, 0.05, "outQuad", { delay = delay }), Sauce:FadeTo(spark, 0, 0.45, "inQuad", { - delay = dur - 0.45, + delay = delay + dur - 0.45, onFinish = function() spark:Hide() end, @@ -408,6 +524,203 @@ function MTH_Celebrate(title, subtitle) end end +local function MTH_FX_ShowPreviewBanner(title, subtitle, r, g, b) + MTH_FX_TitleFS:SetTextColor(r, g, b) + MTH_FX_TitleFS:SetText(title) + MTH_FX_SubFS:SetText(subtitle) + Sauce:Stop(MTH_FX_MsgFrame) + MTH_FX_MsgFrame:SetAlpha(0) + MTH_FX_MsgFrame:Show() + Sauce:Sequence({ + Sauce:Group({ + Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.18), + Sauce:Pulse(MTH_FX_MsgFrame, 1.18, 0.45), + }), + Sauce:FadeOut(MTH_FX_MsgFrame, 0.55, "inQuad", { delay = 2.1 }), + }) +end + +function MTH_CelebrateConfetti(title, subtitle) + if not Sauce or not MTH_FX_EnsureFrames() then return end + MTH_FX_ShowPreviewBanner(title or "Celebration!", subtitle or "Confetti", 1, 0.82, 0.15) + MTH_FX_Container:Show() + local count = table.getn(MTH_FX_Colors) + local i = 1 + while i <= MTH_FX_NUM_CONFETTI do + local bit = MTH_FX_Confetti[i] + local color = MTH_FX_Colors[((i - 1) - math.floor((i - 1) / count) * count) + 1] + local size = 3 + math.random() * 5 + local angle = math.random() * 2 * math.pi + local speed = 90 + math.random() * 210 + local dur = 1.25 + math.random() * 0.7 + bit:SetWidth(size) + bit:SetHeight(size * 1.6) + bit:SetVertexColor(color[1], color[2], color[3]) + bit:SetAlpha(1) + bit:Show() + Sauce:Stop(bit) + Sauce:Group({ + Sauce:Tween(bit, { + type = "mth_ballistic", from = { 0, 0 }, duration = dur, + easing = "linear", anchor = MTH_FX_Container, dur = dur, + vx = math.cos(angle) * speed, vy = math.sin(angle) * speed + 100, gravity = 255, + }), + Sauce:FadeTo(bit, 0, 0.35, "inQuad", { + delay = dur - 0.35, onFinish = function() bit:Hide() end, + }), + }) + i = i + 1 + end +end + +function MTH_CelebrateBlackHole(title, subtitle) + if not Sauce or not MTH_FX_EnsureFrames() then return end + MTH_FX_ShowPreviewBanner(title or "Gravity Well", subtitle or "Black Hole", 0.68, 0.45, 1.00) + MTH_FX_Container:Show() + local colors = { { 0.45, 0.25, 1 }, { 0.30, 0.50, 1 }, { 0.80, 0.55, 1 } } + local i = 1 + while i <= MTH_FX_NUM_BLACKHOLE do + local particle = MTH_FX_BlackHole[i] + local color = colors[((i - 1) - math.floor((i - 1) / 3) * 3) + 1] + local startAngle = math.random() * 2 * math.pi + local radius = 95 + math.random() * 145 + local duration = 1.5 + math.random() * 1.0 + local size = 7 + math.random() * 8 + particle:SetWidth(size) + particle:SetHeight(size) + particle:SetVertexColor(color[1], color[2], color[3]) + particle:SetAlpha(1) + particle:Show() + Sauce:Stop(particle) + Sauce:Group({ + Sauce:Tween(particle, { + type = "mth_spiral", from = 0, to = 1, duration = duration, + easing = "inQuad", anchor = MTH_FX_Container, cx = 0, cy = 0, + radius = radius, startAngle = startAngle, rotations = 1.5 + math.random() * 2.0, + }), + Sauce:Size(particle, { size, size }, { 2, 2 }, duration, "inQuad"), + Sauce:FadeTo(particle, 0, duration * 0.3, "inQuad", { + delay = duration * 0.7, onFinish = function() particle:Hide() end, + }), + }) + i = i + 1 + end +end + +function MTH_CelebrateShockwave(title, subtitle) + if not Sauce or not MTH_FX_EnsureFrames() then return end + MTH_FX_ShowPreviewBanner(title or "Impact!", subtitle or "Shockwave", 1, 0.62, 0.15) + MTH_FX_Container:Show() + local dotsPerWave = 16 + local i = 1 + while i <= MTH_FX_NUM_CONFETTI do + local dot = MTH_FX_Confetti[i] + local wave = math.floor((i - 1) / dotsPerWave) + 1 + local pos = (i - 1) - math.floor((i - 1) / dotsPerWave) * dotsPerWave + local angle = pos / dotsPerWave * 2 * math.pi + local duration = 1.1 + wave * 0.18 + local radius = 115 + wave * 52 + local delay = (wave - 1) * 0.18 + local intensity = 1 - (wave - 1) * 0.20 + dot:SetWidth(5) + dot:SetHeight(5) + dot:SetVertexColor(1 * intensity, 0.65 * intensity, 0.18 * intensity) + dot:SetAlpha(0) + dot:Show() + Sauce:Stop(dot) + Sauce:Group({ + Sauce:Tween(dot, { + type = "mth_ballistic", from = { 0, 0 }, duration = duration, + easing = "outQuad", delay = delay, anchor = MTH_FX_Container, dur = duration, + vx = math.cos(angle) * radius / duration, vy = math.sin(angle) * radius / duration, gravity = 0, + }), + Sauce:FadeTo(dot, 1, 0.04, "outQuad", { delay = delay }), + Sauce:FadeTo(dot, 0, 0.4, "inQuad", { + delay = delay + duration - 0.4, onFinish = function() dot:Hide() end, + }), + }) + i = i + 1 + end +end + +function MTH_CelebrateShatter(title, subtitle) + if not Sauce or not MTH_FX_EnsureFrames() then return end + MTH_FX_ShowPreviewBanner(title or "Shattered!", subtitle or "Shatter", 0.70, 0.85, 1.00) + MTH_FX_Container:Show() + local grid = 4 + local iconSize = 80 + local fragmentSize = iconSize / grid + local i = 1 + while i <= MTH_FX_NUM_SPARKS do + local spark = MTH_FX_Sparks[i] + if i <= grid * grid then + local row = math.floor((i - 1) / grid) + local col = (i - 1) - row * grid + local x = (col - grid / 2 + 0.5) * fragmentSize + local y = ((grid - 1 - row) - grid / 2 + 0.5) * fragmentSize + local duration = 1.1 + math.random() * 0.45 + spark:SetTexture("Interface\\Icons\\Ability_Hunter_SniperShot") + spark:SetTexCoord(col / grid, (col + 1) / grid, row / grid, (row + 1) / grid) + spark:SetWidth(fragmentSize) + spark:SetHeight(fragmentSize) + spark:SetVertexColor(1, 1, 1) + spark:SetAlpha(1) + spark:Show() + Sauce:Stop(spark) + Sauce:Group({ + Sauce:Tween(spark, { + type = "mth_ballistic", from = { x, y }, duration = duration, + easing = "linear", anchor = MTH_FX_Container, dur = duration, + vx = x * 4 + (math.random() - 0.5) * 90, + vy = y * 4 + 95 + math.random() * 80, gravity = 265, + }), + Sauce:FadeTo(spark, 0, 0.35, "inQuad", { + delay = duration - 0.35, onFinish = function() spark:Hide() end, + }), + }) + end + i = i + 1 + end +end + +function MTH_CelebratePreview(style) + style = string.lower(tostring(style or "firework")) + if style == "all" then + if not Sauce then return end + local styles = { "firework", "blackhole", "shockwave", "shatter" } + local steps = {} + local i = 1 + while i <= table.getn(styles) do + local previewStyle = styles[i] + table.insert(steps, Sauce:Run(function() + MTH_CelebratePreview(previewStyle) + end, { defer = true })) + table.insert(steps, Sauce:Delay(3.7)) + i = i + 1 + end + Sauce:Sequence(steps) + return true + end + + if style == "blackhole" then + MTH_CelebrateBlackHole("Celebration Preview", "Black Hole") + return true + elseif style == "shockwave" then + MTH_CelebrateShockwave("Celebration Preview", "Shockwave") + return true + elseif style == "shatter" then + MTH_CelebrateShatter("Celebration Preview", "Shatter") + return true + end + + local labels = { + firework = "Firework Burst", + } + if not labels[style] then return false end + MTH_Celebrate("Celebration Preview", labels[style], style) + return true +end + -- Convenience wrapper used when the hunter's pet learns a new ability/rank. function MTH_CelebratePetAbility(label) if not MTH_FX_IsEnabled("pet.celebrate") then diff --git a/api/hunterbook-tab-trainers.lua b/api/hunterbook-tab-trainers.lua new file mode 100644 index 0000000..240aac9 --- /dev/null +++ b/api/hunterbook-tab-trainers.lua @@ -0,0 +1,225 @@ +if type(MTH_HUNTERBOOK_TABS) ~= "table" then MTH_HUNTERBOOK_TABS = {} end + +local trainerColumns = { + { x = 28, width = 150, align = "LEFT" }, + { x = 180, width = 62, align = "LEFT" }, + { x = 244, width = 43, align = "LEFT" }, + { x = 289, width = 80, align = "LEFT" }, + { x = 371, width = 105, align = "LEFT" }, + { x = 478, width = 74, align = "LEFT" }, +} + +MTH_HUNTERBOOK_TABS.hunterspells = { + headerLabel = "Hunter Spells", + columnLabels = { "Spell", "Rank", "Level", "Price", "Branch", "Status" }, + columnLayout = trainerColumns, +} + +MTH_HUNTERBOOK_TABS.petspells = { + headerLabel = "Pet Spells", + columnLabels = { "Spell", "Rank", "Level", "Price", "Branch", "Status" }, + columnLayout = trainerColumns, +} + +function MTH_BOOKTAB_IsTrainerSpellMode(mode) + return mode == "hunterspells" or mode == "petspells" +end + +local function MTH_BOOKTAB_GetTrainerKind() + if MTH_BOOK_STATE and MTH_BOOK_STATE.mode == "petspells" then return "pet" end + return "hunter" +end + +function MTH_BOOKTAB_FormatTrainerCost(copper) + local value = tonumber(copper) or 0 + local gold = math.floor(value / 10000) + value = value - gold * 10000 + local silver = math.floor(value / 100) + local bronze = value - silver * 100 + local parts = {} + if gold > 0 then table.insert(parts, "|cffffd100" .. tostring(gold) .. "g|r") end + if silver > 0 or gold > 0 then table.insert(parts, "|cffc7c7cf" .. tostring(silver) .. "s|r") end + table.insert(parts, "|cffeda55f" .. tostring(bronze) .. "c|r") + return table.concat(parts, " ") +end + +function MTH_BOOKTAB_GetTrainerDisplayCost(copper) + if type(MTH_TrainerPlan_GetPrice) == "function" then + return MTH_TrainerPlan_GetPrice({ trainerCost = copper }, MTH_BOOK_STATE and MTH_BOOK_STATE.trainerHonoredDiscount) + end + return tonumber(copper) or 0 +end + +function MTH_BOOKTAB_GetTrainerSpellStatus(row) + local kind = MTH_BOOKTAB_GetTrainerKind() + local playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil + local status = type(MTH_TrainerPlan_GetStatus) == "function" and MTH_TrainerPlan_GetStatus(kind, row, playerLevel) or "notyet" + if status == "known" then return "Known", "|cff40ff40" end + if status == "notlearned" then return "Not learned", "|cffffa500" end + return "Not yet learnable", "|cffff4040" +end + +function MTH_BOOKTAB_BuildTrainerSpellResults() + local results = {} + local kind = MTH_BOOKTAB_GetTrainerKind() + local plan = type(MTH_TrainerPlan_Build) == "function" and MTH_TrainerPlan_Build(kind, { + playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil, + honored = MTH_BOOK_STATE and MTH_BOOK_STATE.trainerHonoredDiscount, + }) or nil + if type(plan) ~= "table" or type(plan.rows) ~= "table" then return results end + local search = MTH_BOOK_SafeLower(MTH_BOOK_STATE.search or "") + for _, planned in ipairs(plan.rows) do + local row = planned.row + local level = tonumber(row.requiredLevel) or 0 + local haystack = tostring(row.name or "") .. " " .. tostring(row.rank or "") .. " " .. tostring(row.section or "") .. " " .. tostring(row.description or "") + if type(row.prerequisites) == "table" then + for _, prerequisite in ipairs(row.prerequisites) do + haystack = haystack .. " " .. tostring(prerequisite) + end + end + haystack = MTH_BOOK_SafeLower(haystack) + if (search == "" or string.find(haystack, search, 1, true) ~= nil) + and (not MTH_BOOK_STATE.minLevel or level >= MTH_BOOK_STATE.minLevel) + and (not MTH_BOOK_STATE.maxLevel or level <= MTH_BOOK_STATE.maxLevel) + and (type(MTH_TrainerPlan_MatchesQuick) ~= "function" or MTH_TrainerPlan_MatchesQuick(planned.status, MTH_BOOK_STATE.quick)) + then + table.insert(results, row) + end + end + return results +end + +function MTH_BOOKTAB_UpdateTrainerPlanPanel(detail) + local parent = detail and detail:GetParent() + if not parent then return end + if not parent.mthTrainerPlan then + local panel = CreateFrame("Frame", nil, parent) + panel:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -238) + panel:SetWidth(136) + panel:SetHeight(128) + panel:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) + panel:SetBackdropColor(0.06, 0.06, 0.06, 1) + panel:SetBackdropBorderColor(0.28, 0.28, 0.28, 1) + panel.text = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + panel.text:SetPoint("TOPLEFT", panel, "TOPLEFT", 7, -7) + panel.text:SetWidth(122) + panel.text:SetJustifyH("LEFT") + panel.text:SetJustifyV("TOP") + parent.mthTrainerPlan = panel + end + local plan = type(MTH_TrainerPlan_Build) == "function" and MTH_TrainerPlan_Build(MTH_BOOKTAB_GetTrainerKind(), { + playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil, + honored = MTH_BOOK_STATE.trainerHonoredDiscount, + }) or nil + if not plan then return end + local nextLevel = plan.nextLevel.level + local nextLabel = "Next" + if nextLevel then nextLabel = "At level " .. tostring(nextLevel) end + local function formatBlock(label, summary) + return "|cffffffff" .. label .. "|r\n" + .. "|cff4da6ff" .. tostring(summary.count) .. "|r |cff9a9a9aspells|r |cffffffff| |r" + .. MTH_BOOKTAB_FormatTrainerCost(summary.cost) + end + parent.mthTrainerPlan.text:SetText("|cffffd100Training Plan|r\n" + .. formatBlock("Now", plan.now) + .. "\n\n" .. formatBlock(nextLabel, plan.nextLevel) + .. "\n\n" .. formatBlock("Remaining", plan.remaining)) + parent.mthTrainerPlan:Show() +end + +function MTH_BOOKTAB_GetTrainerSpellTotal() + local catalog = MTH_DS_TrainerCatalog + local rows = catalog and catalog[MTH_BOOKTAB_GetTrainerKind()] or nil + return type(rows) == "table" and table.getn(rows) or 0 +end + +function MTH_BOOKTAB_TrainerSpellSort(a, b) + local al = tonumber(a and a.requiredLevel) or 0 + local bl = tonumber(b and b.requiredLevel) or 0 + if al ~= bl then return al < bl end + local an = MTH_BOOK_SafeLower(a and a.name) + local bn = MTH_BOOK_SafeLower(b and b.name) + if an ~= bn then return an < bn end + return MTH_BOOK_SafeLower(a and a.rank) < MTH_BOOK_SafeLower(b and b.rank) +end + +function MTH_BOOKTAB_GetTrainerSpellSortKey(row, col) + if col == 1 then return MTH_BOOK_SafeLower(row and row.name) end + if col == 2 then return MTH_BOOK_SafeLower(row and row.rank) end + if col == 3 then return tonumber(row and row.requiredLevel) or 0 end + if col == 4 then return tonumber(row and row.trainerCost) or 0 end + if col == 5 then return MTH_BOOK_SafeLower(row and row.section) end + if col == 6 then + local status = MTH_BOOKTAB_GetTrainerSpellStatus(row) + return MTH_BOOK_SafeLower(status) + end + return nil +end + +function MTH_BOOKTAB_GetTrainerSpellRowValues(row) + local requiredLevel = tonumber(row and row.requiredLevel) or 0 + local playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil + local levelColor = (playerLevel and playerLevel >= requiredLevel) and "|cff40ff40" or "|cffff4040" + local status, statusColor = MTH_BOOKTAB_GetTrainerSpellStatus(row) + return { + tostring(row and row.name or "-"), + tostring(row and row.rank or "-"), + levelColor .. tostring(row and row.requiredLevel or "-") .. "|r", + MTH_BOOKTAB_FormatTrainerCost(MTH_BOOKTAB_GetTrainerDisplayCost(row and row.trainerCost)), + tostring(row and row.section or "-"), + statusColor .. status .. "|r", + } +end + +function MTH_BOOKTAB_UpdateTrainerSpellDetail(detail, selected) + if not MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE and MTH_BOOK_STATE.mode) then return false end + MTH_BOOKTAB_UpdateTrainerPlanPanel(detail) + if not selected then + MTH_BOOK_SetDetailText(detail, "|cFFFFD100Trainer Spells|r\n\nSelect a spell rank to inspect its trainer level, undiscounted cost, and prerequisites.") + return true + end + local lines = {} + table.insert(lines, "|cffffff00" .. tostring(selected.name or "Unknown") .. "|r") + if selected.rank and selected.rank ~= "" then table.insert(lines, "|cffaaaaaa" .. tostring(selected.rank) .. "|r") end + table.insert(lines, "") + table.insert(lines, "|cff9a9a9aBranch:|r |cffffffff" .. tostring(selected.section or "-") .. "|r") + local requiredLevel = tonumber(selected.requiredLevel) or 0 + local playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil + local levelColor = (playerLevel and playerLevel >= requiredLevel) and "|cff40ff40" or "|cffff4040" + table.insert(lines, "|cff9a9a9aRequired Level:|r " .. levelColor .. tostring(selected.requiredLevel or "-") .. "|r") + local status, statusColor = MTH_BOOKTAB_GetTrainerSpellStatus(selected) + table.insert(lines, "|cff9a9a9aStatus:|r " .. statusColor .. status .. "|r") + table.insert(lines, "|cff9a9a9aBase Price:|r " .. MTH_BOOKTAB_FormatTrainerCost(selected.trainerCost)) + table.insert(lines, "|cff9a9a9aHonored Price:|r " .. MTH_BOOKTAB_FormatTrainerCost(math.floor((tonumber(selected.trainerCost) or 0) * 0.90))) + if type(selected.prerequisites) == "table" and table.getn(selected.prerequisites) > 0 then + table.insert(lines, "") + table.insert(lines, "|cffffd100Prerequisites|r") + for _, prerequisite in ipairs(selected.prerequisites) do + table.insert(lines, " " .. tostring(prerequisite)) + end + end + if selected.description and selected.description ~= "" then + table.insert(lines, "") + table.insert(lines, "|cffffd100Description|r") + table.insert(lines, "|cffffffff " .. tostring(selected.description) .. "|r") + end + local detailFrame = detail.mthTrainerDetailParent or detail:GetParent() + if not detail.mthTrainerIcon then + detail.mthTrainerIcon = detail:CreateTexture(nil, "ARTWORK") + detail.mthTrainerIcon:SetWidth(28) + detail.mthTrainerIcon:SetHeight(28) + end + detail.mthTrainerIcon:SetParent(detail) + detail.mthTrainerIcon:ClearAllPoints() + detail.mthTrainerIcon:SetPoint("TOPLEFT", detail, "TOPLEFT", 0, 0) + detail.mthTrainerIcon:SetTexture(MTH_BOOK_ResolveIconPath(selected.icon) or "Interface\\Icons\\INV_Misc_QuestionMark") + detail.mthTrainerIcon:Show() + if detail.SetTextInsets then detail:SetTextInsets(4, 4, 36, 4) end + if not detail.mthTrainerDetailScroll then + detail:ClearAllPoints() + detail:SetPoint("TOPLEFT", detailFrame, "TOPLEFT", 8, -42) + detail:SetWidth(detailFrame:GetWidth() - 16) + end + MTH_BOOK_SetDetailText(detail, table.concat(lines, "\n")) + return true +end \ No newline at end of file diff --git a/api/hunterbook.lua b/api/hunterbook.lua index 0576fff..e4ef175 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -147,6 +147,7 @@ local MTH_BOOK_STATE = { familyStatsPercent = false, petHideNoAbilities = true, petHideUnknown = true, + trainerHonoredDiscount = false, forcedBeastId = nil, page = 1, pageSize = 16, @@ -170,6 +171,8 @@ local MTH_BOOK_STATE = { npcs = { col = nil, asc = true }, stable = { col = nil, asc = true }, pethistory = { col = 5, asc = false }, + hunterspells = { col = 3, asc = true }, + petspells = { col = 3, asc = true }, }, sliderDragging = false, } @@ -308,6 +311,17 @@ local MTH_BOOK_CONTENT_LAYOUT_ITEMS = { detailH = 366, } +local MTH_BOOK_CONTENT_LAYOUT_TRAINERS = { + listX = 16, + listY = -146, + listW = 564, + listH = 374, + detailX = 608, + detailY = -146, + detailW = 136, + detailH = 230, +} + local MTH_BOOK_CONTENT_LAYOUT_PETABILITIES = { listX = 16, listY = -106, @@ -363,7 +377,52 @@ local MTH_BOOK_CONTENT_LAYOUT_HISTORY = { detailH = 446, } -local function MTH_BOOK_ApplyContentLayoutForMode() +function MTH_BOOK_ConfigureTrainerDetailScroll(isTrainerMode, detailParent, detailText) + if not (detailParent and detailText) then return end + if isTrainerMode then + if not detailParent.mthTrainerDetailScroll then + local scroll = CreateFrame("ScrollFrame", "MTH_BOOK_TrainerDetailScroll", detailParent, "UIPanelScrollFrameTemplate") + scroll:SetPoint("TOPLEFT", detailParent, "TOPLEFT", 4, -4) + scroll:SetPoint("BOTTOMRIGHT", detailParent, "BOTTOMRIGHT", -24, 4) + detailParent.mthTrainerDetailScroll = scroll + detailText:EnableMouseWheel(true) + detailText:SetScript("OnMouseWheel", function() + local current = scroll:GetVerticalScroll() + if arg1 and arg1 > 0 then + scroll:SetVerticalScroll(current - 24) + else + scroll:SetVerticalScroll(current + 24) + end + end) + end + local scroll = detailParent.mthTrainerDetailScroll + detailText.mthTrainerDetailParent = detailParent + detailText.mthTrainerDetailScroll = scroll + if detailText:GetParent() ~= scroll then detailText:SetParent(scroll) end + detailText:ClearAllPoints() + detailText:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0) + scroll:SetScrollChild(detailText) + scroll:SetWidth(detailParent:GetWidth() - 28) + scroll:SetHeight(detailParent:GetHeight() - 8) + detailText:SetWidth(detailParent:GetWidth() - 28) + detailText:SetHeight(scroll:GetHeight()) + scroll:Show() + else + local scroll = detailParent.mthTrainerDetailScroll + if scroll then scroll:Hide() end + if detailText.mthTrainerDetailParent then + detailText:SetParent(detailText.mthTrainerDetailParent) + detailText:ClearAllPoints() + detailText:SetPoint("TOPLEFT", detailParent, "TOPLEFT", 6, -6) + detailText:SetWidth(detailParent:GetWidth() - 12) + detailText:SetHeight(detailParent:GetHeight() - 12) + if detailText.SetTextInsets then detailText:SetTextInsets(4, 4, 4, 4) end + detailText.mthTrainerDetailScroll = nil + end + end +end + +function MTH_BOOK_ApplyContentLayoutForMode() local listParent = getglobal("MTH_BOOK_ListBackdrop") local detailParent = getglobal("MTH_BOOK_DetailBackdrop") local detailText = getglobal("MTH_BOOK_DetailBackdropDetailText") @@ -374,6 +433,9 @@ local function MTH_BOOK_ApplyContentLayoutForMode() local layout = MTH_BOOK_CONTENT_LAYOUT_DEFAULT if MTH_BOOK_STATE.mode == "petabilities" then layout = MTH_BOOK_CONTENT_LAYOUT_PETABILITIES + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + layout = MTH_BOOK_CONTENT_LAYOUT_TRAINERS elseif MTH_BOOK_IsItemMode() then layout = MTH_BOOK_CONTENT_LAYOUT_ITEMS elseif MTH_BOOK_STATE.mode == "npcs" then @@ -411,6 +473,9 @@ local function MTH_BOOK_ApplyContentLayoutForMode() if listSlider then listSlider:SetHeight(sliderHeight) end + local isTrainerMode = type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) + MTH_BOOK_ConfigureTrainerDetailScroll(isTrainerMode, detailParent, detailText) if MTH_BOOK_STATE.petUI then local paneHeight = layout.listH - 8 @@ -477,6 +542,45 @@ local function MTH_BOOK_DropdownCreateInfo() return {} end +function MTH_BOOK_EnsureTrainerStatusDropdown() + local dropdown = getglobal("MTH_BOOK_TrainerStatusDropdown") + if not dropdown then + local filter = getglobal("MTH_BOOK_Filter1") + local parent = filter and filter:GetParent() + if not parent then return nil end + dropdown = CreateFrame("Frame", "MTH_BOOK_TrainerStatusDropdown", parent, "UIDropDownMenuTemplate") + dropdown:SetPoint("TOPLEFT", parent, "TOPLEFT", 150, -116) + UIDropDownMenu_Initialize(dropdown, function() + local options = { + { value = "all", label = "All" }, + { value = "known", label = "Known" }, + { value = "notlearned", label = "Not learned" }, + { value = "notyet", label = "Not yet learnable" }, + } + for i = 1, table.getn(options) do + local option = options[i] + local value = option.value + local label = option.label + local info = MTH_BOOK_DropdownCreateInfo() + info.text = label + info.checked = MTH_BOOK_STATE.quick == value + info.func = function() + MTH_BOOK_STATE.quick = value + MTH_BOOK_STATE.page = 1 + MTH_BOOK_STATE.selectedEntry = nil + MTH_BOOK_DropdownSetText(dropdown, label) + MTH_BOOK_RefreshFilter() + end + UIDropDownMenu_AddButton(info) + end + end) + UIDropDownMenu_SetWidth(136, dropdown) + UIDropDownMenu_JustifyText("LEFT", dropdown) + dropdown:Hide() + end + return dropdown +end + local function MTH_BOOK_SetSliderFromCursor(slider) if not slider then return end local minValue, maxValue = slider:GetMinMaxValues() @@ -1459,6 +1563,12 @@ local function MTH_BOOK_GetSortKey(entry, col) if col == 6 then return MTH_BOOK_SafeLower(tostring(row.abandonReason or "")) end end + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + return type(MTH_BOOKTAB_GetTrainerSpellSortKey) == "function" + and MTH_BOOKTAB_GetTrainerSpellSortKey(entry, col) or nil + end + local items = MTH_BOOK_GetItemsTable() local item = items and items[entry] if not item then return nil end @@ -1514,6 +1624,10 @@ local function MTH_BOOK_DefaultCompare(a, b) if MTH_BOOK_STATE.mode == "npcs" then return MTH_BOOK_NPCSort(a, b) end if MTH_BOOK_STATE.mode == "stable" then return MTH_BOOK_StableSort(a, b) end if MTH_BOOK_STATE.mode == "pethistory" then return MTH_BOOK_PetHistorySort(a, b) end + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + return type(MTH_BOOKTAB_TrainerSpellSort) == "function" and MTH_BOOKTAB_TrainerSpellSort(a, b) or false + end return MTH_BOOK_ItemSort(a, b) end @@ -1782,6 +1896,15 @@ local function MTH_BOOK_BuildResults() return results end + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + if type(MTH_BOOKTAB_BuildTrainerSpellResults) == "function" then + results = MTH_BOOKTAB_BuildTrainerSpellResults() + end + MTH_BOOK_ApplyActiveSort(results) + return results + end + local items = MTH_BOOK_GetItemsTable() if not items then return results end for itemId, item in pairs(items) do @@ -1825,6 +1948,10 @@ local function MTH_BOOK_GetTotalCount() local store = MTH_BOOK_GetPetDatastore() return (type(store) == "table" and type(store.historyById) == "table") and MTH_BOOK_CountMap(store.historyById) or 0 end + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + return type(MTH_BOOKTAB_GetTrainerSpellTotal) == "function" and MTH_BOOKTAB_GetTrainerSpellTotal() or 0 + end local items = MTH_BOOK_GetItemsTable() return items and MTH_BOOK_CountMap(items) or 0 end @@ -2328,7 +2455,9 @@ end local function MTH_BOOK_UpdateQuickFilterControls() MTH_BOOK_UpdateCheckboxLayoutByMode() - if MTH_BOOK_IsItemMode() then + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + MTH_BOOK_STATE.pageSize = 17 + elseif MTH_BOOK_IsItemMode() then MTH_BOOK_STATE.pageSize = 18 elseif MTH_BOOK_STATE.mode == "families" then MTH_BOOK_STATE.pageSize = MTH_BOOK_MAX_ROWS @@ -2350,6 +2479,7 @@ local function MTH_BOOK_UpdateQuickFilterControls() local itemSubtypeDropdown = getglobal("MTH_BOOK_ItemSubtypeDropdown") local npcFunctionDropdown = getglobal("MTH_BOOK_NPCFunctionDropdown") local npcZoneDropdown = getglobal("MTH_BOOK_NPCZoneDropdown") + local trainerStatusDropdown = MTH_BOOK_EnsureTrainerStatusDropdown() local searchLabel = getglobal("MTH_BOOK_SearchLabel") local minLabel = getglobal("MTH_BOOK_MinLabel") local maxLabel = getglobal("MTH_BOOK_MaxLabel") @@ -2359,6 +2489,7 @@ local function MTH_BOOK_UpdateQuickFilterControls() local hideNoAbilities = getglobal("MTH_BOOK_HideNoAbilities") local hideUnknown = getglobal("MTH_BOOK_HideUnknown") local petOnlyMyLevel = getglobal("MTH_BOOK_PetOnlyMyLevel") + local trainerHonoredDiscount = getglobal("MTH_BOOK_TrainerHonoredDiscount") local petInZoneOnly = getglobal("MTH_BOOK_PetInZoneOnly") local npcInZoneOnly = getglobal("MTH_BOOK_NPCInZoneOnly") local showAllOnMapButton = getglobal("MTH_BOOK_ShowAllOnMapButton") @@ -2369,6 +2500,7 @@ local function MTH_BOOK_UpdateQuickFilterControls() local nextButton = getglobal("MTH_BOOK_NextButton") local listSlider = getglobal("MTH_BOOK_ListSlider") local sliderBackdrop = getglobal("MTH_BOOK_ListSliderBackdrop") + if trainerStatusDropdown then trainerStatusDropdown:Hide() end if MTH_BOOK_STATE.mode == "petabilities" or MTH_BOOK_STATE.mode == "stable" then if stats then stats:Hide() end @@ -2543,6 +2675,59 @@ local function MTH_BOOK_UpdateQuickFilterControls() resetButton:ClearAllPoints() resetButton:SetPoint("TOPLEFT", resetButton:GetParent(), "TOPLEFT", 546, -80) end + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + if searchLabel then searchLabel:Show() end + if minLabel then minLabel:Show() end + if maxLabel then maxLabel:Show() end + if search then search:Show(); search:ClearAllPoints(); search:SetPoint("TOPLEFT", search:GetParent(), "TOPLEFT", 66, -80); search:SetWidth(160) end + if minLevel then minLevel:Show(); minLevel:ClearAllPoints(); minLevel:SetPoint("TOPLEFT", minLevel:GetParent(), "TOPLEFT", 302, -80) end + if maxLevel then maxLevel:Show(); maxLevel:ClearAllPoints(); maxLevel:SetPoint("TOPLEFT", maxLevel:GetParent(), "TOPLEFT", 414, -80) end + if applyButton then applyButton:Show(); applyButton:ClearAllPoints(); applyButton:SetPoint("TOPLEFT", applyButton:GetParent(), "TOPLEFT", 478, -80) end + local resetButton = getglobal("MTH_BOOK_ResetButton") + if resetButton then resetButton:Show(); resetButton:ClearAllPoints(); resetButton:SetPoint("TOPLEFT", resetButton:GetParent(), "TOPLEFT", 546, -80) end + if scanButton then scanButton:Hide() end + for i = 1, 6 do + local btn = getglobal("MTH_BOOK_Filter" .. i) + if btn then btn:Hide() end + end + if trainerStatusDropdown then + local labels = { all = "All", known = "Known", notlearned = "Not learned", notyet = "Not yet learnable" } + trainerStatusDropdown:ClearAllPoints() + trainerStatusDropdown:SetPoint("TOPLEFT", trainerStatusDropdown:GetParent(), "TOPLEFT", 150, -116) + MTH_BOOK_DropdownSetText(trainerStatusDropdown, labels[MTH_BOOK_STATE.quick] or "All") + trainerStatusDropdown:Show() + end + if familyDropdown then familyDropdown:Hide() end + if abilityDropdown then abilityDropdown:Hide() end + if rankDropdown then rankDropdown:Hide() end + if petLearnSourceDropdown then petLearnSourceDropdown:Hide() end + if beastZoneLabel then beastZoneLabel:Hide() end + if beastContinentDropdown then beastContinentDropdown:Hide() end + if beastZoneDropdown then beastZoneDropdown:Hide() end + if itemSubtypeDropdown then itemSubtypeDropdown:Hide() end + if npcFunctionDropdown then npcFunctionDropdown:Hide() end + if npcZoneDropdown then npcZoneDropdown:Hide() end + if hideNoAbilities then hideNoAbilities:Hide() end + if hideUnknown then hideUnknown:Hide() end + if petOnlyMyLevel then petOnlyMyLevel:Hide() end + if trainerHonoredDiscount then + trainerHonoredDiscount:ClearAllPoints() + trainerHonoredDiscount:SetPoint("TOPLEFT", trainerHonoredDiscount:GetParent(), "TOPLEFT", 18, -110) + trainerHonoredDiscount:SetChecked(MTH_BOOK_STATE.trainerHonoredDiscount and 1 or nil) + local trainerHonoredDiscountText = getglobal("MTH_BOOK_TrainerHonoredDiscountText") + if trainerHonoredDiscountText then trainerHonoredDiscountText:SetText("Honored prices") end + trainerHonoredDiscount:Show() + end + if petInZoneOnly then petInZoneOnly:Hide() end + if npcInZoneOnly then npcInZoneOnly:Hide() end + if showAllOnMapButton then showAllOnMapButton:Hide() end + local requireVendor = getglobal("MTH_BOOK_RequireVendor") + local requireDrop = getglobal("MTH_BOOK_RequireDrop") + local requireObject = getglobal("MTH_BOOK_RequireObject") + if requireVendor then requireVendor:Hide() end + if requireDrop then requireDrop:Hide() end + if requireObject then requireObject:Hide() end elseif MTH_BOOK_STATE.mode == "npcs" then if searchLabel then searchLabel:Show() end if minLabel then minLabel:Hide() end @@ -2768,6 +2953,9 @@ local function MTH_BOOK_UpdateQuickFilterControls() if MTH_BOOK_STATE.mode ~= "petabilities" and not MTH_BOOK_IsItemMode() and petOnlyMyLevel then petOnlyMyLevel:Hide() end + if (type(MTH_BOOKTAB_IsTrainerSpellMode) ~= "function" or not MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode)) and trainerHonoredDiscount then + trainerHonoredDiscount:Hide() + end if MTH_BOOK_STATE.mode ~= "pets" and petInZoneOnly then petInZoneOnly:Hide() end @@ -3270,13 +3458,20 @@ end function MTH_BOOK_SetDetailText(detail, text) if not detail then return end local wrapped = tostring(text or "") - if MTH_BOOK_STATE.mode ~= "pethistory" and MTH_BOOK_STATE.mode ~= "pets" and MTH_BOOK_STATE.mode ~= "abilities" then + if MTH_BOOK_STATE.mode ~= "pethistory" and MTH_BOOK_STATE.mode ~= "pets" and MTH_BOOK_STATE.mode ~= "abilities" + and (type(MTH_BOOKTAB_IsTrainerSpellMode) ~= "function" or not MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode)) then wrapped = MTH_BOOK_WrapDetailText(wrapped) end detail._mthLocking = true detail:SetText(wrapped) detail._mthLockedText = wrapped detail._mthLocking = false + local scroll = detail.mthTrainerDetailScroll + if scroll then + local textHeight = detail.GetStringHeight and detail:GetStringHeight() or 0 + detail:SetHeight(math.max(scroll:GetHeight(), textHeight + 12)) + scroll:SetVerticalScroll(0) + end end function MTH_BOOK_ConfigureReadOnlyEditBox(detail) @@ -3323,6 +3518,9 @@ end function MTH_BOOK_UpdateDetail() local detail = getglobal("MTH_BOOK_DetailBackdropDetailText") if not detail then return end + if detail.mthTrainerIcon then detail.mthTrainerIcon:Hide() end + local detailParent = detail:GetParent() + if detailParent and detailParent.mthTrainerPlan then detailParent.mthTrainerPlan:Hide() end MTH_BOOK_HideDetailTitle() -- Hide model skin preview when not in a beast-list mode if MTH_BOOK_STATE.mode ~= "pets" and MTH_BOOK_STATE.mode ~= "abilities" then @@ -3330,6 +3528,11 @@ function MTH_BOOK_UpdateDetail() end local petTop = MTH_BOOK_STATE.petUI and MTH_BOOK_STATE.petUI.detailTop local petBottom = MTH_BOOK_STATE.petUI and MTH_BOOK_STATE.petUI.detailBottom + if type(MTH_BOOKTAB_UpdateTrainerSpellDetail) == "function" + and MTH_BOOKTAB_UpdateTrainerSpellDetail(detail, MTH_BOOK_STATE.selectedEntry) then + MTH_BOOK_UpdateOpenMapButton() + return + end if not MTH_BOOK_STATE.selectedEntry then if MTH_BOOK_ModelViewer_Clear then MTH_BOOK_ModelViewer_Clear() end if MTH_BOOK_STATE.mode == "petabilities" then @@ -4031,6 +4234,12 @@ local function MTH_BOOK_ApplyColumnLayout(parent) end local function MTH_BOOK_GetRowValues(entry) + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + return type(MTH_BOOKTAB_GetTrainerSpellRowValues) == "function" + and MTH_BOOKTAB_GetTrainerSpellRowValues(entry) or { "", "", "", "", "" } + end + if MTH_BOOK_STATE.mode == "pets" then -- MetaHunt DB entry local beast = MTH_DS_Beasts and MTH_DS_Beasts[entry] @@ -4317,6 +4526,9 @@ local function MTH_BOOK_AssignEntry(btn, entry) btn.entry = entry elseif MTH_BOOK_STATE.mode == "pethistory" then btn.entry = { petId = entry } + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + btn.entry = entry else btn.entry = { itemId = entry } end @@ -4482,11 +4694,18 @@ MTH_BOOK_UpdateResults = function() btn.itemIcon:SetTexture(nil) btn.itemIcon:Hide() end + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + local iconPath = MTH_BOOK_ResolveIconPath(entry.icon) + btn.itemIcon:ClearAllPoints() + btn.itemIcon:SetPoint("LEFT", btn, "LEFT", 8, 0) + btn.itemIcon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark") + btn.itemIcon:Show() else btn.itemIcon:SetTexture(nil) btn.itemIcon:Hide() end end + if btn.trainerCoinIcon then btn.trainerCoinIcon:Hide() end if btn.familyAbilityButtons then local abilityEntries = nil local abilityColIndex = nil @@ -4572,6 +4791,7 @@ MTH_BOOK_UpdateResults = function() btn.itemIcon:SetTexture(nil) btn.itemIcon:Hide() end + if btn.trainerCoinIcon then btn.trainerCoinIcon:Hide() end if btn.familyAbilityButtons then for chipIndex = 1, table.getn(btn.familyAbilityButtons) do local chip = btn.familyAbilityButtons[chipIndex] @@ -4628,6 +4848,11 @@ local function MTH_BOOK_UpdateModeLabels() check1 = "" check2 = "" check3 = "" + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + filterNames = { "All", "Known", "Not learned", "Not yet learnable", "", "" } + check1 = "" + check2 = "" + check3 = "" elseif MTH_BOOK_STATE.mode == "npcs" then filterNames = { "", "", "", "", "", "" } check1 = "Alliance" @@ -4769,6 +4994,9 @@ local function MTH_BOOK_SetQuickFilter(index) MTH_BOOK_STATE.quick = "all" elseif MTH_BOOK_STATE.mode == "petabilities" then MTH_BOOK_STATE.quick = "all" + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + local map = { [1] = "all", [2] = "known", [3] = "notlearned", [4] = "notyet" } + MTH_BOOK_STATE.quick = map[index] or "all" elseif MTH_BOOK_STATE.mode == "npcs" then MTH_BOOK_STATE.quick = "all" elseif MTH_BOOK_STATE.mode == "abilities" then @@ -4786,10 +5014,94 @@ end local function MTH_BOOK_StyleSectionTab(button) if not button then return end - button:SetHeight(24) + button:SetHeight(26) + button:SetNormalTexture(nil) + button:SetPushedTexture(nil) + button:SetHighlightTexture(nil) + button:SetDisabledTexture(nil) + if not button.mthMenuBackground then + button.mthMenuBackground = button:CreateTexture(nil, "BACKGROUND") + button.mthMenuBackground:SetAllPoints(button) + button.mthMenuBackground:SetTexture("Interface\\Buttons\\WHITE8X8") + end + local regions = { button:GetRegions() } + for _, region in ipairs(regions) do + if region ~= button.mthMenuBackground and region.GetObjectType + and region:GetObjectType() == "Texture" then + region:Hide() + end + end + button:SetScript("OnEnter", function() + if not this.mthSelected then + this.mthMenuBackground:SetVertexColor(0.20, 0.17, 0.06, 1.00) + end + end) + button:SetScript("OnLeave", function() + if not this.mthSelected then + this.mthMenuBackground:SetVertexColor(0.10, 0.10, 0.10, 1.00) + end + end) button:SetScript("OnShow", nil) end +function MTH_BOOK_EnsureVerticalNavigation(frame) + if not frame then return end + local contentPane = getglobal("MTH_BOOK_ContentPane") + if not contentPane then + contentPane = CreateFrame("Frame", "MTH_BOOK_ContentPane", frame) + contentPane:SetPoint("TOPLEFT", frame, "TOPLEFT", 112, 0) + contentPane:SetWidth(760) + contentPane:SetHeight(560) + + local navBackdrop = CreateFrame("Frame", "MTH_BOOK_NavBackdrop", frame) + navBackdrop:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -42) + navBackdrop:SetWidth(100) + navBackdrop:SetHeight(476) + navBackdrop:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + navBackdrop:SetBackdropColor(0.07, 0.07, 0.07, 1.00) + navBackdrop:SetBackdropBorderColor(0.28, 0.28, 0.28, 1.00) + navBackdrop:SetFrameLevel(1) + + local contentChildren = { + "StatsText", "SearchLabel", "MinLabel", "MaxLabel", "BeastZoneLabel", + "ListBackdrop", "DetailBackdrop", "OpenMapButton", "TopAreaBackdrop", + "Search", "MinLevel", "MaxLevel", "ApplyButton", "PetLearnSourceDropdown", + "ItemSubtypeDropdown", "PetOnlyMyLevel", "PetBookScanButton", "ResetButton", "TrainerHonoredDiscount", + "Filter1", "Filter2", "Filter3", "Filter4", "Filter5", "Filter6", + "FamilyDropdown", "AbilityDropdown", "RankDropdown", "PetInZoneOnly", + "ShowAllOnMapButton", "NPCFunctionDropdown", "NPCInZoneOnly", "RequireVendor", + "RequireDrop", "RequireObject", "HideNoAbilities", "HideUnknown", + "BeastContinentDropdown", "BeastZoneDropdown", "ListSliderBackdrop", "ListSlider", + "PrevButton", "NextButton", + } + for _, suffix in ipairs(contentChildren) do + local child = getglobal("MTH_BOOK_" .. suffix) + if child and child:GetParent() == frame then + local point, relativeTo, relativePoint, x, y = child:GetPoint(1) + child:SetParent(contentPane) + if relativeTo == frame then + child:ClearAllPoints() + child:SetPoint(point, contentPane, relativePoint, x, y) + end + end + end + end + + local topAreaBackdrop = getglobal("MTH_BOOK_TopAreaBackdrop") + if topAreaBackdrop then + topAreaBackdrop:ClearAllPoints() + topAreaBackdrop:SetPoint("TOPLEFT", contentPane, "TOPLEFT", 0, -44) + topAreaBackdrop:SetWidth(760) + end +end + local function MTH_BOOK_UpdateSectionTabs() local sectionPets = getglobal("MTH_BOOK_SectionPets") local sectionFamilies = getglobal("MTH_BOOK_SectionFamilies") @@ -4800,13 +5112,18 @@ local function MTH_BOOK_UpdateSectionTabs() local sectionNPCs = getglobal("MTH_BOOK_SectionNPCs") local sectionStable = getglobal("MTH_BOOK_SectionStable") local sectionPetHistory = getglobal("MTH_BOOK_SectionPetHistory") + local sectionHunterSpells = getglobal("MTH_BOOK_SectionHunterSpells") + local sectionPetSpells = getglobal("MTH_BOOK_SectionPetSpells") local function setState(button, selected) if not button then return end + button.mthSelected = selected and true or false if selected then - PanelTemplates_SelectTab(button) + button.mthMenuBackground:SetVertexColor(0.38, 0.28, 0.05, 1.00) + button:SetTextColor(1.00, 0.82, 0.00) else - PanelTemplates_DeselectTab(button) + button.mthMenuBackground:SetVertexColor(0.10, 0.10, 0.10, 1.00) + button:SetTextColor(0.90, 0.90, 0.90) end end @@ -4819,6 +5136,8 @@ local function MTH_BOOK_UpdateSectionTabs() setState(sectionNPCs, MTH_BOOK_STATE.mode == "npcs") setState(sectionStable, MTH_BOOK_STATE.mode == "stable") setState(sectionPetHistory, MTH_BOOK_STATE.mode == "pethistory") + setState(sectionHunterSpells, MTH_BOOK_STATE.mode == "hunterspells") + setState(sectionPetSpells, MTH_BOOK_STATE.mode == "petspells") end local function MTH_BOOK_LayoutSectionTabs() @@ -4832,40 +5151,23 @@ local function MTH_BOOK_LayoutSectionTabs() local sectionNPCs = getglobal("MTH_BOOK_SectionNPCs") local sectionStable = getglobal("MTH_BOOK_SectionStable") local sectionPetHistory = getglobal("MTH_BOOK_SectionPetHistory") + local sectionHunterSpells = getglobal("MTH_BOOK_SectionHunterSpells") + local sectionPetSpells = getglobal("MTH_BOOK_SectionPetSpells") if not (tabBar and sectionPets and sectionFamilies and sectionPetAbilities and sectionItems and sectionNPCs and sectionProjectiles and sectionAmmoBags) then return end - sectionPets:ClearAllPoints() - sectionPets:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0) - - sectionPetAbilities:ClearAllPoints() - sectionPetAbilities:SetPoint("LEFT", sectionPets, "RIGHT", 2, 0) - - sectionFamilies:ClearAllPoints() - sectionFamilies:SetPoint("LEFT", sectionPetAbilities, "RIGHT", 2, 0) - - sectionNPCs:ClearAllPoints() - sectionNPCs:SetPoint("LEFT", sectionFamilies, "RIGHT", 2, 0) - - sectionItems:ClearAllPoints() - sectionItems:SetPoint("LEFT", sectionNPCs, "RIGHT", 2, 0) - - sectionProjectiles:ClearAllPoints() - sectionProjectiles:SetPoint("LEFT", sectionItems, "RIGHT", 2, 0) - - sectionAmmoBags:ClearAllPoints() - sectionAmmoBags:SetPoint("LEFT", sectionProjectiles, "RIGHT", 2, 0) - - if sectionStable then - sectionStable:ClearAllPoints() - sectionStable:SetPoint("LEFT", sectionAmmoBags, "RIGHT", 2, 0) - end - - if sectionPetHistory then - sectionPetHistory:ClearAllPoints() - if sectionStable then - sectionPetHistory:SetPoint("LEFT", sectionStable, "RIGHT", 2, 0) - else - sectionPetHistory:SetPoint("LEFT", sectionAmmoBags, "RIGHT", 2, 0) + local sections = { + sectionPets, sectionPetAbilities, sectionFamilies, sectionNPCs, sectionItems, + sectionProjectiles, sectionAmmoBags, sectionStable, sectionPetHistory, + sectionHunterSpells, sectionPetSpells, + } + local y = 0 + for _, section in ipairs(sections) do + if section then + section:ClearAllPoints() + section:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, y) + section:SetWidth(96) + section:SetHeight(26) + y = y - 28 end end end @@ -5304,6 +5606,7 @@ local function MTH_BOOK_WireUI(frame) frame:SetBackdropColor(0.05, 0.05, 0.05, 1.00) frame:SetBackdropBorderColor(0.35, 0.35, 0.35, 1.00) end + MTH_BOOK_EnsureVerticalNavigation(frame) local closeButton = getglobal("MTH_BOOK_CloseButton") local applyButton = getglobal("MTH_BOOK_ApplyButton") @@ -5317,6 +5620,9 @@ local function MTH_BOOK_WireUI(frame) local sectionNPCs = getglobal("MTH_BOOK_SectionNPCs") local sectionStable = getglobal("MTH_BOOK_SectionStable") local sectionPetHistory = getglobal("MTH_BOOK_SectionPetHistory") + local sectionHunterSpells = getglobal("MTH_BOOK_SectionHunterSpells") + local sectionPetSpells = getglobal("MTH_BOOK_SectionPetSpells") + local trainerHonoredDiscount = getglobal("MTH_BOOK_TrainerHonoredDiscount") local requireVendor = getglobal("MTH_BOOK_RequireVendor") local requireDrop = getglobal("MTH_BOOK_RequireDrop") local requireObject = getglobal("MTH_BOOK_RequireObject") @@ -5358,6 +5664,14 @@ local function MTH_BOOK_WireUI(frame) if sectionNPCs then sectionNPCs:SetScript("OnClick", function() MTH_BOOK_SetMode("npcs") end) end if sectionStable then sectionStable:SetScript("OnClick", function() MTH_BOOK_SetMode("stable") end) end if sectionPetHistory then sectionPetHistory:SetScript("OnClick", function() MTH_BOOK_SetMode("pethistory") end) end + if sectionHunterSpells then sectionHunterSpells:SetScript("OnClick", function() MTH_BOOK_SetMode("hunterspells") end) end + if sectionPetSpells then sectionPetSpells:SetScript("OnClick", function() MTH_BOOK_SetMode("petspells") end) end + if trainerHonoredDiscount then + trainerHonoredDiscount:SetScript("OnClick", function() + MTH_BOOK_STATE.trainerHonoredDiscount = this:GetChecked() == 1 + MTH_BOOK_UpdateResults() + end) + end MTH_BOOK_StyleSectionTab(sectionPets) MTH_BOOK_StyleSectionTab(sectionFamilies) @@ -5368,7 +5682,10 @@ local function MTH_BOOK_WireUI(frame) MTH_BOOK_StyleSectionTab(sectionNPCs) MTH_BOOK_StyleSectionTab(sectionStable) MTH_BOOK_StyleSectionTab(sectionPetHistory) + MTH_BOOK_StyleSectionTab(sectionHunterSpells) + MTH_BOOK_StyleSectionTab(sectionPetSpells) MTH_BOOK_LayoutSectionTabs() + MTH_BOOK_ApplyContentLayoutForMode() for i = 1, 6 do local filterBtn = getglobal("MTH_BOOK_Filter" .. i) @@ -5384,6 +5701,7 @@ local function MTH_BOOK_WireUI(frame) MTH_BOOK_InitPetLearnSourceDropdown() MTH_BOOK_InitItemSubtypeDropdown() MTH_BOOK_InitNPCFunctionDropdown() + MTH_BOOK_EnsureTrainerStatusDropdown() if petBookScanButton then petBookScanButton:SetScript("OnClick", function() diff --git a/api/hunterbook.xml b/api/hunterbook.xml index 1126347..7da9855 100644 --- a/api/hunterbook.xml +++ b/api/hunterbook.xml @@ -2,7 +2,7 @@ ..\FrameXML\UI.xsd"> - + @@ -128,7 +128,7 @@ - + @@ -188,6 +188,18 @@ + + @@ -235,6 +247,12 @@ + +