From 1680c6fc515ed9c6cb9e86514fe783f3ff0f502e Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 17 Sep 2026 10:40:38 +0200 Subject: [PATCH 01/14] Route feed-tracking debug to the /mth err window --- api/core-feed-tracking.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core-feed-tracking.lua b/api/core-feed-tracking.lua index 95871df..24967f0 100644 --- a/api/core-feed-tracking.lua +++ b/api/core-feed-tracking.lua @@ -30,8 +30,8 @@ MTH_FEED_TRACE = false local function MTH_FEED_Trace(message) if not MTH_FEED_TRACE then return end - if type(DEFAULT_CHAT_FRAME) == "table" and DEFAULT_CHAT_FRAME.AddMessage then - DEFAULT_CHAT_FRAME:AddMessage("|cff88aaff[MTH-FeedTrack]|r " .. tostring(message)) + if MTH_DebugFrame and type(MTH_DebugFrame.AddInfo) == "function" then + MTH_DebugFrame:AddInfo("[MTH-FeedTrack] " .. tostring(message)) end end From b40f9c1095734cc1f5e3c5debbb856a5ede5519b Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 17 Sep 2026 10:40:38 +0200 Subject: [PATCH 02/14] Stop FOM feeding below-minimum-level food to high-level pets --- modules/feedomatic/FeedOMatic.lua | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/feedomatic/FeedOMatic.lua b/modules/feedomatic/FeedOMatic.lua index 322fc92..f7c8705 100644 --- a/modules/feedomatic/FeedOMatic.lua +++ b/modules/feedomatic/FeedOMatic.lua @@ -2483,10 +2483,20 @@ function FOM_FlatFoodList() local isExceptionReject = false; local isLevelOverrideReject = not FOM_IsItemPetLevelCompatible(itemID, petLevel); local isQuarantineReject = FOM_IsQuarantined(itemID, petLevel, familyKey); + -- A food whose known level is below the pet's minimum is refused by the + -- server ("not high enough"); skip it up front so FOM never keeps + -- re-feeding a too-low stack just to free a bag slot. + local isLevelRuleReject = false; + if (petLevel > 0 and tonumber(foodLevel) ~= nil and type(MTH_FEED_GetDerivedMinFoodLevel) == "function") then + local minFoodLevel = MTH_FEED_GetDerivedMinFoodLevel(petLevel); + if (minFoodLevel ~= nil and tonumber(foodLevel) < minFoodLevel) then + isLevelRuleReject = true; + end + end if (isExceptionReject) then rejectedByException = rejectedByException + 1; end - if (isLevelOverrideReject) then + if (isLevelOverrideReject or isLevelRuleReject) then rejectedByLevelOverride = rejectedByLevelOverride + 1; end if (isQuarantineReject) then @@ -2500,7 +2510,7 @@ function FOM_FlatFoodList() unknownLevelCandidates = unknownLevelCandidates + 1; end - if (isCompatible and not isExceptionReject and not isLevelOverrideReject and not isQuarantineReject) then + if (isCompatible and not isExceptionReject and not isLevelOverrideReject and not isQuarantineReject and not isLevelRuleReject) then compatibleCount = compatibleCount + 1; local foodQuality = tonumber(foodLevel); if (foodQuality == nil) then @@ -2511,7 +2521,7 @@ function FOM_FlatFoodList() end end table.insert(foodList, {bag=bagNum, slot=itemNum, link=itemLink, itemId=itemID, count=itemCount, quality=foodQuality, useful=isUseful, temp=FOM_IsTemporaryFood(itemLink), knownLevel=hasKnownLevel}); - elseif (isAbovePreferredLevel and not isExceptionReject and not isLevelOverrideReject and not isQuarantineReject) then + elseif (isAbovePreferredLevel and not isExceptionReject and not isLevelOverrideReject and not isQuarantineReject and not isLevelRuleReject) then overflowCompatibleCount = overflowCompatibleCount + 1; local overflowQuality = tonumber(foodLevel); if (overflowQuality == nil) then From c35258e6153a856086a7a171504f34db746a1540 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 17 Sep 2026 11:25:53 +0200 Subject: [PATCH 03/14] Reuse best-matching pet record on stable scan to stop shell duplication --- api/core-scan-stablemaster.lua | 51 ++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/api/core-scan-stablemaster.lua b/api/core-scan-stablemaster.lua index 766710e..b2bd6f0 100644 --- a/api/core-scan-stablemaster.lua +++ b/api/core-scan-stablemaster.lua @@ -2138,16 +2138,45 @@ local function MTH_PETS_SelectPetIdBySnapshot(store, snapshot, previousPetId) if snapshot.signature and snapshot.signature ~= "" then local list = store.signatureIndex[snapshot.signature] if type(list) == "table" then - local count = 0 - local candidateId = nil + local liveIds = {} for _, listedId in pairs(list) do - if listedId and store.activeById[listedId] then - count = count + 1 - candidateId = listedId + if listedId and store.activeById[tostring(listedId)] then + table.insert(liveIds, tostring(listedId)) end end - if count == 1 then - return candidateId + local liveCount = table.getn(liveIds) + if liveCount == 1 then + return liveIds[1] + elseif liveCount > 1 then + -- Several records share this name+family (earlier fragmentation, or two + -- pets with the same name). Reuse the best existing match instead of + -- minting yet another empty shell that only worsens the fragmentation: + -- require a level match, then prefer loyalty match and records that + -- already carry tame/spellbook history, then most recently seen. + local snapLevel = tonumber(snapshot.level) + local snapLoyalty = tonumber(snapshot.loyaltyLevel) + local bestId = nil + local bestScore = -1 + local bestSeen = -1 + for i = 1, liveCount do + local rowId = liveIds[i] + local row = store.activeById[rowId] + if type(row) == "table" and snapLevel and tonumber(row.level) == snapLevel then + local score = 0 + if snapLoyalty and tonumber(row.loyaltyLevel) == snapLoyalty then score = score + 2 end + if MTH_PETS_RowHasAnyTameMetadata(row) then score = score + 1 end + if type(row.petSpellbook) == "table" then score = score + 1 end + local seen = tonumber(row.lastSeen) or tonumber(row.lastUpdated) or 0 + if score > bestScore or (score == bestScore and seen > bestSeen) then + bestScore = score + bestSeen = seen + bestId = rowId + end + end + end + if bestId then + return bestId + end end end end @@ -2364,10 +2393,14 @@ local function MTH_PETS_RecordStableSlot(pets, slot, raw1, raw2, raw3, raw4, raw local snapshotGuid = snapshot and snapshot.guid or nil local signatureMatches = (existingSignature and snapshotSignature and existingSignature == snapshotSignature) and true or false local guidMatches = (existingGuid and snapshotGuid and existingGuid ~= "" and existingGuid == snapshotGuid) and true or false + -- An empty shell must not lock the slot: a richer same-signature record may + -- exist that we should re-adopt instead (self-heals earlier fragmentation). + local existingHasData = MTH_PETS_RowHasAnyTameMetadata(existingRow) + or type(existingRow.petSpellbook) == "table" or existingRow.petNumber ~= nil - if signatureMatches or guidMatches then + if guidMatches or (signatureMatches and existingHasData) then petId = existingId - else + elseif not signatureMatches then MTH_PETS_LogConsistency("RecordStableSlot remapping stale slot=" .. tostring(slotNumber) .. " existingId=" .. tostring(existingId) .. " existingSignature='" .. tostring(existingSignature or "") .. "'" From 60171b559e6cfd3a31236f331ba3520544025b3a Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Fri, 2 Oct 2026 21:08:45 +0200 Subject: [PATCH 04/14] Beast Lore: colour abilities by known status and add known/unknown filter --- api/hunterbook-tab-beastlore.lua | 46 ++++++++++++++++++++++++ api/hunterbook.lua | 60 +++++++++++++++++++++++++++++++- api/hunterbook.xml | 9 +++-- 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/api/hunterbook-tab-beastlore.lua b/api/hunterbook-tab-beastlore.lua index 4a2fb89..b682392 100644 --- a/api/hunterbook-tab-beastlore.lua +++ b/api/hunterbook-tab-beastlore.lua @@ -24,6 +24,33 @@ function MTH_BOOK_GetBeastAbilitiesSummary(beast) return tostring(beast.abilities) end +-- Like the summary, but colours each ability green when the hunter already knows +-- that exact rank and red when it is still unlearned, to spot tameable upgrades. +function MTH_BOOK_GetBeastAbilitiesColored(beast) + if not beast or not beast.abilities or beast.abilities == "" or beast.abilities == "None" then + return "-" + end + local tokens = MTH_BOOK_SplitAbilities(beast.abilities) + if table.getn(tokens) == 0 then + return "-" + end + local canCheck = type(MTH_IsPetAbilityKnownByHunter) == "function" + local parts = {} + for i = 1, table.getn(tokens) do + local text = tostring(tokens[i]) + if canCheck then + local _, label, rank = MTH_BOOK_ParseAbilityToken(tokens[i]) + if MTH_IsPetAbilityKnownByHunter(label, rank) then + text = "|cff40ff40" .. text .. "|r" + else + text = "|cffff4040" .. text .. "|r" + end + end + table.insert(parts, text) + end + return table.concat(parts, ", ") +end + function MTH_BOOK_GetBeastZoneSummary(beast) if not beast or not beast.coords or table.getn(beast.coords) == 0 then return "-" @@ -282,6 +309,22 @@ function MTH_BOOK_ParseBeastTraits(beast) return traits end +-- "known" keeps beasts with at least one ability the hunter already knows; +-- "unknown" keeps beasts that can still teach something not yet learned. +function MTH_BOOK_BeastMatchesAbilityKnowledge(beast, mode) + if type(MTH_IsPetAbilityKnownByHunter) ~= "function" then return true end + local tokens = MTH_BOOK_SplitAbilities(beast and beast.abilities) + local n = table.getn(tokens) + if n == 0 then return false end + for i = 1, n do + local _, label, rank = MTH_BOOK_ParseAbilityToken(tokens[i]) + local known = MTH_IsPetAbilityKnownByHunter(label, rank) + if mode == "known" and known then return true end + if mode == "unknown" and not known then return true end + end + return false +end + function MTH_BOOK_BeastMatches(beastId, beast) if not beast then return false end local forcedBeastId = MTH_BOOK_STATE and tonumber(MTH_BOOK_STATE.forcedBeastId) or nil @@ -305,6 +348,9 @@ function MTH_BOOK_BeastMatches(beastId, beast) if not MTH_BOOK_HasKnownLevel(beast) then return false end if not MTH_BOOK_HasKnownZone(beast) then return false end end + if MTH_BOOK_STATE.petKnownFilter and MTH_BOOK_STATE.petKnownFilter ~= "all" then + if not MTH_BOOK_BeastMatchesAbilityKnowledge(beast, MTH_BOOK_STATE.petKnownFilter) then return false end + end if MTH_BOOK_STATE.quick ~= "all" then if familyLower ~= MTH_BOOK_STATE.quick then diff --git a/api/hunterbook.lua b/api/hunterbook.lua index 9f4c3b5..ec98403 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -137,6 +137,7 @@ local MTH_BOOK_STATE = { familyStatsPercent = false, petHideNoAbilities = true, petHideUnknown = true, + petKnownFilter = "all", trainerHonoredDiscount = false, forcedBeastId = nil, page = 1, @@ -2336,6 +2337,28 @@ local function MTH_BOOK_UpdateRankDropdownText() MTH_BOOK_DropdownSetText(rankDropdown, label) end +MTH_BOOK_PetKnownFilterOptions = { + { value = "all", label = "Known/Unknown" }, + { value = "known", label = "Known Abilities" }, + { value = "unknown", label = "Unknown Abilities" }, +} + +local function MTH_BOOK_GetKnownFilterLabel(value) + for i = 1, table.getn(MTH_BOOK_PetKnownFilterOptions) do + local option = MTH_BOOK_PetKnownFilterOptions[i] + if option and option.value == value then + return option.label + end + end + return "Known/Unknown" +end + +local function MTH_BOOK_UpdateKnownFilterDropdownText() + local dropdown = getglobal("MTH_BOOK_KnownFilterDropdown") + if not dropdown then return end + MTH_BOOK_DropdownSetText(dropdown, MTH_BOOK_GetKnownFilterLabel(MTH_BOOK_STATE.petKnownFilter or "all")) +end + local function MTH_BOOK_UpdatePetLearnSourceDropdownText() local sourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown") if not sourceDropdown then return end @@ -2483,6 +2506,8 @@ local function MTH_BOOK_UpdateQuickFilterControls() local petInZoneOnly = getglobal("MTH_BOOK_PetInZoneOnly") local npcInZoneOnly = getglobal("MTH_BOOK_NPCInZoneOnly") local showAllOnMapButton = getglobal("MTH_BOOK_ShowAllOnMapButton") + local knownFilterDropdown = getglobal("MTH_BOOK_KnownFilterDropdown") + if knownFilterDropdown then knownFilterDropdown:Hide() end local minLevel = getglobal("MTH_BOOK_MinLevel") local maxLevel = getglobal("MTH_BOOK_MaxLevel") local stats = getglobal("MTH_BOOK_StatsText") @@ -2577,6 +2602,10 @@ local function MTH_BOOK_UpdateQuickFilterControls() rankDropdown:Hide() end end + if knownFilterDropdown then + knownFilterDropdown:Show() + MTH_BOOK_UpdateKnownFilterDropdownText() + end if petInZoneOnly then petInZoneOnly:Show() petInZoneOnly:SetChecked(MTH_BOOK_STATE.petInZoneOnly and 1 or nil) @@ -3063,6 +3092,33 @@ local function MTH_BOOK_InitRankDropdown() MTH_BOOK_UpdateRankDropdownText() end +local function MTH_BOOK_InitKnownFilterDropdown() + local dropdown = getglobal("MTH_BOOK_KnownFilterDropdown") + if not dropdown then return end + + UIDropDownMenu_Initialize(dropdown, function() + for i = 1, table.getn(MTH_BOOK_PetKnownFilterOptions) do + local option = MTH_BOOK_PetKnownFilterOptions[i] + local value = option.value + local label = option.label + local info = MTH_BOOK_DropdownCreateInfo() + info.text = label + info.func = function() + MTH_BOOK_STATE.petKnownFilter = value + MTH_BOOK_STATE.page = 1 + MTH_BOOK_STATE.selectedEntry = nil + MTH_BOOK_DropdownSetText(dropdown, label) + MTH_BOOK_RefreshFilter() + end + info.checked = ((MTH_BOOK_STATE.petKnownFilter or "all") == value) + UIDropDownMenu_AddButton(info) + end + end) + + UIDropDownMenu_SetWidth(140, dropdown) + UIDropDownMenu_JustifyText("LEFT", dropdown) + MTH_BOOK_UpdateKnownFilterDropdownText() +end local function MTH_BOOK_InitPetLearnSourceDropdown() local sourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown") @@ -4236,7 +4292,7 @@ local function MTH_BOOK_GetRowValues(entry) if not beast then return { "", "", "", "", "", "", "", "", "", "" } end local beastDisplayName = (MTH and MTH.GetLocalizedBeastName and MTH:GetLocalizedBeastName(entry, beast.name)) or beast.name local traits = MTH_BOOK_ParseBeastTraits(beast) - local abilities = MTH_BOOK_GetBeastAbilitiesSummary(beast) + local abilities = MTH_BOOK_GetBeastAbilitiesColored(beast) local zone = MTH_BOOK_GetBeastZoneSummary(beast) return { "|cFF33CCFF" .. tostring(entry) .. "|r", @@ -5209,6 +5265,7 @@ local function MTH_BOOK_ResetStateForMode(mode) MTH_BOOK_STATE.npcHideNoZone = true MTH_BOOK_STATE.petHideNoAbilities = false MTH_BOOK_STATE.petHideUnknown = false + MTH_BOOK_STATE.petKnownFilter = "all" MTH_BOOK_STATE.forcedBeastId = nil MTH_BOOK_STATE.petLeftOffset = 0 @@ -5687,6 +5744,7 @@ local function MTH_BOOK_WireUI(frame) MTH_BOOK_InitFamilyDropdown() MTH_BOOK_InitAbilityDropdown() MTH_BOOK_InitRankDropdown() + MTH_BOOK_InitKnownFilterDropdown() MTH_BOOK_WireBeastDropdowns() MTH_BOOK_InitPetLearnSourceDropdown() MTH_BOOK_InitItemSubtypeDropdown() diff --git a/api/hunterbook.xml b/api/hunterbook.xml index 7da9855..4865778 100644 --- a/api/hunterbook.xml +++ b/api/hunterbook.xml @@ -289,15 +289,20 @@ + + + + + From af1a01ac2e12e62bc85a19463ffbbe136f61193d Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Fri, 2 Oct 2026 21:46:11 +0200 Subject: [PATCH 05/14] Fix Beast Lore ability coloring (color chips) and filter-row layout --- api/hunterbook-tab-beastlore.lua | 27 --------------------------- api/hunterbook.lua | 12 +++++++++++- api/hunterbook.xml | 6 +++--- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/api/hunterbook-tab-beastlore.lua b/api/hunterbook-tab-beastlore.lua index b682392..3a1f54a 100644 --- a/api/hunterbook-tab-beastlore.lua +++ b/api/hunterbook-tab-beastlore.lua @@ -24,33 +24,6 @@ function MTH_BOOK_GetBeastAbilitiesSummary(beast) return tostring(beast.abilities) end --- Like the summary, but colours each ability green when the hunter already knows --- that exact rank and red when it is still unlearned, to spot tameable upgrades. -function MTH_BOOK_GetBeastAbilitiesColored(beast) - if not beast or not beast.abilities or beast.abilities == "" or beast.abilities == "None" then - return "-" - end - local tokens = MTH_BOOK_SplitAbilities(beast.abilities) - if table.getn(tokens) == 0 then - return "-" - end - local canCheck = type(MTH_IsPetAbilityKnownByHunter) == "function" - local parts = {} - for i = 1, table.getn(tokens) do - local text = tostring(tokens[i]) - if canCheck then - local _, label, rank = MTH_BOOK_ParseAbilityToken(tokens[i]) - if MTH_IsPetAbilityKnownByHunter(label, rank) then - text = "|cff40ff40" .. text .. "|r" - else - text = "|cffff4040" .. text .. "|r" - end - end - table.insert(parts, text) - end - return table.concat(parts, ", ") -end - function MTH_BOOK_GetBeastZoneSummary(beast) if not beast or not beast.coords or table.getn(beast.coords) == 0 then return "-" diff --git a/api/hunterbook.lua b/api/hunterbook.lua index ec98403..ee8d709 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -4292,7 +4292,7 @@ local function MTH_BOOK_GetRowValues(entry) if not beast then return { "", "", "", "", "", "", "", "", "", "" } end local beastDisplayName = (MTH and MTH.GetLocalizedBeastName and MTH:GetLocalizedBeastName(entry, beast.name)) or beast.name local traits = MTH_BOOK_ParseBeastTraits(beast) - local abilities = MTH_BOOK_GetBeastAbilitiesColored(beast) + local abilities = MTH_BOOK_GetBeastAbilitiesSummary(beast) local zone = MTH_BOOK_GetBeastZoneSummary(beast) return { "|cFF33CCFF" .. tostring(entry) .. "|r", @@ -4786,6 +4786,16 @@ MTH_BOOK_UpdateResults = function() label = label .. "," end chip.text:SetText(label) + -- Colour by whether the hunter already knows this exact ability rank. + if MTH_BOOK_STATE.mode == "pets" and type(MTH_IsPetAbilityKnownByHunter) == "function" then + if MTH_IsPetAbilityKnownByHunter(ability.tooltipName or ability.name, ability.requestedRank) then + chip.text:SetTextColor(0.30, 1.00, 0.30) + else + chip.text:SetTextColor(1.00, 0.35, 0.35) + end + else + chip.text:SetTextColor(1, 1, 1) + end local textWidth = chip.text:GetStringWidth() or 24 if textWidth < 16 then textWidth = 16 end local chipWidth = ((MTH_BOOK_STATE.mode == "families") and 15 or 14) + textWidth diff --git a/api/hunterbook.xml b/api/hunterbook.xml index 4865778..039d73e 100644 --- a/api/hunterbook.xml +++ b/api/hunterbook.xml @@ -291,18 +291,18 @@ - + From ef8daaa5680306f009a6af1c35ccbae007e4d689 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Fri, 2 Oct 2026 23:00:12 +0200 Subject: [PATCH 06/14] Remove known/unknown filter (keep ability coloring); fix pet level-up subtitle --- api/core-level-tracking.lua | 2 +- api/hunterbook-tab-beastlore.lua | 19 ---------- api/hunterbook.lua | 59 -------------------------------- api/hunterbook.xml | 9 ++--- 4 files changed, 3 insertions(+), 86 deletions(-) diff --git a/api/core-level-tracking.lua b/api/core-level-tracking.lua index 810b410..59ddca9 100644 --- a/api/core-level-tracking.lua +++ b/api/core-level-tracking.lua @@ -250,7 +250,7 @@ local function MTH_LT_AnnounceTraining(kind, level, eventKind, preview) 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." + local subtitle = kind == "hunter" and (MTH_LT_HunterLines[level] or "New training is available.") or "Your beast grows stronger. Teach it something nasty." if isMilestone and type(MTH_CelebrateBlackHole) == "function" then MTH_CelebrateBlackHole(isLevelCap and headlineText or "Level " .. tostring(level), subtitle) else diff --git a/api/hunterbook-tab-beastlore.lua b/api/hunterbook-tab-beastlore.lua index 3a1f54a..4a2fb89 100644 --- a/api/hunterbook-tab-beastlore.lua +++ b/api/hunterbook-tab-beastlore.lua @@ -282,22 +282,6 @@ function MTH_BOOK_ParseBeastTraits(beast) return traits end --- "known" keeps beasts with at least one ability the hunter already knows; --- "unknown" keeps beasts that can still teach something not yet learned. -function MTH_BOOK_BeastMatchesAbilityKnowledge(beast, mode) - if type(MTH_IsPetAbilityKnownByHunter) ~= "function" then return true end - local tokens = MTH_BOOK_SplitAbilities(beast and beast.abilities) - local n = table.getn(tokens) - if n == 0 then return false end - for i = 1, n do - local _, label, rank = MTH_BOOK_ParseAbilityToken(tokens[i]) - local known = MTH_IsPetAbilityKnownByHunter(label, rank) - if mode == "known" and known then return true end - if mode == "unknown" and not known then return true end - end - return false -end - function MTH_BOOK_BeastMatches(beastId, beast) if not beast then return false end local forcedBeastId = MTH_BOOK_STATE and tonumber(MTH_BOOK_STATE.forcedBeastId) or nil @@ -321,9 +305,6 @@ function MTH_BOOK_BeastMatches(beastId, beast) if not MTH_BOOK_HasKnownLevel(beast) then return false end if not MTH_BOOK_HasKnownZone(beast) then return false end end - if MTH_BOOK_STATE.petKnownFilter and MTH_BOOK_STATE.petKnownFilter ~= "all" then - if not MTH_BOOK_BeastMatchesAbilityKnowledge(beast, MTH_BOOK_STATE.petKnownFilter) then return false end - end if MTH_BOOK_STATE.quick ~= "all" then if familyLower ~= MTH_BOOK_STATE.quick then diff --git a/api/hunterbook.lua b/api/hunterbook.lua index ee8d709..28cbe3d 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -137,7 +137,6 @@ local MTH_BOOK_STATE = { familyStatsPercent = false, petHideNoAbilities = true, petHideUnknown = true, - petKnownFilter = "all", trainerHonoredDiscount = false, forcedBeastId = nil, page = 1, @@ -2337,28 +2336,6 @@ local function MTH_BOOK_UpdateRankDropdownText() MTH_BOOK_DropdownSetText(rankDropdown, label) end -MTH_BOOK_PetKnownFilterOptions = { - { value = "all", label = "Known/Unknown" }, - { value = "known", label = "Known Abilities" }, - { value = "unknown", label = "Unknown Abilities" }, -} - -local function MTH_BOOK_GetKnownFilterLabel(value) - for i = 1, table.getn(MTH_BOOK_PetKnownFilterOptions) do - local option = MTH_BOOK_PetKnownFilterOptions[i] - if option and option.value == value then - return option.label - end - end - return "Known/Unknown" -end - -local function MTH_BOOK_UpdateKnownFilterDropdownText() - local dropdown = getglobal("MTH_BOOK_KnownFilterDropdown") - if not dropdown then return end - MTH_BOOK_DropdownSetText(dropdown, MTH_BOOK_GetKnownFilterLabel(MTH_BOOK_STATE.petKnownFilter or "all")) -end - local function MTH_BOOK_UpdatePetLearnSourceDropdownText() local sourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown") if not sourceDropdown then return end @@ -2506,8 +2483,6 @@ local function MTH_BOOK_UpdateQuickFilterControls() local petInZoneOnly = getglobal("MTH_BOOK_PetInZoneOnly") local npcInZoneOnly = getglobal("MTH_BOOK_NPCInZoneOnly") local showAllOnMapButton = getglobal("MTH_BOOK_ShowAllOnMapButton") - local knownFilterDropdown = getglobal("MTH_BOOK_KnownFilterDropdown") - if knownFilterDropdown then knownFilterDropdown:Hide() end local minLevel = getglobal("MTH_BOOK_MinLevel") local maxLevel = getglobal("MTH_BOOK_MaxLevel") local stats = getglobal("MTH_BOOK_StatsText") @@ -2602,10 +2577,6 @@ local function MTH_BOOK_UpdateQuickFilterControls() rankDropdown:Hide() end end - if knownFilterDropdown then - knownFilterDropdown:Show() - MTH_BOOK_UpdateKnownFilterDropdownText() - end if petInZoneOnly then petInZoneOnly:Show() petInZoneOnly:SetChecked(MTH_BOOK_STATE.petInZoneOnly and 1 or nil) @@ -3092,34 +3063,6 @@ local function MTH_BOOK_InitRankDropdown() MTH_BOOK_UpdateRankDropdownText() end -local function MTH_BOOK_InitKnownFilterDropdown() - local dropdown = getglobal("MTH_BOOK_KnownFilterDropdown") - if not dropdown then return end - - UIDropDownMenu_Initialize(dropdown, function() - for i = 1, table.getn(MTH_BOOK_PetKnownFilterOptions) do - local option = MTH_BOOK_PetKnownFilterOptions[i] - local value = option.value - local label = option.label - local info = MTH_BOOK_DropdownCreateInfo() - info.text = label - info.func = function() - MTH_BOOK_STATE.petKnownFilter = value - MTH_BOOK_STATE.page = 1 - MTH_BOOK_STATE.selectedEntry = nil - MTH_BOOK_DropdownSetText(dropdown, label) - MTH_BOOK_RefreshFilter() - end - info.checked = ((MTH_BOOK_STATE.petKnownFilter or "all") == value) - UIDropDownMenu_AddButton(info) - end - end) - - UIDropDownMenu_SetWidth(140, dropdown) - UIDropDownMenu_JustifyText("LEFT", dropdown) - MTH_BOOK_UpdateKnownFilterDropdownText() -end - local function MTH_BOOK_InitPetLearnSourceDropdown() local sourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown") if not sourceDropdown then return end @@ -5275,7 +5218,6 @@ local function MTH_BOOK_ResetStateForMode(mode) MTH_BOOK_STATE.npcHideNoZone = true MTH_BOOK_STATE.petHideNoAbilities = false MTH_BOOK_STATE.petHideUnknown = false - MTH_BOOK_STATE.petKnownFilter = "all" MTH_BOOK_STATE.forcedBeastId = nil MTH_BOOK_STATE.petLeftOffset = 0 @@ -5754,7 +5696,6 @@ local function MTH_BOOK_WireUI(frame) MTH_BOOK_InitFamilyDropdown() MTH_BOOK_InitAbilityDropdown() MTH_BOOK_InitRankDropdown() - MTH_BOOK_InitKnownFilterDropdown() MTH_BOOK_WireBeastDropdowns() MTH_BOOK_InitPetLearnSourceDropdown() MTH_BOOK_InitItemSubtypeDropdown() diff --git a/api/hunterbook.xml b/api/hunterbook.xml index 039d73e..7da9855 100644 --- a/api/hunterbook.xml +++ b/api/hunterbook.xml @@ -289,20 +289,15 @@ - - - - - From f8d1aa803340902466e1aa11f4f3cd2194c11130 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Fri, 2 Oct 2026 23:03:29 +0200 Subject: [PATCH 07/14] zTrap: show trap arming cooldown (spiral + seconds) with a toggle --- api/options-zbuttons.lua | 19 ++++++++ modules/zhunter/zButtonTrap.lua | 80 +++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/api/options-zbuttons.lua b/api/options-zbuttons.lua index e7cbad6..7d2a048 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -303,6 +303,13 @@ local function MTH_ZB_EnsureButtonOptionDefaults(buttonName) saved["showammoname"] = saved["showammoname"] and true or false end + if buttonName == "zButtonTrap" then + if saved["cooldown"] == nil then + saved["cooldown"] = true + end + saved["cooldown"] = saved["cooldown"] and true or false + end + return saved end @@ -557,6 +564,18 @@ local function MTH_SetupButtonOptions(containerName, buttonName, displayName, ma buttonObj.tooltip = saved["tooltip"] and true or false end + if buttonName == "zButtonTrap" then + childrenBox:Checkbox(containerName.."ShowCooldown", MTH_ZB_L("ZB_LABEL_SHOW_TRAP_COOLDOWN", "Show cooldown"), { + checked = saved["cooldown"] and true or false, + 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]["cooldown"] = checked + end, + }) + end + if buttonName == "zButtonAmmo" then childrenBox:Checkbox(containerName.."ShowAmmoName", MTH_ZB_L("ZB_LABEL_SHOW_AMMO_NAME", "Show ammo name"), { checked = saved["showammoname"], diff --git a/modules/zhunter/zButtonTrap.lua b/modules/zhunter/zButtonTrap.lua index e8c897e..6fef282 100644 --- a/modules/zhunter/zButtonTrap.lua +++ b/modules/zhunter/zButtonTrap.lua @@ -19,6 +19,7 @@ if not root["zButtonTrap"] then root["zButtonTrap"]["children"] = {} root["zButtonTrap"]["children"]["size"] = 36 root["zButtonTrap"]["children"]["hideonclick"] = 1 + root["zButtonTrap"]["cooldown"] = true end ZHunterMod_Trap_Spells = { @@ -55,6 +56,9 @@ local function zButtonTrap_EnsureConfig() if saved["tooltip"] == nil then saved["tooltip"] = 1 end + if saved["cooldown"] == nil then + saved["cooldown"] = true + end if not saved["parent"] then saved["parent"] = {} end @@ -100,6 +104,81 @@ local function zButtonTrap_ApplyRuntimeSettings() zButtonTrap.fadetimer = saved["children"] and tonumber(saved["children"]["fadetimer"]) or 0 end +-- ---- Trap arming-cooldown display (shared trap re-arm, ~15s) --------------- +local MTH_ZH_TrapCD_Frame = nil +local MTH_ZH_TrapCD_Elapsed = 0 + +local function zButtonTrap_EnsureCooldownText(button) + if not button then return nil end + if button._mthCooldownText then return button._mthCooldownText end + local fs = button:CreateFontString(nil, "OVERLAY") + fs:SetFont("Fonts\\FRIZQT__.TTF", 14, "THICKOUTLINE") + fs:SetTextColor(1, 0.92, 0.3, 1) + fs:SetPoint("CENTER", button, "CENTER", 0, 0) + button._mthCooldownText = fs + return fs +end + +local function zButtonTrap_ClearCooldown(button) + if not button then return end + if button._mthCooldownText then button._mthCooldownText:SetText("") end + if button.cooldown and button._mthCdActive then + CooldownFrame_SetTimer(button.cooldown, 0, 0, 0) + button._mthCdActive = nil + button._mthCdStart = nil + button._mthCdDur = nil + end +end + +local function zButtonTrap_UpdateCooldownFor(button) + if not (button and button.id) then + zButtonTrap_ClearCooldown(button) + return + end + local start, duration, enable = GetSpellCooldown(button.id, "spell") + -- Skip the ~1.5s global cooldown; only show the real trap re-arm timer. + if enable == 1 and duration and duration > 1.5 and start and start > 0 then + local remaining = start + duration - GetTime() + if remaining > 0 then + if button.cooldown and (button._mthCdStart ~= start or button._mthCdDur ~= duration) then + CooldownFrame_SetTimer(button.cooldown, start, duration, 1) + button._mthCdStart = start + button._mthCdDur = duration + button._mthCdActive = 1 + end + local fs = zButtonTrap_EnsureCooldownText(button) + if fs then fs:SetText(tostring(math.ceil(remaining))) end + return + end + end + zButtonTrap_ClearCooldown(button) +end + +local function zButtonTrap_CooldownOnUpdate() + MTH_ZH_TrapCD_Elapsed = MTH_ZH_TrapCD_Elapsed + (arg1 or 0) + if MTH_ZH_TrapCD_Elapsed < 0.1 then return end + MTH_ZH_TrapCD_Elapsed = 0 + if not zButtonTrap then return end + local saved = zButtonTrap_GetSaved() + if not saved["cooldown"] then + zButtonTrap_ClearCooldown(zButtonTrap) + for i = 1, (zButtonTrap.count or 0) do + zButtonTrap_ClearCooldown(getglobal("zButtonTrap"..i)) + end + return + end + zButtonTrap_UpdateCooldownFor(zButtonTrap) + for i = 1, (zButtonTrap.count or 0) do + zButtonTrap_UpdateCooldownFor(getglobal("zButtonTrap"..i)) + end +end + +local function zButtonTrap_EnsureCooldownDriver() + if MTH_ZH_TrapCD_Frame then return end + MTH_ZH_TrapCD_Frame = CreateFrame("Frame", "MTH_ZH_TrapCooldownDriver") + MTH_ZH_TrapCD_Frame:SetScript("OnUpdate", zButtonTrap_CooldownOnUpdate) +end + function zButtonTrap_OnLoad() this:RegisterEvent("VARIABLES_LOADED") @@ -148,6 +227,7 @@ function zButtonTrap_CreateButtons() end zButtonTrap.found = ZSpellButton_SetButtons(zButtonTrap, info) zButtonTrap_ApplyRuntimeSettings() + zButtonTrap_EnsureCooldownDriver() end function zButtonTrap_SetupSizeAndPosition() From bb5c87583539b18a2138c6dc9a3df01dd798f642 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Sat, 3 Oct 2026 17:43:50 +0200 Subject: [PATCH 08/14] Changelog: 2.1.0 entries --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5123e..89eb4c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to MetaHunt will be documented in this file. +## [2.1.0] - Unreleased + +### Added + +- **Beast Lore — ability colouring**: In the Hunter's Book Beast Lore list, every beast's abilities are now coloured — **green** if your hunter already knows that exact rank, **red** if it's still unlearned — so you can spot at a glance which beasts can still teach you something new. + +- **zTrap — trap cooldown**: The zTrap buttons now show the trap re-arm cooldown (the ~15s before you can lay another trap) as a cooldown spiral plus a seconds countdown, on both the parent and child buttons. A new "Show cooldown" toggle lives in Options → zButtons → zTrap (on by default). + +### Fixed + +- **Feed-O-Matic — feeding food that's too low level**: With nearly-full bags, FeedOMatic could keep trying to feed a small stack whose level was too low for your pet, which the server refuses ("That food's level is not high enough"). It now skips below-minimum-level foods and only offers food the pet can actually eat. + +- **Pets — taming & feeding history disappearing**: Fixed a long-standing issue where a pet's taming info and feeding history could vanish from the Book — caused by the server handing out non-unique pet GUIDs that split one pet into several empty duplicate records. The Stable scan now re-adopts the real record instead of an empty duplicate, so the data reappears on your next stable visit (no data rewrite needed). + +- **Pet level-up message**: Removed a nonsensical pet level-up line ("the trainer wants coin") — there's no coin involved when your pet simply gains a level. + + ## [2.0.0] - 2026-09-14 Resurrecting on Octowow. From f6b495d68d1bbed86dd3da8ff6c0e1c1db19a030 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Sun, 4 Oct 2026 20:37:54 +0200 Subject: [PATCH 09/14] zAmmo: total ammo meter (centered % + side gauge) with options --- CHANGELOG.md | 2 + api/options-zbuttons.lua | 30 ++++++ modules/zhunter/zButtonAmmo.lua | 176 ++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89eb4c1..3a7cf1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to MetaHunt will be documented in this file. - **zTrap — trap cooldown**: The zTrap buttons now show the trap re-arm cooldown (the ~15s before you can lay another trap) as a cooldown spiral plus a seconds countdown, on both the parent and child buttons. A new "Show cooldown" toggle lives in Options → zButtons → zTrap (on by default). +- **zAmmo — total ammo meter**: The zAmmo parent button can now show how stocked you are on your equipped weapon's ammo, as a percentage of your quiver/pouch capacity (all matching arrows or bullets across your bags ÷ total quiver/pouch slots × 200). Two heat-coloured display styles (red when low → green when full) are available as toggles in Options → zButtons → zAmmo → Parent Button: a **centered %** on the button, and a **vertical gauge** beside it (which flips to the side opposite the children's expand direction). + ### Fixed - **Feed-O-Matic — feeding food that's too low level**: With nearly-full bags, FeedOMatic could keep trying to feed a small stack whose level was too low for your pet, which the server refuses ("That food's level is not high enough"). It now skips below-minimum-level foods and only offers food the pet can actually eat. diff --git a/api/options-zbuttons.lua b/api/options-zbuttons.lua index 7d2a048..b7b421e 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -301,6 +301,14 @@ local function MTH_ZB_EnsureButtonOptionDefaults(buttonName) saved["showammoname"] = true end saved["showammoname"] = saved["showammoname"] and true or false + if saved["showtotalperc"] == nil then + saved["showtotalperc"] = true + end + saved["showtotalperc"] = saved["showtotalperc"] and true or false + if saved["showtotalgauge"] == nil then + saved["showtotalgauge"] = false + end + saved["showtotalgauge"] = saved["showtotalgauge"] and true or false end if buttonName == "zButtonTrap" then @@ -596,6 +604,28 @@ local function MTH_SetupButtonOptions(containerName, buttonName, displayName, ma end end, }) + parentBox:Checkbox(containerName.."ShowTotalPercent", MTH_ZB_L("ZB_LABEL_SHOW_TOTAL_PERCENT", "Show total % (center)"), { + checked = saved["showtotalperc"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["showtotalperc"] = checked + if buttonObj and buttonObj.ammoname and not buttonObj.isspell then + zButtonAmmo_UpdateButton(buttonObj) + end + end, + }) + parentBox:Checkbox(containerName.."ShowTotalGauge", MTH_ZB_L("ZB_LABEL_SHOW_TOTAL_GAUGE", "Show total gauge (side)"), { + checked = saved["showtotalgauge"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["showtotalgauge"] = checked + if buttonObj and buttonObj.ammoname and not buttonObj.isspell then + zButtonAmmo_UpdateButton(buttonObj) + end + end, + }) end MTH_SetButtonEnabledState(buttonName, buttonObj, saved["enabled"] and true or false) diff --git a/modules/zhunter/zButtonAmmo.lua b/modules/zhunter/zButtonAmmo.lua index 4786c7f..9c57ab1 100644 --- a/modules/zhunter/zButtonAmmo.lua +++ b/modules/zhunter/zButtonAmmo.lua @@ -21,6 +21,7 @@ if not root["zButtonAmmo"] then root["zButtonAmmo"]["children"]["size"] = 36 root["zButtonAmmo"]["children"]["hideonclick"] = 1 root["zButtonAmmo"]["showammoname"] = 1 + root["zButtonAmmo"]["showtotalperc"] = 1 root["zButtonAmmo"]["lastEquipped"] = nil end @@ -431,6 +432,167 @@ local function zButtonAmmo_GetEquippedAmmoInfo() } end +local function zButtonAmmo_EnsureTotalPercentText(button) + if button and not button.ammoTotalPercentText then + button.ammoTotalPercentText = button:CreateFontString(nil, "OVERLAY") + button.ammoTotalPercentText:SetPoint("CENTER", button, "CENTER", 0, 0) + button.ammoTotalPercentText:SetFont("Fonts\\ARIALN.ttf", 13, "THICKOUTLINE") + end + return button and button.ammoTotalPercentText +end + +local function zButtonAmmo_ShouldShowTotalPercent() + local saved = zButtonAmmo_GetSaved() + return saved["showtotalperc"] and true or false +end + +-- "Arrow" or "Bullet" for the currently equipped ranged weapon, or nil if none. +local function zButtonAmmo_GetEquippedAmmoCategory() + if not (GetInventoryItemLink and GetItemInfo) then + return nil + end + local link = GetInventoryItemLink("player", 18) + if not link then + return nil + end + local _, _, id = string.find(link, "Hitem:((%d+).-)") + if not id then + return nil + end + local _, _, _, _, _, weaponType = GetItemInfo(id) + if weaponType == MTH_WEAPON_GUNS then + return "Bullet" + elseif weaponType == MTH_WEAPON_BOWS or weaponType == MTH_WEAPON_CROSSBOWS then + return "Arrow" + end + return nil +end + +-- Capacity = total slots of every equipped quiver (arrows) or ammo pouch +-- (bullets) multiplied by the per-slot stack size. +local zButtonAmmo_StackSize = 200 +local function zButtonAmmo_GetAmmoCapacity(category) + if not category then + return 0 + end + local wantedSubtype = (category == "Bullet") and "Ammo Pouch" or "Quiver" + local totalSlots = 0 + for bag = 1, 4 do + local invSlot = ContainerIDToInventoryID and ContainerIDToInventoryID(bag) or nil + if invSlot then + local link = GetInventoryItemLink and GetInventoryItemLink("player", invSlot) + if link then + local _, _, id = string.find(link, "Hitem:((%d+).-)") + if id then + local _, _, _, _, _, bagSubtype = GetItemInfo(id) + if bagSubtype == wantedSubtype then + totalSlots = totalSlots + (GetContainerNumSlots(bag) or 0) + end + end + end + end + end + return totalSlots * zButtonAmmo_StackSize +end + +-- Sum of all matching ammo across every bag, from the live cache. +local function zButtonAmmo_GetTotalAmmoCount(category) + if not category then + return 0 + end + local total = 0 + for _, info in pairs(zButtonAmmo_Cache) do + if info and info.type == category then + total = total + (tonumber(info.count) or 0) + end + end + return total +end + +local function zButtonAmmo_UpdateTotalPercent(button) + local totalText = zButtonAmmo_EnsureTotalPercentText(button) + if not totalText then + return + end + if not zButtonAmmo_ShouldShowTotalPercent() then + totalText:SetText("") + return + end + local category = zButtonAmmo_GetEquippedAmmoCategory() + local capacity = zButtonAmmo_GetAmmoCapacity(category) + if not category or capacity <= 0 then + totalText:SetText("") + return + end + local perc = zButtonAmmo_GetTotalAmmoCount(category) / capacity + local r, g, b = zButtonAmmo_GetColorGradient(perc) + totalText:SetTextColor(r, g, b, 1) + totalText:SetText(math.floor(perc * 100 + 0.5) .. "%") +end + +local function zButtonAmmo_ShouldShowTotalGauge() + local saved = zButtonAmmo_GetSaved() + return saved["showtotalgauge"] and true or false +end + +local function zButtonAmmo_EnsureTotalGauge(button) + if not button then + return nil + end + if button.ammoTotalGauge then + return button.ammoTotalGauge + end + local gauge = CreateFrame("Frame", nil, button) + gauge:SetWidth(7) + gauge:SetHeight(button:GetHeight() or 36) + local bg = gauge:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(gauge) + bg:SetTexture(0, 0, 0, 0.55) + local bar = CreateFrame("StatusBar", nil, gauge) + bar:SetAllPoints(gauge) + bar:SetOrientation("VERTICAL") + bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") + bar:SetMinMaxValues(0, 1) + bar:SetValue(0) + gauge.bar = bar + button.ammoTotalGauge = gauge + return gauge +end + +local function zButtonAmmo_UpdateTotalGauge(button) + local gauge = zButtonAmmo_EnsureTotalGauge(button) + if not gauge then + return + end + if not zButtonAmmo_ShouldShowTotalGauge() then + gauge:Hide() + return + end + local category = zButtonAmmo_GetEquippedAmmoCategory() + local capacity = zButtonAmmo_GetAmmoCapacity(category) + if not category or capacity <= 0 then + gauge:Hide() + return + end + local perc = zButtonAmmo_GetTotalAmmoCount(category) / capacity + local fill = perc + if fill > 1 then fill = 1 end + if fill < 0 then fill = 0 end + gauge:SetHeight(button:GetHeight() or 36) + gauge:ClearAllPoints() + -- Gauge sits opposite the child expand direction. + local saved = zButtonAmmo_GetSaved() + if saved["firstbutton"] == "LEFT" then + gauge:SetPoint("LEFT", button, "RIGHT", 2, 0) + else + gauge:SetPoint("RIGHT", button, "LEFT", -2, 0) + end + gauge.bar:SetValue(fill) + local r, g, b = zButtonAmmo_GetColorGradient(perc) + gauge.bar:SetStatusBarColor(r, g, b) + gauge:Show() +end + function zButtonAmmo_UpdateButton(button) if not button then button = this @@ -447,6 +609,12 @@ function zButtonAmmo_UpdateButton(button) if button.ammoShortLabelText then button.ammoShortLabelText:SetText("") end + if button == zButtonAmmo and button.ammoTotalPercentText then + button.ammoTotalPercentText:SetText("") + end + if button == zButtonAmmo and button.ammoTotalGauge then + button.ammoTotalGauge:Hide() + end return end @@ -484,6 +652,10 @@ function zButtonAmmo_UpdateButton(button) countText:SetText("Out!") end end + if button == zButtonAmmo then + zButtonAmmo_UpdateTotalPercent(button) + zButtonAmmo_UpdateTotalGauge(button) + end end function zButtonAmmo_SetButtons(parent, ammoList) @@ -640,6 +812,9 @@ local function zButtonAmmo_EnsureSpellOrder(ammoList) if saved["showammoname"] == nil then saved["showammoname"] = 1 end + if saved["showtotalperc"] == nil then + saved["showtotalperc"] = 1 + end if not saved["parent"] then saved["parent"] = {} end @@ -933,6 +1108,7 @@ function zButtonAmmo_Reset() saved["children"]["size"] = 36 saved["children"]["hideonclick"] = 1 saved["showammoname"] = 1 + saved["showtotalperc"] = 1 saved["lastEquipped"] = nil end From 69c6d44b8eada0b2c9ba9f8ea4aa3013557b9105 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Tue, 6 Oct 2026 15:09:19 +0200 Subject: [PATCH 10/14] zAmmo: bottom-counter total toggle + capacity-based counter colouring --- CHANGELOG.md | 6 +++- api/options-zbuttons.lua | 62 +++++++++++++++++++++------------ modules/zhunter/zButtonAmmo.lua | 30 +++++++++++++++- 3 files changed, 74 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7cf1a..5ec2009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,11 @@ All notable changes to MetaHunt will be documented in this file. - **zTrap — trap cooldown**: The zTrap buttons now show the trap re-arm cooldown (the ~15s before you can lay another trap) as a cooldown spiral plus a seconds countdown, on both the parent and child buttons. A new "Show cooldown" toggle lives in Options → zButtons → zTrap (on by default). -- **zAmmo — total ammo meter**: The zAmmo parent button can now show how stocked you are on your equipped weapon's ammo, as a percentage of your quiver/pouch capacity (all matching arrows or bullets across your bags ÷ total quiver/pouch slots × 200). Two heat-coloured display styles (red when low → green when full) are available as toggles in Options → zButtons → zAmmo → Parent Button: a **centered %** on the button, and a **vertical gauge** beside it (which flips to the side opposite the children's expand direction). +- **zAmmo — total ammo meter**: The zAmmo parent button can now show how stocked you are on your equipped weapon's ammo, as a percentage of your quiver/pouch capacity (all matching arrows or bullets across your bags ÷ total quiver/pouch slots × 200). Two heat-coloured display styles (red when low → green when full) are available as toggles in Options → zButtons → zAmmo → Parent Button: a **centered %** on the button, and a **vertical gauge** beside it (which flips to the side opposite the children's expand direction). A third toggle, **"Counter shows total ammo"**, switches the parent's bottom counter from the equipped stack to your full bag total. + +### Changed + +- **zAmmo — capacity-based counter colouring**: The heat colour on the ammo stack counters now scales to your real quiver/pouch capacity instead of a fixed number, so a stack turns green as it approaches filling your quiver (falls back to the old scale when no quiver/pouch is equipped). ### Fixed diff --git a/api/options-zbuttons.lua b/api/options-zbuttons.lua index b7b421e..3d60d93 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -309,6 +309,10 @@ local function MTH_ZB_EnsureButtonOptionDefaults(buttonName) saved["showtotalgauge"] = false end saved["showtotalgauge"] = saved["showtotalgauge"] and true or false + if saved["counttotal"] == nil then + saved["counttotal"] = false + end + saved["counttotal"] = saved["counttotal"] and true or false end if buttonName == "zButtonTrap" then @@ -474,6 +478,42 @@ local function MTH_SetupButtonOptions(containerName, buttonName, displayName, ma parentBox:Note(containerName.."AQFilterDesc", MTH_ZB_L("ZB_DESC_AQ_FILTER", "Hide Qiraji mounts outside AQ40. Show only Qiraji inside.")) end + if buttonName == "zButtonAmmo" then + parentBox:Checkbox(containerName.."ShowTotalPercent", MTH_ZB_L("ZB_LABEL_SHOW_TOTAL_PERCENT", "Show total % (center)"), { + checked = saved["showtotalperc"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["showtotalperc"] = checked + if buttonObj and buttonObj.ammoname and not buttonObj.isspell then + zButtonAmmo_UpdateButton(buttonObj) + end + end, + }) + parentBox:Checkbox(containerName.."ShowTotalGauge", MTH_ZB_L("ZB_LABEL_SHOW_TOTAL_GAUGE", "Show total gauge (side)"), { + checked = saved["showtotalgauge"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["showtotalgauge"] = checked + if buttonObj and buttonObj.ammoname and not buttonObj.isspell then + zButtonAmmo_UpdateButton(buttonObj) + end + end, + }) + parentBox:Checkbox(containerName.."CounterTotal", MTH_ZB_L("ZB_LABEL_COUNTER_TOTAL", "Counter shows total ammo"), { + checked = saved["counttotal"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["counttotal"] = checked + if buttonObj and buttonObj.ammoname and not buttonObj.isspell then + zButtonAmmo_UpdateButton(buttonObj) + end + end, + }) + 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"), @@ -604,28 +644,6 @@ local function MTH_SetupButtonOptions(containerName, buttonName, displayName, ma end end, }) - parentBox:Checkbox(containerName.."ShowTotalPercent", MTH_ZB_L("ZB_LABEL_SHOW_TOTAL_PERCENT", "Show total % (center)"), { - checked = saved["showtotalperc"], - onClick = function() - if not this then return end - local checked = this:GetChecked() == 1 - ZHunterMod_Saved[buttonName]["showtotalperc"] = checked - if buttonObj and buttonObj.ammoname and not buttonObj.isspell then - zButtonAmmo_UpdateButton(buttonObj) - end - end, - }) - parentBox:Checkbox(containerName.."ShowTotalGauge", MTH_ZB_L("ZB_LABEL_SHOW_TOTAL_GAUGE", "Show total gauge (side)"), { - checked = saved["showtotalgauge"], - onClick = function() - if not this then return end - local checked = this:GetChecked() == 1 - ZHunterMod_Saved[buttonName]["showtotalgauge"] = checked - if buttonObj and buttonObj.ammoname and not buttonObj.isspell then - zButtonAmmo_UpdateButton(buttonObj) - end - end, - }) end MTH_SetButtonEnabledState(buttonName, buttonObj, saved["enabled"] and true or false) diff --git a/modules/zhunter/zButtonAmmo.lua b/modules/zhunter/zButtonAmmo.lua index 9c57ab1..f7abe69 100644 --- a/modules/zhunter/zButtonAmmo.lua +++ b/modules/zhunter/zButtonAmmo.lua @@ -509,6 +509,18 @@ local function zButtonAmmo_GetTotalAmmoCount(category) return total end +-- Divisor for per-stack heat colouring: real quiver/pouch capacity, or a +-- fixed fallback when no matching ammo bag is equipped. +local function zButtonAmmo_GetCountDivisor(category) + if category then + local capacity = zButtonAmmo_GetAmmoCapacity(category) + if capacity and capacity > 0 then + return capacity + end + end + return 2000 +end + local function zButtonAmmo_UpdateTotalPercent(button) local totalText = zButtonAmmo_EnsureTotalPercentText(button) if not totalText then @@ -593,6 +605,11 @@ local function zButtonAmmo_UpdateTotalGauge(button) gauge:Show() end +local function zButtonAmmo_ShouldShowTotalCount() + local saved = zButtonAmmo_GetSaved() + return saved["counttotal"] and true or false +end + function zButtonAmmo_UpdateButton(button) if not button then button = this @@ -641,7 +658,7 @@ function zButtonAmmo_UpdateButton(button) button.ammoslot = ammoInfo.slot button.ammocount = ammoInfo.count if countText then - local r, g, b = zButtonAmmo_GetColorGradient((ammoInfo.count or 0) / 2000) + local r, g, b = zButtonAmmo_GetColorGradient((ammoInfo.count or 0) / zButtonAmmo_GetCountDivisor(ammoInfo.type)) countText:SetTextColor(r, g, b, 1) countText:SetText(ammoInfo.count) end @@ -653,6 +670,17 @@ function zButtonAmmo_UpdateButton(button) end end if button == zButtonAmmo then + if countText and zButtonAmmo_ShouldShowTotalCount() then + local category = zButtonAmmo_GetEquippedAmmoCategory() + if category then + local total = zButtonAmmo_GetTotalAmmoCount(category) + local capacity = zButtonAmmo_GetAmmoCapacity(category) + local frac = capacity > 0 and (total / capacity) or (total / 2000) + local r, g, b = zButtonAmmo_GetColorGradient(frac) + countText:SetTextColor(r, g, b, 1) + countText:SetText(total) + end + end zButtonAmmo_UpdateTotalPercent(button) zButtonAmmo_UpdateTotalGauge(button) end From 14f33b35cad0838c858f8c364b78f1c7b73e8b1d Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Tue, 6 Oct 2026 19:43:51 +0200 Subject: [PATCH 11/14] zAmmo low-ammo flash + usage tracker; zPet unhappy flash; split parent/child tooltips --- CHANGELOG.md | 6 + api/options-zbuttons.lua | 45 +++++ modules/zhunter/ZSpellButtonTemplate.lua | 13 +- modules/zhunter/zButtonAmmo.lua | 240 +++++++++++++++++++++++ modules/zhunter/zButtonPet.lua | 66 +++++++ 5 files changed, 369 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec2009..9d934a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ All notable changes to MetaHunt will be documented in this file. - **zAmmo — total ammo meter**: The zAmmo parent button can now show how stocked you are on your equipped weapon's ammo, as a percentage of your quiver/pouch capacity (all matching arrows or bullets across your bags ÷ total quiver/pouch slots × 200). Two heat-coloured display styles (red when low → green when full) are available as toggles in Options → zButtons → zAmmo → Parent Button: a **centered %** on the button, and a **vertical gauge** beside it (which flips to the side opposite the children's expand direction). A third toggle, **"Counter shows total ammo"**, switches the parent's bottom counter from the equipped stack to your full bag total. +- **zAmmo — low-ammo flash**: The zAmmo parent pulses a red glow when your total ammo drops below your SmartAmmo warning threshold (default 200). Toggle in the Parent Button box (on by default). + +- **zAmmo — ammo usage tracker**: Left-click the zAmmo parent to reset a usage counter, then hover it for a report of how much ammo you've burned since the reset — total, elapsed time, average per minute, and a per-type breakdown (how many Rough, Jagged, etc.) with percentages. Counts only ammo actually spent (restocking never corrupts it) and survives `/reload`. The parent tooltip now shows this report instead of the raw item tooltip, with a separate "Show parent tooltip" toggle; the Children "Show Tooltip" option now only affects the child buttons. + +- **zPet — unhappy pet flash**: The zPet parent pulses a red glow while your pet is Unhappy, as a persistent "feed me" reminder (complements the existing flash-on-change). Toggle in the Parent Button box (on by default). + ### Changed - **zAmmo — capacity-based counter colouring**: The heat colour on the ammo stack counters now scales to your real quiver/pouch capacity instead of a fixed number, so a stack turns green as it approaches filling your quiver (falls back to the old scale when no quiver/pouch is equipped). diff --git a/api/options-zbuttons.lua b/api/options-zbuttons.lua index 3d60d93..52cabbd 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -313,6 +313,21 @@ local function MTH_ZB_EnsureButtonOptionDefaults(buttonName) saved["counttotal"] = false end saved["counttotal"] = saved["counttotal"] and true or false + if saved["lowflash"] == nil then + saved["lowflash"] = true + end + saved["lowflash"] = saved["lowflash"] and true or false + if saved["parenttooltip"] == nil then + saved["parenttooltip"] = true + end + saved["parenttooltip"] = saved["parenttooltip"] and true or false + end + + if buttonName == "zButtonPet" then + if saved["unhappyflash"] == nil then + saved["unhappyflash"] = true + end + saved["unhappyflash"] = saved["unhappyflash"] and true or false end if buttonName == "zButtonTrap" then @@ -478,6 +493,16 @@ local function MTH_SetupButtonOptions(containerName, buttonName, displayName, ma parentBox:Note(containerName.."AQFilterDesc", MTH_ZB_L("ZB_DESC_AQ_FILTER", "Hide Qiraji mounts outside AQ40. Show only Qiraji inside.")) end + if buttonName == "zButtonPet" then + parentBox:Checkbox(containerName.."UnhappyFlash", MTH_ZB_L("ZB_LABEL_UNHAPPY_FLASH", "Flash when pet is unhappy"), { + checked = saved["unhappyflash"], + onClick = function() + if not this then return end + ZHunterMod_Saved[buttonName]["unhappyflash"] = this:GetChecked() == 1 + end, + }) + end + if buttonName == "zButtonAmmo" then parentBox:Checkbox(containerName.."ShowTotalPercent", MTH_ZB_L("ZB_LABEL_SHOW_TOTAL_PERCENT", "Show total % (center)"), { checked = saved["showtotalperc"], @@ -512,6 +537,26 @@ local function MTH_SetupButtonOptions(containerName, buttonName, displayName, ma end end, }) + parentBox:Checkbox(containerName.."LowFlash", MTH_ZB_L("ZB_LABEL_LOW_AMMO_FLASH", "Flash when low on ammo"), { + checked = saved["lowflash"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["lowflash"] = checked + if buttonObj and buttonObj.ammoname and not buttonObj.isspell then + zButtonAmmo_UpdateButton(buttonObj) + end + end, + }) + parentBox:Checkbox(containerName.."ParentTooltip", MTH_ZB_L("ZB_LABEL_SHOW_PARENT_TOOLTIP", "Show parent tooltip (usage report)"), { + checked = saved["parenttooltip"], + onClick = function() + if not this then return end + local checked = this:GetChecked() == 1 + ZHunterMod_Saved[buttonName]["parenttooltip"] = checked + if buttonObj then buttonObj.parentTooltip = checked end + end, + }) end local childrenBox = panel:Box("left", containerName.."ChildrenSection", MTH_ZB_L("ZB_SECTION_CHILDREN_BUTTONS", "Children Buttons")) diff --git a/modules/zhunter/ZSpellButtonTemplate.lua b/modules/zhunter/ZSpellButtonTemplate.lua index 6197c96..b6884e3 100644 --- a/modules/zhunter/ZSpellButtonTemplate.lua +++ b/modules/zhunter/ZSpellButtonTemplate.lua @@ -797,7 +797,11 @@ function ZSpellButtonParent_OnEnter(frame) ZSpellButton_SetChildrenExpanded(frame, true) end ZSpellButton_ResetFadeTimer(frame) - if frame.tooltip and frame.id then + local showParentTooltip = frame.parentTooltip + if showParentTooltip == nil then + showParentTooltip = frame.tooltip + end + if showParentTooltip and frame.id then GameTooltip:SetOwner(frame, "ANCHOR_TOPLEFT") local msg, rank @@ -807,6 +811,9 @@ function ZSpellButtonParent_OnEnter(frame) msg = msg.." ("..rank..")" end GameTooltip:SetText(msg, 1, 1, 1) + elseif frame.suppressItemTooltip then + GameTooltip:ClearLines() + GameTooltip:SetText(frame.ammoname or frame.ammolink or "Ammo", 1, 1, 1) else local showedItemTooltip = nil if frame.ammobag and frame.ammoslot and type(GameTooltip.SetBagItem) == "function" then @@ -838,6 +845,10 @@ function ZSpellButtonParent_OnEnter(frame) end end + if type(frame.extraTooltip) == "function" then + frame.extraTooltip(GameTooltip) + end + GameTooltip:AddLine("Alt+Drag To Move This Button") GameTooltip:Show() end diff --git a/modules/zhunter/zButtonAmmo.lua b/modules/zhunter/zButtonAmmo.lua index f7abe69..6a8f0bc 100644 --- a/modules/zhunter/zButtonAmmo.lua +++ b/modules/zhunter/zButtonAmmo.lua @@ -22,6 +22,8 @@ if not root["zButtonAmmo"] then root["zButtonAmmo"]["children"]["hideonclick"] = 1 root["zButtonAmmo"]["showammoname"] = 1 root["zButtonAmmo"]["showtotalperc"] = 1 + root["zButtonAmmo"]["lowflash"] = 1 + root["zButtonAmmo"]["parenttooltip"] = 1 root["zButtonAmmo"]["lastEquipped"] = nil end @@ -521,6 +523,139 @@ local function zButtonAmmo_GetCountDivisor(category) return 2000 end +-- Reset-based ammo usage tracker (reliable, retrospective — no prediction). +-- session (consumed total + per-name breakdown + resetAt) persists in saved; +-- the baseline tables are runtime only. +local zButtonAmmo_LastByName = nil +local zButtonAmmo_LastCategory = nil +local zButtonAmmo_SettleUntil = 0 + +local function zButtonAmmo_EnsureSession() + local saved = zButtonAmmo_GetSaved() + if type(saved["session"]) ~= "table" then + saved["session"] = {} + end + local session = saved["session"] + if tonumber(session["consumed"]) == nil then + session["consumed"] = 0 + end + if type(session["byName"]) ~= "table" then + session["byName"] = {} + end + if tonumber(session["resetAt"]) == nil then + session["resetAt"] = (time and time()) or 0 + end + return session +end + +function zButtonAmmo_ResetSession() + local session = zButtonAmmo_EnsureSession() + session["consumed"] = 0 + session["byName"] = {} + session["resetAt"] = (time and time()) or 0 + zButtonAmmo_LastByName = nil + zButtonAmmo_LastCategory = nil + if MTH and MTH.Print then + MTH:Print("Ammo usage counter reset.") + end +end + +local function zButtonAmmo_CurrentCountsByName(category) + local counts = {} + for name, info in pairs(zButtonAmmo_Cache) do + if info and info.type == category then + counts[name] = (tonumber(info.count) or 0) + end + end + return counts +end + +local function zButtonAmmo_SampleConsumption() + local category = zButtonAmmo_GetEquippedAmmoCategory() + if not category then + zButtonAmmo_LastByName = nil + zButtonAmmo_LastCategory = nil + return + end + local current = zButtonAmmo_CurrentCountsByName(category) + local now = GetTime and GetTime() or 0 + local last = zButtonAmmo_LastByName + local categoryChanged = (category ~= zButtonAmmo_LastCategory) + zButtonAmmo_LastCategory = category + -- Baseline-only during the post-load settle window, on weapon/category + -- swaps, and on the first sample — these are not real shots. + if now < (zButtonAmmo_SettleUntil or 0) or categoryChanged or last == nil then + zButtonAmmo_LastByName = current + return + end + local session = zButtonAmmo_EnsureSession() + for name, lastCount in pairs(last) do + local cur = current[name] or 0 + if cur < lastCount then + local delta = lastCount - cur + session["consumed"] = (tonumber(session["consumed"]) or 0) + delta + session["byName"][name] = (tonumber(session["byName"][name]) or 0) + delta + end + end + zButtonAmmo_LastByName = current +end + +local function zButtonAmmo_FormatElapsed(seconds) + if seconds < 0 then seconds = 0 end + local mins = math.floor(seconds / 60) + local hrs = math.floor(mins / 60) + if hrs > 0 then + return hrs .. "h " .. (mins - hrs * 60) .. "m" + elseif mins > 0 then + return mins .. "m" + end + return math.floor(seconds) .. "s" +end + +function zButtonAmmo_AppendSessionReport(tooltip) + if not (tooltip and tooltip.AddLine) then + return + end + local category = zButtonAmmo_GetEquippedAmmoCategory() + if category then + local total = zButtonAmmo_GetTotalAmmoCount(category) + tooltip:AddLine("In bags: " .. total .. " " .. (category == "Bullet" and "bullets" or "arrows"), 0.8, 0.8, 0.8) + end + local session = zButtonAmmo_EnsureSession() + local used = tonumber(session["consumed"]) or 0 + local elapsed = ((time and time()) or 0) - (tonumber(session["resetAt"]) or 0) + if elapsed < 0 then elapsed = 0 end + tooltip:AddLine("Used since reset: " .. used .. " (" .. zButtonAmmo_FormatElapsed(elapsed) .. ")", 1, 0.82, 0) + if used > 0 and elapsed >= 60 then + tooltip:AddLine(string.format("Avg: ~%.1f /min", used / (elapsed / 60)), 0.7, 0.7, 0.7) + end + local byName = session["byName"] + if type(byName) == "table" and used > 0 then + local rows = {} + for name, cnt in pairs(byName) do + if (tonumber(cnt) or 0) > 0 then + table.insert(rows, { name = name, count = tonumber(cnt) or 0 }) + end + end + if table.getn(rows) > 0 then + table.sort(rows, function(a, b) return a.count > b.count end) + tooltip:AddLine(" ") + tooltip:AddLine("Breakdown:", 0.9, 0.9, 0.9) + for i = 1, table.getn(rows) do + local row = rows[i] + local frac = row.count / used + local right = row.count .. " " .. math.floor(frac * 100 + 0.5) .. "%" + if type(tooltip.AddDoubleLine) == "function" then + tooltip:AddDoubleLine(row.name, right, 1, 1, 1, 0.6, 0.8, 1) + else + tooltip:AddLine(row.name .. ": " .. right, 1, 1, 1) + end + end + end + end + tooltip:AddLine("Left-click: reset counter", 0.5, 0.5, 0.5) +end + local function zButtonAmmo_UpdateTotalPercent(button) local totalText = zButtonAmmo_EnsureTotalPercentText(button) if not totalText then @@ -610,6 +745,86 @@ local function zButtonAmmo_ShouldShowTotalCount() return saved["counttotal"] and true or false end +local function zButtonAmmo_ShouldLowFlash() + local saved = zButtonAmmo_GetSaved() + return saved["lowflash"] and true or false +end + +-- Low-ammo threshold mirrors SmartAmmo's when available. +local function zButtonAmmo_GetLowThreshold() + if type(MTHSmartAmmo_GetLowAmmoThreshold) == "function" then + local t = tonumber(MTHSmartAmmo_GetLowAmmoThreshold()) + if t and t > 0 then + return t + end + end + return 200 +end + +local function zButtonAmmo_EnsureLowFlash(button) + if not button then + return nil + end + if button.ammoLowFlash then + return button.ammoLowFlash + end + local tex = button:CreateTexture(nil, "OVERLAY") + tex:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + tex:SetBlendMode("ADD") + tex:SetVertexColor(1, 0.12, 0.12) + tex:SetPoint("CENTER", button, "CENTER", 0, 0) + tex:SetWidth((button:GetWidth() or 36) * 1.6) + tex:SetHeight((button:GetHeight() or 36) * 1.6) + tex:Hide() + button.ammoLowFlash = tex + return tex +end + +local MTH_ZH_AmmoLowFlashDriver +local zButtonAmmo_LowFlashElapsed = 0 + +local function zButtonAmmo_LowFlashOnUpdate() + if not (zButtonAmmo and zButtonAmmo.ammoLowFlash and zButtonAmmo.ammoLowFlash:IsShown()) then + return + end + zButtonAmmo_LowFlashElapsed = zButtonAmmo_LowFlashElapsed + (arg1 or 0) + zButtonAmmo.ammoLowFlash:SetAlpha(0.45 + 0.4 * math.sin(zButtonAmmo_LowFlashElapsed * 4)) +end + +local function zButtonAmmo_SetLowFlashRunning(running) + if running and not MTH_ZH_AmmoLowFlashDriver then + MTH_ZH_AmmoLowFlashDriver = CreateFrame("Frame", "MTH_ZH_AmmoLowFlashDriver") + end + if not MTH_ZH_AmmoLowFlashDriver then + return + end + MTH_ZH_AmmoLowFlashDriver:SetScript("OnUpdate", running and zButtonAmmo_LowFlashOnUpdate or nil) +end + +local function zButtonAmmo_UpdateLowFlash(button) + local tex = zButtonAmmo_EnsureLowFlash(button) + if not tex then + return + end + local low = false + if zButtonAmmo_ShouldLowFlash() then + local category = zButtonAmmo_GetEquippedAmmoCategory() + if category then + local total = zButtonAmmo_GetTotalAmmoCount(category) + if total < zButtonAmmo_GetLowThreshold() then + low = true + end + end + end + if low then + tex:Show() + zButtonAmmo_SetLowFlashRunning(true) + else + tex:Hide() + zButtonAmmo_SetLowFlashRunning(false) + end +end + function zButtonAmmo_UpdateButton(button) if not button then button = this @@ -632,6 +847,10 @@ function zButtonAmmo_UpdateButton(button) if button == zButtonAmmo and button.ammoTotalGauge then button.ammoTotalGauge:Hide() end + if button == zButtonAmmo and button.ammoLowFlash then + button.ammoLowFlash:Hide() + zButtonAmmo_SetLowFlashRunning(false) + end return end @@ -683,6 +902,7 @@ function zButtonAmmo_UpdateButton(button) end zButtonAmmo_UpdateTotalPercent(button) zButtonAmmo_UpdateTotalGauge(button) + zButtonAmmo_UpdateLowFlash(button) end end @@ -843,6 +1063,12 @@ local function zButtonAmmo_EnsureSpellOrder(ammoList) if saved["showtotalperc"] == nil then saved["showtotalperc"] = 1 end + if saved["lowflash"] == nil then + saved["lowflash"] = 1 + end + if saved["parenttooltip"] == nil then + saved["parenttooltip"] = 1 + end if not saved["parent"] then saved["parent"] = {} end @@ -887,6 +1113,7 @@ local function zButtonAmmo_ApplyRuntimeSettings() end local saved = zButtonAmmo_GetSaved() zButtonAmmo.tooltip = saved["tooltip"] and true or false + zButtonAmmo.parentTooltip = saved["parenttooltip"] and true or false zButtonAmmo.hideonclick = saved["children"] and saved["children"]["hideonclick"] and true or false zButtonAmmo.expandonhover = saved["children"] and saved["children"]["expandonhover"] and true or false zButtonAmmo.fadetimer = saved["children"] and tonumber(saved["children"]["fadetimer"]) or 0 @@ -1013,6 +1240,16 @@ function zButtonAmmo_OnEvent() end zButtonAmmo.customSetButtons = zButtonAmmo_SetButtons zButtonAmmo.customUpdateButton = zButtonAmmo_UpdateButton + zButtonAmmo.extraTooltip = zButtonAmmo_AppendSessionReport + zButtonAmmo.suppressItemTooltip = true + zButtonAmmo.beforeclick = function(btn) + if btn ~= zButtonAmmo then + return false + end + zButtonAmmo_ResetSession() + return true + end + zButtonAmmo_SettleUntil = (GetTime and GetTime() or 0) + 5 zButtonAmmo_CreateButtons() MTH_ZH_AmmoAdjust = CreateFrame("Frame", "MTH_ZH_AmmoAdjust") MTH_ZH_AmmoAdjust:RegisterEvent("UNIT_INVENTORY_CHANGED") @@ -1137,6 +1374,8 @@ function zButtonAmmo_Reset() saved["children"]["hideonclick"] = 1 saved["showammoname"] = 1 saved["showtotalperc"] = 1 + saved["lowflash"] = 1 + saved["parenttooltip"] = 1 saved["lastEquipped"] = nil end @@ -1215,6 +1454,7 @@ function MTH_ZH_AmmoAdjust_OnEvent() local listChanged = (ZHunterMod_Ammo_Buttons ~= ammoList) ZHunterMod_Ammo_Buttons = ammoList zButtonAmmo_RebuildCache() + zButtonAmmo_SampleConsumption() local cacheSignature = zButtonAmmo_GetCacheSignature(ammoList) local rebuiltButtons = false if listChanged or cacheSignature ~= zButtonAmmo_LastCacheSignature then diff --git a/modules/zhunter/zButtonPet.lua b/modules/zhunter/zButtonPet.lua index 88d6a37..ce8c7a1 100644 --- a/modules/zhunter/zButtonPet.lua +++ b/modules/zhunter/zButtonPet.lua @@ -57,6 +57,68 @@ local function zButtonPet_GetSaved() return currentRoot["zButtonPet"] end +-- Persistent "feed me" nudge: pulse the zPet parent while the pet is Unhappy. +local MTH_ZH_PetHappyFlashDriver +local zButtonPet_HappyFlashElapsed = 0 + +local function zButtonPet_EnsureHappyFlash() + local button = getglobal("zButtonPet") + if not button then + return nil + end + if button.petHappyFlash then + return button.petHappyFlash + end + local tex = button:CreateTexture(nil, "OVERLAY") + tex:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + tex:SetBlendMode("ADD") + tex:SetVertexColor(1, 0.15, 0.15) + tex:SetPoint("CENTER", button, "CENTER", 0, 0) + tex:SetWidth((button:GetWidth() or 36) * 1.6) + tex:SetHeight((button:GetHeight() or 36) * 1.6) + tex:Hide() + button.petHappyFlash = tex + return tex +end + +local function zButtonPet_HappyFlashOnUpdate() + local button = getglobal("zButtonPet") + if not (button and button.petHappyFlash and button.petHappyFlash:IsShown()) then + return + end + zButtonPet_HappyFlashElapsed = zButtonPet_HappyFlashElapsed + (arg1 or 0) + button.petHappyFlash:SetAlpha(0.4 + 0.4 * math.sin(zButtonPet_HappyFlashElapsed * 3)) +end + +local function zButtonPet_SetHappyFlashRunning(running) + if running and not MTH_ZH_PetHappyFlashDriver then + MTH_ZH_PetHappyFlashDriver = CreateFrame("Frame", "MTH_ZH_PetHappyFlashDriver") + end + if not MTH_ZH_PetHappyFlashDriver then + return + end + MTH_ZH_PetHappyFlashDriver:SetScript("OnUpdate", running and zButtonPet_HappyFlashOnUpdate or nil) +end + +local function zButtonPet_UpdateHappyFlash(happiness) + local tex = zButtonPet_EnsureHappyFlash() + if not tex then + return + end + local saved = zButtonPet_GetSaved() + local enabled = saved["unhappyflash"] + if enabled == nil then + enabled = 1 + end + if enabled and happiness ~= nil and tonumber(happiness) == 1 then + tex:Show() + zButtonPet_SetHappyFlashRunning(true) + else + tex:Hide() + zButtonPet_SetHappyFlashRunning(false) + end +end + local function zButtonPet_EnsureConfig() local saved = zButtonPet_GetSaved() saved["count"] = nil @@ -131,6 +193,9 @@ local function zButtonPet_EnsureConfig() saved["visible"][i] = 1 end end + if saved["unhappyflash"] == nil then + saved["unhappyflash"] = 1 + end end local function zButtonPet_ApplyRuntimeSettings() @@ -347,6 +412,7 @@ local function zButtonPet_RefreshSavedPetState(saved, corePetInfo) end local prevHappiness = saved["pet"]["happiness"] saved["pet"]["happiness"] = happiness + zButtonPet_UpdateHappyFlash(happiness) -- Animations: flash the pet button when happiness or loyalty changes. if type(MTH_FX_Flash) == "function" and type(MTH_FX_IsEnabled) == "function" then From 357bf1e6da11aa14d1bb8b8cee03ea7cf3f34ed8 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 8 Oct 2026 08:40:37 +0200 Subject: [PATCH 12/14] MM widget: show SKIP on the action cell when the active ammo school is immune --- CHANGELOG.md | 2 ++ modules/expammo/engine.lua | 26 +++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d934a5..df37215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ All notable changes to MetaHunt will be documented in this file. ### Fixed +- **MM Widget — immune schools now show SKIP**: When the current target is immune to the active Explosive Ammunition school, the widget already skipped the special shot (casting Aimed/Steady instead), but the action cell still read "USE" — indistinguishable from a normal shot. It now shows a red **SKIP** with the skipped shot's icon so the skip is visible. + - **Feed-O-Matic — feeding food that's too low level**: With nearly-full bags, FeedOMatic could keep trying to feed a small stack whose level was too low for your pet, which the server refuses ("That food's level is not high enough"). It now skips below-minimum-level foods and only offers food the pet can actually eat. - **Pets — taming & feeding history disappearing**: Fixed a long-standing issue where a pet's taming info and feeding history could vanish from the Book — caused by the server handing out non-unique pet GUIDs that split one pet into several empty duplicate records. The Stable scan now re-adopts the real record instead of an empty duplicate, so the data reappears on your next stable visit (no data rewrite needed). diff --git a/modules/expammo/engine.lua b/modules/expammo/engine.lua index 1ea62ee..9166d7f 100644 --- a/modules/expammo/engine.lua +++ b/modules/expammo/engine.lua @@ -412,6 +412,13 @@ local ICON_STEADY = "Interface\\Icons\\Ability_Hunter_SteadyShot" local ICON_MULTI = "Interface\\Icons\\Ability_UpgradeMoonGlaive" local ICON_SERPENT = "Interface\\Icons\\Ability_Hunter_QuickShot" local ICON_ARCANE = "Interface\\Icons\\Ability_ImpalingBolt" + +-- Consume-spell icon per state (shown on the action cell). +local CONSUME_ICON = { + [EXPLOSIVE] = ICON_MULTI, + [POISONOUS] = ICON_SERPENT, + [ENCHANTED] = ICON_ARCANE, +} -- Aura/effect textures (aura-dump debug capture) local ICON_EXP = "Interface\\Icons\\Ability_SearingArrow" -- Explosive Ammunition debuff local ICON_POI = "Interface\\Icons\\Ability_PoisonArrow" -- Poisonous Ammunition debuff @@ -516,13 +523,15 @@ local function MTH_EA_GetSpellCD(spellName) end -- Action button: what spell to show and what spell to cast right now. --- Returns displaySpell, icon, r, g, b, cooldownRemaining, castSpell +-- Returns displaySpell, icon, r, g, b, cooldownRemaining, castSpell, blocked 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. + -- skip the consume-spell and fall through to Aimed/Steady instead. The cell + -- shows the skipped consume-spell icon in red with a SKIP hint. 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) + local skipIcon = CONSUME_ICON[rt.state] or ICON_AIMED + return SPELL_AIMED, skipIcon, 1.00, 0.25, 0.25, cd, (cd > 0 and SPELL_STEADY or SPELL_AIMED), true end if rt.state == EXPLOSIVE then @@ -718,9 +727,16 @@ MTH_EA_UpdateUI = function() -- ── BOTTOM cell: action button ──────────────────────────── if cellBot then - local displaySpell, icon, r, g, b, cdRemain = MTH_EA_GetActionSpell() + local displaySpell, icon, r, g, b, cdRemain, _castSpell, blocked = MTH_EA_GetActionSpell() - if rt.state == IDLE then + if blocked then + -- Current ammo school is immune: skip the consume-spell. + cellBotBlink = 0 + cellBotIcon:SetTexture(icon) + cellBotIcon:SetVertexColor(r, g, b, 1) + cellBot:SetBackdropBorderColor(1, 0, 0, 1) + cellBotCD:SetText("|cffff3030SKIP|r") + elseif rt.state == IDLE then -- Always display Aimed Shot icon + its CD. -- The action (keybind/click) casts Steady Shot while Aimed is on CD. cellBotIcon:SetTexture(ICON_AIMED) From 19be5f76ead666733609569e26e25cf5ece79fa3 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 8 Oct 2026 09:22:15 +0200 Subject: [PATCH 13/14] Changelog: drop pet level-up line, add pet login-notification fix --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df37215..815ebcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ All notable changes to MetaHunt will be documented in this file. - **Pets — taming & feeding history disappearing**: Fixed a long-standing issue where a pet's taming info and feeding history could vanish from the Book — caused by the server handing out non-unique pet GUIDs that split one pet into several empty duplicate records. The Stable scan now re-adopts the real record instead of an empty duplicate, so the data reappears on your next stable visit (no data rewrite needed). -- **Pet level-up message**: Removed a nonsensical pet level-up line ("the trainer wants coin") — there's no coin involved when your pet simply gains a level. +- **Pet notifications on login**: Your pet's existing level and loyalty no longer trigger celebration pop-ups right after you log in or reload — a short grace period means only genuine in-session gains are announced. ## [2.0.0] - 2026-09-14 From afa9091ab05e1ba351669e8cb716deedf911dbc2 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 8 Oct 2026 11:38:51 +0200 Subject: [PATCH 14/14] Release 2.1.0: version bump + clearer SmartAmmo out-of-ammo message --- CHANGELOG.md | 4 +++- MetaHunt.toc | 2 +- api/constants.lua | 2 +- api/core-framework.lua | 2 +- api/fx-celebrate.lua | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 815ebcd..f659792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to MetaHunt will be documented in this file. -## [2.1.0] - Unreleased +## [2.1.0] - 2026-10-08 ### Added @@ -23,6 +23,8 @@ All notable changes to MetaHunt will be documented in this file. - **zAmmo — capacity-based counter colouring**: The heat colour on the ammo stack counters now scales to your real quiver/pouch capacity instead of a fixed number, so a stack turns green as it approaches filling your quiver (falls back to the old scale when no quiver/pouch is equipped). +- **SmartAmmo — clearer out-of-ammo alert**: The centre-screen auto-fallback message now names the ammo that ran out (e.g. "Out of Rough Arrow!") instead of a generic "Out of ammo!", so you instantly know which stack emptied. + ### Fixed - **MM Widget — immune schools now show SKIP**: When the current target is immune to the active Explosive Ammunition school, the widget already skipped the special shot (casting Aimed/Steady instead), but the action cell still read "USE" — indistinguishable from a normal shot. It now shows a red **SKIP** with the skipped shot's icon so the skip is visible. diff --git a/MetaHunt.toc b/MetaHunt.toc index 09aa0dc..f635a16 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -1,5 +1,5 @@ ## Interface: 11200 -## Version: 2.0.0 +## Version: 2.1.0 ## 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. diff --git a/api/constants.lua b/api/constants.lua index 24b74e8..acb7623 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 "2.0.0" +MTH_CONST.version = MTH_CONST.version or "2.1.0" MTH_CONST.WEAPON_TYPES = { BOWS = "Bows", diff --git a/api/core-framework.lua b/api/core-framework.lua index 305d972..f9e69d7 100644 --- a/api/core-framework.lua +++ b/api/core-framework.lua @@ -1,5 +1,5 @@ MTH = MTH or { - version = "2.0.0", + version = "2.1.0", name = "MetaHunt", modules = {}, config = {}, diff --git a/api/fx-celebrate.lua b/api/fx-celebrate.lua index d76b859..3bdb56b 100644 --- a/api/fx-celebrate.lua +++ b/api/fx-celebrate.lua @@ -754,7 +754,7 @@ function MTH_CelebrateAmmoFallback(previousAmmo, newAmmo) if not MTH_FX_IsEnabled("combat.ammoout") or not Sauce or not MTH_FX_EnsureFrames() then return end MTH_FX_TitleFS:SetTextColor(1, 0.15, 0.15) - MTH_FX_TitleFS:SetText("Out of ammo!") + MTH_FX_TitleFS:SetText("Out of " .. tostring(previousAmmo or "ammo") .. "!") MTH_FX_SubFS:SetText("Equipped " .. tostring(newAmmo or "fallback ammunition")) Sauce:Stop(MTH_FX_MsgFrame)