792 lines
36 KiB
Lua
792 lines
36 KiB
Lua
-- Event-driven achievement tracking, character scan, and hotkey binding.
|
|
|
|
local frame = CreateFrame("Frame", "RelationshipsAchievementsEventFrame", UIParent)
|
|
|
|
-- Turtle/OctoWoW builds expose slightly different event sets. Registering an
|
|
-- unknown event can abort an entire Lua file on some 1.12 clients, so optional
|
|
-- events are protected. The standard Vanilla events remain the primary path.
|
|
local function SafeRegister(name)
|
|
pcall(frame.RegisterEvent, frame, name)
|
|
end
|
|
local trackedEvents = {
|
|
"PLAYER_LOGIN", "PLAYER_ENTERING_WORLD", "PLAYER_LEVEL_UP", "PLAYER_DEAD",
|
|
"QUEST_COMPLETE", "QUEST_TURNED_IN", "ZONE_CHANGED_NEW_AREA", "ZONE_CHANGED",
|
|
"ZONE_CHANGED_INDOORS", "CHAT_MSG_COMBAT_HONOR_GAIN",
|
|
"CHAT_MSG_COMBAT_FACTION_CHANGE", "SKILL_LINES_CHANGED", "CHAT_MSG_LOOT",
|
|
"CHAT_MSG_MONEY", "CHAT_MSG_COMBAT_HOSTILE_DEATH", "CHAT_MSG_COMBAT_XP_GAIN",
|
|
"CHAT_MSG_SYSTEM", "UPDATE_FACTION", "PARTY_MEMBERS_CHANGED", "FRIENDLIST_UPDATE",
|
|
"GUILD_ROSTER_UPDATE", "PLAYER_UNGHOST", "PLAYER_ALIVE", "CHAT_MSG_TEXT_EMOTE",
|
|
"CHAT_MSG_EMOTE", "PLAYER_PVP_RANK_CHANGED", "PLAYER_PVP_KILLS_CHANGED",
|
|
"PLAYER_PVPKILLS_CHANGED", "HONOR_CURRENCY_UPDATE", "HEARTHSTONE_BOUND",
|
|
"BANKFRAME_OPENED", "TRAINER_SHOW", "MAIL_SEND_SUCCESS", "PLAYER_EQUIPMENT_CHANGED",
|
|
"BAG_UPDATE", "CHARACTER_POINTS_CHANGED", "SPELLS_CHANGED", "UNIT_HEALTH", "PLAYER_MONEY",
|
|
-- Vanilla 1.12 event names (PLAYER_EQUIPMENT_CHANGED does not exist here).
|
|
"UNIT_INVENTORY_CHANGED", "ADDON_LOADED", "MERCHANT_SHOW", "MERCHANT_CLOSED"
|
|
}
|
|
for i = 1, table.getn(trackedEvents) do SafeRegister(trackedEvents[i]) end
|
|
|
|
-- ============ PvP rank scanning ============
|
|
-- Vanilla UnitPVPRank("player") returns 0 (no rank) or 5..19 where the
|
|
-- lowest ranked player is Private/Scout = 5. Subtract 4 to get 1..14.
|
|
local function ScanPvPRank()
|
|
if not UnitPVPRank then return end
|
|
local raw = UnitPVPRank("player") or 0
|
|
-- Lifetime stats preserve the highest rank ever earned, even after weekly
|
|
-- decay. Use whichever is higher so installing later still grants every
|
|
-- lower title in the chain.
|
|
if GetPVPLifetimeStats then
|
|
local _, _, highest = GetPVPLifetimeStats()
|
|
if highest and highest > raw then raw = highest end
|
|
end
|
|
if raw == 0 then return end
|
|
local rank = raw
|
|
if rank >= 5 then rank = rank - 4 end
|
|
if rank < 1 then return end
|
|
|
|
if rank >= 1 then RelationshipsAchievements:Unlock(2001) end -- Private / Scout
|
|
if rank >= 3 then RelationshipsAchievements:Unlock(2002) end -- Sergeant / Senior Sergeant
|
|
if rank >= 6 then RelationshipsAchievements:Unlock(2003) end -- Knight / Stone Guard
|
|
if rank >= 9 then RelationshipsAchievements:Unlock(2004) end -- Champion / Centurion
|
|
if rank >= 12 then RelationshipsAchievements:Unlock(2005) end -- Marshal / Legionnaire
|
|
if rank >= 13 then RelationshipsAchievements:Unlock(2006) end -- Field Marshal / Warlord
|
|
if rank >= 14 then
|
|
local _, faction = UnitFactionGroup("player")
|
|
if faction == "Horde" then
|
|
RelationshipsAchievements:Unlock(1003) -- High Warlord
|
|
else
|
|
RelationshipsAchievements:Unlock(1002) -- Grand Marshal
|
|
end
|
|
end
|
|
end
|
|
|
|
local function ScanPvPKills()
|
|
if not GetPVPLifetimeStats then return end
|
|
local honorable = GetPVPLifetimeStats() or 0
|
|
RelationshipsAchievements:UpdateStat("honorKills", honorable)
|
|
if honorable >= 1 then RelationshipsAchievements:Unlock(501) end
|
|
end
|
|
|
|
local function ScanTalents()
|
|
if not GetNumTalentTabs or not GetNumTalents or not GetTalentInfo then return end
|
|
local spent = 0
|
|
for tab = 1, GetNumTalentTabs() do
|
|
for talent = 1, GetNumTalents(tab) do
|
|
local _, _, _, _, rank = GetTalentInfo(tab, talent)
|
|
spent = spent + (rank or 0)
|
|
end
|
|
end
|
|
if spent >= 1 then RelationshipsAchievements:Unlock(1111) end
|
|
if spent >= 51 then RelationshipsAchievements:Unlock(1112) end
|
|
end
|
|
|
|
local previousTalentSpent
|
|
local function ScanTalentChange()
|
|
if not GetNumTalentTabs or not GetNumTalents or not GetTalentInfo then return end
|
|
local spent = 0
|
|
for tab = 1, GetNumTalentTabs() do
|
|
for talent = 1, GetNumTalents(tab) do
|
|
local _, _, _, _, rank = GetTalentInfo(tab, talent)
|
|
spent = spent + (rank or 0)
|
|
end
|
|
end
|
|
if previousTalentSpent and previousTalentSpent > 0 and spent == 0 then RelationshipsAchievements:Unlock(1113) end
|
|
previousTalentSpent = spent
|
|
ScanTalents()
|
|
end
|
|
|
|
-- Extract item quality from a link's color code. Robust when GetItemInfo has
|
|
-- not cached the item yet (common in 1.12 right after login or equip).
|
|
local QUALITY_BY_COLOR = {
|
|
["ff9d9d9d"] = 0, ["ffffffff"] = 1, ["ff1eff00"] = 2,
|
|
["ff0070dd"] = 3, ["ffa335ee"] = 4, ["ffff8000"] = 5,
|
|
}
|
|
local function QualityFromLink(link)
|
|
if not link then return nil end
|
|
local _, _, color = string.find(link, "|c(%x%x%x%x%x%x%x%x)")
|
|
if not color then return nil end
|
|
return QUALITY_BY_COLOR[string.lower(color)]
|
|
end
|
|
|
|
local function ScanEquipmentAndBags()
|
|
if GetInventoryItemLink then
|
|
if GetInventoryItemLink("player", 4) then RelationshipsAchievements:Unlock(1203) end
|
|
if GetInventoryItemLink("player", 19) then RelationshipsAchievements:Unlock(109) end
|
|
-- Well Equipped: every equipped armor/weapon slot must be Rare+.
|
|
-- Slots that are legitimately optional on a spec (shirt, tabard,
|
|
-- ranged, off-hand on 2H users) are skipped from the "all filled"
|
|
-- check but still count when present.
|
|
local requiredSlots = { 1,2,3,5,6,7,8,9,10,11,12,13,14,15,16 }
|
|
local optionalSlots = { 17, 18 } -- off-hand, ranged
|
|
local allRare, missing, epic = true, false, false
|
|
local function eval(slot, required)
|
|
local link = GetInventoryItemLink("player", slot)
|
|
if not link then
|
|
if required then missing = true end
|
|
return
|
|
end
|
|
local _, _, q = GetItemInfo(link)
|
|
local quality = q or QualityFromLink(link)
|
|
if quality and quality >= 4 then epic = true end
|
|
if required and (not quality or quality < 3) then allRare = false end
|
|
local itemName = GetItemInfo(link)
|
|
if itemName == "Atiesh, Greatstaff of the Guardian" then RelationshipsAchievements:Unlock(1004) end
|
|
if itemName == "Thunderfury, Blessed Blade of the Windseeker" then RelationshipsAchievements:Unlock(1005) end
|
|
if itemName == "Sulfuras, Hand of Ragnaros" then RelationshipsAchievements:Unlock(1006) end
|
|
end
|
|
for i = 1, table.getn(requiredSlots) do eval(requiredSlots[i], true) end
|
|
for i = 1, table.getn(optionalSlots) do eval(optionalSlots[i], false) end
|
|
if not missing and allRare then RelationshipsAchievements:Unlock(1208) end
|
|
if epic then RelationshipsAchievements:Unlock(1209) end
|
|
end
|
|
if GetContainerNumSlots then
|
|
local bags, has16 = 0, false
|
|
for bag = 1, 4 do
|
|
local slots = GetContainerNumSlots(bag) or 0
|
|
if slots > 0 then bags = bags + 1 end
|
|
if slots >= 16 then has16 = true end
|
|
end
|
|
if bags >= 4 then RelationshipsAchievements:Unlock(1202) end
|
|
if has16 then RelationshipsAchievements:Unlock(1201) end
|
|
end
|
|
end
|
|
|
|
-- Hidden tooltip used for spellbook scanning. Vanilla 1.12 does not expose
|
|
-- mount / non-combat-pet flags on spells, so we read the tooltip text — it
|
|
-- reliably distinguishes them across every locale-agnostic edge case we care
|
|
-- about because the mount / pet tooltip prefixes are stable in enUS clients.
|
|
local RA_ScanTip = CreateFrame("GameTooltip", "RelationshipsAchievementsScanTip", UIParent, "GameTooltipTemplate")
|
|
RA_ScanTip:SetOwner(UIParent, "ANCHOR_NONE")
|
|
|
|
local function TooltipText(index, book)
|
|
RA_ScanTip:ClearLines()
|
|
RA_ScanTip:SetSpell(index, book)
|
|
local text = ""
|
|
for line = 1, RA_ScanTip:NumLines() do
|
|
local fs = getglobal("RelationshipsAchievementsScanTipTextLeft"..line)
|
|
if fs then
|
|
local t = fs:GetText()
|
|
if t then text = text .. "\n" .. t end
|
|
end
|
|
end
|
|
return string.lower(text)
|
|
end
|
|
|
|
local function ScanSpellbook()
|
|
if not GetSpellName then return end
|
|
local book = BOOKTYPE_SPELL or "spell"
|
|
local mounts, pets = 0, 0
|
|
local hasEpicMount = false
|
|
local seen = {}
|
|
local i = 1
|
|
while true do
|
|
local name, rank = GetSpellName(i, book)
|
|
if not name then break end
|
|
local key = name .. "||" .. (rank or "")
|
|
if not seen[key] then
|
|
seen[key] = true
|
|
local tip = TooltipText(i, book)
|
|
-- Mounts: tooltip includes "mount speed" or "riding" descriptor.
|
|
-- We match both the classic "increases speed by X%" line and the
|
|
-- explicit "summons and dismisses a rideable" phrasing.
|
|
local isMount = string.find(tip, "mount speed", 1, true)
|
|
or string.find(tip, "rideable", 1, true)
|
|
or string.find(tip, "summons and dismisses a rideable", 1, true)
|
|
-- Non-combat pets: tooltip contains "right click to dismiss"
|
|
-- or the classic "companion" descriptor. Explicitly exclude
|
|
-- combat pets (hunter/warlock), which say "summons your pet".
|
|
local isPet = (string.find(tip, "companion", 1, true)
|
|
or string.find(tip, "right click to dismiss", 1, true))
|
|
and not string.find(tip, "summons your", 1, true)
|
|
and not string.find(tip, "combat pet", 1, true)
|
|
if isMount then
|
|
mounts = mounts + 1
|
|
-- Epic ground mounts grant +100% speed in vanilla.
|
|
if string.find(tip, "100%", 1, true) or string.find(tip, "epic", 1, true) then
|
|
hasEpicMount = true
|
|
end
|
|
elseif isPet then
|
|
pets = pets + 1
|
|
end
|
|
end
|
|
i = i + 1
|
|
end
|
|
|
|
-- Fallback: Riding skill unambiguously proves mount ownership even if
|
|
-- tooltip scanning missed a custom mount (Turtle/OctoWoW variants).
|
|
if GetNumSkillLines then
|
|
for s = 1, GetNumSkillLines() do
|
|
local skill, header, _, rank = GetSkillLineInfo(s)
|
|
if not header and skill == "Riding" and rank and rank > 0 then
|
|
if mounts < 1 then mounts = 1 end
|
|
if rank >= 150 then hasEpicMount = true end
|
|
end
|
|
end
|
|
end
|
|
|
|
RelationshipsAchievements:UpdateStat("mountCount", mounts)
|
|
RelationshipsAchievements:UpdateStat("petCount", pets)
|
|
if mounts >= 1 then RelationshipsAchievements:Unlock(211) end
|
|
if hasEpicMount and (UnitLevel("player") or 1) >= 60 then
|
|
RelationshipsAchievements:Unlock(212)
|
|
end
|
|
if pets >= 1 then RelationshipsAchievements:Unlock(1204) end
|
|
end
|
|
|
|
function RelationshipsAchievements_ScanCharacter()
|
|
RelationshipsAchievements:Init()
|
|
|
|
local lvl = UnitLevel("player") or 1
|
|
if lvl >= 5 then RelationshipsAchievements:Unlock(216) end
|
|
if lvl >= 10 then RelationshipsAchievements:Unlock(201) end
|
|
if lvl >= 15 then RelationshipsAchievements:Unlock(218) end
|
|
if lvl >= 20 then RelationshipsAchievements:Unlock(202) end
|
|
if lvl >= 25 then RelationshipsAchievements:Unlock(219) end
|
|
if lvl >= 30 then RelationshipsAchievements:Unlock(203) end
|
|
if lvl >= 35 then RelationshipsAchievements:Unlock(220) end
|
|
if lvl >= 40 then RelationshipsAchievements:Unlock(204) end
|
|
if lvl >= 45 then RelationshipsAchievements:Unlock(221) end
|
|
if lvl >= 50 then RelationshipsAchievements:Unlock(205) end
|
|
if lvl >= 55 then RelationshipsAchievements:Unlock(222) end
|
|
if lvl >= 60 then RelationshipsAchievements:Unlock(206) end
|
|
|
|
-- Custom races and OctoWoW level milestone.
|
|
local race = UnitRace("player")
|
|
if race == "High Elf" or race == "HighElf" then RelationshipsAchievements:Unlock(1302) end
|
|
if race == "Goblin" then RelationshipsAchievements:Unlock(1303) end
|
|
if lvl >= 60 then RelationshipsAchievements:Unlock(1307) end
|
|
|
|
-- Class-at-60 achievements
|
|
if lvl >= 60 then
|
|
local _, class = UnitClass("player")
|
|
if class == "WARRIOR" then RelationshipsAchievements:Unlock(1101)
|
|
elseif class == "PALADIN" then RelationshipsAchievements:Unlock(1102)
|
|
elseif class == "HUNTER" then RelationshipsAchievements:Unlock(1103)
|
|
elseif class == "ROGUE" then RelationshipsAchievements:Unlock(1104)
|
|
elseif class == "PRIEST" then RelationshipsAchievements:Unlock(1105)
|
|
elseif class == "SHAMAN" then RelationshipsAchievements:Unlock(1106)
|
|
elseif class == "MAGE" then RelationshipsAchievements:Unlock(1107)
|
|
elseif class == "WARLOCK" then RelationshipsAchievements:Unlock(1108)
|
|
elseif class == "DRUID" then RelationshipsAchievements:Unlock(1109)
|
|
end
|
|
end
|
|
|
|
if GetGuildInfo and GetGuildInfo("player") then
|
|
RelationshipsAchievements:Unlock(103)
|
|
if GetNumGuildMembers and GetNumGuildMembers() >= 10 then RelationshipsAchievements:Unlock(1703) end
|
|
end
|
|
|
|
if GetNumPartyMembers and GetNumPartyMembers() > 0 then
|
|
RelationshipsAchievements:Unlock(105)
|
|
end
|
|
|
|
if GetNumFriends and GetNumFriends() > 0 then
|
|
RelationshipsAchievements:Unlock(102)
|
|
RelationshipsAchievements:UpdateStat("friendCount", GetNumFriends())
|
|
end
|
|
|
|
if GetInventoryItemLink and GetInventoryItemLink("player", 19) then
|
|
RelationshipsAchievements:Unlock(109)
|
|
end
|
|
|
|
-- Gold-in-bag achievements (retroactive on scan)
|
|
if GetMoney then
|
|
local copper = GetMoney() or 0
|
|
if copper >= 100 then RelationshipsAchievements:Unlock(1901) end -- 1 silver
|
|
if copper >= 10000 then RelationshipsAchievements:Unlock(1902) end -- 1 gold
|
|
if copper >= 5000000 then RelationshipsAchievements:Unlock(223) end -- 500g
|
|
if copper >= 10000000 then RelationshipsAchievements:Unlock(1903) end -- 1000g
|
|
if copper >= 50000000 then RelationshipsAchievements:Unlock(224) end -- 5000g
|
|
if copper >= 100000000 then RelationshipsAchievements:Unlock(1904) end -- 10000g
|
|
end
|
|
|
|
local zc = 0
|
|
for _ in pairs(RelationshipsAchievementsCharDB.zones) do zc = zc + 1 end
|
|
RelationshipsAchievements:UpdateStat("zoneCount", zc)
|
|
|
|
if GetNumSkillLines then
|
|
local highestProf = 0
|
|
local highestWeapon = 0
|
|
for i = 1, GetNumSkillLines() do
|
|
local name, isHeader, _, rank = GetSkillLineInfo(i)
|
|
if not isHeader and rank and rank > 0 then
|
|
if name == "Swords" or name == "Two-Handed Swords"
|
|
or name == "Axes" or name == "Two-Handed Axes"
|
|
or name == "Maces" or name == "Two-Handed Maces"
|
|
or name == "Daggers" or name == "Fist Weapons"
|
|
or name == "Polearms" or name == "Staves"
|
|
or name == "Bows" or name == "Crossbows" or name == "Guns"
|
|
or name == "Wands" or name == "Thrown" then
|
|
if rank > highestWeapon then highestWeapon = rank end
|
|
elseif name == "Cooking" then
|
|
RelationshipsAchievements:UpdateStat("cookingSkill", rank)
|
|
elseif name == "Fishing" then
|
|
RelationshipsAchievements:UpdateStat("fishingSkill", rank)
|
|
elseif name == "First Aid" then
|
|
if rank >= 300 then RelationshipsAchievements:Unlock(707) end
|
|
else
|
|
if rank > highestProf then highestProf = rank end
|
|
if rank >= 300 then
|
|
if name == "Alchemy" then RelationshipsAchievements:Unlock(708) end
|
|
if name == "Blacksmithing" then RelationshipsAchievements:Unlock(709) end
|
|
if name == "Enchanting" then RelationshipsAchievements:Unlock(710) end
|
|
if name == "Engineering" then RelationshipsAchievements:Unlock(711) end
|
|
if name == "Herbalism" then RelationshipsAchievements:Unlock(712) end
|
|
if name == "Leatherworking"then RelationshipsAchievements:Unlock(713) end
|
|
if name == "Mining" then RelationshipsAchievements:Unlock(714) end
|
|
if name == "Skinning" then RelationshipsAchievements:Unlock(715) end
|
|
if name == "Tailoring" then RelationshipsAchievements:Unlock(716) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
RelationshipsAchievements:UpdateStat("profSkill", highestProf)
|
|
RelationshipsAchievements:UpdateStat("weaponSkill", highestWeapon)
|
|
end
|
|
|
|
if GetNumFactions then
|
|
local exalted = 0
|
|
local hasFriendly, hasHonored, hasRevered, hasExalted = false, false, false, false
|
|
for i = 1, GetNumFactions() do
|
|
local name, _, standing = GetFactionInfo(i)
|
|
if standing then
|
|
if standing >= 5 then hasFriendly = true end
|
|
if standing >= 6 then hasHonored = true end
|
|
if standing >= 7 then hasRevered = true end
|
|
if standing >= 8 then
|
|
hasExalted = true
|
|
exalted = exalted + 1
|
|
if name and RelationshipsAchievements_FactionMap[name] then
|
|
RelationshipsAchievements:Unlock(RelationshipsAchievements_FactionMap[name])
|
|
end
|
|
RelationshipsAchievementsCharDB.factionsExalted[name or "?"] = true
|
|
end
|
|
end
|
|
end
|
|
if hasFriendly then RelationshipsAchievements:Unlock(801) end
|
|
if hasHonored then RelationshipsAchievements:Unlock(802) end
|
|
if hasRevered then RelationshipsAchievements:Unlock(803) end
|
|
if hasExalted then RelationshipsAchievements:Unlock(804) end
|
|
RelationshipsAchievements:UpdateStat("exaltedCount", exalted)
|
|
end
|
|
|
|
ScanPvPRank()
|
|
ScanPvPKills()
|
|
ScanTalents()
|
|
ScanEquipmentAndBags()
|
|
ScanSpellbook()
|
|
|
|
RelationshipsAchievements_RefreshUI = RelationshipsAchievements_RefreshUI or function() end
|
|
if RelationshipsAchievementFrame and RelationshipsAchievementFrame:IsVisible() then
|
|
RelationshipsAchievements_RefreshUI()
|
|
end
|
|
end
|
|
|
|
local function BindHotkey()
|
|
if not SetBinding then return end
|
|
local existing = GetBindingAction and GetBindingAction("Y")
|
|
if existing and existing ~= "" and existing ~= "TOGGLERELATIONSHIPSACHIEVEMENT" then
|
|
return
|
|
end
|
|
SetBinding("Y", "TOGGLERELATIONSHIPSACHIEVEMENT")
|
|
end
|
|
|
|
-- ============ SendChatMessage hook (Chatty achievement) ============
|
|
-- Wrapping SendChatMessage is more reliable than reading CHAT_MSG_SAY back,
|
|
-- because whispers, guild chat, and channels each fire different events and
|
|
-- some don't echo the sender field consistently on Turtle / Vanilla.
|
|
local _origSendChatMessage = SendChatMessage
|
|
if _origSendChatMessage then SendChatMessage = function(msg, chatType, language, target)
|
|
_origSendChatMessage(msg, chatType, language, target)
|
|
if msg and msg ~= "" and RelationshipsAchievements and RelationshipsAchievements.AddStat then
|
|
RelationshipsAchievements:AddStat("chatCount", 1)
|
|
end
|
|
end end
|
|
|
|
local _origSendMail = SendMail
|
|
if _origSendMail then SendMail = function(name, subject, body)
|
|
_origSendMail(name, subject, body)
|
|
RelationshipsAchievements:Unlock(110)
|
|
end end
|
|
|
|
-- Auction House API is provided by Blizzard_AuctionUI, which is Load-on-Demand
|
|
-- in 1.12. Hooking at file-load time silently no-ops because the globals are
|
|
-- still nil. HookAuctionHouse() runs again on ADDON_LOADED "Blizzard_AuctionUI"
|
|
-- so the hooks attach as soon as the AH frame is opened for the first time.
|
|
local auctionHooked = false
|
|
local function HookAuctionHouse()
|
|
if auctionHooked then return end
|
|
if not PostAuction and not BuyoutAuction and not PlaceAuctionBid then return end
|
|
auctionHooked = true
|
|
if PostAuction then
|
|
local _orig = PostAuction
|
|
PostAuction = function(minBid, buyoutPrice, runTime, stackSize, numStacks)
|
|
_orig(minBid, buyoutPrice, runTime, stackSize, numStacks)
|
|
RelationshipsAchievements:AddStat("auctionsListed", numStacks or 1)
|
|
end
|
|
end
|
|
if BuyoutAuction then
|
|
local _orig = BuyoutAuction
|
|
BuyoutAuction = function(index)
|
|
_orig(index)
|
|
RelationshipsAchievements:Unlock(107)
|
|
RelationshipsAchievements:AddStat("auctionsWon", 1)
|
|
end
|
|
end
|
|
-- Many AH addons (and the default UI in some 1.12 builds) route the
|
|
-- Buyout button through PlaceAuctionBid(list, index, bid) with bid ==
|
|
-- buyout price. Hook it too so wins are always counted.
|
|
if PlaceAuctionBid then
|
|
local _orig = PlaceAuctionBid
|
|
PlaceAuctionBid = function(listType, index, bid)
|
|
_orig(listType, index, bid)
|
|
local _, _, _, _, _, _, _, _, buyout = GetAuctionItemInfo(listType, index)
|
|
if buyout and bid and bid >= buyout and buyout > 0 then
|
|
RelationshipsAchievements:Unlock(107)
|
|
RelationshipsAchievements:AddStat("auctionsWon", 1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
HookAuctionHouse() -- attempt immediately in case the UI is already loaded.
|
|
|
|
-- ============ Vendor sales tracking ============
|
|
-- The merchant window is always the default UI; hooks are safe to install at
|
|
-- file-load time. Two vectors cover every sell path:
|
|
-- * UseContainerItem while MerchantFrame is visible = shift-click / right-
|
|
-- click sell from the bag.
|
|
-- * Drag-and-drop onto the merchant → PickupContainerItem then a click on
|
|
-- the merchant, which resolves via UseContainerItem too, so one hook
|
|
-- catches both cases.
|
|
local function MerchantOpen()
|
|
return MerchantFrame and MerchantFrame:IsVisible()
|
|
end
|
|
local _origUseContainerItem = UseContainerItem
|
|
if _origUseContainerItem then UseContainerItem = function(bag, slot, target)
|
|
local sellable = false
|
|
if MerchantOpen() and GetContainerItemLink then
|
|
local link = GetContainerItemLink(bag, slot)
|
|
-- Skip if slot is empty or item is bound-cannot-sell? A no-op sell is
|
|
-- harmless — the vendor simply refuses — so we count only when the
|
|
-- API had something to act on.
|
|
if link then sellable = true end
|
|
end
|
|
_origUseContainerItem(bag, slot, target)
|
|
if sellable then
|
|
RelationshipsAchievements:AddStat("vendorSales", 1)
|
|
end
|
|
end end
|
|
|
|
local _origStaticPopupShow = StaticPopup_Show
|
|
if _origStaticPopupShow then StaticPopup_Show = function(which, text_arg1, text_arg2, data)
|
|
local popup = _origStaticPopupShow(which, text_arg1, text_arg2, data)
|
|
if which == "CONFIRM_BINDER" then RelationshipsAchievementsCharDB.pendingInnBind = true end
|
|
return popup
|
|
end end
|
|
|
|
-- ============ DoEmote hook (Say Hello / Dance Off) ============
|
|
-- Hooking DoEmote is the cleanest way: the token is stable ("wave", "dance")
|
|
-- across all locales, unlike the CHAT_MSG_TEXT_EMOTE text which is localized.
|
|
local _origDoEmote = DoEmote
|
|
if _origDoEmote then
|
|
DoEmote = function(token, target)
|
|
_origDoEmote(token, target)
|
|
if not token then return end
|
|
local t = string.lower(token)
|
|
if t == "wave" then
|
|
RelationshipsAchievements:Unlock(1701)
|
|
elseif t == "dance" then
|
|
RelationshipsAchievements:Unlock(1702)
|
|
end
|
|
end
|
|
end
|
|
|
|
local loginScanDone = false
|
|
|
|
local function OnLogin()
|
|
RelationshipsAchievements:Init()
|
|
RelationshipsAchievements:Unlock(101)
|
|
RelationshipsAchievements:Unlock(1301)
|
|
BindHotkey()
|
|
if not loginScanDone then
|
|
loginScanDone = true
|
|
local scanner = CreateFrame("Frame")
|
|
local acc = 0
|
|
scanner:SetScript("OnUpdate", function()
|
|
acc = acc + (arg1 or 0)
|
|
if acc >= 2 then
|
|
RelationshipsAchievements_ScanCharacter()
|
|
if ShowFriends then ShowFriends() end
|
|
if GuildRoster then GuildRoster() end
|
|
this:SetScript("OnUpdate", nil)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
local function OnLevelUp()
|
|
RelationshipsAchievements_ScanCharacter()
|
|
end
|
|
|
|
local function OnQuestComplete()
|
|
-- QUEST_COMPLETE is the reliable 1.12 turn-in event. Custom clients that
|
|
-- also emit QUEST_TURNED_IN are de-duplicated by time stamp below.
|
|
local now = time()
|
|
if RelationshipsAchievementsCharDB.lastQuestCredit == now then return end
|
|
RelationshipsAchievementsCharDB.lastQuestCredit = now
|
|
RelationshipsAchievements:AddStat("questCount", 1)
|
|
local title = GetTitleText and GetTitleText() or ""
|
|
local low = string.lower(title or "")
|
|
if string.find(low, "attunement to the core") then RelationshipsAchievements:Unlock(683) end
|
|
if string.find(low, "blackhand's command") then RelationshipsAchievements:Unlock(682) end
|
|
if string.find(low, "onyxia") or string.find(low, "great masquerade") then RelationshipsAchievements:Unlock(684) end
|
|
if string.find(low, "head of onyxia") or string.find(low, "victory for the alliance") or string.find(low, "victory for the horde") then RelationshipsAchievements:Unlock(2104) end
|
|
if string.find(low, "silithyst") then RelationshipsAchievements:AddStat("silithystTurnIns", 1) end
|
|
if string.find(low, "class") then RelationshipsAchievements:Unlock(312) end
|
|
end
|
|
|
|
local function OnZoneChanged()
|
|
local zone = GetRealZoneText() or GetZoneText()
|
|
if not zone or zone == "" then return end
|
|
|
|
if RelationshipsAchievements_CityMap and RelationshipsAchievements_CityMap[zone] then
|
|
RelationshipsAchievements:Unlock(RelationshipsAchievements_CityMap[zone])
|
|
RelationshipsAchievementsCharDB.cities = RelationshipsAchievementsCharDB.cities or {}
|
|
if not RelationshipsAchievementsCharDB.cities[zone] then
|
|
RelationshipsAchievementsCharDB.cities[zone] = true
|
|
local a, h = 0, 0
|
|
for c in pairs(RelationshipsAchievementsCharDB.cities) do
|
|
if RelationshipsAchievements_AllianceCities[c] then a = a + 1 end
|
|
if RelationshipsAchievements_HordeCities[c] then h = h + 1 end
|
|
end
|
|
RelationshipsAchievements:UpdateStat("allianceCitiesVisited", a)
|
|
RelationshipsAchievements:UpdateStat("hordeCitiesVisited", h)
|
|
if a >= 3 then RelationshipsAchievements:Unlock(1807) end
|
|
if h >= 3 then RelationshipsAchievements:Unlock(1808) end
|
|
end
|
|
end
|
|
local lowZone = string.lower(zone)
|
|
if string.find(lowZone, "karazhan crypt") then RelationshipsAchievements:Unlock(1305) end
|
|
if string.find(lowZone, "emerald sanctum") then RelationshipsAchievements:Unlock(1308) end
|
|
|
|
if not RelationshipsAchievementsCharDB.zones[zone] then
|
|
RelationshipsAchievementsCharDB.zones[zone] = true
|
|
local id = RelationshipsAchievements_ZoneMap[zone]
|
|
if id then RelationshipsAchievements:Unlock(id) end
|
|
local count = 0
|
|
for _ in pairs(RelationshipsAchievementsCharDB.zones) do count = count + 1 end
|
|
RelationshipsAchievements:UpdateStat("zoneCount", count)
|
|
end
|
|
end
|
|
|
|
local function OnHonorGain(msg)
|
|
-- Objective/bonus honor uses this event too, so never increment blindly.
|
|
-- Lifetime stats give the authoritative HK total and cannot double-count.
|
|
if GetPVPLifetimeStats then
|
|
local before = RelationshipsAchievements:GetStat("honorKills")
|
|
ScanPvPKills()
|
|
local after = RelationshipsAchievements:GetStat("honorKills")
|
|
if after > before then
|
|
RelationshipsAchievements:Unlock(501)
|
|
RelationshipsAchievements:UpdateStat("pvpKills", after)
|
|
end
|
|
elseif msg and string.find(string.lower(msg), "honorable kill") then
|
|
RelationshipsAchievements:Unlock(501)
|
|
RelationshipsAchievements:AddStat("honorKills", 1)
|
|
RelationshipsAchievements:AddStat("pvpKills", 1)
|
|
end
|
|
end
|
|
|
|
local function OnFactionChange(msg)
|
|
if not msg then return end
|
|
-- Recompute from the faction table. Incrementing from chat inflated the
|
|
-- count when duplicate messages fired and could award 5/10 Exalted early.
|
|
RelationshipsAchievements_ScanCharacter()
|
|
end
|
|
|
|
local function OnSkillLinesChanged()
|
|
RelationshipsAchievements_ScanCharacter()
|
|
end
|
|
|
|
local function OnLootMessage(msg)
|
|
if not msg then return end
|
|
local clean = string.gsub(msg, ",", "")
|
|
local _, _, g = string.find(clean, "(%d+) Gold")
|
|
if g then
|
|
RelationshipsAchievements:AddStat("goldLooted", tonumber(g))
|
|
end
|
|
local low = string.lower(msg)
|
|
if string.find(low, "fish") or string.find(low, "snapper") or string.find(low, "trout")
|
|
or string.find(low, "salmon") or string.find(low, "catfish") or string.find(low, "eel") then
|
|
RelationshipsAchievements:Unlock(1401)
|
|
RelationshipsAchievements:AddStat("fishCount", 1)
|
|
end
|
|
if string.find(low, "you create") and (string.find(low, "food") or string.find(low, "meal")
|
|
or string.find(low, "roast") or string.find(low, "stew")) then RelationshipsAchievements:Unlock(1501) end
|
|
end
|
|
|
|
local function OnEnemyDeath(msg)
|
|
if not msg then return end
|
|
if string.find(msg, "You have slain") or string.find(msg, "slain by you") then
|
|
RelationshipsAchievements:AddStat("killCount", 1)
|
|
end
|
|
local bossMap = RelationshipsAchievements_BossMap or {}
|
|
for boss, info in pairs(bossMap) do
|
|
if string.find(msg, boss, 1, true) then
|
|
RelationshipsAchievements:Unlock(info.id)
|
|
local seen = info.raid and RelationshipsAchievementsCharDB.raidBosses or RelationshipsAchievementsCharDB.dungeonBosses
|
|
if not seen[boss] then
|
|
seen[boss] = true
|
|
RelationshipsAchievements:AddStat(info.raid and "raidsCleared" or "dungeonsCleared", 1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function OnSystemMessage(msg)
|
|
if not msg then return end
|
|
local _, _, winner = string.find(msg, "^(.-) has defeated ")
|
|
if winner and winner == UnitName("player") then
|
|
RelationshipsAchievements:AddStat("duelWins", 1)
|
|
end
|
|
if string.find(msg, "home is now") or string.find(msg, "bound to") then
|
|
RelationshipsAchievements:Unlock(108)
|
|
RelationshipsAchievementsCharDB.pendingInnBind = nil
|
|
end
|
|
local low = string.lower(msg)
|
|
if string.find(low, "learned a new") or string.find(low, "you have learned") then RelationshipsAchievements:Unlock(1110) end
|
|
if string.find(low, "darkmoon faire") then RelationshipsAchievements:Unlock(911) end
|
|
if string.find(low, "trick or treat") then RelationshipsAchievements:Unlock(903) end
|
|
if string.find(low, "lunar festival") then RelationshipsAchievements:Unlock(909) end
|
|
if string.find(low, "winter veil") then RelationshipsAchievements:Unlock(901) end
|
|
-- Battleground victories (system messages contain "Warsong Gulch", etc.)
|
|
if string.find(msg, "[Vv]ictory") or string.find(msg, "wins!") then
|
|
if string.find(msg, "Warsong Gulch") then
|
|
RelationshipsAchievements:Unlock(505)
|
|
RelationshipsAchievements:AddStat("wsgWins", 1)
|
|
RelationshipsAchievements:AddStat("bgWins", 1)
|
|
elseif string.find(msg, "Alterac Valley") then
|
|
RelationshipsAchievements:Unlock(506)
|
|
RelationshipsAchievements:AddStat("avWins", 1)
|
|
RelationshipsAchievements:AddStat("bgWins", 1)
|
|
elseif string.find(msg, "Arathi Basin") then
|
|
RelationshipsAchievements:Unlock(507)
|
|
RelationshipsAchievements:AddStat("abWins", 1)
|
|
RelationshipsAchievements:AddStat("bgWins", 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Emote handler: catch /wave and /dance in every locale by matching the
|
|
-- token via DoEmote hook AND as a fallback matching the English text here.
|
|
local function OnTextEmote(msg, sender)
|
|
if not msg then return end
|
|
local me = UnitName("player")
|
|
-- vanilla emote text always begins with "You " when it's the local player,
|
|
-- or the player's name otherwise.
|
|
local mine = (sender and sender == me) or string.find(msg, "^You ")
|
|
if not mine then return end
|
|
if string.find(string.lower(msg), "wave") then
|
|
RelationshipsAchievements:Unlock(1701)
|
|
end
|
|
if string.find(string.lower(msg), "dance") then
|
|
RelationshipsAchievements:Unlock(1702)
|
|
end
|
|
end
|
|
|
|
frame:SetScript("OnEvent", function()
|
|
if event == "PLAYER_LOGIN" then
|
|
OnLogin()
|
|
elseif event == "PLAYER_ENTERING_WORLD" then
|
|
RelationshipsAchievements:Init()
|
|
BindHotkey()
|
|
OnZoneChanged()
|
|
ScanPvPRank()
|
|
elseif event == "PLAYER_LEVEL_UP" then
|
|
OnLevelUp()
|
|
elseif event == "PLAYER_DEAD" or event == "PLAYER_UNGHOST" or event == "PLAYER_ALIVE" then
|
|
if event == "PLAYER_DEAD" then
|
|
RelationshipsAchievements:Unlock(207)
|
|
RelationshipsAchievements:AddStat("deaths", 1)
|
|
end
|
|
elseif event == "QUEST_COMPLETE" or event == "QUEST_TURNED_IN" then
|
|
OnQuestComplete()
|
|
elseif event == "ZONE_CHANGED_NEW_AREA" or event == "ZONE_CHANGED" or event == "ZONE_CHANGED_INDOORS" then
|
|
OnZoneChanged()
|
|
elseif event == "CHAT_MSG_COMBAT_HONOR_GAIN" then
|
|
OnHonorGain(arg1)
|
|
elseif event == "CHAT_MSG_COMBAT_FACTION_CHANGE" then
|
|
OnFactionChange(arg1)
|
|
elseif event == "SKILL_LINES_CHANGED" then
|
|
OnSkillLinesChanged()
|
|
elseif event == "CHAT_MSG_LOOT" or event == "CHAT_MSG_MONEY" then
|
|
OnLootMessage(arg1)
|
|
elseif event == "CHAT_MSG_COMBAT_HOSTILE_DEATH" or event == "CHAT_MSG_COMBAT_XP_GAIN" then
|
|
OnEnemyDeath(arg1)
|
|
elseif event == "CHAT_MSG_SYSTEM" then
|
|
OnSystemMessage(arg1)
|
|
elseif event == "CHAT_MSG_TEXT_EMOTE" or event == "CHAT_MSG_EMOTE" then
|
|
OnTextEmote(arg1, arg2)
|
|
elseif event == "PLAYER_PVP_RANK_CHANGED"
|
|
or event == "HONOR_CURRENCY_UPDATE"
|
|
or event == "PLAYER_PVPKILLS_CHANGED" or event == "PLAYER_PVP_KILLS_CHANGED" then
|
|
ScanPvPRank()
|
|
ScanPvPKills()
|
|
elseif event == "HEARTHSTONE_BOUND" then
|
|
RelationshipsAchievements:Unlock(108)
|
|
RelationshipsAchievementsCharDB.pendingInnBind = nil
|
|
elseif event == "BANKFRAME_OPENED" then
|
|
RelationshipsAchievements:Unlock(106)
|
|
elseif event == "TRAINER_SHOW" then
|
|
RelationshipsAchievements:Unlock(111)
|
|
elseif event == "MAIL_SEND_SUCCESS" then
|
|
RelationshipsAchievements:Unlock(110)
|
|
elseif event == "PLAYER_EQUIPMENT_CHANGED"
|
|
or event == "UNIT_INVENTORY_CHANGED"
|
|
or event == "BAG_UPDATE" then
|
|
if event == "UNIT_INVENTORY_CHANGED" and arg1 ~= "player" then return end
|
|
ScanEquipmentAndBags()
|
|
elseif event == "ADDON_LOADED" then
|
|
if arg1 == "Blizzard_AuctionUI" then HookAuctionHouse() end
|
|
elseif event == "MERCHANT_SHOW" then
|
|
-- Rescan equipment when opening a vendor — buying gear or repairs
|
|
-- often coincides with equipping the item immediately after.
|
|
ScanEquipmentAndBags()
|
|
ScanSpellbook()
|
|
elseif event == "CHARACTER_POINTS_CHANGED" then
|
|
ScanTalentChange()
|
|
elseif event == "SPELLS_CHANGED" then
|
|
ScanSpellbook()
|
|
elseif event == "PLAYER_MONEY" then
|
|
if GetMoney then
|
|
local copper = GetMoney() or 0
|
|
if copper >= 100 then RelationshipsAchievements:Unlock(1901) end
|
|
if copper >= 10000 then RelationshipsAchievements:Unlock(1902) end
|
|
if copper >= 5000000 then RelationshipsAchievements:Unlock(223) end
|
|
if copper >= 10000000 then RelationshipsAchievements:Unlock(1903) end
|
|
if copper >= 50000000 then RelationshipsAchievements:Unlock(224) end
|
|
if copper >= 100000000 then RelationshipsAchievements:Unlock(1904) end
|
|
end
|
|
elseif event == "UNIT_HEALTH" and arg1 == "player" then
|
|
local maxHealth = UnitHealthMax("player") or 0
|
|
if maxHealth > 0 and UnitHealth("player") > 0 and UnitHealth("player") * 10 < maxHealth then
|
|
RelationshipsAchievements:Unlock(1605)
|
|
end
|
|
elseif event == "UPDATE_FACTION" then
|
|
RelationshipsAchievements_ScanCharacter()
|
|
elseif event == "PARTY_MEMBERS_CHANGED" then
|
|
if GetNumPartyMembers and GetNumPartyMembers() > 0 then
|
|
RelationshipsAchievements:Unlock(105)
|
|
end
|
|
elseif event == "FRIENDLIST_UPDATE" then
|
|
if GetNumFriends and GetNumFriends() > 0 then
|
|
RelationshipsAchievements:Unlock(102)
|
|
RelationshipsAchievements:UpdateStat("friendCount", GetNumFriends())
|
|
end
|
|
elseif event == "GUILD_ROSTER_UPDATE" then
|
|
if GetGuildInfo and GetGuildInfo("player") then
|
|
RelationshipsAchievements:Unlock(103)
|
|
if GetNumGuildMembers and GetNumGuildMembers() >= 10 then RelationshipsAchievements:Unlock(1703) end
|
|
end
|
|
end
|
|
end)
|