9 Commits

Author SHA1 Message Date
Relationship d0b8fff6f9 Upload files to "/" 2026-07-23 12:19:29 +00:00
Relationship 9bbbf4ac45 Upload files to "/" 2026-07-23 12:19:21 +00:00
Relationship 5542971559 Upload files to "/" 2026-07-23 12:06:36 +00:00
Relationship e634c4fad2 Upload files to "/" 2026-07-23 12:06:26 +00:00
Relationship 6d6cabadb6 Update README.md 2026-07-23 11:14:24 +00:00
Relationship 8149862f76 Upload files to "/" 2026-07-23 11:12:26 +00:00
Relationship 6438daee4b Upload files to "/" 2026-07-23 03:37:31 +00:00
Relationship 01dff34305 Upload files to "/" 2026-07-23 03:37:22 +00:00
Relationship 9f6f4b6f27 Update README.md 2026-07-23 02:54:29 +00:00
6 changed files with 943 additions and 102 deletions
+19
View File
@@ -226,12 +226,31 @@ SlashCmdList["RELATIONSHIPSACHIEVEMENTS"] = function(msg)
if RelationshipsAchievements_ShowToast and RelationshipsAchievements_ById[206] then if RelationshipsAchievements_ShowToast and RelationshipsAchievements_ById[206] then
RelationshipsAchievements_ShowToast(RelationshipsAchievements_ById[206]) RelationshipsAchievements_ShowToast(RelationshipsAchievements_ById[206])
end end
elseif cmd == "minimap" then
local sub = string.lower(rest or "")
if sub == "show" then
if RelationshipsAchievements_Minimap_Show then RelationshipsAchievements_Minimap_Show() end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Minimap button shown.|r")
elseif sub == "hide" then
if RelationshipsAchievements_Minimap_Hide then RelationshipsAchievements_Minimap_Hide() end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Minimap button hidden.|r")
elseif sub == "reset" then
if RelationshipsAchievements_Minimap_Reset then RelationshipsAchievements_Minimap_Reset() end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Minimap button position reset.|r")
else
if RelationshipsAchievements_Minimap_Toggle then RelationshipsAchievements_Minimap_Toggle() end
end
elseif cmd == "played" then
if RequestTimePlayed then RequestTimePlayed() end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Relationships Achievements:|r reading /played for quest count...")
elseif cmd == "help" then elseif cmd == "help" then
DEFAULT_CHAT_FRAME:AddMessage("|cffffd200Relationships Achievements commands:|r") DEFAULT_CHAT_FRAME:AddMessage("|cffffd200Relationships Achievements commands:|r")
DEFAULT_CHAT_FRAME:AddMessage(" /rach - toggle window") DEFAULT_CHAT_FRAME:AddMessage(" /rach - toggle window")
DEFAULT_CHAT_FRAME:AddMessage(" /rach unlock <id> - unlock achievement by id") DEFAULT_CHAT_FRAME:AddMessage(" /rach unlock <id> - unlock achievement by id")
DEFAULT_CHAT_FRAME:AddMessage(" /rach reset - wipe all progress") DEFAULT_CHAT_FRAME:AddMessage(" /rach reset - wipe all progress")
DEFAULT_CHAT_FRAME:AddMessage(" /rach points - show total points") DEFAULT_CHAT_FRAME:AddMessage(" /rach points - show total points")
DEFAULT_CHAT_FRAME:AddMessage(" /rach minimap [show|hide|reset] - control minimap button")
DEFAULT_CHAT_FRAME:AddMessage(" /rach played - sync quest count from /played")
DEFAULT_CHAT_FRAME:AddMessage(" /rach scan - rescan character stats") DEFAULT_CHAT_FRAME:AddMessage(" /rach scan - rescan character stats")
DEFAULT_CHAT_FRAME:AddMessage(" /rach test - show a test toast") DEFAULT_CHAT_FRAME:AddMessage(" /rach test - show a test toast")
else else
+4 -4
View File
@@ -48,7 +48,7 @@ RelationshipsAchievements_List = {
{ id = 106, cat = 1, name = "Money in the Bank", desc = "Use a bank for the first time.", points = 10, icon = "Interface\\Icons\\INV_Misc_Bag_15" }, { id = 106, cat = 1, name = "Money in the Bank", desc = "Use a bank for the first time.", points = 10, icon = "Interface\\Icons\\INV_Misc_Bag_15" },
{ id = 107, cat = 1, name = "Big Spender", desc = "Purchase an item from an auction house.", points = 10, icon = "Interface\\Icons\\INV_Misc_Coin_02" }, { id = 107, cat = 1, name = "Big Spender", desc = "Purchase an item from an auction house.", points = 10, icon = "Interface\\Icons\\INV_Misc_Coin_02" },
{ id = 108, cat = 1, name = "Take a Chill Pill", desc = "Bind to an inn.", points = 5, icon = "Interface\\Icons\\INV_Misc_Rune_01" }, { id = 108, cat = 1, name = "Take a Chill Pill", desc = "Bind to an inn.", points = 5, icon = "Interface\\Icons\\INV_Misc_Rune_01" },
{ id = 109, cat = 1, name = "Represent", desc = "Equip a tabard.", points = 10, icon = "Interface\\Icons\\INV_Shirt_GuildTabard_01" }, { id = 109, cat = 1, name = "Represent", desc = "Equip a tabard.", points = 10, icon = "Interface\\Icons\\INV_Shirt_Guildtabard_01" },
{ id = 110, cat = 1, name = "Mailbox Hero", desc = "Send your first piece of mail.", points = 5, icon = "Interface\\Icons\\INV_Letter_04" }, { id = 110, cat = 1, name = "Mailbox Hero", desc = "Send your first piece of mail.", points = 5, icon = "Interface\\Icons\\INV_Letter_04" },
{ id = 111, cat = 1, name = "Trainee", desc = "Visit a class trainer.", points = 5, icon = "Interface\\Icons\\INV_Misc_Book_11" }, { id = 111, cat = 1, name = "Trainee", desc = "Visit a class trainer.", points = 5, icon = "Interface\\Icons\\INV_Misc_Book_11" },
@@ -223,7 +223,7 @@ RelationshipsAchievements_List = {
{ id = 707, cat = 7, name = "Master of the Bandage",desc = "Reach 300 in First Aid.", points = 10, icon = "Interface\\Icons\\INV_Misc_Bandage_15" }, { id = 707, cat = 7, name = "Master of the Bandage",desc = "Reach 300 in First Aid.", points = 10, icon = "Interface\\Icons\\INV_Misc_Bandage_15" },
{ id = 708, cat = 7, name = "Master Alchemist", desc = "Reach 300 in Alchemy.", points = 10, icon = "Interface\\Icons\\Trade_Alchemy" }, { id = 708, cat = 7, name = "Master Alchemist", desc = "Reach 300 in Alchemy.", points = 10, icon = "Interface\\Icons\\Trade_Alchemy" },
{ id = 709, cat = 7, name = "Master Blacksmith", desc = "Reach 300 in Blacksmithing.", points = 10, icon = "Interface\\Icons\\Trade_BlackSmithing" }, { id = 709, cat = 7, name = "Master Blacksmith", desc = "Reach 300 in Blacksmithing.", points = 10, icon = "Interface\\Icons\\Trade_BlackSmithing" },
{ id = 710, cat = 7, name = "Master Enchanter", desc = "Reach 300 in Enchanting.", points = 10, icon = "Interface\\Icons\\INV_Enchant_ShardBrilliantLarge" }, { id = 710, cat = 7, name = "Master Enchanter", desc = "Reach 300 in Enchanting.", points = 10, icon = "Interface\\Icons\\Trade_Engraving" },
{ id = 711, cat = 7, name = "Master Engineer", desc = "Reach 300 in Engineering.", points = 10, icon = "Interface\\Icons\\Trade_Engineering" }, { id = 711, cat = 7, name = "Master Engineer", desc = "Reach 300 in Engineering.", points = 10, icon = "Interface\\Icons\\Trade_Engineering" },
{ id = 712, cat = 7, name = "Master Herbalist", desc = "Reach 300 in Herbalism.", points = 10, icon = "Interface\\Icons\\INV_Misc_Herb_07" }, { id = 712, cat = 7, name = "Master Herbalist", desc = "Reach 300 in Herbalism.", points = 10, icon = "Interface\\Icons\\INV_Misc_Herb_07" },
{ id = 713, cat = 7, name = "Master Leatherworker", desc = "Reach 300 in Leatherworking.", points = 10, icon = "Interface\\Icons\\Trade_LeatherWorking" }, { id = 713, cat = 7, name = "Master Leatherworker", desc = "Reach 300 in Leatherworking.", points = 10, icon = "Interface\\Icons\\Trade_LeatherWorking" },
@@ -335,14 +335,14 @@ RelationshipsAchievements_List = {
{ id = 1601, cat = 16, name = "One Life to Live", desc = "Roll a Hardcore character.", points = 10, icon = "Interface\\Icons\\Spell_Holy_GreaterHeal" }, { id = 1601, cat = 16, name = "One Life to Live", desc = "Roll a Hardcore character.", points = 10, icon = "Interface\\Icons\\Spell_Holy_GreaterHeal" },
{ id = 1602, cat = 16, name = "Survivor", desc = "Reach level 20 on a Hardcore character.", points = 15, icon = "Interface\\Icons\\Spell_Holy_Heal" }, { id = 1602, cat = 16, name = "Survivor", desc = "Reach level 20 on a Hardcore character.", points = 15, icon = "Interface\\Icons\\Spell_Holy_Heal" },
{ id = 1603, cat = 16, name = "Untouchable", desc = "Reach level 40 on a Hardcore character.", points = 25, icon = "Interface\\Icons\\Spell_Holy_Renew" }, { id = 1603, cat = 16, name = "Untouchable", desc = "Reach level 40 on a Hardcore character.", points = 25, icon = "Interface\\Icons\\Spell_Holy_Renew" },
{ id = 1604, cat = 16, name = "Hardcore Hero", desc = "Reach level 60 on a Hardcore character.", points = 50, icon = "Interface\\Icons\\Spell_Holy_Resurrection" }, { id = 1604, cat = 16, name = "Hardcore Hero", desc = "Reach level 60 on a Hardcore character.", points = 50, icon = "Interface\\Icons\\Spell_Holy_GuardianSpirit" },
{ id = 1605, cat = 16, name = "Close Call", desc = "Survive dropping below 10% health.", points = 10, icon = "Interface\\Icons\\Spell_Shadow_Twilight" }, { id = 1605, cat = 16, name = "Close Call", desc = "Survive dropping below 10% health.", points = 10, icon = "Interface\\Icons\\Spell_Shadow_Twilight" },
{ id = 1606, cat = 16, name = "No Retreat", desc = "Defeat an elite of your level solo.", points = 15, icon = "Interface\\Icons\\Ability_Warrior_Innerrage" }, { id = 1606, cat = 16, name = "No Retreat", desc = "Defeat an elite of your level solo.", points = 15, icon = "Interface\\Icons\\Ability_Warrior_Innerrage" },
-- ============ SOCIAL ============ -- ============ SOCIAL ============
{ id = 1701, cat = 17, name = "Say Hello", desc = "Emote /wave at another player.", points = 5, icon = "Interface\\Icons\\INV_Misc_Head_Human_02" }, { id = 1701, cat = 17, name = "Say Hello", desc = "Emote /wave at another player.", points = 5, icon = "Interface\\Icons\\INV_Misc_Head_Human_02" },
{ id = 1702, cat = 17, name = "Dance Off", desc = "Use the /dance emote.", points = 5, icon = "Interface\\Icons\\INV_Shirt_GuildTabard_01" }, { id = 1702, cat = 17, name = "Dance Off", desc = "Use the /dance emote.", points = 5, icon = "Interface\\Icons\\INV_Shirt_GuildTabard_01" },
{ id = 1703, cat = 17, name = "Guildmate", desc = "Have 10 members in your guild.", points = 10, icon = "Interface\\Icons\\INV_Shirt_GuildTabard_01" }, { id = 1703, cat = 17, name = "Guildmate", desc = "Have 10 members in your guild.", points = 10, icon = "Interface\\Icons\\INV_Shirt_Guildtabard_01" },
{ id = 1704, cat = 17, name = "Popular", desc = "Add 5 friends to your friends list.", points = 10, icon = "Interface\\Icons\\INV_Misc_Head_Human_02", { id = 1704, cat = 17, name = "Popular", desc = "Add 5 friends to your friends list.", points = 10, icon = "Interface\\Icons\\INV_Misc_Head_Human_02",
progress = { stat = "friendCount", max = 5 } }, progress = { stat = "friendCount", max = 5 } },
{ id = 1705, cat = 17, name = "Chatty", desc = "Send 100 messages in chat.", points = 10, icon = "Interface\\Icons\\INV_Letter_04", { id = 1705, cat = 17, name = "Chatty", desc = "Send 100 messages in chat.", points = 10, icon = "Interface\\Icons\\INV_Letter_04",
+751 -79
View File
@@ -14,12 +14,17 @@ local trackedEvents = {
"ZONE_CHANGED_INDOORS", "CHAT_MSG_COMBAT_HONOR_GAIN", "ZONE_CHANGED_INDOORS", "CHAT_MSG_COMBAT_HONOR_GAIN",
"CHAT_MSG_COMBAT_FACTION_CHANGE", "SKILL_LINES_CHANGED", "CHAT_MSG_LOOT", "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_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", "CHAT_MSG_SYSTEM", "UPDATE_FACTION", "PARTY_MEMBERS_CHANGED", "FRIENDLIST_UPDATE",
"GUILD_ROSTER_UPDATE", "PLAYER_UNGHOST", "PLAYER_ALIVE", "CHAT_MSG_TEXT_EMOTE", "GUILD_ROSTER_UPDATE", "PLAYER_UNGHOST", "PLAYER_ALIVE", "CHAT_MSG_TEXT_EMOTE",
"CHAT_MSG_EMOTE", "PLAYER_PVP_RANK_CHANGED", "PLAYER_PVP_KILLS_CHANGED", "CHAT_MSG_EMOTE", "PLAYER_PVP_RANK_CHANGED", "PLAYER_PVP_KILLS_CHANGED",
"PLAYER_PVPKILLS_CHANGED", "HONOR_CURRENCY_UPDATE", "HEARTHSTONE_BOUND", "PLAYER_PVPKILLS_CHANGED", "HONOR_CURRENCY_UPDATE", "HEARTHSTONE_BOUND",
"BANKFRAME_OPENED", "TRAINER_SHOW", "MAIL_SEND_SUCCESS", "PLAYER_EQUIPMENT_CHANGED", "BANKFRAME_OPENED", "TRAINER_SHOW", "MAIL_SEND_SUCCESS", "PLAYER_EQUIPMENT_CHANGED",
"BAG_UPDATE", "CHARACTER_POINTS_CHANGED", "SPELLS_CHANGED", "UNIT_HEALTH", "PLAYER_MONEY" "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",
"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 for i = 1, table.getn(trackedEvents) do SafeRegister(trackedEvents[i]) end
@@ -92,29 +97,48 @@ local function ScanTalentChange()
ScanTalents() ScanTalents()
end 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() local function ScanEquipmentAndBags()
if GetInventoryItemLink then if GetInventoryItemLink then
if GetInventoryItemLink("player", 4) then RelationshipsAchievements:Unlock(1203) end if GetInventoryItemLink("player", 4) then RelationshipsAchievements:Unlock(1203) end
if GetInventoryItemLink("player", 19) then RelationshipsAchievements:Unlock(109) end if GetInventoryItemLink("player", 19) then RelationshipsAchievements:Unlock(109) end
local allRare, haveGear, epic = true, false, false -- Well Equipped: every equipped armor/weapon slot must be Rare+.
for slot = 1, 18 do -- Slots that are legitimately optional on a spec (shirt, tabard,
if slot ~= 4 and slot ~= 18 then -- 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) local link = GetInventoryItemLink("player", slot)
if link then if not link then
haveGear = true if required then missing = true end
local _, _, quality = GetItemInfo(link) return
end
local _, _, q = GetItemInfo(link)
local quality = q or QualityFromLink(link)
if quality and quality >= 4 then epic = true end if quality and quality >= 4 then epic = true end
if not quality or quality < 3 then allRare = false end if required and (not quality or quality < 3) then allRare = false end
local itemName = GetItemInfo(link) local itemName = GetItemInfo(link)
if itemName == "Atiesh, Greatstaff of the Guardian" then RelationshipsAchievements:Unlock(1004) end 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 == "Thunderfury, Blessed Blade of the Windseeker" then RelationshipsAchievements:Unlock(1005) end
if itemName == "Sulfuras, Hand of Ragnaros" then RelationshipsAchievements:Unlock(1006) end if itemName == "Sulfuras, Hand of Ragnaros" then RelationshipsAchievements:Unlock(1006) end
else
allRare = false
end end
end for i = 1, table.getn(requiredSlots) do eval(requiredSlots[i], true) end
end for i = 1, table.getn(optionalSlots) do eval(optionalSlots[i], false) end
if haveGear and allRare then RelationshipsAchievements:Unlock(1208) end if not missing and allRare then RelationshipsAchievements:Unlock(1208) end
if epic then RelationshipsAchievements:Unlock(1209) end if epic then RelationshipsAchievements:Unlock(1209) end
end end
if GetContainerNumSlots then if GetContainerNumSlots then
@@ -129,27 +153,569 @@ local function ScanEquipmentAndBags()
end 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")
-- ============ 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)
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
-- ============ 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() local function ScanSpellbook()
if not GetSpellName then return end if not GetSpellName then return end
local book = BOOKTYPE_SPELL or "spell"
local mounts, pets = 0, 0 local mounts, pets = 0, 0
local hasEpicMount = false
local seen = {}
local i = 1 local i = 1
while true do while true do
local name, rank = GetSpellName(i, BOOKTYPE_SPELL or "spell") local name, rank = GetSpellName(i, book)
if not name then break end if not name then break end
local low = string.lower(name) local key = name .. "||" .. (rank or "")
if string.find(low, "summon ") and (string.find(low, "horse") or string.find(low, "ram") if not seen[key] then
or string.find(low, "tiger") or string.find(low, "raptor") or string.find(low, "wolf") seen[key] = true
or string.find(low, "kodo") or string.find(low, "mechanostrider")) then mounts = mounts + 1 end local tip = TooltipText(i, book)
if string.find(low, "summon ") and not string.find(low, "demon") and not string.find(low, "elemental") then pets = pets + 1 end -- 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".
-- 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 i = i + 1
end 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
-- 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("mountCount", mounts)
RelationshipsAchievements:UpdateStat("petCount", pets) RelationshipsAchievements:UpdateStat("petCount", pets)
if mounts >= 1 then RelationshipsAchievements:Unlock(211) end if mounts >= 1 then RelationshipsAchievements:Unlock(211) end
if mounts >= 1 and (UnitLevel("player") or 1) >= 60 then RelationshipsAchievements:Unlock(212) end if hasEpicMount and (UnitLevel("player") or 1) >= 60 then
RelationshipsAchievements:Unlock(212)
end
if pets >= 1 then RelationshipsAchievements:Unlock(1204) end if pets >= 1 then RelationshipsAchievements:Unlock(1204) end
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() function RelationshipsAchievements_ScanCharacter()
RelationshipsAchievements:Init() RelationshipsAchievements:Init()
@@ -292,6 +858,9 @@ function RelationshipsAchievements_ScanCharacter()
ScanTalents() ScanTalents()
ScanEquipmentAndBags() ScanEquipmentAndBags()
ScanSpellbook() ScanSpellbook()
if RelationshipsAchievements_ScanCompanions then
RelationshipsAchievements_ScanCompanions()
end
RelationshipsAchievements_RefreshUI = RelationshipsAchievements_RefreshUI or function() end RelationshipsAchievements_RefreshUI = RelationshipsAchievements_RefreshUI or function() end
if RelationshipsAchievementFrame and RelationshipsAchievementFrame:IsVisible() then if RelationshipsAchievementFrame and RelationshipsAchievementFrame:IsVisible() then
@@ -326,44 +895,99 @@ if _origSendMail then SendMail = function(name, subject, body)
RelationshipsAchievements:Unlock(110) RelationshipsAchievements:Unlock(110)
end end end end
local _origPostAuction = PostAuction -- Auction House API is provided by Blizzard_AuctionUI, which is Load-on-Demand
if _origPostAuction then PostAuction = function(minBid, buyoutPrice, runTime, stackSize, numStacks) -- in 1.12. Hooking at file-load time silently no-ops because the globals are
_origPostAuction(minBid, buyoutPrice, runTime, stackSize, numStacks) -- 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
-- Vanilla 1.12 uses StartAuction(minBid, buyoutPrice, runTime) to list the
-- item currently in the auction sell slot. PostAuction is a TBC+ API and
-- does not exist on Turtle/Octowow, which is why listings were never
-- being counted. Hook both so the addon keeps working if Turtle ever
-- back-ports PostAuction.
if not StartAuction and not PostAuction and not BuyoutAuction and not PlaceAuctionBid then return end
auctionHooked = true
if StartAuction then
local _orig = StartAuction
StartAuction = function(minBid, buyoutPrice, runTime)
_orig(minBid, buyoutPrice, runTime)
-- GetAuctionSellItemInfo returns (name, texture, count, ...); count is the stack size.
local count = 1
if GetAuctionSellItemInfo then
local _, _, c = GetAuctionSellItemInfo()
if c and c > 0 then count = c end
end
RelationshipsAchievements:AddStat("auctionsListed", 1)
-- (We count one auction per listing action; `count` is stack size, not # of auctions.)
local _ = count
end
end
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) RelationshipsAchievements:AddStat("auctionsListed", numStacks or 1)
end end end
end
local _origBuyoutAuction = BuyoutAuction if BuyoutAuction then
if _origBuyoutAuction then BuyoutAuction = function(index) local _orig = BuyoutAuction
_origBuyoutAuction(index) BuyoutAuction = function(index)
_orig(index)
RelationshipsAchievements:Unlock(107) RelationshipsAchievements:Unlock(107)
RelationshipsAchievements:AddStat("auctionsWon", 1) 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 end
if RelationshipsAchievementsCharDB then end
RelationshipsAchievementsCharDB.pendingInnBind = nil -- 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 end end end
-- Hook GetQuestReward(): fires exactly once when the player accepts a quest local _origStaticPopupShow = StaticPopup_Show
-- reward. Much more reliable than QUEST_COMPLETE (which fires each time the if _origStaticPopupShow then StaticPopup_Show = function(which, text_arg1, text_arg2, data)
-- reward window opens) for tracking completed-quest counters. local popup = _origStaticPopupShow(which, text_arg1, text_arg2, data)
local _origGetQuestReward = GetQuestReward if which == "CONFIRM_BINDER" then RelationshipsAchievementsCharDB.pendingInnBind = true end
if _origGetQuestReward then GetQuestReward = function(choice) return popup
OnQuestComplete()
return _origGetQuestReward(choice)
end end end end
-- ============ DoEmote hook (Say Hello / Dance Off) ============ -- ============ DoEmote hook (Say Hello / Dance Off) ============
@@ -410,7 +1034,7 @@ local function OnLevelUp()
RelationshipsAchievements_ScanCharacter() RelationshipsAchievements_ScanCharacter()
end end
OnQuestComplete = function() local function OnQuestComplete()
-- QUEST_COMPLETE is the reliable 1.12 turn-in event. Custom clients that -- 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. -- also emit QUEST_TURNED_IN are de-duplicated by time stamp below.
local now = time() local now = time()
@@ -525,8 +1149,31 @@ local function OnEnemyDeath(msg)
end end
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) local function OnSystemMessage(msg)
if not msg then return end if not msg then return end
TryParseQuestCount(msg)
local _, _, winner = string.find(msg, "^(.-) has defeated ") local _, _, winner = string.find(msg, "^(.-) has defeated ")
if winner and winner == UnitName("player") then if winner and winner == UnitName("player") then
RelationshipsAchievements:AddStat("duelWins", 1) RelationshipsAchievements:AddStat("duelWins", 1)
@@ -541,39 +1188,23 @@ local function OnSystemMessage(msg)
if string.find(low, "trick or treat") then RelationshipsAchievements:Unlock(903) 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, "lunar festival") then RelationshipsAchievements:Unlock(909) end
if string.find(low, "winter veil") then RelationshipsAchievements:Unlock(901) end if string.find(low, "winter veil") then RelationshipsAchievements:Unlock(901) end
-- Battleground victories. On 1.12 the win message reads "The Alliance wins!" -- Battleground victories (system messages contain "Warsong Gulch", etc.)
-- or "The Horde wins!" via CHAT_MSG_BG_SYSTEM_NEUTRAL and does NOT include if string.find(msg, "[Vv]ictory") or string.find(msg, "wins!") then
-- the map name, so we look up the current BG from GetRealZoneText() and if string.find(msg, "Warsong Gulch") then
-- 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:Unlock(505)
RelationshipsAchievements:AddStat("wsgWins", 1) RelationshipsAchievements:AddStat("wsgWins", 1)
RelationshipsAchievements:AddStat("bgWins", 1) RelationshipsAchievements:AddStat("bgWins", 1)
elseif string.find(zone, "Alterac Valley") then elseif string.find(msg, "Alterac Valley") then
RelationshipsAchievements:Unlock(506) RelationshipsAchievements:Unlock(506)
RelationshipsAchievements:AddStat("avWins", 1) RelationshipsAchievements:AddStat("avWins", 1)
RelationshipsAchievements:AddStat("bgWins", 1) RelationshipsAchievements:AddStat("bgWins", 1)
elseif string.find(zone, "Arathi Basin") then elseif string.find(msg, "Arathi Basin") then
RelationshipsAchievements:Unlock(507) RelationshipsAchievements:Unlock(507)
RelationshipsAchievements:AddStat("abWins", 1) RelationshipsAchievements:AddStat("abWins", 1)
RelationshipsAchievements:AddStat("bgWins", 1) RelationshipsAchievements:AddStat("bgWins", 1)
end end
end end
end end
end
end
-- Emote handler: catch /wave and /dance in every locale by matching the -- 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. -- token via DoEmote hook AND as a fallback matching the English text here.
@@ -621,10 +1252,7 @@ frame:SetScript("OnEvent", function()
OnLootMessage(arg1) OnLootMessage(arg1)
elseif event == "CHAT_MSG_COMBAT_HOSTILE_DEATH" or event == "CHAT_MSG_COMBAT_XP_GAIN" then elseif event == "CHAT_MSG_COMBAT_HOSTILE_DEATH" or event == "CHAT_MSG_COMBAT_XP_GAIN" then
OnEnemyDeath(arg1) OnEnemyDeath(arg1)
elseif event == "CHAT_MSG_SYSTEM" elseif event == "CHAT_MSG_SYSTEM" then
or event == "CHAT_MSG_BG_SYSTEM_NEUTRAL"
or event == "CHAT_MSG_BG_SYSTEM_ALLIANCE"
or event == "CHAT_MSG_BG_SYSTEM_HORDE" then
OnSystemMessage(arg1) OnSystemMessage(arg1)
elseif event == "CHAT_MSG_TEXT_EMOTE" or event == "CHAT_MSG_EMOTE" then elseif event == "CHAT_MSG_TEXT_EMOTE" or event == "CHAT_MSG_EMOTE" then
OnTextEmote(arg1, arg2) OnTextEmote(arg1, arg2)
@@ -642,8 +1270,18 @@ frame:SetScript("OnEvent", function()
RelationshipsAchievements:Unlock(111) RelationshipsAchievements:Unlock(111)
elseif event == "MAIL_SEND_SUCCESS" then elseif event == "MAIL_SEND_SUCCESS" then
RelationshipsAchievements:Unlock(110) RelationshipsAchievements:Unlock(110)
elseif event == "PLAYER_EQUIPMENT_CHANGED" or event == "BAG_UPDATE" then 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() 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 elseif event == "CHARACTER_POINTS_CHANGED" then
ScanTalentChange() ScanTalentChange()
elseif event == "SPELLS_CHANGED" then elseif event == "SPELLS_CHANGED" then
@@ -679,5 +1317,39 @@ frame:SetScript("OnEvent", function()
RelationshipsAchievements:Unlock(103) RelationshipsAchievements:Unlock(103)
if GetNumGuildMembers and GetNumGuildMembers() >= 10 then RelationshipsAchievements:Unlock(1703) end if GetNumGuildMembers and GetNumGuildMembers() >= 10 then RelationshipsAchievements:Unlock(1703) end
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
end) end)
+147
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
<img src="https://i.ibb.co/z1W5z0T/a.gif"/>
# Relationships Achievements # Relationships Achievements
A **WotLK-style achievement system** for **OctoWoW**. A **WotLK-style achievement system** for **OctoWoW**.
+3 -2
View File
@@ -1,8 +1,8 @@
## Interface: 11200 ## Interface: 11200
## Title: Relationships Achievements ## Title: Relationships Achievements
## Notes: WotLK-style achievement system for OctoWoW ## Notes: WotLK-style achievement system for OctoWoW
## Author: Lovable ## Author: Relationship
## Version: 1.0 ## Version: 1.1
## SavedVariables: RelationshipsAchievementsDB ## SavedVariables: RelationshipsAchievementsDB
## SavedVariablesPerCharacter: RelationshipsAchievementsCharDB ## SavedVariablesPerCharacter: RelationshipsAchievementsCharDB
## DefaultState: Enabled ## DefaultState: Enabled
@@ -13,5 +13,6 @@ Core.lua
Toast.lua Toast.lua
Detail.lua Detail.lua
UI.lua UI.lua
Minimap.lua
Events.lua Events.lua
Bindings.xml Bindings.xml