diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5123e..f659792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,39 @@ All notable changes to MetaHunt will be documented in this file. +## [2.1.0] - 2026-10-08 + +### 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). + +- **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). + +- **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. + +- **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 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 Resurrecting on Octowow. 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-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 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/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/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 "") .. "'" 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) diff --git a/api/hunterbook.lua b/api/hunterbook.lua index 9f4c3b5..28cbe3d 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -3063,7 +3063,6 @@ local function MTH_BOOK_InitRankDropdown() MTH_BOOK_UpdateRankDropdownText() end - local function MTH_BOOK_InitPetLearnSourceDropdown() local sourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown") if not sourceDropdown then return end @@ -4730,6 +4729,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/options-zbuttons.lua b/api/options-zbuttons.lua index e7cbad6..52cabbd 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -301,6 +301,40 @@ 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 + if saved["counttotal"] == nil then + 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 + if saved["cooldown"] == nil then + saved["cooldown"] = true + end + saved["cooldown"] = saved["cooldown"] and true or false end return saved @@ -459,6 +493,72 @@ 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"], + 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, + }) + 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")) childrenBox:Slider(containerName.."ButtonSize", { label = MTH_ZB_L("ZB_LABEL_BUTTON_SIZE", "Button Size"), @@ -557,6 +657,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/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) 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 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 4786c7f..6a8f0bc 100644 --- a/modules/zhunter/zButtonAmmo.lua +++ b/modules/zhunter/zButtonAmmo.lua @@ -21,6 +21,9 @@ 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"]["lowflash"] = 1 + root["zButtonAmmo"]["parenttooltip"] = 1 root["zButtonAmmo"]["lastEquipped"] = nil end @@ -431,6 +434,397 @@ 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 + +-- 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 + +-- 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 + 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 + +local function zButtonAmmo_ShouldShowTotalCount() + local saved = zButtonAmmo_GetSaved() + 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 @@ -447,6 +841,16 @@ 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 + if button == zButtonAmmo and button.ammoLowFlash then + button.ammoLowFlash:Hide() + zButtonAmmo_SetLowFlashRunning(false) + end return end @@ -473,7 +877,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 @@ -484,6 +888,22 @@ function zButtonAmmo_UpdateButton(button) countText:SetText("Out!") 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) + zButtonAmmo_UpdateLowFlash(button) + end end function zButtonAmmo_SetButtons(parent, ammoList) @@ -640,6 +1060,15 @@ local function zButtonAmmo_EnsureSpellOrder(ammoList) if saved["showammoname"] == nil then saved["showammoname"] = 1 end + 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 @@ -684,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 @@ -810,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") @@ -933,6 +1373,9 @@ function zButtonAmmo_Reset() saved["children"]["size"] = 36 saved["children"]["hideonclick"] = 1 saved["showammoname"] = 1 + saved["showtotalperc"] = 1 + saved["lowflash"] = 1 + saved["parenttooltip"] = 1 saved["lastEquipped"] = nil end @@ -1011,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 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()