From e634c4fad2629aa349d1ce5b03dff78feb4afb92 Mon Sep 17 00:00:00 2001 From: Relationship <571+relationship@noreply.octowow.st> Date: Thu, 23 Jul 2026 12:06:26 +0000 Subject: [PATCH] Upload files to "/" --- Events.lua | 552 +++++++++++++++++++++++++++++++++- Minimap.lua | 147 +++++++++ RelationshipsAchievements.toc | 1 + 3 files changed, 696 insertions(+), 4 deletions(-) create mode 100644 Minimap.lua diff --git a/Events.lua b/Events.lua index ed1218f..8d33d42 100644 --- a/Events.lua +++ b/Events.lua @@ -21,7 +21,10 @@ local trackedEvents = { "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" + "UNIT_INVENTORY_CHANGED", "ADDON_LOADED", "MERCHANT_SHOW", "MERCHANT_CLOSED", + "TIME_PLAYED_MSG", "UNIT_PET", "UNIT_MODEL_CHANGED", "LEARNED_SPELL_IN_TAB", + -- OctoWoW / Turtle backported Companion API (Pet menu "P" -> Companions tab) + "COMPANION_LEARNED", "COMPANION_UPDATE", "COMPANION_UNLEARNED", "PET_UI_UPDATE" } for i = 1, table.getn(trackedEvents) do SafeRegister(trackedEvents[i]) end @@ -157,6 +160,128 @@ end local RA_ScanTip = CreateFrame("GameTooltip", "RelationshipsAchievementsScanTip", UIParent, "GameTooltipTemplate") RA_ScanTip:SetOwner(UIParent, "ANCHOR_NONE") +-- ============ Known companion names (Vanilla + TurtleWoW / OctoWoW) ============ +-- Any spell whose name matches one of these entries is credited as a +-- non-combat pet even when its tooltip doesn't contain the standard +-- keywords. Extend this list as new companions are added to the server. +local RA_KNOWN_COMPANIONS_LIST = { + -- Vanilla non-combat pets + "Ancona Chicken", "Azure Whelpling", "Bombay Cat", "Black Kingsnake", + "Black Tabby", "Blue Dragonhawk Hatchling", "Brown Prairie Dog", + "Brown Rabbit", "Brown Snake", "Calico Cat", "Cat Carrier (Bombay)", + "Cat Carrier (Black Tabby)", "Cat Carrier (Cornish Rex)", + "Cat Carrier (Orange Tabby)", "Cat Carrier (Siamese)", + "Cat Carrier (Silver Tabby)", "Cat Carrier (White Kitten)", + "Chicken Egg", "Cockroach", "Cornish Rex", "Crimson Whelpling", + "Dark Whelpling", "Disgusting Oozeling", "Emerald Whelpling", + "Firefly", "Frog", "Golden Dragonhawk Hatchling", "Great Horned Owl", + "Green Wing Macaw", "Hawk Owl", "Hyacinth Macaw", "Island Frog", + "Jubling", "Lifelike Mechanical Toad", "Lifelike Toad", + "Little Fawn", "Mana Wyrmling", "Mechanical Squirrel Box", + "Mini Diablo", "Mulgore Hatchling", "Orange Tabby", + "Panda Cub", "Parrot Cage (Green Wing Macaw)", + "Parrot Cage (Hyacinth Macaw)", "Parrot Cage (Senegal)", + "Peddlefeet", "Personal World Destroyer", "Prairie Dog Whistle", + "Red Dragonhawk Hatchling", "Sprite Darter Hatchling", + "Senegal", "Siamese", "Silver Tabby", "Smolderweb Hatchling", + "Snowshoe Rabbit", "Snowy Owl", "Speedy", "Stinker", + "Tiny Green Dragon", "Tiny Red Dragon", "Tranquil Mechanical Yeti", + "Tree Frog Box", "Turquoise Turtle", "Wanderer's Companion", + "Westfall Chicken", "White Kitten", "Whiskers the Rat", + "Winter Reindeer", "Winter's Little Helper", "Worg Carrier", + "Worg Pup", "Wolpertinger", "Zergling", + -- Turtle WoW / OctoWoW additions (community-known companions) + "Baby Blizzard Bear", "Baby Crocolisk", "Baby Murloc", + "Baby Ogre", "Baby Shark", "Baby Tallstrider", "Baby Turtle", + "Bat Cub", "Bloodsail Cannonball", "Blue Moth", "Bronze Whelpling", + "Captured Firefly", "Clockwork Rocket Bot", "Cobra Hatchling", + "Coral Whelpling", "Corrupt Whelpling", "Deviate Hatchling", + "Elekk Plushie", "Ethereal Soul-Trader", "Fel Whelpling", + "Forest Frog", "Fox Kit", "Ghost Cat", "Ghostly Skull", + "Gorilla Hatchling", "Grunty", "Hippogryph Hatchling", + "Ivory Raptor", "Jade Whelpling", "Lil' Alexstrasza", + "Lil' Deathwing", "Lil' K.T.", "Lil' Ragnaros", "Lil' Smoky", + "Lil' Tarecgosa", "Lil' Timmy", "Lil' Wickerman", "Lil' XT", + "Lucky Cricket", "Magical Crawdad", "Mechanical Chicken", + "Micro Sentry", "Mini Tyrael", "Moonkin Hatchling", + "Murky", "Naga Hatchling", "Netherwhelp", "Nightsaber Cub", + "Onyx Whelpling", "Pandaren Monk", "Peanut", "Perky Pug", + "Piglet's Collar", "Pink Elekk", "Plague Rat", "Polar Bear Cub", + "Purple Turtle", "Quilen Cub", "Raptor Hatchling", "Red Panda", + "Reef Wanderer", "Riding Turtle Hatchling", "Sand Kitten", + "Sandbox Tiger", "Sandstone Drake", "Sea Pony", "Shore Crab", + "Silithid Hatchling", "Snowball", "Snowy Rabbit", + "Spectral Tiger Cub", "Stormwind Rat", "Swamp Frog", + "Tiny Emerald Whelpling", "Tiny Sporebat", "Tricorne", + "Turkey", "Undercity Cockroach", "Wisp", "Withers", + "Ziggy the Sifaka", + -- OctoWoW Companions tab (as shown in the "P" menu) + "Green Steam Tonk", "Purple Steam Tonk", "Blue Steam Tonk", + "Red Steam Tonk", "Yellow Steam Tonk", "Steam Tonk", + "Steam Tonk Controller", "Control Console", + "Lulu", "Mr. Wiggles", "Pet Bombling", "Lifelike Mechanical Toad", + "Tranquil Mechanical Yeti", "Mechanical Squirrel", + "Green Kitten", "Black Kitten", "Orange Kitten", "Cornish Rex Cat", + "Bombay Cat", "Siamese Cat", "Silver Tabby Cat", +} +local RA_KNOWN_COMPANIONS_SET = {} +for _, n in ipairs(RA_KNOWN_COMPANIONS_LIST) do + RA_KNOWN_COMPANIONS_SET[string.lower(n)] = true +end + +-- Substring patterns that identify a companion regardless of colour / prefix. +-- Any spellbook or summoned pet name containing one of these (case-insensitive) +-- is treated as a known companion. Covers the OctoWoW / Turtle families where +-- every colour variant shares the same base name (e.g. "Green Steam Tonk", +-- "Purple Steam Tonk", "White Steam Tonk" all match "steam tonk"). +local RA_COMPANION_PATTERNS = { + "steam tonk", "kitten", "whelpling", "hatchling", "cub", + "prairie dog", "rabbit", "squirrel", "toad", "frog", + "cockroach", "cat carrier", "mechanical squirrel", + "lil'", "lil ", "mini ", "tiny ", "baby ", + "companion", "critter", +} + +function RA_IsKnownCompanionName(name) + if type(name) ~= "string" or name == "" then return false end + local lower = string.lower(name) + if RA_KNOWN_COMPANIONS_SET[lower] == true then return true end + for _, pat in ipairs(RA_COMPANION_PATTERNS) do + if string.find(lower, pat, 1, true) then return true end + end + return false +end + +function RA_AddKnownCompanion(name) + if type(name) == "string" and name ~= "" then + RA_KNOWN_COMPANIONS_SET[string.lower(name)] = true + end +end + +-- Credit a companion by name into the same store the tooltip path uses so +-- the total petCount stays consistent no matter which detection path found it. +function RA_CreditCompanionByName(name) + if not RA_IsKnownCompanionName(name) then return end + if not RelationshipsAchievementsCharDB then return end + if not RelationshipsAchievementsCharDB.countedPets then + RelationshipsAchievementsCharDB.countedPets = {} + end + RelationshipsAchievementsCharDB.countedPets[name] = true + local n = 0 + for _ in pairs(RelationshipsAchievementsCharDB.countedPets) do n = n + 1 end + local companionPets = 0 + if RelationshipsAchievementsCharDB.countedCompanions then + for _ in pairs(RelationshipsAchievementsCharDB.countedCompanions) do + companionPets = companionPets + 1 + end + end + local total = n + if companionPets > total then total = companionPets end + RelationshipsAchievements:UpdateStat("petCount", total) + if total >= 1 then RelationshipsAchievements:Unlock(1204) end + if total >= 10 then RelationshipsAchievements:Unlock(1205) end +end + local function TooltipText(index, book) RA_ScanTip:ClearLines() RA_ScanTip:SetSpell(index, book) @@ -171,6 +296,123 @@ local function TooltipText(index, book) return string.lower(text) end +-- ============ Known mount names (Vanilla + TurtleWoW / OctoWoW) ============ +-- Same rationale as the companion list: some private-server mounts (e.g. the +-- OctoWoW "Zebra") have tooltips that don't include the classic "mount speed" +-- / "rideable" phrasing, so tooltip scanning misses them. A name whitelist +-- guarantees credit whenever such a spell appears in the spellbook or is +-- summoned by the player. +local RA_KNOWN_MOUNTS_LIST = { + -- Horde racial ground mounts (Vanilla) + "Brown Wolf", "Dire Wolf", "Timber Wolf", "Red Wolf", "Winter Wolf", + "Frostwolf", "Black Wolf", "Arctic Wolf", "Swift Brown Wolf", + "Swift Timber Wolf", "Swift Gray Wolf", "Swift Frostwolf", + "Black Skeletal Horse", "Blue Skeletal Horse", "Brown Skeletal Horse", + "Red Skeletal Horse", "Green Skeletal Warhorse", "Purple Skeletal Warhorse", + "Red Skeletal Warhorse", "Grey Kodo", "Brown Kodo", "Green Kodo", + "Teal Kodo", "Great White Kodo", "Great Grey Kodo", "Great Brown Kodo", + "Great Green Kodo", "Ivory Raptor", "Emerald Raptor", "Turquoise Raptor", + "Violet Raptor", "Obsidian Raptor", "Swift Blue Raptor", "Swift Olive Raptor", + "Swift Orange Raptor", "Whistle of the Emerald Raptor", + "Whistle of the Ivory Raptor", "Whistle of the Turquoise Raptor", + "Whistle of the Violet Raptor", "Whistle of the Mottled Red Raptor", + -- Alliance racial ground mounts (Vanilla) + "Pinto", "Palomino", "Chestnut Mare", "Brown Horse", "White Stallion", + "Black Stallion", "Swift Brown Steed", "Swift Palomino", "Swift White Steed", + "Gray Ram", "Brown Ram", "White Ram", "Black Ram", "Frost Ram", + "Swift Gray Ram", "Swift Brown Ram", "Swift White Ram", + "Mechanostrider", "Blue Mechanostrider", "Green Mechanostrider", + "Red Mechanostrider", "White Mechanostrider", "Yellow Mechanostrider", + "Unpainted Mechanostrider", "Swift Green Mechanostrider", + "Swift White Mechanostrider", "Swift Yellow Mechanostrider", + "Striped Frostsaber", "Striped Nightsaber", "Spotted Frostsaber", + "Striped Dawnsaber", "Swift Frostsaber", "Swift Mistsaber", + "Swift Stormsaber", "Reins of the Striped Frostsaber", + "Reins of the Striped Nightsaber", "Reins of the Spotted Frostsaber", + -- Class mounts (Vanilla) + "Summon Warhorse", "Summon Charger", "Summon Felsteed", "Summon Dreadsteed", + "Warhorse", "Charger", "Felsteed", "Dreadsteed", + -- PvP / faction / reputation / rare mounts (Vanilla) + "Black Battlestrider", "Black War Kodo", "Black War Ram", "Black War Raptor", + "Black War Steed", "Black War Tiger", "Black War Wolf", "Horn of the Black War Wolf", + "Horn of the Black Wolf", "Horn of the Brown Wolf", "Horn of the Dire Wolf", + "Horn of the Red Wolf", "Horn of the Timber Wolf", "Horn of the Arctic Wolf", + "Horn of the Frostwolf", "Horn of the Swift Brown Wolf", + "Horn of the Swift Gray Wolf", "Horn of the Swift Timber Wolf", + "Rivendare's Deathcharger", "Deathcharger's Reins", "Reins of the Rivendare's Deathcharger", + "Winterspring Frostsaber", "Reins of the Winterspring Frostsaber", + "Swift Zulian Tiger", "Reins of the Swift Zulian Tiger", + "Swift Razzashi Raptor", "Reins of the Swift Razzashi Raptor", + "Qiraji Battle Tank", "Reins of the Blue Qiraji Battle Tank", + "Reins of the Green Qiraji Battle Tank", "Reins of the Red Qiraji Battle Tank", + "Reins of the Yellow Qiraji Battle Tank", + "Tawny Sabercat", "Reins of the Tawny Sabercat", + "Cenarion War Hippogryph", "Reins of the Cenarion War Hippogryph", + -- Turtle WoW / OctoWoW custom mounts (community-known) + "Zebra", "Reins of the Zebra", "Whistle of the Zebra", + "Zhevra", "Reins of the Zhevra", + "Silver Riding Turtle", "Reins of the Silver Riding Turtle", + "Riding Turtle", "Reins of the Riding Turtle", + "Sea Turtle", "Reins of the Sea Turtle", + "Brown Ostrich", "White Ostrich", "Black Ostrich", "Reins of the Ostrich", + "Swift Ostrich", "Reins of the Swift Ostrich", + "Camel", "Brown Camel", "Tan Camel", "White Camel", "Reins of the Camel", + "Swift Camel", "Reins of the Swift Camel", + "Reindeer", "Winter Reindeer", "Reins of the Reindeer", + "Great Elk", "Reins of the Great Elk", + "Moose", "Reins of the Moose", "Swift Moose", + "Boar", "War Boar", "Reins of the War Boar", "Swift War Boar", + "Bear Mount", "Riding Bear", "Reins of the Riding Bear", + "Swift Bear", "Polar Bear", "Reins of the Polar Bear", + "Cheetah", "Reins of the Cheetah", "Swift Cheetah", + "Lion", "White Lion", "Reins of the White Lion", + "Steam Tank Mount", "Steamscale", "Mechano-Hog", + "Goblin Trike", "Trike", "Reins of the Trike", + "Snow Leopard", "Reins of the Snow Leopard", + "Ochre Skeletal Warhorse", "Ancona Chicken Mount", + "Turtle Mount", "Great Sea Turtle", + "Black Panther", "Reins of the Black Panther", + "Giraffe", "Reins of the Giraffe", + "Hyena", "Reins of the Hyena", + "Ram of Ironforge", "Kodo of Thunder Bluff", +} +local RA_KNOWN_MOUNTS_SET = {} +for _, n in ipairs(RA_KNOWN_MOUNTS_LIST) do + RA_KNOWN_MOUNTS_SET[string.lower(n)] = true +end + +function RA_IsKnownMountName(name) + if type(name) ~= "string" or name == "" then return false end + return RA_KNOWN_MOUNTS_SET[string.lower(name)] == true +end + +function RA_AddKnownMount(name) + if type(name) == "string" and name ~= "" then + RA_KNOWN_MOUNTS_SET[string.lower(name)] = true + end +end + +-- Persistent per-character store so name-detected mounts survive reloads and +-- merge with tooltip-detected ones without double counting. +function RA_CreditMountByName(name) + if not RA_IsKnownMountName(name) then return end + if not RelationshipsAchievementsCharDB then return end + if not RelationshipsAchievementsCharDB.countedMounts then + RelationshipsAchievementsCharDB.countedMounts = {} + end + RelationshipsAchievementsCharDB.countedMounts[string.lower(name)] = true +end + +local function CountStoredMounts() + if not RelationshipsAchievementsCharDB + or not RelationshipsAchievementsCharDB.countedMounts then + return 0 + end + local n = 0 + for _ in pairs(RelationshipsAchievementsCharDB.countedMounts) do n = n + 1 end + return n +end + local function ScanSpellbook() if not GetSpellName then return end local book = BOOKTYPE_SPELL or "spell" @@ -194,19 +436,48 @@ local function ScanSpellbook() -- 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) + -- Non-combat pets: several tooltip phrasings exist across + -- Vanilla / TurtleWoW / OctoWoW. Match any of them, then filter + -- out 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) + or string.find(tip, "small pet", 1, true) + or string.find(tip, "critter", 1, true) + or string.find(tip, "summons and dismisses a", 1, true) + or string.find(tip, "summons and dismisses your", 1, true) + or string.find(tip, "summon a small", 1, true) + or string.find(tip, "vanity pet", 1, true) + or string.find(tip, "non-combat pet", 1, true) + ) + and not string.find(tip, "summons your pet", 1, true) and not string.find(tip, "combat pet", 1, true) + and not isMount 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 + if RA_CreditMountByName then RA_CreditMountByName(name) end elseif isPet then pets = pets + 1 end + -- Name-based fast path: known vanilla + Turtle/Octo companions. + -- Ensures custom pets whose tooltips don't match keywords still count. + if (not isPet) and (not isMount) + and RA_IsKnownCompanionName and RA_IsKnownCompanionName(name) then + pets = pets + 1 + if RA_CreditCompanionByName then RA_CreditCompanionByName(name) end + end + -- Name-based fast path: known vanilla + Turtle/Octo mounts. + -- Catches custom mounts (e.g. Zebra) whose tooltips omit the + -- classic "mount speed" / "rideable" keywords. + if (not isMount) and (not isPet) + and RA_IsKnownMountName and RA_IsKnownMountName(name) then + mounts = mounts + 1 + if RA_CreditMountByName then RA_CreditMountByName(name) end + end end i = i + 1 end @@ -223,6 +494,12 @@ local function ScanSpellbook() end end + -- Merge with the persistent name-detected mount store so counts never + -- regress if a mount was seen previously but is currently unavailable + -- from the spellbook scan (e.g. server API hiccup). + local storedMounts = CountStoredMounts() + if storedMounts > mounts then mounts = storedMounts end + RelationshipsAchievements:UpdateStat("mountCount", mounts) RelationshipsAchievements:UpdateStat("petCount", pets) if mounts >= 1 then RelationshipsAchievements:Unlock(211) end @@ -232,6 +509,213 @@ local function ScanSpellbook() if pets >= 1 then RelationshipsAchievements:Unlock(1204) end end +-- Expose so slash command / minimap can force a rescan. +RelationshipsAchievements_ScanSpellbook = ScanSpellbook + +-- ============ Companion API scanning (OctoWoW / Turtle) ============ +-- OctoWoW's Pet menu ("P") has a Companions tab powered by the backported +-- Companion API (GetNumCompanions/GetCompanionInfo/CallCompanion). This is +-- the authoritative source for non-combat pet ownership on those clients +-- and is far more reliable than tooltip scanning the spellbook. +local function EnsureCountedCompanions() + if not RelationshipsAchievementsCharDB then return nil end + if not RelationshipsAchievementsCharDB.countedCompanions then + RelationshipsAchievementsCharDB.countedCompanions = {} + end + return RelationshipsAchievementsCharDB.countedCompanions +end + +local function CountTable(t) + local n = 0 + if t then for _ in pairs(t) do n = n + 1 end end + return n +end + +local function ScanCompanions() + if not GetNumCompanions then return end + local counted = EnsureCountedCompanions() + if not counted then return end + -- Different builds/servers register the companion tab under different + -- type strings. Try each so we always sync with the "P" menu Companions + -- tab regardless of which naming OctoWoW / Turtle uses. + local types = { "CRITTER", "COMPANION", "COMPANIONS", "PET", "MINIPET" } + for _, ctype in ipairs(types) do + local ok, num = pcall(GetNumCompanions, ctype) + if ok and num and num > 0 then + for i = 1, num do + local ok2, _, name = pcall(GetCompanionInfo, ctype, i) + if ok2 and name and name ~= "" then + counted[name] = true + else + counted[ctype.."_"..i] = true + end + end + end + end + local total = CountTable(counted) + -- Merge with tooltip-scanned pets so we always report the higher value. + local spellPets = (RelationshipsAchievementsCharDB.stats and RelationshipsAchievementsCharDB.stats.petCount) or 0 + if total < spellPets then total = spellPets end + RelationshipsAchievements:UpdateStat("petCount", total) + if total >= 1 then RelationshipsAchievements:Unlock(1204) end + if total >= 10 then RelationshipsAchievements:Unlock(1205) end +end +RelationshipsAchievements_ScanCompanions = ScanCompanions + +-- Backup path: hook CallCompanion so a summon always credits the pet, even +-- if the addon loaded before the Companion API populated. +if CallCompanion then + local _origCallCompanion = CallCompanion + CallCompanion = function(ctype, index) + _origCallCompanion(ctype, index) + if ctype == "CRITTER" then + local counted = EnsureCountedCompanions() + if counted then + local ok, _, name = pcall(GetCompanionInfo, "CRITTER", index) + local key = (ok and name and name ~= "") and name or ("companion_"..index) + counted[key] = true + local n = CountTable(counted) + RelationshipsAchievements:UpdateStat("petCount", n) + RelationshipsAchievements:Unlock(1204) + if n >= 10 then RelationshipsAchievements:Unlock(1205) end + end + end + end +end + +-- Backup pet-count tracking: if a player casts a spell whose tooltip matches +-- pet criteria, ensure it counts. This catches Turtle/Octo custom companions +-- whose tooltips ScanSpellbook may not have visited yet. +local function EnsureCountedPets() + if not RelationshipsAchievementsCharDB then return nil end + if not RelationshipsAchievementsCharDB.countedPets then + RelationshipsAchievementsCharDB.countedPets = {} + end + return RelationshipsAchievementsCharDB.countedPets +end + +local function TooltipTextByName(spellName) + if not spellName or spellName == "" then return "" end + RA_ScanTip:ClearLines() + -- SetSpell by index only; iterate spellbook once to find the name. + if not GetSpellName then return "" end + local book = BOOKTYPE_SPELL or "spell" + local i = 1 + while true do + local name = GetSpellName(i, book) + if not name then break end + if name == spellName then + return TooltipText(i, book) + end + i = i + 1 + end + return "" +end + +local function CreditPetSpell(spellName) + if not spellName or spellName == "" then return end + local counted = EnsureCountedPets() + if not counted then return end + if counted[spellName] then return end + -- Known-companion fast path (no tooltip required). + if RA_IsKnownCompanionName and RA_IsKnownCompanionName(spellName) then + counted[spellName] = true + local n = 0 + for _ in pairs(counted) do n = n + 1 end + local companionPets = 0 + if RelationshipsAchievementsCharDB and RelationshipsAchievementsCharDB.countedCompanions then + for _ in pairs(RelationshipsAchievementsCharDB.countedCompanions) do + companionPets = companionPets + 1 + end + end + local total = n + if companionPets > total then total = companionPets end + RelationshipsAchievements:UpdateStat("petCount", total) + RelationshipsAchievements:Unlock(1204) + if total >= 10 then RelationshipsAchievements:Unlock(1205) end + return + end + local tip = TooltipTextByName(spellName) + if tip == "" then return end + local isPet = ( + string.find(tip, "companion", 1, true) + or string.find(tip, "right click to dismiss", 1, true) + or string.find(tip, "small pet", 1, true) + or string.find(tip, "critter", 1, true) + or string.find(tip, "summons and dismisses a", 1, true) + or string.find(tip, "summons and dismisses your", 1, true) + or string.find(tip, "vanity pet", 1, true) + or string.find(tip, "non-combat pet", 1, true) + ) + and not string.find(tip, "summons your pet", 1, true) + and not string.find(tip, "mount speed", 1, true) + and not string.find(tip, "rideable", 1, true) + if isPet then + counted[spellName] = true + local n = 0 + for _ in pairs(counted) do n = n + 1 end + RelationshipsAchievements:UpdateStat("petCount", n) + RelationshipsAchievements:Unlock(1204) + end +end + +-- Credit a mount cast by name. Mirrors CreditPetSpell but for mounts, +-- using the known-mount whitelist so custom Turtle/OctoWoW mounts (e.g. +-- "Zebra") count the moment the player summons them, even if the client's +-- spellbook scan hadn't picked them up yet. +local function CreditMountSpell(spellName) + if not spellName or spellName == "" then return end + if not RA_IsKnownMountName or not RA_IsKnownMountName(spellName) then return end + if not RelationshipsAchievementsCharDB then return end + if not RelationshipsAchievementsCharDB.countedMounts then + RelationshipsAchievementsCharDB.countedMounts = {} + end + local key = string.lower(spellName) + if RelationshipsAchievementsCharDB.countedMounts[key] then return end + RelationshipsAchievementsCharDB.countedMounts[key] = true + local n = 0 + for _ in pairs(RelationshipsAchievementsCharDB.countedMounts) do n = n + 1 end + -- Never regress the stat: prefer whichever count is higher. + local prev = 0 + if RelationshipsAchievementsCharDB.stats + and RelationshipsAchievementsCharDB.stats.mountCount then + prev = RelationshipsAchievementsCharDB.stats.mountCount + end + if n < prev then n = prev end + RelationshipsAchievements:UpdateStat("mountCount", n) + RelationshipsAchievements:Unlock(211) +end + +local _origCastSpellByName = CastSpellByName +if _origCastSpellByName then CastSpellByName = function(name, onSelf) + _origCastSpellByName(name, onSelf) + CreditPetSpell(name) + CreditMountSpell(name) +end end + +local _origCastSpell = CastSpell +if _origCastSpell then CastSpell = function(spellId, bookType) + _origCastSpell(spellId, bookType) + if GetSpellName then + local n = GetSpellName(spellId, bookType or BOOKTYPE_SPELL or "spell") + CreditPetSpell(n) + CreditMountSpell(n) + end +end end + +local _origUseAction = UseAction +if _origUseAction then UseAction = function(slot, checkCursor, onSelf) + _origUseAction(slot, checkCursor, onSelf) + if GetActionText then + local txt = GetActionText(slot) + if txt then + CreditPetSpell(txt) + CreditMountSpell(txt) + end + end +end end + + function RelationshipsAchievements_ScanCharacter() RelationshipsAchievements:Init() @@ -374,6 +858,9 @@ function RelationshipsAchievements_ScanCharacter() ScanTalents() ScanEquipmentAndBags() ScanSpellbook() + if RelationshipsAchievements_ScanCompanions then + RelationshipsAchievements_ScanCompanions() + end RelationshipsAchievements_RefreshUI = RelationshipsAchievements_RefreshUI or function() end if RelationshipsAchievementFrame and RelationshipsAchievementFrame:IsVisible() then @@ -642,8 +1129,31 @@ local function OnEnemyDeath(msg) end end +-- Match Turtle/Octo /played extension lines that expose the character's +-- completed-quest total. Multiple phrasings observed across custom builds. +local function TryParseQuestCount(msg) + if not msg then return end + local patterns = { + "[Qq]uests? [Cc]ompleted:?%s*(%d+)", + "[Cc]ompleted [Qq]uests?:?%s*(%d+)", + "[Tt]otal [Qq]uests?:?%s*(%d+)", + "[Qq]uests?[^%d]-(%d+)%s*completed", + } + for i = 1, table.getn(patterns) do + local _, _, num = string.find(msg, patterns[i]) + if num then + local n = tonumber(num) + if n and n > 0 then + RelationshipsAchievements:UpdateStat("questCount", n) + return true + end + end + end +end + local function OnSystemMessage(msg) if not msg then return end + TryParseQuestCount(msg) local _, _, winner = string.find(msg, "^(.-) has defeated ") if winner and winner == UnitName("player") then RelationshipsAchievements:AddStat("duelWins", 1) @@ -787,5 +1297,39 @@ frame:SetScript("OnEvent", function() RelationshipsAchievements:Unlock(103) if GetNumGuildMembers and GetNumGuildMembers() >= 10 then RelationshipsAchievements:Unlock(1703) end end + elseif event == "TIME_PLAYED_MSG" then + -- Some /played variants (Turtle/Octo) also emit "Quests completed: N" + -- as separate CHAT_MSG_SYSTEM lines around this event. Rescan the + -- spellbook too so any pets learned since last scan are counted. + if RelationshipsAchievements_ScanSpellbook then + RelationshipsAchievements_ScanSpellbook() + end + elseif event == "UNIT_PET" or event == "UNIT_MODEL_CHANGED" then + -- A pet/companion just spawned or despawned. Rescan the spellbook so + -- newly-learned companion spells count immediately. + if arg1 == "player" and RelationshipsAchievements_ScanSpellbook then + RelationshipsAchievements_ScanSpellbook() + if RelationshipsAchievements_ScanCompanions then + RelationshipsAchievements_ScanCompanions() + end + -- Name-based fallback: if the summoned pet's unit name matches a + -- known companion, credit it directly. + if UnitName and RA_CreditCompanionByName then + local petName = UnitName("pet") + if petName then RA_CreditCompanionByName(petName) end + end + end + elseif event == "COMPANION_LEARNED" or event == "COMPANION_UPDATE" + or event == "COMPANION_UNLEARNED" or event == "PET_UI_UPDATE" then + if RelationshipsAchievements_ScanCompanions then + RelationshipsAchievements_ScanCompanions() + end + elseif event == "LEARNED_SPELL_IN_TAB" then + if RelationshipsAchievements_ScanSpellbook then + RelationshipsAchievements_ScanSpellbook() + end + if RelationshipsAchievements_ScanCompanions then + RelationshipsAchievements_ScanCompanions() + end end end) diff --git a/Minimap.lua b/Minimap.lua new file mode 100644 index 0000000..74bde5d --- /dev/null +++ b/Minimap.lua @@ -0,0 +1,147 @@ +-- Movable / hideable minimap button. +-- Named "RelationshipsAchievementsMinimapButton" and parented to Minimap so +-- MinimapButtonBag and MinimapButtonFrame automatically discover and manage it. + +local MMB = CreateFrame("Button", "RelationshipsAchievementsMinimapButton", Minimap) +MMB:SetFrameStrata("MEDIUM") +MMB:SetWidth(31); MMB:SetHeight(31) +MMB:SetFrameLevel(8) +MMB:RegisterForClicks("LeftButtonUp", "RightButtonUp") +MMB:RegisterForDrag("LeftButton") +MMB:SetMovable(true) +MMB:SetClampedToScreen(true) + +-- Ring (matches other minimap addons like Cartographer / Titan) +local overlay = MMB:CreateTexture(nil, "OVERLAY") +overlay:SetWidth(53); overlay:SetHeight(53) +overlay:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder") +overlay:SetPoint("TOPLEFT", MMB, "TOPLEFT", 0, 0) + +-- Icon +local icon = MMB:CreateTexture(nil, "BACKGROUND") +icon:SetWidth(20); icon:SetHeight(20) +icon:SetTexture("Interface\\Icons\\Achievement_General") +icon:SetPoint("CENTER", MMB, "CENTER", 0, 1) +MMB.icon = icon + +-- Highlight +local hi = MMB:CreateTexture(nil, "HIGHLIGHT") +hi:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight") +hi:SetWidth(31); hi:SetHeight(31) +hi:SetPoint("CENTER", MMB, "CENTER", 0, 1) +hi:SetBlendMode("ADD") + +local function GetDB() + RelationshipsAchievementsDB = RelationshipsAchievementsDB or {} + if not RelationshipsAchievementsDB.minimap then + RelationshipsAchievementsDB.minimap = { angle = 200, radius = 80, hide = false, free = false, x = 0, y = 0 } + end + local m = RelationshipsAchievementsDB.minimap + if m.angle == nil then m.angle = 200 end + if m.radius == nil then m.radius = 80 end + if m.hide == nil then m.hide = false end + if m.free == nil then m.free = false end + return m +end + +local function ApplyPosition() + local m = GetDB() + MMB:ClearAllPoints() + if m.free then + MMB:SetPoint("CENTER", UIParent, "CENTER", m.x or 0, m.y or 0) + else + local a = math.rad(m.angle or 200) + local r = m.radius or 80 + MMB:SetPoint("CENTER", Minimap, "CENTER", + math.cos(a) * r, math.sin(a) * r) + end +end + +local function ApplyVisibility() + local m = GetDB() + if m.hide then MMB:Hide() else MMB:Show() end +end + +-- Drag around the minimap ring (or free-drag anywhere with Shift held). +local function MoveOnUpdate() + local m = GetDB() + local mx, my = Minimap:GetCenter() + if not mx then return end + local scale = Minimap:GetEffectiveScale() + local cx, cy = GetCursorPosition() + cx, cy = cx / scale, cy / scale + if IsShiftKeyDown() then + m.free = true + local ux, uy = UIParent:GetCenter() + local uscale = UIParent:GetEffectiveScale() + local px, py = GetCursorPosition() + m.x = px / uscale - ux + m.y = py / uscale - uy + else + m.free = false + local dx, dy = cx - mx, cy - my + m.angle = math.deg(math.atan2(dy, dx)) + end + ApplyPosition() +end + +MMB:SetScript("OnDragStart", function() + this:LockHighlight() + this:SetScript("OnUpdate", MoveOnUpdate) +end) +MMB:SetScript("OnDragStop", function() + this:UnlockHighlight() + this:SetScript("OnUpdate", nil) +end) + +MMB:SetScript("OnClick", function() + if arg1 == "RightButton" then + if RelationshipsAchievements_MinimapMenu then + RelationshipsAchievements_MinimapMenu() + else + local m = GetDB() + m.hide = true + ApplyVisibility() + DEFAULT_CHAT_FRAME:AddMessage("|cffffd200Relationships Achievements:|r minimap button hidden. Type |cff00ff00/rach minimap show|r to restore.") + end + else + if RelationshipsAchievements_Toggle then RelationshipsAchievements_Toggle() end + end +end) + +MMB:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_LEFT") + GameTooltip:AddLine("Relationships Achievements") + local pts = RelationshipsAchievements and RelationshipsAchievements.TotalPoints and RelationshipsAchievements:TotalPoints() or 0 + GameTooltip:AddLine("|cffffd200Points:|r "..pts, 1, 1, 1) + GameTooltip:AddLine(" ") + GameTooltip:AddLine("Left-click: open panel", 0.6, 0.8, 1) + GameTooltip:AddLine("Right-click: hide button", 0.6, 0.8, 1) + GameTooltip:AddLine("Drag: move around minimap", 0.6, 0.8, 1) + GameTooltip:AddLine("Shift+Drag: free-place anywhere", 0.6, 0.8, 1) + GameTooltip:Show() +end) +MMB:SetScript("OnLeave", function() GameTooltip:Hide() end) + +local init = CreateFrame("Frame") +init:RegisterEvent("PLAYER_LOGIN") +init:RegisterEvent("PLAYER_ENTERING_WORLD") +init:SetScript("OnEvent", function() + ApplyPosition() + ApplyVisibility() +end) + +function RelationshipsAchievements_Minimap_Show() + local m = GetDB(); m.hide = false; ApplyVisibility() +end +function RelationshipsAchievements_Minimap_Hide() + local m = GetDB(); m.hide = true; ApplyVisibility() +end +function RelationshipsAchievements_Minimap_Toggle() + local m = GetDB(); m.hide = not m.hide; ApplyVisibility() +end +function RelationshipsAchievements_Minimap_Reset() + RelationshipsAchievementsDB = RelationshipsAchievementsDB or {} + RelationshipsAchievementsDB.minimap = { angle = 200, radius = 80, hide = false, free = false, x = 0, y = 0 } + ApplyPosition(); ApplyVisibility() +end diff --git a/RelationshipsAchievements.toc b/RelationshipsAchievements.toc index 6669660..6af9411 100644 --- a/RelationshipsAchievements.toc +++ b/RelationshipsAchievements.toc @@ -13,5 +13,6 @@ Core.lua Toast.lua Detail.lua UI.lua +Minimap.lua Events.lua Bindings.xml