From 04fd6ee1dbe3dfb68e7db022eb819cdb7aaef1c1 Mon Sep 17 00:00:00 2001 From: Relationship <571+relationship@noreply.octowow.st> Date: Thu, 23 Jul 2026 02:43:14 +0000 Subject: [PATCH] Upload files to "/" --- Detail.lua | 180 +++++++++ Events.lua | 683 ++++++++++++++++++++++++++++++++++ RelationshipsAchievements.toc | 17 + Toast.lua | 98 +++++ UI.lua | 528 ++++++++++++++++++++++++++ 5 files changed, 1506 insertions(+) create mode 100644 Detail.lua create mode 100644 Events.lua create mode 100644 RelationshipsAchievements.toc create mode 100644 Toast.lua create mode 100644 UI.lua diff --git a/Detail.lua b/Detail.lua new file mode 100644 index 0000000..abbaa07 --- /dev/null +++ b/Detail.lua @@ -0,0 +1,180 @@ +-- Achievement detail popup — a movable/closable frame that shows a single +-- achievement's icon, name, description, points, and unlock date. Opened by +-- clicking an achievement chat link, the toast, or a row in the panel. + +local popups = {} +local MAX_POPUPS = 6 + +local function FormatDate(t) + if not t then return "" end + return date("%m/%d/%y", t) +end + +local function CreatePopup(index) + local f = CreateFrame("Frame", "RelationshipsAchievementDetailPopup"..index, UIParent) + f:SetWidth(340); f:SetHeight(96) + f:SetPoint("CENTER", UIParent, "CENTER", 0, -60 - ((index - 1) * 110)) + f:SetFrameStrata("FULLSCREEN_DIALOG") + f:SetToplevel(true) + f:SetMovable(true); f:EnableMouse(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:Hide() + + f:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border", + tile = true, tileSize = 32, edgeSize = 16, + insets = { left = 5, right = 5, top = 5, bottom = 5 }, + }) + f:SetBackdropColor(0, 0, 0, 0.9) + + local header = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") + header:SetPoint("TOP", f, "TOP", 0, -8) + header:SetText("Achievement") + header:SetTextColor(1, 0.82, 0) + f.header = header + + local icon = f:CreateTexture(nil, "ARTWORK") + icon:SetWidth(48); icon:SetHeight(48) + icon:SetPoint("TOPLEFT", f, "TOPLEFT", 14, -24) + f.icon = icon + + local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 12, -2) + title:SetPoint("RIGHT", f, "RIGHT", -12, 0) + title:SetJustifyH("LEFT") + title:SetTextColor(1, 0.82, 0) + f.title = title + + local desc = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4) + desc:SetPoint("RIGHT", f, "RIGHT", -12, 0) + desc:SetJustifyH("LEFT") + desc:SetTextColor(1, 1, 1) + f.desc = desc + + local date = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + date:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 14, 8) + date:SetTextColor(0.7, 0.7, 0.7) + f.date = date + + local points = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") + points:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 8) + points:SetTextColor(1, 0.82, 0) + f.points = points + + local close = CreateFrame("Button", nil, f, "UIPanelCloseButton") + close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2) + close:SetScript("OnClick", function() f:Hide() end) + + if not UISpecialFrames then UISpecialFrames = {} end + tinsert(UISpecialFrames, "RelationshipsAchievementDetailPopup"..index) + + return f +end + +local function AcquirePopup() + for i = 1, table.getn(popups) do + if not popups[i]:IsVisible() then + return popups[i] + end + end + if table.getn(popups) < MAX_POPUPS then + local p = CreatePopup(table.getn(popups) + 1) + table.insert(popups, p) + return p + end + return popups[1] +end + +function RelationshipsAchievements_ShowDetail(ach) + if not ach then return end + local f = AcquirePopup() + + local unlocked = RelationshipsAchievements and RelationshipsAchievements:IsUnlocked(ach.id) + if unlocked then + f.header:SetText("Achievement Earned") + else + f.header:SetText("Achievement") + end + f.title:SetText(ach.name or "") + f.desc:SetText(ach.desc or "") + f.icon:SetTexture(ach.icon or "Interface\\Icons\\INV_Misc_QuestionMark") + if ach.points and ach.points > 0 then + f.points:SetText(ach.points.." pts") + else + f.points:SetText("") + end + if unlocked then + local t = RelationshipsAchievements:GetUnlockTime(ach.id) + f.date:SetText(FormatDate(t)) + else + f.date:SetText("Not yet earned") + end + f:Show() + f:Raise() +end + +-- ============ Chat hyperlink handling ============ + +local function HandleAchievementLink(link) + if not link then return false end + if string.sub(link, 1, 6) ~= "relach" then return false end + -- link format: "relach:" + local _, _, idStr = string.find(link, "^relach:(%d+)") + local id = tonumber(idStr) + if id and RelationshipsAchievements_ById and RelationshipsAchievements_ById[id] then + RelationshipsAchievements_ShowDetail(RelationshipsAchievements_ById[id]) + return true + end + return true +end + +local orig_SetItemRef = SetItemRef +function SetItemRef(link, text, button) + if HandleAchievementLink(link) then return end + if orig_SetItemRef then + return orig_SetItemRef(link, text, button) + end +end + +-- Safely install a script handler. Vanilla 1.12 ChatFrames only support +-- OnHyperlinkClick; OnHyperlinkShow was added in later clients and setting +-- it here throws "ChatFrameN doesn't have a 'OnHyperlinkShow' script". +-- Wrapping in pcall lets us support both without erroring on 1.12. +local function SafeHookScript(frame, scriptName, newFn) + local prev + pcall(function() prev = frame:GetScript(scriptName) end) + local ok = pcall(function() + frame:SetScript(scriptName, function() + if HandleAchievementLink(arg1) then return end + if prev then prev() end + end) + end) + return ok +end + +local function InstallChatHooks() + local num = NUM_CHAT_WINDOWS or 7 + for i = 1, num do + local cf = getglobal("ChatFrame"..i) + if cf then + if cf.SetHyperlinksEnabled then + pcall(function() cf:SetHyperlinksEnabled(true) end) + end + SafeHookScript(cf, "OnHyperlinkClick") + SafeHookScript(cf, "OnHyperlinkShow") + end + end +end + +InstallChatHooks() + +local _hookFrame = CreateFrame("Frame") +_hookFrame:RegisterEvent("PLAYER_LOGIN") +_hookFrame:RegisterEvent("PLAYER_ENTERING_WORLD") +_hookFrame:SetScript("OnEvent", function() + InstallChatHooks() +end) diff --git a/Events.lua b/Events.lua new file mode 100644 index 0000000..edcf73b --- /dev/null +++ b/Events.lua @@ -0,0 +1,683 @@ +-- 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", "CHAT_MSG_BG_SYSTEM_NEUTRAL", "CHAT_MSG_BG_SYSTEM_ALLIANCE", "CHAT_MSG_BG_SYSTEM_HORDE", "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" +} +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 + +local function ScanEquipmentAndBags() + if GetInventoryItemLink then + if GetInventoryItemLink("player", 4) then RelationshipsAchievements:Unlock(1203) end + if GetInventoryItemLink("player", 19) then RelationshipsAchievements:Unlock(109) end + local allRare, haveGear, epic = true, false, false + for slot = 1, 18 do + if slot ~= 4 and slot ~= 18 then + local link = GetInventoryItemLink("player", slot) + if link then + haveGear = true + local _, _, quality = GetItemInfo(link) + if quality and quality >= 4 then epic = true end + if 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 + else + allRare = false + end + end + end + if haveGear 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 + +local function ScanSpellbook() + if not GetSpellName then return end + local mounts, pets = 0, 0 + local i = 1 + while true do + local name, rank = GetSpellName(i, BOOKTYPE_SPELL or "spell") + if not name then break end + local low = string.lower(name) + if string.find(low, "summon ") and (string.find(low, "horse") or string.find(low, "ram") + or string.find(low, "tiger") or string.find(low, "raptor") or string.find(low, "wolf") + or string.find(low, "kodo") or string.find(low, "mechanostrider")) then mounts = mounts + 1 end + if string.find(low, "summon ") and not string.find(low, "demon") and not string.find(low, "elemental") then pets = pets + 1 end + i = i + 1 + end + RelationshipsAchievements:UpdateStat("mountCount", mounts) + RelationshipsAchievements:UpdateStat("petCount", pets) + if mounts >= 1 then RelationshipsAchievements:Unlock(211) end + if mounts >= 1 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 + +local _origPostAuction = PostAuction +if _origPostAuction then PostAuction = function(minBid, buyoutPrice, runTime, stackSize, numStacks) + _origPostAuction(minBid, buyoutPrice, runTime, stackSize, numStacks) + RelationshipsAchievements:AddStat("auctionsListed", numStacks or 1) +end end + +local _origBuyoutAuction = BuyoutAuction +if _origBuyoutAuction then BuyoutAuction = function(index) + _origBuyoutAuction(index) + RelationshipsAchievements:Unlock(107) + RelationshipsAchievements:AddStat("auctionsWon", 1) +end end + +local OnQuestComplete + +-- Hook ConfirmBinder(): the innkeeper popup's OnAccept calls this API and it +-- fires exactly once per successful bind. The HEARTHSTONE_BOUND event does not +-- exist in vanilla 1.12, and the CHAT_MSG_SYSTEM string fallback below is +-- localised / worded differently on some Turtle/Octo builds, so hooking the +-- API is the only reliable trigger for re-binding to a new inn. +local _origConfirmBinder = ConfirmBinder +if _origConfirmBinder then ConfirmBinder = function() + _origConfirmBinder() + if RelationshipsAchievements and RelationshipsAchievements.Unlock then + RelationshipsAchievements:Unlock(108) + end + if RelationshipsAchievementsCharDB then + RelationshipsAchievementsCharDB.pendingInnBind = nil + end +end end + +-- Hook GetQuestReward(): fires exactly once when the player accepts a quest +-- reward. Much more reliable than QUEST_COMPLETE (which fires each time the +-- reward window opens) for tracking completed-quest counters. +local _origGetQuestReward = GetQuestReward +if _origGetQuestReward then GetQuestReward = function(choice) + OnQuestComplete() + return _origGetQuestReward(choice) +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 + +OnQuestComplete = function() + -- 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. On 1.12 the win message reads "The Alliance wins!" + -- or "The Horde wins!" via CHAT_MSG_BG_SYSTEM_NEUTRAL and does NOT include + -- the map name, so we look up the current BG from GetRealZoneText() and + -- credit the player's faction only. Guard with pendingBgWin so duplicate + -- messages (both faction and neutral system chat) don't double-count. + if string.find(msg, "wins!") or string.find(msg, "[Vv]ictory") then + local zone = (GetRealZoneText and GetRealZoneText()) or "" + local _, myFaction = UnitFactionGroup("player") + local winner + if string.find(msg, "Alliance") then winner = "Alliance" + elseif string.find(msg, "Horde") then winner = "Horde" + end + local key = zone .. "|" .. (winner or "?") + if RelationshipsAchievementsCharDB and RelationshipsAchievementsCharDB.lastBgWinKey ~= key then + RelationshipsAchievementsCharDB.lastBgWinKey = key + if not winner or winner == myFaction then + if string.find(zone, "Warsong Gulch") then + RelationshipsAchievements:Unlock(505) + RelationshipsAchievements:AddStat("wsgWins", 1) + RelationshipsAchievements:AddStat("bgWins", 1) + elseif string.find(zone, "Alterac Valley") then + RelationshipsAchievements:Unlock(506) + RelationshipsAchievements:AddStat("avWins", 1) + RelationshipsAchievements:AddStat("bgWins", 1) + elseif string.find(zone, "Arathi Basin") then + RelationshipsAchievements:Unlock(507) + RelationshipsAchievements:AddStat("abWins", 1) + RelationshipsAchievements:AddStat("bgWins", 1) + end + end + 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" + or event == "CHAT_MSG_BG_SYSTEM_NEUTRAL" + or event == "CHAT_MSG_BG_SYSTEM_ALLIANCE" + or event == "CHAT_MSG_BG_SYSTEM_HORDE" 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 == "BAG_UPDATE" then + ScanEquipmentAndBags() + 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) diff --git a/RelationshipsAchievements.toc b/RelationshipsAchievements.toc new file mode 100644 index 0000000..3f3c425 --- /dev/null +++ b/RelationshipsAchievements.toc @@ -0,0 +1,17 @@ +## Interface: 11200 +## Title: Relationships Achievements +## Notes: WotLK-style achievement system for OctoWoW +## Author: Lovable +## Version: 1.0 +## SavedVariables: RelationshipsAchievementsDB +## SavedVariablesPerCharacter: RelationshipsAchievementsCharDB +## DefaultState: Enabled +## OptionalDeps: + +Database.lua +Core.lua +Toast.lua +Detail.lua +UI.lua +Events.lua +Bindings.xml diff --git a/Toast.lua b/Toast.lua new file mode 100644 index 0000000..965927f --- /dev/null +++ b/Toast.lua @@ -0,0 +1,98 @@ +-- Achievement toast popup (the "gold banner" that appears on unlock) + +local TOAST_DURATION = 5.0 +local FADE_DURATION = 1.0 + +local toast + +local function CreateToast() + local f = CreateFrame("Button", "RelationshipsAchievementToast", UIParent) + f:SetWidth(320) + f:SetHeight(80) + f:SetPoint("TOP", UIParent, "TOP", 0, -120) + f:SetFrameStrata("HIGH") + f:EnableMouse(true) + f:RegisterForClicks("LeftButtonUp") + f:SetScript("OnClick", function() + if this.ach and RelationshipsAchievements_ShowDetail then + RelationshipsAchievements_ShowDetail(this.ach) + end + end) + f:Hide() + + f:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border", + tile = true, tileSize = 32, edgeSize = 16, + insets = { left = 5, right = 5, top = 5, bottom = 5 }, + }) + f:SetBackdropColor(0, 0, 0, 0.9) + + local header = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") + header:SetPoint("TOP", f, "TOP", 0, -8) + header:SetText("Achievement Unlocked!") + header:SetTextColor(1, 0.82, 0) + f.header = header + + local icon = f:CreateTexture(nil, "ARTWORK") + icon:SetWidth(48); icon:SetHeight(48) + icon:SetPoint("LEFT", f, "LEFT", 14, -6) + f.icon = icon + + local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 14, -2) + title:SetPoint("RIGHT", f, "RIGHT", -10, 0) + title:SetJustifyH("LEFT") + title:SetTextColor(1, 1, 1) + f.title = title + + local points = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") + points:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 8) + points:SetTextColor(1, 0.82, 0) + f.points = points + + f.state = "hidden" + f.stateTime = 0 + + f:SetScript("OnUpdate", function() + local elapsed = arg1 or 0 + this.stateTime = this.stateTime + elapsed + if this.state == "showing" then + local a = this.stateTime / 0.4 + if a >= 1 then a = 1; this.state = "shown"; this.stateTime = 0 end + this:SetAlpha(a) + elseif this.state == "shown" then + if this.stateTime >= TOAST_DURATION then + this.state = "hiding"; this.stateTime = 0 + end + elseif this.state == "hiding" then + local a = 1 - (this.stateTime / FADE_DURATION) + if a <= 0 then + a = 0 + this:Hide() + this.state = "hidden" + end + this:SetAlpha(a) + end + end) + + return f +end + +function RelationshipsAchievements_ShowToast(ach) + if not ach then return end + if not toast then toast = CreateToast() end + toast.ach = ach + toast.title:SetText(ach.name) + toast.icon:SetTexture(ach.icon or "Interface\\Icons\\INV_Misc_QuestionMark") + if ach.points and ach.points > 0 then + toast.points:SetText(ach.points.." points") + else + toast.points:SetText("") + end + toast:SetAlpha(0) + toast:Show() + toast.state = "showing" + toast.stateTime = 0 + PlaySoundFile("Sound\\Interface\\LevelUp.wav") +end diff --git a/UI.lua b/UI.lua new file mode 100644 index 0000000..ff09f9f --- /dev/null +++ b/UI.lua @@ -0,0 +1,528 @@ +-- WotLK-style Relationships Achievement UI +-- Left column: categories with per-category progress (Summary + real categories). +-- Right side: either a summary landing panel or a scrolling list of achievements. + +local FRAME_W, FRAME_H = 840, 620 +local ROW_H = 72 +local VISIBLE_ROWS = 7 +local RECENT_COUNT = 14 +local RECENT_ROW_H = 34 + +local selectedCategory = 0 +local scrollOffset = 0 + +local rows = {} +local catButtons = {} +local displayCats = {} +local summaryPanel +local listPanel + +local function FormatDate(t) + if not t then return "" end + return date("%m/%d/%y", t) +end + +local function GetAchievementsForCategory(catId) + local out = {} + for i = 1, table.getn(RelationshipsAchievements_List) do + if RelationshipsAchievements_List[i].cat == catId then + table.insert(out, RelationshipsAchievements_List[i]) + end + end + return out +end + +local function BuildDisplayCats() + displayCats = { { id = 0, name = "Summary" } } + for i = 1, table.getn(RelationshipsAchievements_Categories) do + table.insert(displayCats, RelationshipsAchievements_Categories[i]) + end +end + +-- ============ Progress bar helper ============ +-- Text is parented to the StatusBar itself at OVERLAY so it always renders +-- ABOVE the green fill texture (which is on the bar at ARTWORK layer). +local function CreateProgressBar(parent, width, height) + local wrap = CreateFrame("Frame", nil, parent) + if width then wrap:SetWidth(width) end + if height then wrap:SetHeight(height) end + wrap:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + wrap:SetBackdropColor(0, 0, 0, 0.7) + wrap:SetBackdropBorderColor(0.4, 0.3, 0.1, 1) + + local bar = CreateFrame("StatusBar", nil, wrap) + bar:SetPoint("TOPLEFT", wrap, "TOPLEFT", 3, -3) + bar:SetPoint("BOTTOMRIGHT", wrap, "BOTTOMRIGHT", -3, 3) + bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") + bar:SetStatusBarColor(0.15, 0.65, 0.15) + bar:SetMinMaxValues(0, 1) + bar:SetValue(0) + + -- Text is a child of the bar at OVERLAY so it draws above the fill. + local text = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + text:SetPoint("CENTER", bar, "CENTER", 0, 0) + text:SetTextColor(1, 1, 1) + text:SetDrawLayer("OVERLAY", 7) + + wrap.bar = bar + wrap.text = text + wrap.SetMinMaxValues = function(self, a, b) bar:SetMinMaxValues(a, b) end + wrap.SetValue = function(self, v) bar:SetValue(v) end + wrap.SetStatusBarColor = function(self, r,g,b,a) bar:SetStatusBarColor(r,g,b,a) end + + return wrap +end + +-- ============ Category buttons ============ + +local function UpdateCategoryButtons() + for i = 1, table.getn(displayCats) do + local btn = catButtons[i] + local cat = displayCats[i] + if btn then + if cat.id == 0 then + btn.text:SetText("Summary") + else + local done, total = RelationshipsAchievements:CategoryProgress(cat.id) + btn.text:SetText(cat.name.." ("..done.."/"..total..")") + end + if cat.id == selectedCategory then + btn.highlight:Show() + btn.text:SetTextColor(1, 0.82, 0) + else + btn.highlight:Hide() + btn.text:SetTextColor(1, 1, 1) + end + end + end +end + +-- ============ Achievement rows ============ + +local function UpdateRows() + local list = GetAchievementsForCategory(selectedCategory) + local n = table.getn(list) + if scrollOffset < 0 then scrollOffset = 0 end + if scrollOffset > math.max(0, n - VISIBLE_ROWS) then + scrollOffset = math.max(0, n - VISIBLE_ROWS) + end + + for i = 1, VISIBLE_ROWS do + local row = rows[i] + local ach = list[i + scrollOffset] + if ach then + row.ach = ach + row.icon:SetTexture(ach.icon or "Interface\\Icons\\INV_Misc_QuestionMark") + row.title:SetText(ach.name) + row.desc:SetText(ach.desc or "") + if ach.points and ach.points > 0 then + row.points:SetText(ach.points) + else + row.points:SetText("") + end + + local unlocked = RelationshipsAchievements:IsUnlocked(ach.id) + if unlocked then + row.icon:SetVertexColor(1, 1, 1) + row.title:SetTextColor(0.15, 0.75, 0.15) + row.desc:SetTextColor(1, 1, 1) + row.date:SetText(FormatDate(RelationshipsAchievements:GetUnlockTime(ach.id))) + row.bg:SetVertexColor(0.2, 0.15, 0.05, 0.8) + else + row.icon:SetVertexColor(0.35, 0.35, 0.35) + row.title:SetTextColor(0.6, 0.6, 0.6) + row.desc:SetTextColor(0.5, 0.5, 0.5) + row.date:SetText("") + row.bg:SetVertexColor(0.1, 0.1, 0.1, 0.6) + end + + if ach.account then row.acctTag:Show() else row.acctTag:Hide() end + + local cur, max = RelationshipsAchievements:GetProgress(ach.id) + if cur and max and not unlocked then + row.progressBar:SetMinMaxValues(0, max) + row.progressBar:SetValue(cur) + row.progressBar.text:SetText(cur.."/"..max) + row.progressBar:Show() + else + row.progressBar:Hide() + end + + row:Show() + else + row:Hide() + end + end +end + +-- ============ Summary landing page ============ + +local function CollectRecentUnlocks(limit) + local all = {} + for i = 1, table.getn(RelationshipsAchievements_List) do + local a = RelationshipsAchievements_List[i] + local t = RelationshipsAchievements:GetUnlockTime(a.id) + if t then + table.insert(all, { ach = a, time = t }) + end + end + table.sort(all, function(x, y) return x.time > y.time end) + local out = {} + for i = 1, math.min(limit, table.getn(all)) do + table.insert(out, all[i]) + end + return out +end + +local function UpdateSummary() + if not summaryPanel then return end + + local recent = CollectRecentUnlocks(RECENT_COUNT) + for i = 1, RECENT_COUNT do + local r = summaryPanel.recentRows[i] + local entry = recent[i] + if entry then + r.icon:SetTexture(entry.ach.icon or "Interface\\Icons\\INV_Misc_QuestionMark") + r.title:SetText(entry.ach.name) + r.date:SetText(FormatDate(entry.time)) + r.points:SetText((entry.ach.points and entry.ach.points > 0) and entry.ach.points or "") + r.ach = entry.ach + r:Show() + else + r:Hide() + end + end + if table.getn(recent) == 0 and summaryPanel.emptyLabel then + summaryPanel.emptyLabel:Show() + elseif summaryPanel.emptyLabel then + summaryPanel.emptyLabel:Hide() + end +end + +local function ApplySelectionVisibility() + if selectedCategory == 0 then + if summaryPanel then summaryPanel:Show() end + if listPanel then listPanel:Hide() end + else + if summaryPanel then summaryPanel:Hide() end + if listPanel then listPanel:Show() end + end +end + +function RelationshipsAchievements_RefreshUI() + UpdateCategoryButtons() + ApplySelectionVisibility() + if selectedCategory == 0 then + UpdateSummary() + else + UpdateRows() + end + if RelationshipsAchievementFrame then + local total = RelationshipsAchievements:TotalAvailable() + local done = RelationshipsAchievements:TotalUnlocked() + local pts = RelationshipsAchievements:TotalPoints() + if RelationshipsAchievementFrame.summary then + RelationshipsAchievementFrame.summary:SetText(done.."/"..total.." achievements |cffffd200"..pts.."|r points") + end + if RelationshipsAchievementFrame.overallBar then + RelationshipsAchievementFrame.overallBar:SetMinMaxValues(0, total) + RelationshipsAchievementFrame.overallBar:SetValue(done) + RelationshipsAchievementFrame.overallBar.text:SetText(done.." / "..total) + end + end +end + +-- Clicking a row in the main list should NOT open the popup box; the row +-- is already visible in the panel. Left intentionally as a no-op so future +-- selection/highlight logic can hook in without re-introducing the popup. +local function OnRowClick() +end + +local function CreateRow(parent, index) + local row = CreateFrame("Button", nil, parent) + row:SetWidth(600); row:SetHeight(ROW_H - 4) + row:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, -((index - 1) * ROW_H) - 6) + row:RegisterForClicks("LeftButtonUp") + row:SetScript("OnClick", OnRowClick) + + local bg = row:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(row) + bg:SetTexture("Interface\\Buttons\\WHITE8x8") + bg:SetVertexColor(0.1, 0.1, 0.1, 0.6) + row.bg = bg + + local icon = row:CreateTexture(nil, "ARTWORK") + icon:SetWidth(44); icon:SetHeight(44) + icon:SetPoint("TOPLEFT", row, "TOPLEFT", 8, -6) + row.icon = icon + + local title = row:CreateFontString(nil, "OVERLAY", "GameFontNormal") + title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -2) + title:SetJustifyH("LEFT") + row.title = title + + local desc = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2) + desc:SetPoint("RIGHT", row, "RIGHT", -60, 0) + desc:SetJustifyH("LEFT") + desc:SetHeight(18) + row.desc = desc + + local pbar = CreateProgressBar(row, nil, 16) + pbar:SetHeight(16) + pbar:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 62, 6) + pbar:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -60, 6) + pbar:Hide() + row.progressBar = pbar + + local points = row:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + points:SetPoint("TOPRIGHT", row, "TOPRIGHT", -10, -6) + points:SetTextColor(1, 0.82, 0) + row.points = points + + local date = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + date:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -10, 6) + date:SetTextColor(0.7, 0.7, 0.7) + row.date = date + + local acctTag = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + acctTag:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -110, 6) + acctTag:SetTextColor(0.4, 0.7, 1) + acctTag:SetText("Account-wide") + row.acctTag = acctTag + + return row +end + +local function CreateCategoryButton(parent, index, cat) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(170); btn:SetHeight(22) + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, -((index - 1) * 24) - 4) + + local hl = btn:CreateTexture(nil, "BACKGROUND") + hl:SetAllPoints(btn) + hl:SetTexture("Interface\\Buttons\\WHITE8x8") + hl:SetVertexColor(0.4, 0.3, 0.1, 0.7) + hl:Hide() + btn.highlight = hl + + local t = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal") + t:SetPoint("LEFT", btn, "LEFT", 8, 0) + t:SetJustifyH("LEFT") + btn.text = t + + btn:SetScript("OnClick", function() + selectedCategory = cat.id + scrollOffset = 0 + RelationshipsAchievements_RefreshUI() + end) + + return btn +end + +-- ============ Summary panel construction ============ + +local function CreateSummaryPanel(parent) + local panel = CreateFrame("Frame", nil, parent) + panel:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0) + panel:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0) + panel:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + panel:SetBackdropColor(0, 0, 0, 0.6) + panel:SetBackdropBorderColor(0.4, 0.3, 0.1, 1) + + local RIGHT_INSET = 14 + local recentHeader = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + recentHeader:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, -12) + recentHeader:SetText("Most Recent Achievements") + recentHeader:SetTextColor(1, 0.82, 0) + + panel.recentRows = {} + for i = 1, RECENT_COUNT do + local r = CreateFrame("Button", nil, panel) + r:SetHeight(RECENT_ROW_H - 2) + r:SetPoint("TOPLEFT", recentHeader, "BOTTOMLEFT", 0, -6 - ((i - 1) * RECENT_ROW_H)) + r:SetPoint("RIGHT", panel, "RIGHT", -RIGHT_INSET, 0) + r:RegisterForClicks("LeftButtonUp") + + local bg = r:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(r) + bg:SetTexture("Interface\\Buttons\\WHITE8x8") + bg:SetVertexColor(0.2, 0.15, 0.05, 0.7) + + local icon = r:CreateTexture(nil, "ARTWORK") + icon:SetWidth(20); icon:SetHeight(20) + icon:SetPoint("LEFT", r, "LEFT", 4, 0) + r.icon = icon + + local title = r:CreateFontString(nil, "OVERLAY", "GameFontNormal") + title:SetPoint("LEFT", icon, "RIGHT", 8, 0) + title:SetTextColor(0.15, 0.75, 0.15) + r.title = title + + -- Fixed width + right-justify so the LEFT edge of the points field + -- stays put whether the value is "5", "10", "25" or "50". Without a + -- fixed width, a shorter number shifts the date column right and + -- knocks the row out of alignment. + local pts = r:CreateFontString(nil, "OVERLAY", "GameFontNormal") + pts:SetPoint("RIGHT", r, "RIGHT", -10, 0) + pts:SetWidth(28) + pts:SetJustifyH("RIGHT") + pts:SetTextColor(1, 0.82, 0) + r.points = pts + + local date = r:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + date:SetPoint("RIGHT", pts, "LEFT", -14, 0) + date:SetWidth(64) + date:SetJustifyH("RIGHT") + date:SetTextColor(0.7, 0.7, 0.7) + r.date = date + + r:SetScript("OnClick", function() + if this and this.ach then + local ach = this.ach + selectedCategory = ach.cat or 0 + local list = GetAchievementsForCategory(selectedCategory) + local idx = 1 + for j = 1, table.getn(list) do + if list[j] == ach then idx = j; break end + end + scrollOffset = idx - 1 + local maxOff = math.max(0, table.getn(list) - VISIBLE_ROWS) + if scrollOffset > maxOff then scrollOffset = maxOff end + RelationshipsAchievements_RefreshUI() + -- Do not open the popup; the user is taken to the achievement + -- inside the main UI (category selected + scrolled into view). + end + end) + + panel.recentRows[i] = r + end + + local empty = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + empty:SetPoint("TOPLEFT", recentHeader, "BOTTOMLEFT", 4, -10) + empty:SetText("No achievements earned yet. Get out there!") + empty:SetTextColor(0.7, 0.7, 0.7) + empty:Hide() + panel.emptyLabel = empty + + + return panel +end + +local function CreateAchievementFrame() + BuildDisplayCats() + + local f = CreateFrame("Frame", "RelationshipsAchievementFrame", UIParent) + f:SetWidth(FRAME_W); f:SetHeight(FRAME_H) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + f:SetFrameStrata("DIALOG") + f:SetMovable(false); f:EnableMouse(true) + f:SetClampedToScreen(true) + f:Hide() + + f:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border", + tile = true, tileSize = 32, edgeSize = 32, + insets = { left = 11, right = 12, top = 12, bottom = 11 }, + }) + + local titleBg = f:CreateTexture(nil, "ARTWORK") + titleBg:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header") + titleBg:SetWidth(460); titleBg:SetHeight(64) + titleBg:SetPoint("TOP", f, "TOP", 0, 12) + + -- Center vertically inside the gold title header texture (which is 64px tall + -- anchored 12px above the frame top, so its visual center sits ~20px above f:TOP). + local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") + title:SetPoint("CENTER", f, "TOP", 0, -8) + title:SetText("Relationships Achievements") + title:SetTextColor(1, 0.82, 0) + + local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + summary:SetPoint("TOP", f, "TOP", 0, -28) + f.summary = summary + + local overall = CreateProgressBar(f, FRAME_W - 60, 16) + overall:SetPoint("TOP", f, "TOP", 0, -48) + f.overallBar = overall + + local close = CreateFrame("Button", nil, f, "UIPanelCloseButton") + close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -4) + close:SetScript("OnClick", function() f:Hide() end) + + -- Category panel + local catPanel = CreateFrame("Frame", nil, f) + catPanel:SetWidth(180); catPanel:SetHeight(FRAME_H - 105) + catPanel:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -76) + catPanel:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + catPanel:SetBackdropColor(0, 0, 0, 0.6) + catPanel:SetBackdropBorderColor(0.4, 0.3, 0.1, 1) + + for i = 1, table.getn(displayCats) do + catButtons[i] = CreateCategoryButton(catPanel, i, displayCats[i]) + end + + local host = CreateFrame("Frame", nil, f) + host:SetPoint("TOPLEFT", catPanel, "TOPRIGHT", 8, 0) + host:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 29) + + listPanel = CreateFrame("Frame", nil, host) + listPanel:SetPoint("TOPLEFT", host, "TOPLEFT", 0, 0) + listPanel:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", 0, 0) + listPanel:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = false, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + listPanel:SetBackdropColor(0, 0, 0, 0.6) + listPanel:SetBackdropBorderColor(0.4, 0.3, 0.1, 1) + + for i = 1, VISIBLE_ROWS do + rows[i] = CreateRow(listPanel, i) + end + + listPanel:EnableMouseWheel(true) + listPanel:SetScript("OnMouseWheel", function() + local delta = arg1 or 0 + scrollOffset = scrollOffset - delta + UpdateRows() + end) + + summaryPanel = CreateSummaryPanel(host) + + tinsert(UISpecialFrames, "RelationshipsAchievementFrame") + + return f +end + +function RelationshipsAchievements_Toggle() + if not RelationshipsAchievementFrame then + CreateAchievementFrame() + end + if RelationshipsAchievementFrame:IsVisible() then + RelationshipsAchievementFrame:Hide() + else + RelationshipsAchievementFrame:Show() + if RelationshipsAchievements and not RelationshipsAchievements:IsUnlocked(104) then + RelationshipsAchievements:Unlock(104) + end + RelationshipsAchievements_RefreshUI() + end +end