13 Commits

Author SHA1 Message Date
Relationship fc4dbc4fb3 Upload files to "/" 2026-07-23 12:29:47 +00:00
Relationship 7b57a45a1b Upload files to "/" 2026-07-23 12:29:41 +00:00
Relationship a5633fd9e2 Add files via upload 2026-07-18 20:15:06 +01:00
Relationship ccfb78c3a6 Add files via upload 2026-07-18 18:50:56 +01:00
Relationship 9cdcc1814f Update README.md 2026-07-18 18:37:07 +01:00
Relationship 0c9af0e9d2 Update README.md 2026-07-16 17:57:27 +01:00
Relationship 8f4480bf14 Add files via upload 2026-07-16 17:56:42 +01:00
Relationship a276cca444 Add files via upload 2026-07-16 17:34:22 +01:00
Relationship 4502a1db7e Add files via upload 2026-07-16 17:03:20 +01:00
Relationship fda04907c2 Update README.md 2026-07-15 15:32:46 +01:00
Relationship e95e65fca5 Add files via upload 2026-07-12 17:16:47 +01:00
Relationship 044b6593ec Add files via upload 2026-07-12 17:06:21 +01:00
Relationship fbb3e94b93 Add files via upload 2026-07-12 17:02:18 +01:00
6 changed files with 409 additions and 98 deletions
+280 -15
View File
@@ -6,7 +6,7 @@ RelationshipsAttune = RelationshipsAttune or {}
local RA = RelationshipsAttune
local L = RelationshipsAttune_L
RA.VERSION = "1.4.0"
RA.VERSION = "1.4.4"
RA.COMM_PREFIX = "RATTUNE"
RA.playerName = nil
@@ -70,6 +70,56 @@ function RA:InitDB()
if not RelationshipsAttuneCharDB.announced then RelationshipsAttuneCharDB.announced = {} end
if not RelationshipsAttuneCharDB.progress then RelationshipsAttuneCharDB.progress = {} end
if not RelationshipsAttuneCharDB.expanded then RelationshipsAttuneCharDB.expanded = {} end
if not RelationshipsAttuneCharDB.forcedSteps then RelationshipsAttuneCharDB.forcedSteps = {} end
if not RelationshipsAttuneCharDB.forcedAtt then RelationshipsAttuneCharDB.forcedAtt = {} end
end
-- ---------------------------------------------------------------
-- Force-complete (manual override) helpers
-- ---------------------------------------------------------------
function RA:IsStepForced(attId, i)
local t = RelationshipsAttuneCharDB.forcedSteps
return t and t[attId] and t[attId][i] == true
end
function RA:SetStepForced(attId, i, v)
if not attId or not i then return end
local t = RelationshipsAttuneCharDB.forcedSteps
if not t then RelationshipsAttuneCharDB.forcedSteps = {}; t = RelationshipsAttuneCharDB.forcedSteps end
if not t[attId] then t[attId] = {} end
if v then t[attId][i] = true else t[attId][i] = nil end
self:RecomputeAll()
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
end
function RA:IsAttForced(attId)
local t = RelationshipsAttuneCharDB.forcedAtt
return t and t[attId] == true
end
function RA:SetAttForced(attId, v)
if not attId then return end
if not RelationshipsAttuneCharDB.forcedAtt then
RelationshipsAttuneCharDB.forcedAtt = {}
end
if not RelationshipsAttuneCharDB.completedAttunements then
RelationshipsAttuneCharDB.completedAttunements = {}
end
if v then
RelationshipsAttuneCharDB.forcedAtt[attId] = true
RelationshipsAttuneCharDB.completedAttunements[attId] = true
else
RelationshipsAttuneCharDB.forcedAtt[attId] = nil
RelationshipsAttuneCharDB.completedAttunements[attId] = nil
if RelationshipsAttuneCharDB.forcedSteps then
RelationshipsAttuneCharDB.forcedSteps[attId] = nil
end
if RelationshipsAttuneCharDB.announced then
RelationshipsAttuneCharDB.announced[attId] = nil
end
end
self:RecomputeAll()
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
end
-- ---------------------------------------------------------------
@@ -81,10 +131,16 @@ end
-- ---------------------------------------------------------------
function RA:ScanQuestLog()
if not GetNumQuestLogEntries then return end
-- Rebuild transient quest states from the live log so that abandoning a
-- quest reverts its step colour. "turnedin" is authoritative and preserved.
-- Snapshot previous transient states so we can detect a turn-in on clients
-- that don't fire QUEST_TURNED_IN (stock 1.12). A quest that was
-- "complete" (ready to hand in) and is now missing from the log is
-- almost certainly turned in -- abandoning a quest requires it to be
-- accepted-but-not-complete. This is what actually credits MC's
-- "Attunement to the Core" (7487), BWL, Naxx, and any other
-- quest-only final step.
local prev = RelationshipsAttuneCharDB.quests or {}
local kept = {}
for qid, st in pairs(RelationshipsAttuneCharDB.quests) do
for qid, st in pairs(prev) do
if st == "turnedin" then kept[qid] = "turnedin" end
end
RelationshipsAttuneCharDB.quests = kept
@@ -92,6 +148,7 @@ function RA:ScanQuestLog()
RA:BuildNameIndex()
local qNameMap = RA._questNameToId or {}
local seenThisScan = {}
local numEntries = GetNumQuestLogEntries()
for i = 1, numEntries do
local ok, title, level, tag, isHeader, _, isComplete, _, questId =
@@ -104,7 +161,9 @@ function RA:ScanQuestLog()
if (not qid) and title then
qid = qNameMap[string.lower(title)]
end
if qid and RelationshipsAttuneCharDB.quests[qid] ~= "turnedin" then
if qid then
seenThisScan[qid] = true
if RelationshipsAttuneCharDB.quests[qid] ~= "turnedin" then
if isComplete and isComplete == 1 then
RelationshipsAttuneCharDB.quests[qid] = "complete"
else
@@ -115,6 +174,85 @@ function RA:ScanQuestLog()
end
end
-- Retroactive turn-in detection: any quest we tracked as "complete" last
-- scan that isn't in the log any more got handed in.
for qid, st in pairs(prev) do
if st == "complete" and not seenThisScan[qid] then
RelationshipsAttuneCharDB.quests[qid] = "turnedin"
end
end
end
-- ---------------------------------------------------------------
-- Completed-quest scan (server-side completed quest cache).
--
-- Turtle WoW / OctoWoW backport QueryQuestsCompleted() +
-- GetQuestsCompleted(t), which return every quest the character has
-- ever turned in -- including quests completed BEFORE the addon was
-- installed. Without this, the addon can only see quests that are
-- currently in the log (or that were "complete" in a previous scan
-- and then vanished), so a player who is already half-way through a
-- chain shows no progress until they accept the next quest.
--
-- We pcall everything so stock 1.12 clients that don't expose these
-- APIs simply skip the scan and keep the existing behaviour.
-- ---------------------------------------------------------------
function RA:ScanCompletedQuests()
-- Build the set of quest ids we actually care about so we don't
-- pollute the DB with thousands of unrelated turn-ins.
if not RelationshipsAttune_Data then return end
local wanted = {}
for i = 1, table.getn(RelationshipsAttune_Data) do
local att = RelationshipsAttune_Data[i]
if att and att.steps then
for j = 1, table.getn(att.steps) do
local s = att.steps[j]
if s and s.type == "quest" and s.id then
wanted[s.id] = true
end
end
end
end
local completed = {}
local haveAny = false
-- Preferred: bulk table fill.
if type(GetQuestsCompleted) == "function" then
local ok = pcall(GetQuestsCompleted, completed)
if ok then
for _ in pairs(completed) do haveAny = true break end
end
end
-- Fallback: per-id check (Turtle exposes IsQuestCompleted on some builds).
if not haveAny and type(IsQuestCompleted) == "function" then
for qid in pairs(wanted) do
local ok, res = pcall(IsQuestCompleted, qid)
if ok and res then
completed[qid] = true
haveAny = true
end
end
end
if not haveAny then
-- Kick the server to populate the cache; the QUEST_QUERY_COMPLETE
-- event handler will re-run this scan when the reply arrives.
if type(QueryQuestsCompleted) == "function" then
pcall(QueryQuestsCompleted)
end
return
end
for qid in pairs(wanted) do
if completed[qid] and RelationshipsAttuneCharDB.quests[qid] ~= "turnedin" then
RelationshipsAttuneCharDB.quests[qid] = "turnedin"
end
end
end
function RA:MarkQuestTurnedIn(questId)
if not questId then return end
RelationshipsAttuneCharDB.quests[questId] = "turnedin"
@@ -175,10 +313,24 @@ function RA:BuildNameIndex()
for j = 1, table.getn(att.steps) do
local s = att.steps[j]
if s and s.name and s.id then
local nlow = string.lower(s.name)
if s.type == "item" then
map[string.lower(s.name)] = s.id
map[nlow] = s.id
elseif s.type == "quest" then
qmap[string.lower(s.name)] = s.id
qmap[nlow] = s.id
-- Data.lua disambiguates faction-specific quests
-- with a "(Alliance)" / "(Horde)" suffix that
-- the in-game quest log does NOT include. Index
-- the stripped variant so the name fallback in
-- ScanQuestLog matches on stock 1.12 clients
-- that don't return a questId.
local _, _, stripped = string.find(nlow, "^(.-)%s*%(alliance%)%s*$")
if not stripped then
_, _, stripped = string.find(nlow, "^(.-)%s*%(horde%)%s*$")
end
if stripped and stripped ~= "" and not qmap[stripped] then
qmap[stripped] = s.id
end
end
end
end
@@ -329,9 +481,13 @@ function RA:EvaluateStep(step)
return (UnitLevel("player") or 0) >= (step.level or 0), false
elseif step.type == "quest" then
local st = RelationshipsAttuneCharDB.quests[step.id]
if st == "complete" or st == "turnedin" then
if st == "turnedin" then
return true, false
elseif st == "inlog" then
elseif st == "complete" or st == "inlog" then
-- Quest is in the log (either accepted or ready to turn in).
-- Treat both as "on this step" (orange) until it is actually
-- turned in. Retroactive completion will still mark this
-- step green once the player advances to a later step.
return false, true
end
return false, false
@@ -369,6 +525,20 @@ function RA:EvaluateAttunement(att)
stepsInProgress[i] = nil
end
end
-- Apply per-step forced completions (manual overrides from the UI).
-- These are treated exactly like a real completion for tracking purposes.
local forced = RelationshipsAttuneCharDB.forcedSteps and RelationshipsAttuneCharDB.forcedSteps[att.id]
if forced then
for i = 1, numSteps do
local step = att.steps[i]
if step and self:StepApplies(step) and forced[i] and steps[i] == false then
steps[i] = true
stepsInProgress[i] = false
doneCount = doneCount + 1
end
end
end
-- Short-circuit: if the "final" step is complete, treat the whole
-- attunement as done. Prefer an explicit final=true step; fall back to
-- the last item step (key), then the last applicable step.
@@ -398,6 +568,34 @@ function RA:EvaluateAttunement(att)
end
end
end
-- Retroactive completion: if the player is on a later step (done or in
-- progress), any earlier quest/item steps must already be done. Quests
-- get turned in and key items get consumed, so their live state can
-- flip back to "not done" even though the player clearly finished
-- them. Level and reputation steps have their own live truth and are
-- left alone.
local laterActive = false
for i = numSteps, 1, -1 do
local step = att.steps[i]
if step and self:StepApplies(step) then
if laterActive and steps[i] == false then
local t = step.type
if t == "quest" or t == "item" then
steps[i] = true
stepsInProgress[i] = false
doneCount = doneCount + 1
if t == "quest" then
RelationshipsAttuneCharDB.quests[step.id] = "turnedin"
elseif t == "item" then
RelationshipsAttuneCharDB.items[step.id] = true
end
end
end
if steps[i] == true or stepsInProgress[i] == true then
laterActive = true
end
end
end
local persistedDone = RelationshipsAttuneCharDB.completedAttunements and RelationshipsAttuneCharDB.completedAttunements[att.id]
if persistedDone or (finalIdx and steps[finalIdx] == true) then
for i = 1, numSteps do
@@ -481,12 +679,27 @@ function RA:MarkDirty() RA._dirty = true end
-- Full periodic rescan (quest log + bags + recompute + UI refresh).
function RA:AutoScan()
RA:ScanQuestLog()
RA:ScanCompletedQuests()
RA:ScanBags()
RA:RecomputeAll()
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
end
function RA:_OnUpdate(elapsed)
-- Elapsed can arrive as a function argument, as the global arg1, or
-- (on some 1.12 builds) not at all. Fall back to GetTime() deltas so
-- the periodic auto-scan always advances.
if type(elapsed) ~= "number" then elapsed = nil end
if not elapsed and type(arg1) == "number" then elapsed = arg1 end
if not elapsed and GetTime then
local now = GetTime()
if RA._lastUpdate then
elapsed = now - RA._lastUpdate
else
elapsed = 0
end
RA._lastUpdate = now
end
elapsed = elapsed or 0
RA._throttle = RA._throttle + elapsed
RA._periodic = RA._periodic + elapsed
@@ -508,6 +721,7 @@ function RA:_OnUpdate(elapsed)
end
if fired then
RA:WarmItemCache()
RA:ScanCompletedQuests()
RA:ScanBags()
RA:RecomputeAll()
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
@@ -565,17 +779,45 @@ local function slashHandler(msg)
print("Minimap button shown. Drag to reposition around the minimap.")
end
end
elseif string.find(msg, "^pos") then
-- /attune pos <x> <y> -- set exact offset from UIParent CENTER.
-- Values may be off-screen (e.g. -9999) to park the button out of
-- the way while still letting MinimapButtonBag collect it.
local _, _, sx, sy = string.find(msg, "pos%s+(%-?%d+%.?%d*)%s+(%-?%d+%.?%d*)")
local x, y = tonumber(sx), tonumber(sy)
if x and y and RA.Minimap then
local o = RelationshipsAttuneDB.options
o.minimapX, o.minimapY = x, y
RA.Minimap:Refresh()
print("Minimap button moved to " .. x .. ", " .. y .. ".")
else
print("Usage: /attune pos <x> <y> (offsets from screen center; can be off-screen)")
end
elseif msg == "offscreen" or msg == "park" then
if RA.Minimap then
local o = RelationshipsAttuneDB.options
o.minimapX, o.minimapY = -10000, -10000
RA.Minimap:Refresh()
print("Minimap button parked off-screen. MinimapButtonBag (if installed) can still collect it. Use /attune resetpos to bring it back.")
end
elseif msg == "resetpos" then
if RA.Minimap then
local o = RelationshipsAttuneDB.options
o.minimapX, o.minimapY = 0, -140
RA.Minimap:Refresh()
print("Minimap button position reset.")
end
elseif msg == "sync" then
if RA.Comm then RA.Comm:RequestSync() end
print(L.MSG_SYNC_SENT)
elseif msg == "scan" then
RA:ScanQuestLog(); RA:ScanBags(); RA:RecomputeAll()
RA:ScanQuestLog(); RA:ScanCompletedQuests(); RA:ScanBags(); RA:RecomputeAll()
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
print(L.MSG_SCANNED)
elseif msg == "reset" then
RelationshipsAttuneCharDB = nil
RA:InitDB()
RA:ScanQuestLog(); RA:ScanBags(); RA:RecomputeAll()
RA:ScanQuestLog(); RA:ScanCompletedQuests(); RA:ScanBags(); RA:RecomputeAll()
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
print(L.MSG_RESET)
elseif msg == "debug" or string.find(msg, "^find") then
@@ -613,6 +855,9 @@ local function slashHandler(msg)
print(" /attune show - open the window")
print(" /attune hide - close the window (or hide the minimap button)")
print(" /attune minimap - toggle the minimap button")
print(" /attune pos x y - place the minimap button at exact offset (can be off-screen)")
print(" /attune offscreen- park the minimap button off-screen (MBB can still collect it)")
print(" /attune resetpos - restore the minimap button to a visible default position")
print(" /attune scan - force a rescan of quests + bags")
print(" /attune sync - request sync from your group")
print(" /attune debug - dump keyring + item detection")
@@ -648,6 +893,7 @@ pcall(function() ef:RegisterEvent("ITEM_PUSH") end)
pcall(function() ef:RegisterEvent("PLAYER_XP_UPDATE") end)
pcall(function() ef:RegisterEvent("ZONE_CHANGED_NEW_AREA") end)
pcall(function() ef:RegisterEvent("SKILL_LINES_CHANGED") end)
pcall(function() ef:RegisterEvent("QUEST_QUERY_COMPLETE") end)
ef:SetScript("OnEvent", function()
local e = event
@@ -659,8 +905,15 @@ ef:SetScript("OnEvent", function()
RA.playerFaction = UnitFactionGroup("player") or "Neutral"
RA:BuildNameIndex()
RA:WarmItemCache()
RA:ScanQuestLog(); RA:ScanBags(); RA:RecomputeAll()
RA._retryAt = { GetTime() + 3, GetTime() + 10 }
-- ScanCompletedQuests seeds the "already turned-in" state from the
-- server's completed-quest cache so mid-chain attunements show the
-- player's actual current step immediately at load, instead of
-- waiting until the next quest in the chain is accepted.
RA:ScanQuestLog(); RA:ScanCompletedQuests(); RA:ScanBags(); RA:RecomputeAll()
-- The completed-quest cache is often empty on first login until
-- QueryQuestsCompleted() round-trips to the server. Schedule extra
-- rescans so the UI catches up as soon as the cache is populated.
RA._retryAt = { GetTime() + 2, GetTime() + 5, GetTime() + 10 }
if RA.Comm and RA.Comm.Init then RA.Comm:Init() end
if RA.UI and RA.UI.Init then RA.UI:Init() end
if RA.Minimap and RA.Minimap.Init then RA.Minimap:Init() end
@@ -689,6 +942,13 @@ ef:SetScript("OnEvent", function()
elseif e == "QUEST_TURNED_IN" then
if arg1 then RA:MarkQuestTurnedIn(arg1) end
elseif e == "QUEST_QUERY_COMPLETE" then
if RA.initialized then
RA:ScanCompletedQuests()
RA:RecomputeAll()
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
end
elseif e == "CHAT_MSG_ADDON" then
if arg1 == RA.COMM_PREFIX and RA.Comm and RA.Comm.OnMessage then
RA.Comm:OnMessage(arg2, arg3, arg4)
@@ -696,7 +956,12 @@ ef:SetScript("OnEvent", function()
end
end)
ef:SetScript("OnUpdate", function()
ef:SetScript("OnUpdate", function(_self, _elapsed)
if not RA.initialized then return end
RA:_OnUpdate(arg1)
-- Vanilla 1.12 delivers elapsed via the global arg1; some private-server
-- builds pass it as a positional parameter instead. Try both.
local e = arg1
if type(e) ~= "number" then e = _elapsed end
if type(e) ~= "number" then e = _self end
RA:_OnUpdate(e)
end)
+8 -69
View File
@@ -209,16 +209,16 @@ RelationshipsAttune_Data = {
{ type = "level", level = 60, name = "Reach Level 60", icon = ICON_LEVEL },
{ type = "rep", faction = "Argent Dawn", standing = 5,
name = "Honored with the Argent Dawn", icon = ICON_REP,
desc = "Required to purchase the Naxxramas attunement from Archmage Angela Dosantos. Higher rep reduces the gold cost (60g Honored, 40g Revered, 20g Exalted).",
desc = "Required to purchase the Naxxramas attunement from Archmage Angela Dosantos. Higher rep with the Argent Dawn drastically reduces the material cost (see steps below).",
start = "Turn in Scourgestones / Minion's Scourgestones to Argent Dawn quartermasters at Light's Hope Chapel, Eastern Plaguelands (76, 53)." },
{ type = "quest", id = 9121, name = "The Dread Citadel - Naxxramas! (Honored, 60g)", icon = ICON_ACTIVE, final = true,
desc = "Pay 60g to Angela Dosantos - available at Honored with Argent Dawn.",
{ type = "quest", id = 9121, name = "The Dread Citadel - Naxxramas! (Honored)", icon = ICON_ACTIVE, final = true,
desc = "Honored cost: 60 gold, 5 Arcane Crystals, 2 Nexus Crystals, and 1 Righteous Orb. Turn in to Archmage Angela Dosantos.",
start = "Archmage Angela Dosantos, Light's Hope Chapel, Eastern Plaguelands (76, 53)." },
{ type = "quest", id = 9122, name = "The Dread Citadel - Naxxramas! (Revered, 40g)", icon = ICON_ACTIVE, final = true,
desc = "Discounted Naxx attunement offered at Revered with Argent Dawn.",
{ type = "quest", id = 9122, name = "The Dread Citadel - Naxxramas! (Revered)", icon = ICON_ACTIVE, final = true,
desc = "Revered cost: 30 gold, 2 Arcane Crystals, and 1 Nexus Crystal. Turn in to Archmage Angela Dosantos.",
start = "Archmage Angela Dosantos, Light's Hope Chapel, Eastern Plaguelands (76, 53)." },
{ type = "quest", id = 9123, name = "The Dread Citadel - Naxxramas! (Exalted, 20g)", icon = ICON_ACTIVE, final = true,
desc = "Cheapest Naxx attunement, offered only at Exalted with Argent Dawn.",
{ type = "quest", id = 9123, name = "The Dread Citadel - Naxxramas! (Exalted)", icon = ICON_ACTIVE, final = true,
desc = "Exalted cost: completely free. Turn in to Archmage Angela Dosantos.",
start = "Archmage Angela Dosantos, Light's Hope Chapel, Eastern Plaguelands (76, 53)." },
},
},
@@ -307,7 +307,7 @@ RelationshipsAttune_Data = {
-- VANILLA KEYS & DUNGEON ACCESS
----------------------------------------------------------------
{
id = "ubrs", name = "Upper Blackrock Spire (Seal of Ascension)", category = "Keys & Access", faction = "Both",
id = "ubrs", name = "Upper Blackrock Spire (Seal)", category = "Keys & Access", faction = "Both",
icon = "Interface\\Icons\\INV_Misc_Key_04",
steps = {
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
@@ -537,17 +537,6 @@ RelationshipsAttune_Data = {
start = "Discs of Norgannon, Chamber of Khaz'mul, Uldaman, Badlands (42, 20)." },
},
},
{
id = "hydraxian", name = "Hydraxian Waterlords (MC Runes)", category = "Keys & Access", faction = "Both",
icon = "Interface\\Icons\\Spell_Frost_SummonWaterElemental",
steps = {
{ type = "level", level = 60, name = "Reach Level 60", icon = ICON_LEVEL },
{ type = "rep", faction = "Hydraxian Waterlords", standing = 5,
name = "Honored with the Hydraxian Waterlords", icon = ICON_REP,
desc = "Needed to purchase Aqual Quintessence to douse the Molten Core runes for Majordomo.",
start = "Duke Hydraxis, small island off south-east Azshara (79, 76). Turn in water elemental drops (Azshara/Silithus) and MC boss items." },
},
},
----------------------------------------------------------------
-- TURTLE / OCTO WOW CUSTOM CONTENT
-- Placeholder ids where the live client id isn't confirmed; the
@@ -579,56 +568,6 @@ RelationshipsAttune_Data = {
start = "Awarded from Archmage Alturus (Morgan's Plot, Deadwind Pass 40, 75) at the end of the LKH attunement." },
},
},
{
id = "es", name = "Emerald Sanctum", category = "Turtle", faction = "Both",
icon = "Interface\\Icons\\INV_Misc_Head_Dragon_Green",
steps = {
{ type = "level", level = 60, name = "Reach Level 60", icon = ICON_LEVEL },
{ type = "quest", id = 40020, name = "Emerald Sanctum attunement", icon = ICON_QUEST, final = true,
desc = "Emerald Sanctum attunement chain (Hinterlands portal). Speak with Keeper Remulos and collect the Sanctum Sigil from the four green dragons around Azeroth (Ysondre, Emeriss, Lethon, Taerar).",
start = "Keeper Remulos avatar, eastern Hinterlands near Seradane (55, 22)." },
},
},
{
id = "cgrove", name = "Crescent Grove (Turtle 20-man)", category = "Turtle", faction = "Both",
icon = "Interface\\Icons\\INV_Misc_Herb_02",
steps = {
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
{ type = "quest", id = 40100, name = "Crescent Grove attunement", icon = ICON_QUEST, final = true,
desc = "Turtle-WoW 20-man Emerald-themed raid in Ashenvale. Speak to the druid representative at the grove entrance to start the chain (collect corrupted samples inside the outer ring).",
start = "Ashenvale, near the Crescent Grove portal south of Warsong Lumber Camp (~40, 63)." },
},
},
{
id = "gilneas", name = "Gilneas City", category = "Turtle", faction = "Both",
icon = "Interface\\Icons\\Ability_Mount_WhiteDireWolf",
steps = {
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
{ type = "quest", id = 40030, name = "Gilneas City attunement", icon = ICON_QUEST, final = true,
desc = "Attunement for the Gilneas City dungeon. Speak with Lord Darius Crowley at the Greymane Wall and complete the intro quest chain in Silverpine.",
start = "Lord Darius Crowley, Greymane Wall entrance, southern Silverpine Forest (43, 66)." },
},
},
{
id = "hateforge", name = "Hateforge Quarry", category = "Turtle", faction = "Both",
icon = "Interface\\Icons\\INV_Hammer_16",
steps = {
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
{ type = "quest", id = 40040, name = "Hateforge Quarry attunement", icon = ICON_QUEST, final = true,
desc = "Attunement for the Hateforge Quarry dungeon. Turtle Dark Iron chain out of Burning Steppes.",
start = "Overseer Maltorius, Flame Crest camp, northern Burning Steppes (62, 24)." },
},
},
{
id = "stormwind_vault", name = "Stormwind Vault", category = "Turtle", faction = "Both",
icon = "Interface\\Icons\\INV_Misc_Coin_01",
steps = {
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
{ type = "quest", id = 40050, name = "Stormwind Vault attunement", icon = ICON_QUEST, final = true,
desc = "Attunement for the Stormwind Vault dungeon (Turtle). Alliance intro from the Stockade warden; Horde variant from a SI:7 defector in Booty Bay.",
start = "Warden Thelwater, Stormwind Stockade entrance, Mage Quarter, Stormwind City (50, 68). Horde variant: Booty Bay, Stranglethorn (27, 77)." },
},
},
----------------------------------------------------------------
-- DRAGONMAW RETREAT (Turtle/Octo custom, Wetlands, patch 1.18)
----------------------------------------------------------------
+26 -5
View File
@@ -52,22 +52,35 @@ end
MM._updatePosition = applyPosition
local function onDragUpdate()
-- Do all math in the button's own effective scale. The button is
-- parented to Minimap, whose effective scale can differ from
-- UIParent's; mixing scales makes the icon drift away from the cursor.
local btn = this
local bscale = btn:GetEffectiveScale() or 1
local uscale = UIParent:GetEffectiveScale() or 1
local mx, my = GetCursorPosition()
local scale = UIParent:GetEffectiveScale() or 1
mx = mx / scale; my = my / scale
mx = mx / bscale
my = my / bscale
local ucx, ucy = UIParent:GetCenter()
if not ucx then return end
-- Convert UIParent center from UIParent scale into btn scale.
ucx = ucx * uscale / bscale
ucy = ucy * uscale / bscale
local o = opts()
o.minimapX = mx - ucx
o.minimapY = my - ucy
applyPosition(this)
applyPosition(btn)
end
function MM:Create()
if getglobal(BTN_NAME) then return getglobal(BTN_NAME) end
-- Parent to UIParent so we're not clipped to the minimap area.
local btn = CreateFrame("Button", BTN_NAME, UIParent)
-- Parent to Minimap so button-collector addons (MinimapButtonBag /
-- MBB, and similar) can discover and manage this button the same way
-- they discover Blizzard's built-in minimap buttons. SetPoint still
-- anchors to UIParent CENTER so the button can be dragged anywhere
-- on screen -- the anchor frame is independent of the parent frame.
local btn = CreateFrame("Button", BTN_NAME, Minimap)
btn:SetWidth(31); btn:SetHeight(31)
btn:SetFrameStrata("MEDIUM")
btn:SetFrameLevel(8)
@@ -75,6 +88,12 @@ function MM:Create()
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:RegisterForDrag("LeftButton", "RightButton")
btn:SetMovable(true)
-- Allow the button to sit off-screen (via /attune pos or /attune offscreen)
-- while remaining discoverable by MinimapButtonBag.
if btn.SetClampedToScreen then btn:SetClampedToScreen(false) end
-- MBB-friendly highlight so the button visually matches other
-- minimap buttons once it has been swept into the bag.
btn:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
-- Round border ring (standard minimap-button look)
local overlay = btn:CreateTexture(nil, "OVERLAY")
@@ -89,6 +108,7 @@ function MM:Create()
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 7, -6)
btn.icon = icon
btn:SetScript("OnClick", function()
if RA.UI and RA.UI.Toggle then RA.UI:Toggle() end
end)
@@ -107,6 +127,7 @@ function MM:Create()
GameTooltip:AddLine("Relationships Attune", 1, 0.82, 0)
GameTooltip:AddLine("Left-click: open / close window", 0.9, 0.9, 0.9)
GameTooltip:AddLine("Drag: move anywhere on screen", 0.9, 0.9, 0.9)
GameTooltip:AddLine("/attune offscreen - park off-screen (MBB safe)", 0.6, 0.8, 1)
GameTooltip:AddLine("/attune hide - hide this button", 0.6, 0.8, 1)
GameTooltip:Show()
end)
+1 -1
View File
@@ -1,4 +1,4 @@
<img src="https://cdn.discordapp.com/attachments/1510333697277300856/1524819837270884362/kkk.gif?ex=6a5122a2&is=6a4fd122&hm=b82d19b995b2d1f8b252fcbaf44dcaa7b5f30255fb1401f50f931919c980a4ce&"/>
<img src="https://i.postimg.cc/rwVg8mmF/v.png"/>
# RelationshipsAttune
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: Relationships Attune
## Notes: Track Vanilla and Turtle/Octo WoW attunement progress
## Author: Relationship
## Version: 1.4
## Version: 1.9
## X-Category: Quest
## SavedVariables: RelationshipsAttuneDB
## SavedVariablesPerCharacter: RelationshipsAttuneCharDB
+86
View File
@@ -26,6 +26,55 @@ local C_TURTLE = { 0.55, 0.85, 1.00 }
local C_KEYS = { 0.95, 0.75, 0.35 }
local C_INPROGRESS = { 1.00, 0.60, 0.15 } -- orange: quest accepted / step in progress
-- ---------------------------------------------------------------
-- Force-complete confirmation dialogs (StaticPopup)
-- In vanilla 1.12, StaticPopup OnAccept is called as OnAccept(dialog.data),
-- so the data arrives as arg1 (Lua 5.0 vararg). `this` inside OnAccept is
-- NOT the dialog frame, so this.data is nil. We stash the data on the
-- dialog itself via OnShow (which does run with this = dialog) and read
-- it back from arg1 with a dialog fallback for safety.
-- ---------------------------------------------------------------
StaticPopupDialogs = StaticPopupDialogs or {}
local function RATTUNE_StashData()
-- OnShow runs with `this` = the dialog frame. StaticPopup_Show already
-- assigns dialog.data, but we snapshot it into an upvalue table so the
-- OnAccept closures can find it even on clients that don't forward arg1.
if this then RATTUNE_LastPopupData = this.data end
end
-- Attunement id awaiting the user's confirmation for a force / unforce.
-- StaticPopup OnAccept in vanilla 1.12 does not reliably receive dialog.data
-- as arg1, so we snapshot the id here at click time and read it back on accept.
RATTUNE_PendingForceAtt = nil
StaticPopupDialogs["RATTUNE_FORCE_ATT"] = {
text = "Force complete the entire \"%s\" attunement?\n\nEvery step will be marked as done for tracking. You can undo this later.",
button1 = "Force Complete",
button2 = "Cancel",
OnAccept = function()
local id = RATTUNE_PendingForceAtt
RATTUNE_PendingForceAtt = nil
if id then RA:SetAttForced(id, true) end
end,
OnCancel = function() RATTUNE_PendingForceAtt = nil end,
timeout = 0, whileDead = 1, hideOnEscape = 1, showAlert = 1,
}
StaticPopupDialogs["RATTUNE_UNFORCE_ATT"] = {
text = "Undo forced completion for \"%s\"?\n\nProgress will revert to real, scanned state.",
button1 = "Undo",
button2 = "Cancel",
OnAccept = function()
local id = RATTUNE_PendingForceAtt
RATTUNE_PendingForceAtt = nil
if id then RA:SetAttForced(id, false) end
end,
OnCancel = function() RATTUNE_PendingForceAtt = nil end,
timeout = 0, whileDead = 1, hideOnEscape = 1, showAlert = 1,
}
local selectedPlayer = nil
local selectedRaid = nil
@@ -196,6 +245,29 @@ function UI:Build()
detail.category = fs(frame.rightPane, "", 10, 0.75, 0.75, 0.75)
detail.category:SetPoint("TOPLEFT", detail.name, "BOTTOMLEFT", 0, -2)
-- Force Complete (attunement-level) button. Sits in the top-right of the
-- right pane, hidden when viewing another player's data.
detail.forceBtn = CreateFrame("Button", nil, frame.rightPane, "UIPanelButtonTemplate")
detail.forceBtn:SetWidth(150); detail.forceBtn:SetHeight(20)
detail.forceBtn:SetPoint("TOPRIGHT", frame.rightPane, "TOPRIGHT", -4, -4)
detail.forceBtn:SetText("Force Complete")
detail.forceBtn:SetScript("OnClick", function()
local attId = selectedRaid
if not attId then return end
local att
local data = RelationshipsAttune_Data
for i = 1, table.getn(data) do
if data[i].id == attId then att = data[i]; break end
end
if not att then return end
RATTUNE_PendingForceAtt = attId
if RA:IsAttForced(attId) then
StaticPopup_Show("RATTUNE_UNFORCE_ATT", att.name)
else
StaticPopup_Show("RATTUNE_FORCE_ATT", att.name)
end
end)
-- Progress bar: give it explicit left+right anchors AND a border so it
-- always has a resolvable width, then size the fill from that width.
detail.barBG = solid(frame.rightPane, "BORDER", 0, 0, 0, 0.90)
@@ -566,6 +638,20 @@ function UI:RenderDetail()
detail.barFill:SetVertexColor(c[1], c[2], c[3], 0.95)
detail.barText:SetText(done .. " / " .. total .. " (" .. math.floor(pct * 100 + 0.5) .. "%)")
-- Update attunement-level Force button (only for your own data)
if detail.forceBtn then
if selectedPlayer ~= nil then
detail.forceBtn:Hide()
else
if RA:IsAttForced(att.id) then
detail.forceBtn:SetText("Undo Force Complete")
else
detail.forceBtn:SetText("Force Complete")
end
detail.forceBtn:Show()
end
end
-- Build applicable step list
local applicable = {}
for i = 1, table.getn(att.steps) do