Add files via upload
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
-- RelationshipsAttune: Comm.lua
|
||||||
|
-- Party/Raid/Guild addon-channel sync. Small, fixed-format messages so
|
||||||
|
-- we don't need a serializer library.
|
||||||
|
|
||||||
|
RelationshipsAttune = RelationshipsAttune or {}
|
||||||
|
local RA = RelationshipsAttune
|
||||||
|
RA.Comm = {}
|
||||||
|
local C = RA.Comm
|
||||||
|
|
||||||
|
-- Message formats (all pipe-delimited, single line):
|
||||||
|
-- V|<version> -> version ping
|
||||||
|
-- R -> request full sync
|
||||||
|
-- S|<attId>|<doneCount>|<total>|<doneBool> -> per-attunement status
|
||||||
|
-- D|<attId>|<b1><b2>...<bN> -> per-step done bits
|
||||||
|
--
|
||||||
|
-- Bits are '1'/'0' chars so we stay printable.
|
||||||
|
|
||||||
|
local function send(msg, channel, target)
|
||||||
|
if not msg then return end
|
||||||
|
channel = channel or "PARTY"
|
||||||
|
if channel == "WHISPER" then
|
||||||
|
SendAddonMessage(RA.COMM_PREFIX, msg, channel, target)
|
||||||
|
else
|
||||||
|
SendAddonMessage(RA.COMM_PREFIX, msg, channel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function currentChannel()
|
||||||
|
if GetNumRaidMembers and GetNumRaidMembers() > 0 then return "RAID" end
|
||||||
|
if GetNumPartyMembers and GetNumPartyMembers() > 0 then return "PARTY" end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function C:Init()
|
||||||
|
-- On login, broadcast a version ping and share current state.
|
||||||
|
local ch = currentChannel()
|
||||||
|
if ch then
|
||||||
|
send("V|" .. RA.VERSION, ch)
|
||||||
|
self:BroadcastAll(ch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function C:RequestSync()
|
||||||
|
local ch = currentChannel()
|
||||||
|
if not ch then RA.print("No group to sync with."); return end
|
||||||
|
send("R", ch)
|
||||||
|
self:BroadcastAll(ch)
|
||||||
|
end
|
||||||
|
|
||||||
|
function C:BroadcastAll(channel, target)
|
||||||
|
channel = channel or currentChannel()
|
||||||
|
if not channel then return end
|
||||||
|
local data = RelationshipsAttune_Data
|
||||||
|
for i = 1, table.getn(data) do
|
||||||
|
local att = data[i]
|
||||||
|
local prog = RelationshipsAttuneCharDB.progress[att.id]
|
||||||
|
if prog and prog.applicable then
|
||||||
|
send(string.format("S|%s|%d|%d|%d",
|
||||||
|
att.id, prog.doneCount, prog.total, prog.done and 1 or 0),
|
||||||
|
channel, target)
|
||||||
|
-- pack steps
|
||||||
|
local bits = ""
|
||||||
|
for s = 1, prog.total do
|
||||||
|
bits = bits .. (prog.steps[s] and "1" or "0")
|
||||||
|
end
|
||||||
|
send("D|" .. att.id .. "|" .. bits, channel, target)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensurePlayer(sender)
|
||||||
|
RelationshipsAttuneDB.players = RelationshipsAttuneDB.players or {}
|
||||||
|
local key = sender -- vanilla addon sender = "Name" (no realm)
|
||||||
|
if not RelationshipsAttuneDB.players[key] then
|
||||||
|
RelationshipsAttuneDB.players[key] = {
|
||||||
|
name = sender, realm = RA.playerRealm, faction = "Unknown",
|
||||||
|
progress = {}, updated = time and time() or 0,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return RelationshipsAttuneDB.players[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
function C:OnMessage(msg, channel, sender)
|
||||||
|
if not msg or not sender then return end
|
||||||
|
if sender == RA.playerName then return end -- ignore self-echo
|
||||||
|
|
||||||
|
local _, _, tag, rest = string.find(msg, "^(%a)|?(.*)$")
|
||||||
|
if not tag then return end
|
||||||
|
local p = ensurePlayer(sender)
|
||||||
|
p.updated = time and time() or 0
|
||||||
|
|
||||||
|
if tag == "V" then
|
||||||
|
-- version ping; nothing to do beyond note
|
||||||
|
p.version = rest
|
||||||
|
|
||||||
|
elseif tag == "R" then
|
||||||
|
-- someone wants my data
|
||||||
|
self:BroadcastAll(channel, sender)
|
||||||
|
|
||||||
|
elseif tag == "S" then
|
||||||
|
-- S|attId|done|total|doneBool
|
||||||
|
local _, _, attId, d, t, full = string.find(rest, "^([^|]+)|(%d+)|(%d+)|(%d+)$")
|
||||||
|
if attId then
|
||||||
|
p.progress = p.progress or {}
|
||||||
|
local prog = p.progress[attId] or { steps = {} }
|
||||||
|
prog.applicable = true
|
||||||
|
prog.doneCount = tonumber(d) or 0
|
||||||
|
prog.total = tonumber(t) or 0
|
||||||
|
prog.done = (tonumber(full) == 1)
|
||||||
|
p.progress[attId] = prog
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif tag == "D" then
|
||||||
|
-- D|attId|bits
|
||||||
|
local _, _, attId, bits = string.find(rest, "^([^|]+)|([01]*)$")
|
||||||
|
if attId and bits then
|
||||||
|
p.progress = p.progress or {}
|
||||||
|
local prog = p.progress[attId] or { steps = {}, done = false, doneCount = 0, total = string.len(bits) }
|
||||||
|
local steps = {}
|
||||||
|
for i = 1, string.len(bits) do
|
||||||
|
steps[i] = (string.sub(bits, i, i) == "1")
|
||||||
|
end
|
||||||
|
prog.steps = steps
|
||||||
|
prog.total = prog.total or string.len(bits)
|
||||||
|
p.progress[attId] = prog
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
|
||||||
|
end
|
||||||
@@ -0,0 +1,650 @@
|
|||||||
|
-- RelationshipsAttune: Core.lua
|
||||||
|
-- Event handling, scanning, throttled auto-scan, faction-aware evaluation.
|
||||||
|
-- Lua 5.0 / WoW 1.12 safe (Turtle/Octo compatible).
|
||||||
|
|
||||||
|
RelationshipsAttune = RelationshipsAttune or {}
|
||||||
|
local RA = RelationshipsAttune
|
||||||
|
local L = RelationshipsAttune_L
|
||||||
|
|
||||||
|
RA.VERSION = "1.2.7"
|
||||||
|
RA.COMM_PREFIX = "RATTUNE"
|
||||||
|
|
||||||
|
RA.playerName = nil
|
||||||
|
RA.playerRealm = nil
|
||||||
|
RA.playerKey = nil
|
||||||
|
RA.playerFaction = nil
|
||||||
|
RA.initialized = false
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Utilities
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
local function tsize(t)
|
||||||
|
if not t then return 0 end
|
||||||
|
local n = 0
|
||||||
|
for _ in pairs(t) do n = n + 1 end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
RA.tsize = tsize
|
||||||
|
|
||||||
|
local function print(msg)
|
||||||
|
if DEFAULT_CHAT_FRAME then
|
||||||
|
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[RAttune]|r " .. tostring(msg))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
RA.print = print
|
||||||
|
|
||||||
|
local STANDING = {
|
||||||
|
Hated = 1, Hostile = 2, Unfriendly = 3, Neutral = 4,
|
||||||
|
Friendly = 5, Honored = 6, Revered = 7, Exalted = 8,
|
||||||
|
}
|
||||||
|
local DATA_STANDING_MAP = { [1]=2, [2]=3, [3]=4, [4]=5, [5]=6, [6]=7, [7]=8 }
|
||||||
|
|
||||||
|
-- Return true if this step is applicable to the current player faction.
|
||||||
|
function RA:StepApplies(step)
|
||||||
|
if step.faction and step.faction ~= "Both" and step.faction ~= RA.playerFaction then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Saved-variables init
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
function RA:InitDB()
|
||||||
|
if not RelationshipsAttuneDB then RelationshipsAttuneDB = {} end
|
||||||
|
if not RelationshipsAttuneDB.players then RelationshipsAttuneDB.players = {} end
|
||||||
|
if not RelationshipsAttuneDB.options then
|
||||||
|
RelationshipsAttuneDB.options = {}
|
||||||
|
end
|
||||||
|
local o = RelationshipsAttuneDB.options
|
||||||
|
if o.autoShare == nil then o.autoShare = true end
|
||||||
|
if o.autoScanSeconds == nil then o.autoScanSeconds = 20 end
|
||||||
|
if type(o.minimapAngle) ~= "number" then o.minimapAngle = 200 end
|
||||||
|
if type(o.minimapRadius) ~= "number" then o.minimapRadius = 80 end
|
||||||
|
if o.minimapHide == nil then o.minimapHide = false end
|
||||||
|
|
||||||
|
if not RelationshipsAttuneCharDB then RelationshipsAttuneCharDB = {} end
|
||||||
|
if not RelationshipsAttuneCharDB.quests then RelationshipsAttuneCharDB.quests = {} end
|
||||||
|
if not RelationshipsAttuneCharDB.items then RelationshipsAttuneCharDB.items = {} end
|
||||||
|
if not RelationshipsAttuneCharDB.completedAttunements then RelationshipsAttuneCharDB.completedAttunements = {} end
|
||||||
|
if not RelationshipsAttuneCharDB.progress then RelationshipsAttuneCharDB.progress = {} end
|
||||||
|
if not RelationshipsAttuneCharDB.expanded then RelationshipsAttuneCharDB.expanded = {} end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Quest log scan (vanilla API)
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
function RA:ScanQuestLog()
|
||||||
|
if not GetNumQuestLogEntries then return end
|
||||||
|
local numEntries = GetNumQuestLogEntries()
|
||||||
|
for i = 1, numEntries do
|
||||||
|
local title, level, tag, isHeader, _, isComplete, _, questId =
|
||||||
|
GetQuestLogTitle(i)
|
||||||
|
if (not isHeader) and questId then
|
||||||
|
if isComplete and isComplete == 1 then
|
||||||
|
RelationshipsAttuneCharDB.quests[questId] = "complete"
|
||||||
|
else
|
||||||
|
if not RelationshipsAttuneCharDB.quests[questId] then
|
||||||
|
RelationshipsAttuneCharDB.quests[questId] = "inlog"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function RA:MarkQuestTurnedIn(questId)
|
||||||
|
if not questId then return end
|
||||||
|
RelationshipsAttuneCharDB.quests[questId] = "turnedin"
|
||||||
|
self:RecomputeAll()
|
||||||
|
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Item scan: bags, equipped slots, and keyring
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
local function rememberItemLink(link)
|
||||||
|
if not link then return end
|
||||||
|
local _, _, idStr = string.find(link, "item:(%d+)")
|
||||||
|
local linkId
|
||||||
|
if idStr then
|
||||||
|
linkId = tonumber(idStr)
|
||||||
|
if linkId then RelationshipsAttuneCharDB.items[linkId] = true end
|
||||||
|
end
|
||||||
|
-- Resolve by name so a custom Turtle/Octo item whose live client id
|
||||||
|
-- differs from the id declared in Data.lua still gets credited.
|
||||||
|
-- Try GetItemInfo first (works when the item is cached), then fall back
|
||||||
|
-- to parsing the bracketed display name straight out of the link. On
|
||||||
|
-- 1.12 / Turtle / Octo, keyring items very often return a link but
|
||||||
|
-- GetItemInfo returns nil because the item is not cached client-side,
|
||||||
|
-- so the bracketed-name parse is what actually credits the item.
|
||||||
|
local resolvedName
|
||||||
|
if GetItemInfo then
|
||||||
|
local ok, n = pcall(GetItemInfo, link)
|
||||||
|
if ok and n then resolvedName = n end
|
||||||
|
end
|
||||||
|
if not resolvedName then
|
||||||
|
local _, _, bracket = string.find(link, "%[(.-)%]")
|
||||||
|
if bracket and bracket ~= "" then resolvedName = bracket end
|
||||||
|
end
|
||||||
|
if resolvedName then
|
||||||
|
RA:BuildNameIndex()
|
||||||
|
if RA._nameToId then
|
||||||
|
local mapped = RA._nameToId[string.lower(resolvedName)]
|
||||||
|
if mapped and mapped ~= linkId then
|
||||||
|
RelationshipsAttuneCharDB.items[mapped] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rememberItemId(id)
|
||||||
|
if not id then return end
|
||||||
|
id = tonumber(id)
|
||||||
|
if id then RelationshipsAttuneCharDB.items[id] = true end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build (and cache) a name -> item id map from RelationshipsAttune_Data so we
|
||||||
|
-- can identify keyring items even when the client refuses to return a link
|
||||||
|
-- for them (a known 1.12 / Turtle / Octo quirk for keyring slots).
|
||||||
|
RA._nameToId = nil
|
||||||
|
function RA:BuildNameIndex()
|
||||||
|
if RA._nameToId then return RA._nameToId end
|
||||||
|
local map = {}
|
||||||
|
if RelationshipsAttune_Data then
|
||||||
|
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 == "item" and s.name and s.id then
|
||||||
|
map[string.lower(s.name)] = s.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
RA._nameToId = map
|
||||||
|
return map
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hidden tooltip used to force item info to resolve for keyring slots.
|
||||||
|
local _raTip
|
||||||
|
local _raTipName = "RelationshipsAttuneScanTip"
|
||||||
|
local function ensureTip()
|
||||||
|
if _raTip then return _raTip end
|
||||||
|
_raTip = CreateFrame("GameTooltip", _raTipName, nil, "GameTooltipTemplate")
|
||||||
|
_raTip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||||
|
return _raTip
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read the first tooltip line (item name) after a SetBagItem call.
|
||||||
|
-- 1.12 exposes frame globals via getglobal(), not _G.
|
||||||
|
local function tipItemName()
|
||||||
|
local fs
|
||||||
|
if getglobal then fs = getglobal(_raTipName .. "TextLeft1") end
|
||||||
|
if (not fs) and _G then fs = _G[_raTipName .. "TextLeft1"] end
|
||||||
|
if fs and fs.GetText then
|
||||||
|
local t = fs:GetText()
|
||||||
|
if t and t ~= "" then return t end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function scanContainer(containerId)
|
||||||
|
if not GetContainerNumSlots then return end
|
||||||
|
local ok, slots = pcall(GetContainerNumSlots, containerId)
|
||||||
|
if not ok or not slots or slots < 1 then return end
|
||||||
|
for slot = 1, slots do
|
||||||
|
local ok2, link = pcall(GetContainerItemLink, containerId, slot)
|
||||||
|
if ok2 then rememberItemLink(link) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scan a keyring-like container. Uses every fallback we know of so that
|
||||||
|
-- keys register on vanilla 1.12, Turtle, and Octo regardless of which
|
||||||
|
-- bag id / API path actually works on that client build:
|
||||||
|
-- 1) GetContainerItemLink -> parse item id + bracketed name from link.
|
||||||
|
-- 2) SetBagItem tooltip -> read visible line as name.
|
||||||
|
-- 3) GetContainerItemInfo -> at minimum proves a slot is occupied so
|
||||||
|
-- we keep probing even when NumSlots lies.
|
||||||
|
local function scanKeyringContainer(bag)
|
||||||
|
if not GetContainerNumSlots then return end
|
||||||
|
local ok, slots = pcall(GetContainerNumSlots, bag)
|
||||||
|
if not ok or not slots then slots = 0 end
|
||||||
|
-- Some 1.12 clients report 0 slots for the keyring until it has been
|
||||||
|
-- opened at least once. Probe a reasonable upper bound in that case.
|
||||||
|
local probe = slots
|
||||||
|
if probe < 1 then probe = 32 end
|
||||||
|
|
||||||
|
local tip = ensureTip()
|
||||||
|
local nameMap = RA:BuildNameIndex()
|
||||||
|
|
||||||
|
for slot = 1, probe do
|
||||||
|
pcall(function() tip:ClearLines() end)
|
||||||
|
local setOk = false
|
||||||
|
pcall(function() tip:SetBagItem(bag, slot); setOk = true end)
|
||||||
|
|
||||||
|
-- Path 1: link
|
||||||
|
local link
|
||||||
|
local okL, l = pcall(GetContainerItemLink, bag, slot)
|
||||||
|
if okL then link = l end
|
||||||
|
if link then rememberItemLink(link) end
|
||||||
|
|
||||||
|
-- Path 2: tooltip visible name (works even when link is nil).
|
||||||
|
if setOk then
|
||||||
|
local name = tipItemName()
|
||||||
|
if name and nameMap then
|
||||||
|
local id = nameMap[string.lower(name)]
|
||||||
|
if id then rememberItemId(id) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Path 3: GetContainerItemInfo. We can't get a name from it, but
|
||||||
|
-- if it returns a texture the slot is definitely occupied; that
|
||||||
|
-- lets us know it's worth continuing to probe even when the
|
||||||
|
-- earlier paths returned nothing on this build.
|
||||||
|
-- (No-op for identification, kept for future diagnostics.)
|
||||||
|
if GetContainerItemInfo then
|
||||||
|
pcall(GetContainerItemInfo, bag, slot)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pcall(function() tip:Hide() end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Warm the client-side item cache for every item id we care about. On
|
||||||
|
-- vanilla 1.12 the first GetItemInfo(id) call for an uncached item kicks
|
||||||
|
-- off a server fetch and returns nil; a later call returns real data.
|
||||||
|
-- Doing this at load time (and again after a short delay) means that by
|
||||||
|
-- the time we parse a keyring link, GetItemInfo can actually resolve it.
|
||||||
|
function RA:WarmItemCache()
|
||||||
|
if not GetItemInfo or not RelationshipsAttune_Data then return end
|
||||||
|
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 == "item" and s.id then
|
||||||
|
pcall(GetItemInfo, s.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function RA:ScanBags()
|
||||||
|
if not GetContainerNumSlots then return end
|
||||||
|
RA:BuildNameIndex()
|
||||||
|
for bag = 0, 4 do
|
||||||
|
scanContainer(bag)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Keyring: try every index the various 1.12 forks use. Vanilla/Turtle/
|
||||||
|
-- Octo typically expose it as -2; a few builds use -1, -3, or define
|
||||||
|
-- KEYRING_CONTAINER to something else entirely. Scanning a bogus id
|
||||||
|
-- is harmless because scanKeyringContainer pcall-guards every call.
|
||||||
|
local seen = {}
|
||||||
|
local function tryBag(b)
|
||||||
|
if b == nil or seen[b] then return end
|
||||||
|
seen[b] = true
|
||||||
|
scanKeyringContainer(b)
|
||||||
|
end
|
||||||
|
tryBag(-2)
|
||||||
|
tryBag(-1)
|
||||||
|
tryBag(-3)
|
||||||
|
tryBag(KEYRING_CONTAINER)
|
||||||
|
|
||||||
|
if GetInventoryItemLink then
|
||||||
|
for slot = 1, 19 do
|
||||||
|
local ok, link = pcall(GetInventoryItemLink, "player", slot)
|
||||||
|
if ok then rememberItemLink(link) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Back-compat shim: older code paths may still call this.
|
||||||
|
function RA:ForceKeyringResolve()
|
||||||
|
scanKeyringContainer(-2)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Reputation lookup
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
function RA:GetReputationStanding(factionName)
|
||||||
|
if not GetNumFactions then return 0 end
|
||||||
|
local numFactions = GetNumFactions()
|
||||||
|
for i = 1, numFactions do
|
||||||
|
local name, _, standingId, _, _, _, _, _, isHeader = GetFactionInfo(i)
|
||||||
|
if name == factionName and not isHeader then
|
||||||
|
return standingId or 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Compute attunement progress
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
function RA:EvaluateStep(step)
|
||||||
|
if step.type == "level" then
|
||||||
|
return (UnitLevel("player") or 0) >= (step.level or 0)
|
||||||
|
elseif step.type == "quest" then
|
||||||
|
local st = RelationshipsAttuneCharDB.quests[step.id]
|
||||||
|
return st == "complete" or st == "turnedin"
|
||||||
|
elseif step.type == "item" then
|
||||||
|
return RelationshipsAttuneCharDB.items[step.id] == true
|
||||||
|
elseif step.type == "rep" then
|
||||||
|
local need = DATA_STANDING_MAP[step.standing] or step.standing or 5
|
||||||
|
local have = self:GetReputationStanding(step.faction)
|
||||||
|
return have >= need
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function RA:EvaluateAttunement(att)
|
||||||
|
if att.faction and att.faction ~= "Both" and att.faction ~= RA.playerFaction then
|
||||||
|
return { applicable = false, done = false, steps = {}, doneCount = 0, total = 0 }
|
||||||
|
end
|
||||||
|
local steps, doneCount, total = {}, 0, 0
|
||||||
|
local numSteps = table.getn(att.steps)
|
||||||
|
for i = 1, numSteps do
|
||||||
|
local step = att.steps[i]
|
||||||
|
if self:StepApplies(step) then
|
||||||
|
local ok = self:EvaluateStep(step) and true or false
|
||||||
|
steps[i] = ok
|
||||||
|
total = total + 1
|
||||||
|
if ok then doneCount = doneCount + 1 end
|
||||||
|
else
|
||||||
|
steps[i] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Short-circuit: if the player already owns the final key/item (or a step
|
||||||
|
-- explicitly flagged final=true) then the entire attunement is done, even
|
||||||
|
-- if earlier bookkeeping steps (prereq quests, reputation) can no longer
|
||||||
|
-- be detected on this character. This handles the case where the key was
|
||||||
|
-- earned before the addon was installed.
|
||||||
|
local finalIdx = nil
|
||||||
|
for i = numSteps, 1, -1 do
|
||||||
|
local step = att.steps[i]
|
||||||
|
if step.final and self:StepApplies(step) then
|
||||||
|
finalIdx = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not finalIdx then
|
||||||
|
-- Fall back to the last applicable item step (typically the key).
|
||||||
|
for i = numSteps, 1, -1 do
|
||||||
|
local step = att.steps[i]
|
||||||
|
if step.type == "item" and self:StepApplies(step) then
|
||||||
|
finalIdx = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not finalIdx then
|
||||||
|
-- If there is no key/item step, the last applicable step is the final
|
||||||
|
-- proof we can see. This covers quest-only or reputation attunements.
|
||||||
|
for i = numSteps, 1, -1 do
|
||||||
|
local step = att.steps[i]
|
||||||
|
if self:StepApplies(step) then
|
||||||
|
finalIdx = i
|
||||||
|
break
|
||||||
|
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
|
||||||
|
if steps[i] == false then
|
||||||
|
steps[i] = true
|
||||||
|
doneCount = doneCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if total > 0 and doneCount == total then
|
||||||
|
RelationshipsAttuneCharDB.completedAttunements[att.id] = true
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
applicable = true,
|
||||||
|
done = (total > 0 and doneCount == total),
|
||||||
|
steps = steps,
|
||||||
|
doneCount = doneCount,
|
||||||
|
total = total,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function RA:RecomputeAll()
|
||||||
|
for i = 1, table.getn(RelationshipsAttune_Data) do
|
||||||
|
local att = RelationshipsAttune_Data[i]
|
||||||
|
RelationshipsAttuneCharDB.progress[att.id] = self:EvaluateAttunement(att)
|
||||||
|
end
|
||||||
|
RelationshipsAttuneDB.players[RA.playerKey] = {
|
||||||
|
name = RA.playerName,
|
||||||
|
realm = RA.playerRealm,
|
||||||
|
faction = RA.playerFaction,
|
||||||
|
level = UnitLevel("player") or 0,
|
||||||
|
class = UnitClass("player"),
|
||||||
|
version = RA.VERSION,
|
||||||
|
updated = time and time() or 0,
|
||||||
|
progress = RelationshipsAttuneCharDB.progress,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Throttled auto-scan
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
RA._throttle = 0
|
||||||
|
RA._dirty = false
|
||||||
|
RA._minRescan = 2.0
|
||||||
|
|
||||||
|
function RA:MarkDirty() RA._dirty = true end
|
||||||
|
|
||||||
|
function RA:_OnUpdate(elapsed)
|
||||||
|
RA._throttle = RA._throttle + (elapsed or 0)
|
||||||
|
local interval = (RelationshipsAttuneDB and RelationshipsAttuneDB.options
|
||||||
|
and RelationshipsAttuneDB.options.autoScanSeconds) or 20
|
||||||
|
if interval < 5 then interval = 5 end
|
||||||
|
|
||||||
|
-- Delayed post-login retry passes. GetItemInfo populates async on
|
||||||
|
-- 1.12, and the keyring itself may not be populated at PLAYER_LOGIN,
|
||||||
|
-- so we re-warm the cache and rescan a couple of times shortly after
|
||||||
|
-- login to catch keys that weren't resolvable on the first pass.
|
||||||
|
if RA._retryAt and GetTime then
|
||||||
|
local now = GetTime()
|
||||||
|
local pending = {}
|
||||||
|
local fired = false
|
||||||
|
for i = 1, table.getn(RA._retryAt) do
|
||||||
|
local t = RA._retryAt[i]
|
||||||
|
if now >= t then
|
||||||
|
fired = true
|
||||||
|
else
|
||||||
|
table.insert(pending, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if fired then
|
||||||
|
RA:WarmItemCache()
|
||||||
|
RA:ScanBags()
|
||||||
|
RA:RecomputeAll()
|
||||||
|
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
|
||||||
|
end
|
||||||
|
if table.getn(pending) == 0 then
|
||||||
|
RA._retryAt = nil
|
||||||
|
else
|
||||||
|
RA._retryAt = pending
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if RA._dirty and RA._throttle >= RA._minRescan then
|
||||||
|
RA._dirty = false
|
||||||
|
RA._throttle = 0
|
||||||
|
RA:ScanQuestLog()
|
||||||
|
RA:ScanBags()
|
||||||
|
RA:RecomputeAll()
|
||||||
|
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if RA._throttle >= interval then
|
||||||
|
RA._throttle = 0
|
||||||
|
RA:ScanQuestLog()
|
||||||
|
RA:ScanBags()
|
||||||
|
RA:RecomputeAll()
|
||||||
|
if RA.UI and RA.UI.Refresh then RA.UI:Refresh() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Slash commands
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
local function slashHandler(msg)
|
||||||
|
msg = string.lower(msg or "")
|
||||||
|
-- trim
|
||||||
|
msg = string.gsub(msg, "^%s+", "")
|
||||||
|
msg = string.gsub(msg, "%s+$", "")
|
||||||
|
|
||||||
|
if msg == "show" then
|
||||||
|
if RA.UI then RA.UI:Show() end
|
||||||
|
elseif msg == "hide" then
|
||||||
|
-- If the window is open, /attune hide closes it; otherwise it hides the minimap button.
|
||||||
|
if RA.UI and RA.UI:IsShown() then
|
||||||
|
RA.UI:Hide()
|
||||||
|
elseif RA.Minimap then
|
||||||
|
RA.Minimap:Hide()
|
||||||
|
print("Minimap button hidden. Type /attune minimap to show it again.")
|
||||||
|
end
|
||||||
|
elseif msg == "minimap" or msg == "mm" then
|
||||||
|
if RA.Minimap then
|
||||||
|
RA.Minimap:Toggle()
|
||||||
|
local o = RelationshipsAttuneDB.options
|
||||||
|
if o.minimapHide then
|
||||||
|
print("Minimap button hidden. /attune minimap to show it.")
|
||||||
|
else
|
||||||
|
print("Minimap button shown. Drag to reposition around the minimap.")
|
||||||
|
end
|
||||||
|
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()
|
||||||
|
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()
|
||||||
|
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
|
||||||
|
local _, _, arg = string.find(msg, "find%s+(%d+)")
|
||||||
|
RA:ScanBags()
|
||||||
|
if arg then
|
||||||
|
local id = tonumber(arg)
|
||||||
|
print("Item " .. id .. " owned: " .. tostring(RelationshipsAttuneCharDB.items[id] == true))
|
||||||
|
end
|
||||||
|
print("Keyring dump:")
|
||||||
|
local probeBags = { -2, -1 }
|
||||||
|
if KEYRING_CONTAINER and KEYRING_CONTAINER ~= -2 and KEYRING_CONTAINER ~= -1 then
|
||||||
|
table.insert(probeBags, KEYRING_CONTAINER)
|
||||||
|
end
|
||||||
|
local tip = ensureTip()
|
||||||
|
for _, bag in ipairs(probeBags) do
|
||||||
|
local ok, slots = pcall(GetContainerNumSlots, bag)
|
||||||
|
if not ok or not slots then slots = 0 end
|
||||||
|
local probe = slots > 0 and slots or 32
|
||||||
|
print(" bag " .. bag .. " GetContainerNumSlots=" .. tostring(slots) .. " probing=" .. probe)
|
||||||
|
for s = 1, probe do
|
||||||
|
pcall(function() tip:ClearLines() end)
|
||||||
|
pcall(function() tip:SetBagItem(bag, s) end)
|
||||||
|
local ok2, link = pcall(GetContainerItemLink, bag, s)
|
||||||
|
local name = tipItemName()
|
||||||
|
if (ok2 and link) or name then
|
||||||
|
print(" [" .. s .. "] link=" .. tostring(link) .. " name=" .. tostring(name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pcall(function() tip:Hide() end)
|
||||||
|
print("Scarlet Key (7146) owned: " .. tostring(RelationshipsAttuneCharDB.items[7146] == true))
|
||||||
|
elseif msg == "help" or msg == "?" then
|
||||||
|
print("Relationships Attune - commands:")
|
||||||
|
print(" /attune - toggle the window")
|
||||||
|
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 scan - force a rescan of quests + bags")
|
||||||
|
print(" /attune sync - request sync from your group")
|
||||||
|
print(" /attune debug - dump keyring + item detection")
|
||||||
|
print(" /attune find <id>- check if a specific item is detected")
|
||||||
|
print(" /attune reset - wipe this character's data and rescan")
|
||||||
|
else
|
||||||
|
if RA.UI then RA.UI:Toggle() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
SLASH_RELATIONSHIPSATTUNE1 = "/rattune"
|
||||||
|
SLASH_RELATIONSHIPSATTUNE2 = "/relationshipsattune"
|
||||||
|
SLASH_RELATIONSHIPSATTUNE3 = "/attune"
|
||||||
|
SlashCmdList["RELATIONSHIPSATTUNE"] = slashHandler
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Event frame
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
local ef = CreateFrame("Frame", "RelationshipsAttuneEventFrame")
|
||||||
|
ef:RegisterEvent("PLAYER_LOGIN")
|
||||||
|
ef:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||||
|
ef:RegisterEvent("QUEST_LOG_UPDATE")
|
||||||
|
ef:RegisterEvent("BAG_UPDATE")
|
||||||
|
ef:RegisterEvent("PLAYER_LEVEL_UP")
|
||||||
|
ef:RegisterEvent("UPDATE_FACTION")
|
||||||
|
ef:RegisterEvent("CHAT_MSG_ADDON")
|
||||||
|
pcall(function() ef:RegisterEvent("QUEST_TURNED_IN") end)
|
||||||
|
pcall(function() ef:RegisterEvent("KEYRING_UPDATE") end)
|
||||||
|
|
||||||
|
ef:SetScript("OnEvent", function()
|
||||||
|
local e = event
|
||||||
|
if e == "PLAYER_LOGIN" then
|
||||||
|
RA:InitDB()
|
||||||
|
RA.playerName = UnitName("player")
|
||||||
|
RA.playerRealm = GetRealmName() or "Unknown"
|
||||||
|
RA.playerKey = RA.playerName .. "-" .. RA.playerRealm
|
||||||
|
RA.playerFaction = UnitFactionGroup("player") or "Neutral"
|
||||||
|
RA:BuildNameIndex()
|
||||||
|
RA:WarmItemCache()
|
||||||
|
RA:ScanQuestLog(); RA:ScanBags(); RA:RecomputeAll()
|
||||||
|
-- Retry pass: on 1.12 GetItemInfo populates async, and the
|
||||||
|
-- keyring is not always populated at PLAYER_LOGIN. Schedule two
|
||||||
|
-- delayed rescans so keys still get credited without user action.
|
||||||
|
RA._retryAt = { GetTime() + 3, 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
|
||||||
|
RA.initialized = true
|
||||||
|
print(L.ADDON_NAME .. " v" .. RA.VERSION .. " " .. L.MSG_LOADED
|
||||||
|
.. " (" .. tostring(RA.playerFaction) .. "). Type /attune for options.")
|
||||||
|
|
||||||
|
elseif e == "PLAYER_ENTERING_WORLD" then
|
||||||
|
if RA.initialized then
|
||||||
|
RA.playerFaction = UnitFactionGroup("player") or RA.playerFaction or "Neutral"
|
||||||
|
RA:MarkDirty()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif e == "QUEST_LOG_UPDATE"
|
||||||
|
or e == "BAG_UPDATE"
|
||||||
|
or e == "KEYRING_UPDATE"
|
||||||
|
or e == "PLAYER_LEVEL_UP"
|
||||||
|
or e == "UPDATE_FACTION" then
|
||||||
|
if RA.initialized then RA:MarkDirty() end
|
||||||
|
|
||||||
|
elseif e == "QUEST_TURNED_IN" then
|
||||||
|
if arg1 then RA:MarkQuestTurnedIn(arg1) 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)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
ef:SetScript("OnUpdate", function()
|
||||||
|
if not RA.initialized then return end
|
||||||
|
RA:_OnUpdate(arg1)
|
||||||
|
end)
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
-- RelationshipsAttune: Data.lua
|
||||||
|
-- Attunement definitions with icons + hover descriptions for the UI.
|
||||||
|
--
|
||||||
|
-- Sources: wowhead classic attunement guides (Onyxia, MC, BWL, Naxx, AQ40).
|
||||||
|
-- Turtle/Octo custom raids keep placeholder ids until server confirms them.
|
||||||
|
--
|
||||||
|
-- Each step:
|
||||||
|
-- { type = "level", level = 60, name = "...", icon = "..." }
|
||||||
|
-- { type = "quest", id = <qid>, name = "...", icon = "...",
|
||||||
|
-- faction = "Alliance"|"Horde"|nil, desc = "...", start = "..." }
|
||||||
|
-- { type = "item", id = <iid>, name = "...", icon = "...", desc = "...", start = "..." }
|
||||||
|
-- { type = "rep", faction = "Argent Dawn", standing = 5, name = "...",
|
||||||
|
-- desc = "...", start = "..." }
|
||||||
|
--
|
||||||
|
-- Standing: 1=Hostile ... 4=Neutral, 5=Honored, 6=Revered, 7=Exalted.
|
||||||
|
|
||||||
|
local ICON_QUEST = "Interface\\GossipFrame\\AvailableQuestIcon"
|
||||||
|
local ICON_ACTIVE = "Interface\\GossipFrame\\ActiveQuestIcon"
|
||||||
|
local ICON_KEY = "Interface\\Icons\\INV_Misc_Key_03"
|
||||||
|
local ICON_LEVEL = "Interface\\Icons\\Spell_Nature_EnchantArmor"
|
||||||
|
local ICON_REP = "Interface\\Icons\\Achievement_Reputation_01"
|
||||||
|
local ICON_ITEM = "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||||
|
|
||||||
|
RelationshipsAttune_Data = {
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- ONYXIA'S LAIR - full Drakefire Amulet chain (both factions)
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "ony", name = "Onyxia's Lair", category = "Vanilla", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Head_Dragon_01",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL,
|
||||||
|
desc = "Minimum level to start either faction's Onyxia attunement chain." },
|
||||||
|
|
||||||
|
------------------------------------------------------------------
|
||||||
|
-- ALLIANCE CHAIN (starts Burning Steppes -> Redridge -> Stormwind
|
||||||
|
-- -> BRD -> back to Stormwind -> Winterspring -> UBRS).
|
||||||
|
------------------------------------------------------------------
|
||||||
|
{ type = "quest", id = 4182, faction = "Alliance",
|
||||||
|
name = "Dragonkin Menace", icon = ICON_QUEST,
|
||||||
|
desc = "Slay black dragonkin around Burning Steppes to open the chain.",
|
||||||
|
start = "Helendis Riverhorn, Morgan's Vigil, Burning Steppes (86, 69)." },
|
||||||
|
{ type = "quest", id = 4183, faction = "Alliance",
|
||||||
|
name = "The True Masters (Helendis)", icon = ICON_QUEST,
|
||||||
|
desc = "Deliver Helendis Riverhorn's Letter to Magistrate Solomon in Lakeshire.",
|
||||||
|
start = "Helendis Riverhorn, Morgan's Vigil, Burning Steppes (86, 69)." },
|
||||||
|
{ type = "quest", id = 4184, faction = "Alliance",
|
||||||
|
name = "The True Masters (Solomon -> Bolvar)", icon = ICON_QUEST,
|
||||||
|
desc = "Take Solomon's Plea to Highlord Bolvar Fordragon in Stormwind Keep.",
|
||||||
|
start = "Magistrate Solomon, Lakeshire town hall, Redridge Mountains (30, 44)." },
|
||||||
|
{ type = "quest", id = 4185, faction = "Alliance",
|
||||||
|
name = "The True Masters (Prestor)", icon = ICON_QUEST,
|
||||||
|
desc = "Speak with Lady Katrana Prestor, then return to Bolvar.",
|
||||||
|
start = "Highlord Bolvar Fordragon, Stormwind Keep throne room (78, 18)." },
|
||||||
|
{ type = "quest", id = 4186, faction = "Alliance",
|
||||||
|
name = "The True Masters (Bolvar's Decree)", icon = ICON_QUEST,
|
||||||
|
desc = "Return Bolvar's Decree to Magistrate Solomon in Lakeshire.",
|
||||||
|
start = "Highlord Bolvar Fordragon, Stormwind Keep (78, 18)." },
|
||||||
|
{ type = "quest", id = 4223, faction = "Alliance",
|
||||||
|
name = "The True Masters (Maxwell)", icon = ICON_QUEST,
|
||||||
|
desc = "Report to Marshal Maxwell at Morgan's Vigil.",
|
||||||
|
start = "Magistrate Solomon, Lakeshire, Redridge Mountains (30, 44)." },
|
||||||
|
{ type = "quest", id = 4224, faction = "Alliance",
|
||||||
|
name = "The True Masters (Ragged John)", icon = ICON_QUEST,
|
||||||
|
desc = "Find Ragged John in a cave north of Thaurissan, then return to Maxwell.",
|
||||||
|
start = "Marshal Maxwell, Morgan's Vigil, Burning Steppes (85, 69). Ragged John: cave at (65, 24)." },
|
||||||
|
{ type = "quest", id = 4241, faction = "Alliance",
|
||||||
|
name = "Marshal Windsor", icon = ICON_QUEST,
|
||||||
|
desc = "Enter Blackrock Depths and find Marshal Windsor in his cell.",
|
||||||
|
start = "Marshal Maxwell, Morgan's Vigil, Burning Steppes (85, 69). BRD: Blackrock Mountain." },
|
||||||
|
{ type = "quest", id = 4242, faction = "Alliance",
|
||||||
|
name = "Abandoned Hope", icon = ICON_QUEST,
|
||||||
|
desc = "Return the bad news from Windsor to Marshal Maxwell.",
|
||||||
|
start = "Marshal Windsor, Windsor Cell, Blackrock Depths jail." },
|
||||||
|
{ type = "quest", id = 4264, faction = "Alliance",
|
||||||
|
name = "A Crumpled Up Note", icon = ICON_QUEST,
|
||||||
|
desc = "Random drop item from BRD slaves/mobs; return it to Windsor.",
|
||||||
|
start = "Loot A Crumpled Up Note from BRD slaves (outside instance) or trash inside." },
|
||||||
|
{ type = "quest", id = 4282, faction = "Alliance",
|
||||||
|
name = "A Shred of Hope", icon = ICON_QUEST,
|
||||||
|
desc = "Kill General Angerforge and Golem Lord Argelmach in BRD and loot Windsor's Lost Information from each.",
|
||||||
|
start = "Marshal Windsor, Windsor Cell, Blackrock Depths jail." },
|
||||||
|
{ type = "quest", id = 4322, faction = "Alliance",
|
||||||
|
name = "Jail Break!", icon = ICON_QUEST,
|
||||||
|
desc = "Escort Marshal Windsor out of BRD, then report to Marshal Maxwell.",
|
||||||
|
start = "Marshal Windsor, Windsor Cell, Blackrock Depths jail." },
|
||||||
|
{ type = "quest", id = 6402, faction = "Alliance",
|
||||||
|
name = "Stormwind Rendezvous", icon = ICON_QUEST,
|
||||||
|
desc = "Speak to Squire Rowe at the Stormwind gates to summon Reginald Windsor.",
|
||||||
|
start = "Marshal Maxwell, Morgan's Vigil, Burning Steppes (85, 69). Squire Rowe: Stormwind gates (70, 86)." },
|
||||||
|
{ type = "quest", id = 6403, faction = "Alliance",
|
||||||
|
name = "The Great Masquerade", icon = ICON_ACTIVE,
|
||||||
|
desc = "Escort Reginald Windsor to Stormwind Keep; unmask Lady Prestor as Onyxia.",
|
||||||
|
start = "Reginald Windsor, Stormwind City gates (after Rendezvous)." },
|
||||||
|
{ type = "quest", id = 6501, faction = "Alliance",
|
||||||
|
name = "The Dragon's Eye", icon = ICON_QUEST,
|
||||||
|
desc = "Take the Fragment of the Dragon's Eye to Haleh in Mazthoril, Winterspring.",
|
||||||
|
start = "Highlord Bolvar Fordragon, Stormwind Keep (78, 18). Haleh: Mazthoril cave, Winterspring (55, 51)." },
|
||||||
|
{ type = "quest", id = 6502, faction = "Alliance",
|
||||||
|
name = "Drakefire Amulet (Alliance)", icon = ICON_ACTIVE,
|
||||||
|
desc = "Kill General Drakkisath in Upper Blackrock Spire and loot Blood of the Black Dragon Champion; return to Haleh.",
|
||||||
|
start = "Haleh, Mazthoril cave, Winterspring (55, 51)." },
|
||||||
|
|
||||||
|
------------------------------------------------------------------
|
||||||
|
-- HORDE CHAIN (Kargath -> LBRS -> Orgrimmar -> UBRS -> Desolace
|
||||||
|
-- -> Western Plaguelands -> Dustwallow -> Tanaris/Winterspring/
|
||||||
|
-- Swamp of Sorrows/Wetlands -> Desolace -> UBRS).
|
||||||
|
------------------------------------------------------------------
|
||||||
|
{ type = "quest", id = 4903, faction = "Horde",
|
||||||
|
name = "Warlord's Command", icon = ICON_QUEST,
|
||||||
|
desc = "Kill Highlord Omokk, War Master Voone and Overlord Wyrmthalak in LBRS; loot Important Blackrock Documents.",
|
||||||
|
start = "Warlord Goretooth, Kargath command tent, Badlands (6, 48)." },
|
||||||
|
{ type = "quest", id = 4941, faction = "Horde",
|
||||||
|
name = "Eitrigg's Wisdom", icon = ICON_QUEST,
|
||||||
|
desc = "Speak to Eitrigg in Orgrimmar, then turn in to Thrall.",
|
||||||
|
start = "Warlord Goretooth, Kargath, Badlands (6, 48). Eitrigg/Thrall: Valley of Wisdom, Orgrimmar (32, 38)." },
|
||||||
|
{ type = "quest", id = 4974, faction = "Horde",
|
||||||
|
name = "For The Horde!", icon = ICON_QUEST,
|
||||||
|
desc = "Kill Warchief Rend Blackhand in UBRS and return his head to Thrall (grants Warchief's Blessing).",
|
||||||
|
start = "Thrall, Grommash Hold, Valley of Wisdom, Orgrimmar (32, 38)." },
|
||||||
|
{ type = "quest", id = 6566, faction = "Horde",
|
||||||
|
name = "What the Wind Carries", icon = ICON_QUEST,
|
||||||
|
desc = "Listen to Thrall's tale and turn in.",
|
||||||
|
start = "Thrall, Grommash Hold, Valley of Wisdom, Orgrimmar (32, 38)." },
|
||||||
|
{ type = "quest", id = 6567, faction = "Horde",
|
||||||
|
name = "The Champion of the Horde", icon = ICON_QUEST,
|
||||||
|
desc = "Find Rexxar patrolling between Desolace, Stonetalon and Feralas.",
|
||||||
|
start = "Thrall, Grommash Hold, Orgrimmar (32, 38). Rexxar: Desolace patrol path." },
|
||||||
|
{ type = "quest", id = 6568, faction = "Horde",
|
||||||
|
name = "The Testament of Rexxar", icon = ICON_QUEST,
|
||||||
|
desc = "Deliver Rexxar's Testament to Myranda the Hag in Western Plaguelands.",
|
||||||
|
start = "Rexxar, Desolace (roaming). Myranda: near Uther's Tomb, Western Plaguelands (51, 78)." },
|
||||||
|
{ type = "quest", id = 6569, faction = "Horde",
|
||||||
|
name = "Oculus Illusions", icon = ICON_QUEST,
|
||||||
|
desc = "Collect 20 Black Dragonspawn Eyes from UBRS dragonkin for your disguise.",
|
||||||
|
start = "Myranda the Hag, Western Plaguelands (51, 78)." },
|
||||||
|
{ type = "quest", id = 6570, faction = "Horde",
|
||||||
|
name = "Emberstrife", icon = ICON_QUEST,
|
||||||
|
desc = "Use Amulet of Draconic Subversion and speak to Emberstrife in his den, Dustwallow Marsh.",
|
||||||
|
start = "Myranda the Hag, Western Plaguelands (51, 78). Emberstrife's Den: Dustwallow Marsh (57, 87)." },
|
||||||
|
{ type = "quest", id = 6584, faction = "Horde",
|
||||||
|
name = "The Test of Skulls, Chronalis", icon = ICON_QUEST,
|
||||||
|
desc = "Kill Chronalis (Bronze) outside Caverns of Time, Tanaris.",
|
||||||
|
start = "Emberstrife, Emberstrife's Den, Dustwallow Marsh (57, 87). Chronalis: Tanaris (65, 50)." },
|
||||||
|
{ type = "quest", id = 6582, faction = "Horde",
|
||||||
|
name = "The Test of Skulls, Scryer", icon = ICON_QUEST,
|
||||||
|
desc = "Kill Scryer (Blue) inside Mazthoril cave, Winterspring.",
|
||||||
|
start = "Emberstrife, Emberstrife's Den, Dustwallow Marsh (57, 87). Scryer: Mazthoril (53, 56)." },
|
||||||
|
{ type = "quest", id = 6583, faction = "Horde",
|
||||||
|
name = "The Test of Skulls, Somnus", icon = ICON_QUEST,
|
||||||
|
desc = "Kill Somnus (Green) patrolling south-east Swamp of Sorrows.",
|
||||||
|
start = "Emberstrife, Emberstrife's Den, Dustwallow Marsh (57, 87). Somnus: Swamp of Sorrows (~80, 70)." },
|
||||||
|
{ type = "quest", id = 6585, faction = "Horde",
|
||||||
|
name = "The Test of Skulls, Axtroz", icon = ICON_QUEST,
|
||||||
|
desc = "Unlocks after the other three Skull quests. Kill Axtroz (Red) at Grim Batol, Wetlands.",
|
||||||
|
start = "Emberstrife, Emberstrife's Den, Dustwallow Marsh (57, 87). Axtroz: Wetlands (85, 49)." },
|
||||||
|
{ type = "quest", id = 6601, faction = "Horde",
|
||||||
|
name = "Ascension...", icon = ICON_QUEST,
|
||||||
|
desc = "Take the Dull Drakefire Amulet to Rexxar in Desolace (not to Drakkisath).",
|
||||||
|
start = "Emberstrife, Emberstrife's Den, Dustwallow Marsh (57, 87). Rexxar: Desolace patrol." },
|
||||||
|
{ type = "quest", id = 6602, faction = "Horde",
|
||||||
|
name = "Blood of the Black Dragon Champion", icon = ICON_ACTIVE,
|
||||||
|
desc = "Kill General Drakkisath in UBRS, loot his blood and return to Rexxar for the Drakefire Amulet.",
|
||||||
|
start = "Rexxar, Desolace patrol path." },
|
||||||
|
|
||||||
|
------------------------------------------------------------------
|
||||||
|
-- Final reward (both factions).
|
||||||
|
------------------------------------------------------------------
|
||||||
|
{ type = "item", id = 16309, name = "Drakefire Amulet",
|
||||||
|
icon = "Interface\\Icons\\INV_Jewelry_Talisman_11",
|
||||||
|
desc = "Must be in every raid member's bags (not bank) to enter Onyxia's Lair.",
|
||||||
|
start = "Awarded by Haleh (Alliance, Winterspring) or Rexxar (Horde, Desolace patrol)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- MOLTEN CORE
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "mc", name = "Molten Core", category = "Vanilla", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\Spell_Fire_SelfDestruct",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
|
||||||
|
{ type = "item", id = 17203, name = "Sulfuron Ingot (optional binder)",
|
||||||
|
icon = "Interface\\Icons\\INV_Ingot_08",
|
||||||
|
desc = "Not required for attunement itself, but drops in MC; listed for group planning.",
|
||||||
|
start = "Trash drops throughout Molten Core." },
|
||||||
|
{ type = "quest", id = 7487, name = "The Molten Core (BRD group summon)", icon = ICON_QUEST,
|
||||||
|
desc = "Group prereq that starts the Attunement chain via Lothos Riftwaker.",
|
||||||
|
start = "Lothos Riftwaker, Blackrock Mountain (top of chained platform near the MC portal)." },
|
||||||
|
{ type = "quest", id = 7848, name = "Attunement to the Core", icon = ICON_ACTIVE,
|
||||||
|
desc = "Turn in a Core Fragment (from Molten Core mobs) to Lothos Riftwaker; grants direct teleport into MC from BRD.",
|
||||||
|
start = "Lothos Riftwaker, Blackrock Mountain (BRD entrance platform)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- BLACKWING LAIR
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "bwl", name = "Blackwing Lair", category = "Vanilla", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Head_Dragon_Black",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 58, name = "Reach Level 58", icon = ICON_LEVEL },
|
||||||
|
{ type = "item", id = 12344, name = "Seal of Ascension (UBRS key prereq)", icon = ICON_KEY,
|
||||||
|
desc = "You must be able to enter UBRS to reach Vaelan; requires the Seal of Ascension chain.",
|
||||||
|
start = "Chain starts with Kibler at Flame Crest, Burning Steppes (62, 24), and Jorgen at Morgan's Vigil (85, 68)." },
|
||||||
|
{ type = "quest", id = 7761, name = "Blackhand's Command", icon = ICON_ACTIVE,
|
||||||
|
desc = "Read the scroll dropped by Vaelan behind the hidden door in UBRS, then use the orb at the top of UBRS to enter BWL.",
|
||||||
|
start = "Vaelan, hidden alcove past the UBRS orb room, Upper Blackrock Spire." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- NAXXRAMAS
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "naxx", name = "Naxxramas", category = "Vanilla", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\Spell_Shadow_DeathPact",
|
||||||
|
steps = {
|
||||||
|
{ 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).",
|
||||||
|
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,
|
||||||
|
desc = "Pay 60g to Angela Dosantos - available at Honored with Argent Dawn.",
|
||||||
|
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,
|
||||||
|
desc = "Discounted Naxx attunement offered at Revered with Argent Dawn.",
|
||||||
|
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,
|
||||||
|
desc = "Cheapest Naxx attunement, offered only at Exalted with Argent Dawn.",
|
||||||
|
start = "Archmage Angela Dosantos, Light's Hope Chapel, Eastern Plaguelands (76, 53)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- AQ20 - Ruins of Ahn'Qiraj
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "aq20", name = "Ruins of Ahn'Qiraj (20)", category = "Vanilla", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_QirajiCrystal_02",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 60, name = "Reach Level 60", icon = ICON_LEVEL },
|
||||||
|
{ type = "rep", faction = "Cenarion Circle", standing = 4,
|
||||||
|
name = "Neutral with Cenarion Circle", icon = ICON_REP,
|
||||||
|
desc = "Automatic from Silithus mobs after the war effort opens; no separate attunement item.",
|
||||||
|
start = "Windcaller Yessendra / Proudhorn, Cenarion Hold, Silithus (50, 33)." },
|
||||||
|
{ type = "quest", id = 8743, name = "The Gates of Ahn'Qiraj (server-wide event)", icon = ICON_QUEST,
|
||||||
|
desc = "The AQ gates must have been opened server-side. Once open, anyone can walk in.",
|
||||||
|
start = "Anachronos, Caverns of Time entrance, Tanaris (65, 50)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- AQ40 - Temple of Ahn'Qiraj (Scepter of the Shifting Sands)
|
||||||
|
-- Chain sourced from CixiDelmont's Attune addon (AttuneData id=10).
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "aq40", name = "Temple of Ahn'Qiraj (40)", category = "Vanilla", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_QirajiCrystal_05",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 60, name = "Reach Level 60", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 8286, name = "What Tomorrow Brings", icon = ICON_QUEST,
|
||||||
|
desc = "Speak with Anachronos at the Caverns of Time to begin the Scepter chain.",
|
||||||
|
start = "Anachronos, Caverns of Time entrance, Tanaris (65, 50). Talk to him at the Gates of Ahn'Qiraj first (Silithus)." },
|
||||||
|
{ type = "quest", id = 8288, name = "Only One May Rise", icon = ICON_QUEST,
|
||||||
|
desc = "Kill Vaelastrasz in Blackwing Lair and loot the Vessel of Rebirth.",
|
||||||
|
start = "Anachronos, Silithus / Caverns of Time." },
|
||||||
|
{ type = "quest", id = 8301, name = "The Path of the Righteous", icon = ICON_QUEST,
|
||||||
|
desc = "Continue Anachronos' story after handing in the Vessel.",
|
||||||
|
start = "Anachronos, Caverns of Time entrance, Tanaris (65, 50)." },
|
||||||
|
{ type = "rep", faction = "Brood of Nozdormu", standing = 4,
|
||||||
|
name = "Neutral with Brood of Nozdormu", icon = ICON_REP,
|
||||||
|
desc = "Kill AQ40 trash / bosses to raise Nozdormu rep from Hated to Neutral; required before Anachronos will continue.",
|
||||||
|
start = "Kill Qiraji inside AQ40; automatic once AQ40 is open." },
|
||||||
|
{ type = "quest", id = 8303, name = "Chamber of the Aspects", icon = ICON_QUEST,
|
||||||
|
desc = "Anachronos sends you to speak with the four dragon Aspect representatives.",
|
||||||
|
start = "Anachronos, Caverns of Time entrance, Tanaris (65, 50)." },
|
||||||
|
{ type = "quest", id = 8305, name = "Anachronos", icon = ICON_QUEST,
|
||||||
|
desc = "Return to Anachronos with the Aspects' answers.",
|
||||||
|
start = "Anachronos, Caverns of Time entrance, Tanaris (65, 50)." },
|
||||||
|
{ type = "quest", id = 8519, name = "The Might of Kalimdor", icon = ICON_QUEST,
|
||||||
|
desc = "Anachronos gives you the Broken Scepter of the Shifting Sands and directs you to the Aspects.",
|
||||||
|
start = "Anachronos, Caverns of Time entrance, Tanaris (65, 50)." },
|
||||||
|
{ type = "quest", id = 8555, name = "The Nightmare's Corruption (Red)", icon = ICON_QUEST,
|
||||||
|
desc = "Kill Vaelastrasz (BWL), loot Vaelastrasz's Tooth for Caelestrasz.",
|
||||||
|
start = "Caelestrasz, Anachronos' platform, Caverns of Time." },
|
||||||
|
{ type = "quest", id = 8733, name = "Beyond the Grave (Green)", icon = ICON_QUEST,
|
||||||
|
desc = "Investigate the pool inside The Temple of Atal'Hakkar for Merithra.",
|
||||||
|
start = "Merithra of the Dream, Caverns of Time." },
|
||||||
|
{ type = "quest", id = 8734, name = "Draconic for Dummies (Blue)", icon = ICON_QUEST,
|
||||||
|
desc = "Arygos sends you to research Nightmare Scale fragments across Azeroth.",
|
||||||
|
start = "Arygos, Caverns of Time." },
|
||||||
|
{ type = "quest", id = 8735, name = "Stones of Binding (Bronze)", icon = ICON_QUEST,
|
||||||
|
desc = "Retrieve five Nightmare Engravings from world dragons (Ysondre, Emeriss, Lethon, Taerar and Azuregos) for Kalecgos.",
|
||||||
|
start = "Kalecgos, Caverns of Time." },
|
||||||
|
{ type = "quest", id = 8736, name = "Preparations", icon = ICON_QUEST,
|
||||||
|
desc = "Return to Anachronos with the Nightmare Scale.",
|
||||||
|
start = "Anachronos, Caverns of Time." },
|
||||||
|
{ type = "quest", id = 8741, name = "The Charge of the Dragonflights", icon = ICON_QUEST,
|
||||||
|
desc = "The Aspects reforge the scepter. Deliver the Fragment of the Nightmare's Corruption.",
|
||||||
|
start = "Anachronos, Caverns of Time." },
|
||||||
|
{ type = "quest", id = 8742, name = "The Hand of the Righteous", icon = ICON_QUEST,
|
||||||
|
desc = "Final scepter reforging quest. Rewards Scepter of the Shifting Sands.",
|
||||||
|
start = "Anachronos, Caverns of Time." },
|
||||||
|
{ type = "item", id = 21175, name = "Scepter of the Shifting Sands",
|
||||||
|
icon = "Interface\\Icons\\INV_Hammer_25",
|
||||||
|
desc = "Use this item at the Scarab Gong outside AQ to trigger the 10-hour war and open the gates server-wide.",
|
||||||
|
start = "Reward from 'The Hand of the Righteous'." },
|
||||||
|
{ type = "quest", id = 8743, name = "Bang a Gong!", icon = ICON_ACTIVE,
|
||||||
|
desc = "Ring the Scarab Gong at the Gates of Ahn'Qiraj to open both AQ raids server-wide. First person to ring gets Scarab Lord title + mount.",
|
||||||
|
start = "Scarab Gong, Gates of Ahn'Qiraj, Silithus." },
|
||||||
|
{ type = "item", id = 21176, name = "Black Qiraji Resonating Crystal (Scarab Lord mount)",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_QirajiCrystal_04",
|
||||||
|
desc = "Awarded only to the player who rings the gong within the 10-hour war window.",
|
||||||
|
start = "Reward for completing 'Bang a Gong!' during the war event." },
|
||||||
|
{ type = "quest", id = 8745, name = "Ouro's Message (post-war attunement)", icon = ICON_QUEST,
|
||||||
|
desc = "Anyone can walk into AQ40 once the gates are open; no personal attunement item required after the event.",
|
||||||
|
start = "Anachronos - concludes the Scepter chain." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- VANILLA KEYS & DUNGEON ACCESS
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "ubrs", name = "Upper Blackrock Spire", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Key_04",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 4903, name = "Kibler's Exotic Pets (chain start)", icon = ICON_QUEST,
|
||||||
|
desc = "Opens Jorgen's Seal of Ascension chain.",
|
||||||
|
start = "Kibler, Flame Crest, Burning Steppes (62, 24)." },
|
||||||
|
{ type = "quest", id = 4743, name = "Seal of Ascension", icon = ICON_QUEST,
|
||||||
|
desc = "Requires the Unadorned Seal of Ascension (drops in LBRS) plus three gemstones from LBRS bosses.",
|
||||||
|
start = "Jorgen, Morgan's Vigil, Burning Steppes (85, 68)." },
|
||||||
|
{ type = "item", id = 12344, name = "Seal of Ascension", icon = ICON_KEY,
|
||||||
|
desc = "Opens the door to Upper Blackrock Spire at the top of the LBRS instance.",
|
||||||
|
start = "Awarded by Jorgen at Morgan's Vigil after completing the Seal of Ascension chain." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "brd", name = "Blackrock Depths", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Key_06",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 47, name = "Reach Level 47", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 4136, name = "Dark Iron Legacy", icon = ICON_QUEST,
|
||||||
|
desc = "Rewards the Shadowforge Key for opening the deeper doors in BRD.",
|
||||||
|
start = "Franclorn Forgewright's ghost, inside Blackrock Depths (Detention Block hallway near his statue)." },
|
||||||
|
{ type = "item", id = 11000, name = "Shadowforge Key", icon = ICON_KEY,
|
||||||
|
desc = "Opens Shadowforge City / Manufactory doors deep in BRD.",
|
||||||
|
start = "Reward from 'Dark Iron Legacy'." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "scholo", name = "Scholomance", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Key_11",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 5382, name = "The Key to Scholomance", icon = ICON_QUEST,
|
||||||
|
desc = "Requires Blood of Innocents (Scarlet Monastery), Viewing Room Key (Scholomance) and Deed to Caer Darrow.",
|
||||||
|
start = "Alliance: Alexi Barov, Hearthglen, Western Plaguelands (44, 21). Horde: Alexi Barov, Ruins of Andorhal (42, 68)." },
|
||||||
|
{ type = "item", id = 13704, name = "Skeleton Key", icon = ICON_KEY,
|
||||||
|
desc = "Opens the front door of Scholomance in Caer Darrow.",
|
||||||
|
start = "Reward from 'The Key to Scholomance'." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "strat_live", name = "Stratholme (Service Entrance)", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Key_07",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
|
||||||
|
{ type = "item", id = 12382, name = "Key to the City", icon = ICON_KEY,
|
||||||
|
desc = "Opens the Service Entrance shortcut into Stratholme Live.",
|
||||||
|
start = "Drops from Magistrate Barthilas, patrolling the main road of Stratholme Live." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "dm", name = "Dire Maul", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Key_09",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
|
||||||
|
{ type = "item", id = 18249, name = "Crescent Key", icon = ICON_KEY,
|
||||||
|
desc = "Opens locked doors and cages throughout Dire Maul (all three wings).",
|
||||||
|
start = "Pickpocket from Kobold Geomancers inside Dire Maul West (Rogue only), lockpick at 300, or buy from the AH." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "gnomer", name = "Gnomeregan (Workshop Shortcut)", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Key_08",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 24, name = "Reach Level 24", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 2904, name = "Grime-Encrusted Ring", icon = ICON_QUEST,
|
||||||
|
desc = "Drop-quest item off Gnomeregan mobs; leads into 'Return to Marvon'.",
|
||||||
|
start = "Loot Grime-Encrusted Ring inside Gnomeregan (Dun Morogh 25, 40)." },
|
||||||
|
{ type = "quest", id = 2905, name = "Return to Marvon", icon = ICON_QUEST,
|
||||||
|
desc = "Rewards the Workshop Key for skipping Grubbis.",
|
||||||
|
start = "Marvon Rivetseeker, Steamwheedle Port camp, Tanaris (66, 22)." },
|
||||||
|
{ type = "item", id = 9240, name = "Workshop Key", icon = ICON_KEY,
|
||||||
|
desc = "Opens the shortcut door in Gnomeregan bypassing the Grubbis wing.",
|
||||||
|
start = "Reward from 'Return to Marvon'." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "mara", name = "Maraudon (Portal Shortcut)", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Idol_02",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 40, name = "Reach Level 40", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 7064, name = "Legends of Maraudon", icon = ICON_QUEST,
|
||||||
|
desc = "Prereq for the Scepter of Celebras.",
|
||||||
|
start = "Celebras the Redeemed, freed on the Orange side of Maraudon after the vine tunnel." },
|
||||||
|
{ type = "quest", id = 7065, name = "The Scepter of Celebras", icon = ICON_QUEST,
|
||||||
|
desc = "Rewards Scepter of Celebras (teleport into inner Maraudon).",
|
||||||
|
start = "Celebras the Redeemed, Maraudon (Orange side)." },
|
||||||
|
{ type = "item", id = 17191, name = "Scepter of Celebras", icon = ICON_KEY,
|
||||||
|
desc = "Teleports directly to the Princess wing of Maraudon.",
|
||||||
|
start = "Reward from 'The Scepter of Celebras'." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "st", name = "Sunken Temple (Jammal'an)", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Idol_03",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 50, name = "Reach Level 50", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 3373, name = "The Ancient Egg / Jammal'an the Prophet", icon = ICON_QUEST,
|
||||||
|
desc = "Activates the six-statue puzzle so Jammal'an can be summoned.",
|
||||||
|
start = "Yeh'kinya, Steamwheedle Port, Tanaris (66, 22). Sunken Temple: Swamp of Sorrows (69, 53)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
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." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "sm_cath", name = "Scarlet Monastery (Cathedral)", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Key_02",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 35, name = "Reach Level 35", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 1050, name = "Clear the Scarlet Monastery Library", icon = ICON_QUEST,
|
||||||
|
desc = "Clear the Library wing and loot the small chest behind the final boss to obtain the Scarlet Key.",
|
||||||
|
start = "Scarlet Monastery Library wing entrance, Tirisfal Glades (85, 32)." },
|
||||||
|
{ type = "item", id = 7146, name = "Scarlet Key", icon = ICON_KEY,
|
||||||
|
desc = "Opens the front doors to the Scarlet Monastery Cathedral wing.",
|
||||||
|
start = "Found in a small loot chest behind Arcanist Doan at the end of the Library wing (not a boss drop)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "brd_prison", name = "BRD Prison Cell Key", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Key_05",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 52, name = "Reach Level 52", icon = ICON_LEVEL },
|
||||||
|
{ type = "item", id = 11266, name = "Prison Cell Key", icon = ICON_KEY,
|
||||||
|
desc = "Unlocks the Detention Block cells in Blackrock Depths (frees Marshal Windsor, Dughal, Kharan, etc).",
|
||||||
|
start = "Drops from Warder Stilgiss in the Detention Block, Blackrock Depths." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "sgorge", name = "Searing Gorge Access", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Gem_Ruby_02",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 45, name = "Reach Level 45", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 3441, faction = "Alliance",
|
||||||
|
name = "A Bindings Blockade (Alliance)", icon = ICON_QUEST,
|
||||||
|
desc = "Opens the Thorium Brotherhood gate from Loch Modan into Searing Gorge.",
|
||||||
|
start = "Prospector Stormpike, The Military Ward, Ironforge." },
|
||||||
|
{ type = "quest", id = 3701, faction = "Horde",
|
||||||
|
name = "The Flame's Casing (Horde)", icon = ICON_QUEST,
|
||||||
|
desc = "Rewards Horde access through the Thorium Brotherhood gate into Searing Gorge.",
|
||||||
|
start = "Krakle, Kargath, Badlands (3, 45)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "uldaman", name = "Uldaman (Discs of Norgannon)", category = "Keys & Access", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Gem_Diamond_01",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 40, name = "Reach Level 40", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 2278, name = "The Lost Tablets of Will", icon = ICON_QUEST,
|
||||||
|
desc = "Prereq to activating the Discs of Norgannon so Archaedas can be reached.",
|
||||||
|
start = "Alliance: Advisor Belgrum, Ironforge. Horde: Rigglefuzz, Hillsbrad Foothills." },
|
||||||
|
{ type = "quest", id = 2418, name = "The Platinum Discs", icon = ICON_QUEST,
|
||||||
|
desc = "Interact with the Discs of Norgannon inside the Chamber of Khaz'mul to spawn Archaedas.",
|
||||||
|
start = "Uldaman, Badlands (42, 20)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- TURTLE / OCTO WOW CUSTOM CONTENT
|
||||||
|
-- Placeholder ids; replace once your server confirms them.
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "kara10", name = "Karazhan Crypt (10)", category = "Turtle", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Gem_Sapphire_02",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 55, name = "Reach Level 55", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 40001, name = "TODO: Crypt attunement quest", icon = ICON_QUEST,
|
||||||
|
desc = "Custom Turtle attunement chain for Karazhan Crypt (10-man).",
|
||||||
|
start = "Koren the Seeker, Morgan's Plot camp, Deadwind Pass (40, 75)." },
|
||||||
|
{ type = "item", id = 60001, name = "TODO: Crypt Key", icon = ICON_KEY,
|
||||||
|
desc = "Key required to enter the crypt beneath Karazhan.",
|
||||||
|
start = "Awarded from Koren the Seeker after completing the Karazhan Crypt attunement chain." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id = "kara40", name = "Lower Karazhan Halls (40)", category = "Turtle", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Gem_Sapphire_01",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 60, name = "Reach Level 60", icon = ICON_LEVEL },
|
||||||
|
{ type = "quest", id = 40010, name = "TODO: LKH attunement", icon = ICON_QUEST,
|
||||||
|
desc = "Custom Turtle attunement chain for Lower Karazhan Halls (40-man).",
|
||||||
|
start = "Archmage Alturus, Morgan's Plot camp, Deadwind Pass (40, 75)." },
|
||||||
|
{ type = "item", id = 60010, name = "TODO: LKH Key", icon = ICON_KEY,
|
||||||
|
desc = "40-man Karazhan key.",
|
||||||
|
start = "Awarded from Archmage Alturus 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 = "TODO: ES attunement", icon = ICON_QUEST,
|
||||||
|
desc = "Emerald Sanctum attunement chain (Hinterlands portal).",
|
||||||
|
start = "Keeper Remulos avatar, eastern Hinterlands near Seradane (55, 22)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
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 = "TODO: Gilneas attunement", icon = ICON_QUEST,
|
||||||
|
desc = "Attunement for the Gilneas City dungeon.",
|
||||||
|
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 = "TODO: Hateforge attunement", icon = ICON_QUEST,
|
||||||
|
desc = "Attunement for the Hateforge Quarry dungeon.",
|
||||||
|
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 = "TODO: SW Vault attunement", icon = ICON_QUEST,
|
||||||
|
desc = "Attunement for the Stormwind Vault dungeon.",
|
||||||
|
start = "Warden Thelwater, Stormwind Stockade entrance, Mage Quarter, Stormwind City (50, 68)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
-- DRAGONMAW RETREAT (Turtle/Octo custom, Wetlands, patch 1.18)
|
||||||
|
-- Key: "Lower Reserve Key". The scanner also identifies keyring
|
||||||
|
-- items by tooltip name, so this works even if the live client
|
||||||
|
-- assigns a different numeric id than the placeholder below.
|
||||||
|
----------------------------------------------------------------
|
||||||
|
{
|
||||||
|
id = "dmr", name = "Dragonmaw Retreat", category = "Turtle", faction = "Both",
|
||||||
|
icon = "Interface\\Icons\\INV_Misc_Head_Dragon_Bronze",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 25, name = "Reach Level 25", icon = ICON_LEVEL,
|
||||||
|
desc = "Minimum level to enter Dragonmaw Retreat." },
|
||||||
|
|
||||||
|
{ type = "quest", id = 41810, faction = "Alliance",
|
||||||
|
name = "To Crush The Dragonmaw", icon = ICON_QUEST,
|
||||||
|
desc = "Alliance intro quest for Dragonmaw Retreat.",
|
||||||
|
start = "Captain Stoutfist, Menethil Harbor, Wetlands." },
|
||||||
|
{ type = "quest", id = 41811, faction = "Alliance",
|
||||||
|
name = "Blackheart's Demise", icon = ICON_QUEST,
|
||||||
|
desc = "Alliance follow-up to slay Overlord Blackheart inside the dungeon.",
|
||||||
|
start = "Captain Stoutfist, Menethil Harbor, Wetlands." },
|
||||||
|
|
||||||
|
{ type = "quest", id = 41820, faction = "Horde",
|
||||||
|
name = "Cavernweb Extract", icon = ICON_QUEST,
|
||||||
|
desc = "Horde intro quest — collect spider extract inside Dragonmaw Retreat.",
|
||||||
|
start = "Okul, Hammerfall, Arathi Highlands." },
|
||||||
|
{ type = "quest", id = 41821, faction = "Horde",
|
||||||
|
name = "A Blaze Unending", icon = ICON_QUEST,
|
||||||
|
desc = "Horde follow-up in Dragonmaw Retreat.",
|
||||||
|
start = "Shara Blazen, Tarren Mill, Hillsbrad Foothills." },
|
||||||
|
|
||||||
|
{ type = "quest", id = 41830, name = "Gowlfang's Defeat", icon = ICON_QUEST,
|
||||||
|
desc = "Neutral quest — defeat the gnoll leader Gowlfang.",
|
||||||
|
start = "Grimbite, Green Belt gnoll camp, central Wetlands." },
|
||||||
|
{ type = "quest", id = 41831, name = "The Dragonmaw Brood", icon = ICON_QUEST,
|
||||||
|
desc = "Neutral quest — cull the drakes and whelps inside the retreat.",
|
||||||
|
start = "Nidiszanz, Dragonmaw Gates, Wetlands." },
|
||||||
|
|
||||||
|
{ type = "item", id = 61810, name = "Lower Reserve Key",
|
||||||
|
icon = ICON_KEY, final = true,
|
||||||
|
desc = "Opens the gates of Dragonmaw Retreat / Dragonmaw Reserve in the Wetlands.",
|
||||||
|
start = "Looted from Dragonmaw guards in the Dragonmaw Reserve outpost, Wetlands." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
-- RelationshipsAttune: Locales.lua
|
||||||
|
-- Simple English string table. Add more locales by extending RelationshipsAttune_L.
|
||||||
|
|
||||||
|
RelationshipsAttune_L = {
|
||||||
|
ADDON_NAME = "Relationships Attune",
|
||||||
|
SHORT_NAME = "RelationshipsAttune",
|
||||||
|
SLASH_HELP = "Relationships Attune commands:",
|
||||||
|
SLASH_SHOW = "/rattune show - show the window",
|
||||||
|
SLASH_HIDE = "/rattune hide - hide the window",
|
||||||
|
SLASH_TOGGLE = "/rattune - toggle the window",
|
||||||
|
SLASH_SYNC = "/rattune sync - request attunement data from your group",
|
||||||
|
SLASH_SCAN = "/rattune scan - rescan your own quest log and bags",
|
||||||
|
SLASH_RESET = "/rattune reset - wipe stored data for this character",
|
||||||
|
|
||||||
|
MSG_LOADED = "loaded. Type /rattune for options.",
|
||||||
|
MSG_SCANNED = "Rescanned your attunements.",
|
||||||
|
MSG_SYNC_SENT = "Requested attunement data from group.",
|
||||||
|
MSG_RESET = "Character data reset.",
|
||||||
|
|
||||||
|
UI_TITLE = "Relationships Attune",
|
||||||
|
UI_PLAYERS = "Players",
|
||||||
|
UI_STEPS = "Steps",
|
||||||
|
UI_COMPLETE = "Complete",
|
||||||
|
UI_IN_PROGRESS = "In Progress",
|
||||||
|
UI_NOT_STARTED = "Not Started",
|
||||||
|
UI_UNKNOWN = "Unknown",
|
||||||
|
UI_YOU = "You",
|
||||||
|
|
||||||
|
STEP_QUEST = "Quest",
|
||||||
|
STEP_ITEM = "Item",
|
||||||
|
STEP_REP = "Reputation",
|
||||||
|
STEP_LEVEL = "Level",
|
||||||
|
}
|
||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
-- RelationshipsAttune: Minimap.lua
|
||||||
|
-- A standalone, freely-movable, hideable button (parented to UIParent so it
|
||||||
|
-- can be dragged anywhere on the screen, not just around the minimap ring).
|
||||||
|
-- Pure Lua 5.0 / WoW 1.12 safe (Turtle/Octo compatible).
|
||||||
|
--
|
||||||
|
-- Options are stored in RelationshipsAttuneDB.options:
|
||||||
|
-- minimapX : number (x offset from UIParent CENTER, in UI pixels)
|
||||||
|
-- minimapY : number (y offset from UIParent CENTER, in UI pixels)
|
||||||
|
-- minimapHide : boolean (true = hidden)
|
||||||
|
-- Legacy angle/radius fields are migrated to X/Y on first load.
|
||||||
|
|
||||||
|
RelationshipsAttune = RelationshipsAttune or {}
|
||||||
|
local RA = RelationshipsAttune
|
||||||
|
RA.Minimap = {}
|
||||||
|
local MM = RA.Minimap
|
||||||
|
|
||||||
|
local BTN_NAME = "RelationshipsAttuneMinimapButton"
|
||||||
|
local ICON = "Interface\\Icons\\INV_Misc_Book_09"
|
||||||
|
|
||||||
|
local function opts()
|
||||||
|
if not RelationshipsAttuneDB then RelationshipsAttuneDB = {} end
|
||||||
|
if not RelationshipsAttuneDB.options then RelationshipsAttuneDB.options = {} end
|
||||||
|
local o = RelationshipsAttuneDB.options
|
||||||
|
|
||||||
|
-- Migrate legacy angle/radius (around the minimap) into free X/Y.
|
||||||
|
if type(o.minimapX) ~= "number" or type(o.minimapY) ~= "number" then
|
||||||
|
local ang = (type(o.minimapAngle) == "number") and o.minimapAngle or 200
|
||||||
|
local rad = (type(o.minimapRadius) == "number") and o.minimapRadius or 80
|
||||||
|
local r = ang * math.pi / 180
|
||||||
|
local mx, my = 0, 0
|
||||||
|
if Minimap and Minimap.GetCenter then
|
||||||
|
local cx, cy = Minimap:GetCenter()
|
||||||
|
if cx then mx, my = cx, cy end
|
||||||
|
end
|
||||||
|
local ux, uy = 0, 0
|
||||||
|
if UIParent and UIParent.GetCenter then
|
||||||
|
local ucx, ucy = UIParent:GetCenter()
|
||||||
|
if ucx then ux, uy = ucx, ucy end
|
||||||
|
end
|
||||||
|
o.minimapX = (mx - ux) + math.cos(r) * rad
|
||||||
|
o.minimapY = (my - uy) + math.sin(r) * rad
|
||||||
|
end
|
||||||
|
if o.minimapHide == nil then o.minimapHide = false end
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyPosition(btn)
|
||||||
|
local o = opts()
|
||||||
|
btn:ClearAllPoints()
|
||||||
|
btn:SetPoint("CENTER", UIParent, "CENTER", o.minimapX, o.minimapY)
|
||||||
|
end
|
||||||
|
MM._updatePosition = applyPosition
|
||||||
|
|
||||||
|
local function onDragUpdate()
|
||||||
|
local mx, my = GetCursorPosition()
|
||||||
|
local scale = UIParent:GetEffectiveScale() or 1
|
||||||
|
mx = mx / scale; my = my / scale
|
||||||
|
local ucx, ucy = UIParent:GetCenter()
|
||||||
|
if not ucx then return end
|
||||||
|
local o = opts()
|
||||||
|
o.minimapX = mx - ucx
|
||||||
|
o.minimapY = my - ucy
|
||||||
|
applyPosition(this)
|
||||||
|
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)
|
||||||
|
btn:SetWidth(31); btn:SetHeight(31)
|
||||||
|
btn:SetFrameStrata("MEDIUM")
|
||||||
|
btn:SetFrameLevel(8)
|
||||||
|
btn:EnableMouse(true)
|
||||||
|
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||||
|
btn:RegisterForDrag("LeftButton", "RightButton")
|
||||||
|
btn:SetMovable(true)
|
||||||
|
|
||||||
|
-- Round border ring (standard minimap-button look)
|
||||||
|
local overlay = btn:CreateTexture(nil, "OVERLAY")
|
||||||
|
overlay:SetWidth(53); overlay:SetHeight(53)
|
||||||
|
overlay:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
|
||||||
|
overlay:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
|
||||||
|
|
||||||
|
local icon = btn:CreateTexture(nil, "BACKGROUND")
|
||||||
|
icon:SetWidth(20); icon:SetHeight(20)
|
||||||
|
icon:SetTexture(ICON)
|
||||||
|
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
||||||
|
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)
|
||||||
|
|
||||||
|
btn:SetScript("OnDragStart", function()
|
||||||
|
this:LockHighlight()
|
||||||
|
this:SetScript("OnUpdate", onDragUpdate)
|
||||||
|
end)
|
||||||
|
btn:SetScript("OnDragStop", function()
|
||||||
|
this:UnlockHighlight()
|
||||||
|
this:SetScript("OnUpdate", nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
btn:SetScript("OnEnter", function()
|
||||||
|
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
|
||||||
|
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 hide - hide this button", 0.6, 0.8, 1)
|
||||||
|
GameTooltip:Show()
|
||||||
|
end)
|
||||||
|
btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||||
|
|
||||||
|
applyPosition(btn)
|
||||||
|
self.btn = btn
|
||||||
|
return btn
|
||||||
|
end
|
||||||
|
|
||||||
|
function MM:Show()
|
||||||
|
local o = opts()
|
||||||
|
o.minimapHide = false
|
||||||
|
local btn = self:Create()
|
||||||
|
btn:Show()
|
||||||
|
end
|
||||||
|
|
||||||
|
function MM:Hide()
|
||||||
|
local o = opts()
|
||||||
|
o.minimapHide = true
|
||||||
|
if self.btn then self.btn:Hide() end
|
||||||
|
end
|
||||||
|
|
||||||
|
function MM:Toggle()
|
||||||
|
local o = opts()
|
||||||
|
if o.minimapHide then self:Show() else self:Hide() end
|
||||||
|
end
|
||||||
|
|
||||||
|
function MM:Refresh()
|
||||||
|
local o = opts()
|
||||||
|
local btn = self:Create()
|
||||||
|
if o.minimapHide then btn:Hide() else btn:Show() end
|
||||||
|
applyPosition(btn)
|
||||||
|
end
|
||||||
|
|
||||||
|
function MM:Init()
|
||||||
|
self:Refresh()
|
||||||
|
end
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Relationships Attune
|
||||||
|
|
||||||
|
A Vanilla-only attunement tracker for **Turtle WoW / Octo WoW** (client 1.12,
|
||||||
|
Lua 5.0). Same shape as the classic *Attune* addon — per-character progress,
|
||||||
|
step lists, group sync — trimmed to Vanilla content plus Turtle/Octo custom
|
||||||
|
raids.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Copy the folder `RelationshipsAttune/` into:
|
||||||
|
|
||||||
|
```
|
||||||
|
<WoW>/Interface/AddOns/RelationshipsAttune/
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure the folder contains `RelationshipsAttune.toc` at its root.
|
||||||
|
Restart the client (or `/reload`).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
/rattune toggle the window
|
||||||
|
/rattune show show
|
||||||
|
/rattune hide hide
|
||||||
|
/rattune scan rescan quests + bags
|
||||||
|
/rattune sync request attunement data from your party/raid
|
||||||
|
/rattune reset wipe this character's stored data
|
||||||
|
```
|
||||||
|
|
||||||
|
## What it tracks
|
||||||
|
|
||||||
|
**Vanilla:** Onyxia, Molten Core, Blackwing Lair, Naxxramas, UBRS key,
|
||||||
|
Scholomance key, AQ40 (level gate).
|
||||||
|
|
||||||
|
**Turtle / Octo custom:** Karazhan Crypt (10), Lower Karazhan Halls (40),
|
||||||
|
Emerald Sanctum, Gilneas, Hateforge Quarry, Stormwind Vault.
|
||||||
|
|
||||||
|
The custom entries currently use placeholder quest/item IDs marked with
|
||||||
|
`TODO:` in `Data.lua`. Replace those numbers with the real IDs from your
|
||||||
|
server and the tracker will light up automatically.
|
||||||
|
|
||||||
|
## Adding or fixing entries
|
||||||
|
|
||||||
|
Open `Data.lua` and edit the `RelationshipsAttune_Data` table. Each
|
||||||
|
attunement is:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
id = "kara10", name = "Karazhan Crypt (10)",
|
||||||
|
category = "Turtle", faction = "Both",
|
||||||
|
steps = {
|
||||||
|
{ type = "level", level = 55, name = "Level 55" },
|
||||||
|
{ type = "quest", id = 40001, name = "Crypt attunement" },
|
||||||
|
{ type = "item", id = 60001, name = "Crypt Key" },
|
||||||
|
{ type = "rep", faction = "Argent Dawn", standing = 5, name = "Honored" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Step types: `level`, `quest`, `item`, `rep`. Standing uses
|
||||||
|
`1=Hostile ... 4=Neutral, 5=Honored, 6=Revered, 7=Exalted`.
|
||||||
|
|
||||||
|
## Group sync
|
||||||
|
|
||||||
|
When you're in a party or raid the addon sends a compact per-attunement
|
||||||
|
summary over the addon channel (`RATTUNE` prefix). Click **Sync** in the
|
||||||
|
window or run `/rattune sync` to force an exchange. Group members without
|
||||||
|
the addon are simply ignored.
|
||||||
|
|
||||||
|
## Notes on the 1.12 client
|
||||||
|
|
||||||
|
- Uses only APIs available in 1.12 (`CreateFrame`, `SendAddonMessage`,
|
||||||
|
`GetContainerItemLink`, `GetQuestLogTitle`, `GetFactionInfo`).
|
||||||
|
- Registers `QUEST_TURNED_IN` defensively — Turtle backports it, real
|
||||||
|
1.12 doesn't; without it the addon falls back to scanning the quest
|
||||||
|
log's Complete state and remembering completions in saved variables.
|
||||||
|
- No external libraries required.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
RelationshipsAttune.toc
|
||||||
|
Locales.lua
|
||||||
|
Data.lua
|
||||||
|
Core.lua
|
||||||
|
Comm.lua
|
||||||
|
UI.lua
|
||||||
|
README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Author: RelationshipsAttune · Version 1.0.0
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## Interface: 11200
|
||||||
|
## Title: Relationships Attune
|
||||||
|
## Notes: Track Vanilla and Turtle/Octo WoW attunement progress
|
||||||
|
## Author: Relationship
|
||||||
|
## Version: 1.0
|
||||||
|
## X-Category: Quest
|
||||||
|
## SavedVariables: RelationshipsAttuneDB
|
||||||
|
## SavedVariablesPerCharacter: RelationshipsAttuneCharDB
|
||||||
|
|
||||||
|
Locales.lua
|
||||||
|
Data.lua
|
||||||
|
Core.lua
|
||||||
|
Comm.lua
|
||||||
|
Minimap.lua
|
||||||
|
UI.lua
|
||||||
@@ -0,0 +1,711 @@
|
|||||||
|
-- RelationshipsAttune: UI.lua
|
||||||
|
-- Left raid list is a FauxScrollFrame so it never overflows the box.
|
||||||
|
-- Step rows on the right have hover tooltips (name / desc / where to start),
|
||||||
|
-- are clickable to expand extra info, and highlight green when complete.
|
||||||
|
-- Pure Lua, no XML, 1.12 / Lua 5.0 compatible (Turtle/Octo).
|
||||||
|
|
||||||
|
RelationshipsAttune = RelationshipsAttune or {}
|
||||||
|
local RA = RelationshipsAttune
|
||||||
|
local L = RelationshipsAttune_L
|
||||||
|
RA.UI = {}
|
||||||
|
local UI = RA.UI
|
||||||
|
|
||||||
|
-- Textures
|
||||||
|
local TEX_WHITE = "Interface\\Buttons\\WHITE8X8"
|
||||||
|
local TEX_CHECK = "Interface\\RaidFrame\\ReadyCheck-Ready"
|
||||||
|
local TEX_CROSS = "Interface\\RaidFrame\\ReadyCheck-NotReady"
|
||||||
|
local TEX_WAIT = "Interface\\RaidFrame\\ReadyCheck-Waiting"
|
||||||
|
local TEX_QMARK = "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||||
|
|
||||||
|
-- Colors
|
||||||
|
local C_DONE = { 0.20, 0.90, 0.30 }
|
||||||
|
local C_PARTIAL = { 0.95, 0.80, 0.20 }
|
||||||
|
local C_NONE = { 0.90, 0.30, 0.30 }
|
||||||
|
local C_UNKNOWN = { 0.55, 0.55, 0.55 }
|
||||||
|
local C_TURTLE = { 0.55, 0.85, 1.00 }
|
||||||
|
local C_KEYS = { 0.95, 0.75, 0.35 }
|
||||||
|
|
||||||
|
local selectedPlayer = nil
|
||||||
|
local selectedRaid = nil
|
||||||
|
|
||||||
|
-- Layout constants
|
||||||
|
local FRAME_W, FRAME_H = 1120, 600
|
||||||
|
local LEFT_W = 300
|
||||||
|
local ROW_H = 41
|
||||||
|
local STEP_ROW_H = 28
|
||||||
|
local STEP_EXPAND_H = 74
|
||||||
|
local HEADER_H = 60
|
||||||
|
local VISIBLE_RAIDS = 11
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Helpers
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
local function statusColor(prog)
|
||||||
|
if not prog or not prog.applicable then return C_UNKNOWN end
|
||||||
|
if prog.done then return C_DONE end
|
||||||
|
if prog.doneCount and prog.doneCount > 0 then return C_PARTIAL end
|
||||||
|
return C_NONE
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fs(parent, text, size, r, g, b, flags)
|
||||||
|
local f = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||||
|
if text then f:SetText(text) end
|
||||||
|
f:SetFont("Fonts\\FRIZQT__.TTF", size or 11, flags or "")
|
||||||
|
if r then f:SetTextColor(r, g, b) end
|
||||||
|
return f
|
||||||
|
end
|
||||||
|
|
||||||
|
local function iconTex(parent, path, size)
|
||||||
|
local t = parent:CreateTexture(nil, "ARTWORK")
|
||||||
|
t:SetWidth(size); t:SetHeight(size)
|
||||||
|
if path then t:SetTexture(path) end
|
||||||
|
t:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
local function solid(parent, layer, r, g, b, a)
|
||||||
|
local t = parent:CreateTexture(nil, layer or "BACKGROUND")
|
||||||
|
t:SetTexture(TEX_WHITE)
|
||||||
|
t:SetVertexColor(r or 0, g or 0, b or 0, a or 1)
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resolveStepIcon(step)
|
||||||
|
if step.icon then return step.icon end
|
||||||
|
if step.type == "item" and step.id and GetItemIcon then
|
||||||
|
local i = GetItemIcon(step.id)
|
||||||
|
if i then return i end
|
||||||
|
end
|
||||||
|
return TEX_QMARK
|
||||||
|
end
|
||||||
|
|
||||||
|
local function categoryColor(cat)
|
||||||
|
if cat == "Turtle" then return C_TURTLE end
|
||||||
|
if cat == "Keys & Access" then return C_KEYS end
|
||||||
|
return { 1, 1, 1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Root frame
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
local frame
|
||||||
|
local playerTabs = {}
|
||||||
|
local raidButtons = {}
|
||||||
|
local stepRows = {}
|
||||||
|
local detail = {}
|
||||||
|
|
||||||
|
function UI:Build()
|
||||||
|
if frame then return end
|
||||||
|
|
||||||
|
frame = CreateFrame("Frame", "RelationshipsAttuneFrame", UIParent)
|
||||||
|
frame:SetWidth(FRAME_W); frame:SetHeight(FRAME_H)
|
||||||
|
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||||
|
frame:SetFrameStrata("HIGH")
|
||||||
|
frame:SetMovable(true); frame:EnableMouse(true)
|
||||||
|
frame:RegisterForDrag("LeftButton")
|
||||||
|
frame:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||||
|
frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||||||
|
frame:SetBackdrop({
|
||||||
|
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||||
|
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||||
|
tile = true, tileSize = 32, edgeSize = 32,
|
||||||
|
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||||
|
})
|
||||||
|
-- Solid opaque overlay to make the window much less transparent
|
||||||
|
-- (the DialogBox background alone is quite see-through in vanilla).
|
||||||
|
frame.bgSolid = solid(frame, "BACKGROUND", 0.04, 0.05, 0.08, 0.94)
|
||||||
|
frame.bgSolid:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -13)
|
||||||
|
frame.bgSolid:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -13, 12)
|
||||||
|
-- Subtle gold-tinted top plate for the header
|
||||||
|
frame.bgHeader = solid(frame, "BACKGROUND", 0.12, 0.10, 0.06, 0.55)
|
||||||
|
frame.bgHeader:SetPoint("TOPLEFT", frame.bgSolid, "TOPLEFT", 0, 0)
|
||||||
|
frame.bgHeader:SetPoint("TOPRIGHT", frame.bgSolid, "TOPRIGHT", 0, 0)
|
||||||
|
frame.bgHeader:SetHeight(28)
|
||||||
|
frame:Hide()
|
||||||
|
|
||||||
|
local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
|
||||||
|
close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6)
|
||||||
|
|
||||||
|
-- Small title at the top left of the UI
|
||||||
|
frame.title = fs(frame, "Relationships Attunement", 11, 1, 0.82, 0, "OUTLINE")
|
||||||
|
frame.title:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, -18)
|
||||||
|
|
||||||
|
-- Player selector strip sits below the title inside the existing header
|
||||||
|
-- space; do not move the divider or columns.
|
||||||
|
frame.playerStrip = CreateFrame("Frame", nil, frame)
|
||||||
|
frame.playerStrip:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, -36)
|
||||||
|
frame.playerStrip:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -32, -36)
|
||||||
|
frame.playerStrip:SetHeight(22)
|
||||||
|
|
||||||
|
local div = solid(frame, "ARTWORK", 1, 0.82, 0, 0.35)
|
||||||
|
div:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -66)
|
||||||
|
div:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -16, -66)
|
||||||
|
div:SetHeight(1)
|
||||||
|
|
||||||
|
-- Left column
|
||||||
|
frame.leftPane = CreateFrame("Frame", nil, frame)
|
||||||
|
frame.leftPane:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -72)
|
||||||
|
frame.leftPane:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 16, 50)
|
||||||
|
frame.leftPane:SetWidth(LEFT_W)
|
||||||
|
|
||||||
|
-- Panel backing for the left list, dark and mostly opaque
|
||||||
|
local leftBG = solid(frame.leftPane, "BACKGROUND", 0.02, 0.03, 0.05, 0.60)
|
||||||
|
leftBG:SetAllPoints(frame.leftPane)
|
||||||
|
|
||||||
|
frame.leftScroll = CreateFrame("ScrollFrame", "RelationshipsAttuneRaidScroll",
|
||||||
|
frame.leftPane, "FauxScrollFrameTemplate")
|
||||||
|
frame.leftScroll:SetPoint("TOPLEFT", frame.leftPane, "TOPLEFT", 0, 0)
|
||||||
|
frame.leftScroll:SetPoint("BOTTOMRIGHT", frame.leftPane, "BOTTOMRIGHT", -20, 0)
|
||||||
|
frame.leftScroll:SetScript("OnVerticalScroll", function()
|
||||||
|
FauxScrollFrame_OnVerticalScroll(ROW_H + 2, function() UI:RenderRaidList() end)
|
||||||
|
end)
|
||||||
|
frame.leftPane:EnableMouseWheel(true)
|
||||||
|
frame.leftPane:SetScript("OnMouseWheel", function()
|
||||||
|
local step = ROW_H + 2
|
||||||
|
local off = FauxScrollFrame_GetOffset(frame.leftScroll) or 0
|
||||||
|
local total = table.getn(RelationshipsAttune_Data)
|
||||||
|
off = off - (arg1 or 0)
|
||||||
|
if off < 0 then off = 0 end
|
||||||
|
if off > (total - VISIBLE_RAIDS) then off = total - VISIBLE_RAIDS end
|
||||||
|
if off < 0 then off = 0 end
|
||||||
|
frame.leftScroll:SetVerticalScroll(off * step)
|
||||||
|
FauxScrollFrame_OnVerticalScroll(step, function() UI:RenderRaidList() end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Vertical divider
|
||||||
|
local vdiv = solid(frame, "ARTWORK", 1, 0.82, 0, 0.30)
|
||||||
|
vdiv:SetPoint("TOPLEFT", frame.leftPane, "TOPRIGHT", 4, 0)
|
||||||
|
vdiv:SetPoint("BOTTOMLEFT", frame.leftPane, "BOTTOMRIGHT", 4, 0)
|
||||||
|
vdiv:SetWidth(1)
|
||||||
|
|
||||||
|
-- Right column
|
||||||
|
frame.rightPane = CreateFrame("Frame", nil, frame)
|
||||||
|
frame.rightPane:SetPoint("TOPLEFT", frame.leftPane, "TOPRIGHT", 14, 0)
|
||||||
|
frame.rightPane:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -16, 50)
|
||||||
|
|
||||||
|
local rightBG = solid(frame.rightPane, "BACKGROUND", 0.02, 0.03, 0.05, 0.50)
|
||||||
|
rightBG:SetAllPoints(frame.rightPane)
|
||||||
|
|
||||||
|
detail.icon = iconTex(frame.rightPane, TEX_QMARK, 40)
|
||||||
|
detail.icon:SetPoint("TOPLEFT", frame.rightPane, "TOPLEFT", 4, -4)
|
||||||
|
|
||||||
|
detail.name = fs(frame.rightPane, "", 15, 1, 0.82, 0, "OUTLINE")
|
||||||
|
detail.name:SetPoint("TOPLEFT", detail.icon, "TOPRIGHT", 10, -2)
|
||||||
|
|
||||||
|
detail.category = fs(frame.rightPane, "", 10, 0.75, 0.75, 0.75)
|
||||||
|
detail.category:SetPoint("TOPLEFT", detail.name, "BOTTOMLEFT", 0, -2)
|
||||||
|
|
||||||
|
-- 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)
|
||||||
|
detail.barBG:SetPoint("TOPLEFT", frame.rightPane, "TOPLEFT", 4, -HEADER_H)
|
||||||
|
detail.barBG:SetPoint("TOPRIGHT", frame.rightPane, "TOPRIGHT", -4, -HEADER_H)
|
||||||
|
detail.barBG:SetHeight(18)
|
||||||
|
|
||||||
|
-- Thin gold outline for the bar
|
||||||
|
local blt = solid(frame.rightPane, "ARTWORK", 1, 0.82, 0, 0.55)
|
||||||
|
blt:SetPoint("TOPLEFT", detail.barBG, "TOPLEFT", 0, 0)
|
||||||
|
blt:SetPoint("TOPRIGHT", detail.barBG, "TOPRIGHT", 0, 0)
|
||||||
|
blt:SetHeight(1)
|
||||||
|
local blb = solid(frame.rightPane, "ARTWORK", 1, 0.82, 0, 0.55)
|
||||||
|
blb:SetPoint("BOTTOMLEFT", detail.barBG, "BOTTOMLEFT", 0, 0)
|
||||||
|
blb:SetPoint("BOTTOMRIGHT", detail.barBG, "BOTTOMRIGHT", 0, 0)
|
||||||
|
blb:SetHeight(1)
|
||||||
|
|
||||||
|
detail.barFill = solid(frame.rightPane, "ARTWORK", 0.20, 0.75, 0.30, 0.95)
|
||||||
|
detail.barFill:SetPoint("TOPLEFT", detail.barBG, "TOPLEFT", 1, -1)
|
||||||
|
detail.barFill:SetPoint("BOTTOMLEFT", detail.barBG, "BOTTOMLEFT", 1, 1)
|
||||||
|
detail.barFill:SetWidth(1)
|
||||||
|
|
||||||
|
detail.barText = fs(frame.rightPane, "", 12, 1, 1, 1, "OUTLINE")
|
||||||
|
detail.barText:SetPoint("CENTER", detail.barBG, "CENTER", 0, 0)
|
||||||
|
|
||||||
|
-- Step host
|
||||||
|
frame.stepHost = CreateFrame("Frame", nil, frame.rightPane)
|
||||||
|
frame.stepHost:SetPoint("TOPLEFT", frame.rightPane, "TOPLEFT", 4, -HEADER_H - 26)
|
||||||
|
frame.stepHost:SetPoint("BOTTOMRIGHT", frame.rightPane, "BOTTOMRIGHT", -4, 0)
|
||||||
|
-- Vanilla clients can report an unresolved height for a frame positioned by
|
||||||
|
-- two anchors during the first render. Give the expanded-step area a fixed
|
||||||
|
-- resolved height so six default-expanded rows reach the same bottom line as
|
||||||
|
-- the left pane instead of stopping early with a blank band underneath.
|
||||||
|
frame.stepHost:SetHeight(FRAME_H - 72 - 50 - HEADER_H - 26)
|
||||||
|
frame.stepHost.page = 0
|
||||||
|
frame.stepHost.pageCount = 1
|
||||||
|
frame.stepHost:EnableMouseWheel(true)
|
||||||
|
frame.stepHost:SetScript("OnMouseWheel", function()
|
||||||
|
-- arg1 = +1 wheel up = previous page, -1 wheel down = next page.
|
||||||
|
local delta = arg1 or 0
|
||||||
|
local p = (frame.stepHost.page or 0) - delta
|
||||||
|
local pc = frame.stepHost.pageCount or 1
|
||||||
|
if p < 0 then p = 0 end
|
||||||
|
if p > pc - 1 then p = pc - 1 end
|
||||||
|
frame.stepHost.page = p
|
||||||
|
UI:RenderDetail()
|
||||||
|
end)
|
||||||
|
-- Also accept wheel events over the whole right pane so the user does not
|
||||||
|
-- have to hover the exact step area.
|
||||||
|
frame.rightPane:EnableMouseWheel(true)
|
||||||
|
frame.rightPane:SetScript("OnMouseWheel", function()
|
||||||
|
local script = frame.stepHost:GetScript("OnMouseWheel")
|
||||||
|
if script then script() end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Bottom buttons (Sync Group removed)
|
||||||
|
local mmBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||||
|
mmBtn:SetWidth(120); mmBtn:SetHeight(22)
|
||||||
|
mmBtn:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -18, 14)
|
||||||
|
mmBtn:SetText("Toggle Minimap")
|
||||||
|
mmBtn:SetScript("OnClick", function()
|
||||||
|
if RA.Minimap then RA.Minimap:Toggle() end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local scanBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||||
|
scanBtn:SetWidth(80); scanBtn:SetHeight(22)
|
||||||
|
scanBtn:SetPoint("RIGHT", mmBtn, "LEFT", -6, 0)
|
||||||
|
scanBtn:SetText("Rescan")
|
||||||
|
scanBtn:SetScript("OnClick", function()
|
||||||
|
RA:ScanQuestLog(); RA:ScanBags(); RA:RecomputeAll(); UI:Refresh()
|
||||||
|
end)
|
||||||
|
|
||||||
|
local hint = fs(frame, "Hover Over A Step For Details - Click To Minimise And Expand A Step. Type \"/Attune\" To Open This Panel.",
|
||||||
|
10, 0.75, 0.75, 0.75)
|
||||||
|
hint:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 20, 20)
|
||||||
|
|
||||||
|
-- One-shot delayed refresh: on first Show, frame widths need a tick to settle
|
||||||
|
-- (barBG:GetWidth() can return 0 the first time), so we re-render after a short delay.
|
||||||
|
frame._pending = 0
|
||||||
|
frame:SetScript("OnUpdate", function()
|
||||||
|
if not this._needsRelayout then return end
|
||||||
|
this._pending = (this._pending or 0) + (arg1 or 0)
|
||||||
|
if this._pending >= 0.05 then
|
||||||
|
this._needsRelayout = false
|
||||||
|
this._pending = 0
|
||||||
|
UI:Refresh()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
frame:SetScript("OnShow", function()
|
||||||
|
this._needsRelayout = true
|
||||||
|
this._pending = 0
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Data helpers
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
local function playerList()
|
||||||
|
local youLabel
|
||||||
|
if RA.playerName then
|
||||||
|
local fac = RA.playerFaction or "Neutral"
|
||||||
|
youLabel = RA.playerName .. " | " .. fac
|
||||||
|
else
|
||||||
|
youLabel = L.UI_YOU
|
||||||
|
end
|
||||||
|
local list = { { key = nil, label = youLabel, isYou = true } }
|
||||||
|
if RelationshipsAttuneDB and RelationshipsAttuneDB.players then
|
||||||
|
for key, p in pairs(RelationshipsAttuneDB.players) do
|
||||||
|
if key ~= RA.playerKey and key ~= RA.playerName then
|
||||||
|
table.insert(list, { key = key, label = p.name or key })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
local function progressFor(playerKey, attId)
|
||||||
|
if playerKey == nil then
|
||||||
|
return RelationshipsAttuneCharDB.progress[attId]
|
||||||
|
end
|
||||||
|
local p = RelationshipsAttuneDB.players[playerKey]
|
||||||
|
return p and p.progress and p.progress[attId] or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isExpanded(attId, i)
|
||||||
|
local e = RelationshipsAttuneCharDB.expanded
|
||||||
|
-- Default to expanded for every step, including rows created after scroll.
|
||||||
|
-- Only an explicit false from a manual click collapses a row.
|
||||||
|
if e and e[attId] and e[attId][i] == false then return nil end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toggleExpanded(attId, i)
|
||||||
|
local e = RelationshipsAttuneCharDB.expanded
|
||||||
|
if not e[attId] then e[attId] = {} end
|
||||||
|
if isExpanded(attId, i) then
|
||||||
|
e[attId][i] = false
|
||||||
|
else
|
||||||
|
e[attId][i] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- Rendering
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
local function renderPlayerTabs()
|
||||||
|
local list = playerList()
|
||||||
|
for i = 1, table.getn(playerTabs) do playerTabs[i]:Hide() end
|
||||||
|
|
||||||
|
local x = 0
|
||||||
|
for i = 1, table.getn(list) do
|
||||||
|
local entry = list[i]
|
||||||
|
local b = playerTabs[i]
|
||||||
|
if not b then
|
||||||
|
b = CreateFrame("Button", nil, frame.playerStrip)
|
||||||
|
b:SetHeight(20)
|
||||||
|
b.bg = solid(b, "BACKGROUND", 1, 1, 1, 0.10)
|
||||||
|
b.bg:SetAllPoints(b)
|
||||||
|
b.text = fs(b, "", 11, 1, 1, 1)
|
||||||
|
b.text:SetPoint("CENTER", b, "CENTER", 0, 0)
|
||||||
|
playerTabs[i] = b
|
||||||
|
end
|
||||||
|
b.entry = entry
|
||||||
|
b.text:SetText(entry.label)
|
||||||
|
local w = b.text:GetStringWidth() + 20
|
||||||
|
b:SetWidth(w)
|
||||||
|
b:ClearAllPoints()
|
||||||
|
b:SetPoint("LEFT", frame.playerStrip, "LEFT", x, 0)
|
||||||
|
x = x + w + 4
|
||||||
|
|
||||||
|
local isSel = (entry.key == selectedPlayer)
|
||||||
|
if entry.isYou then
|
||||||
|
local fac = RA.playerFaction
|
||||||
|
if fac == "Alliance" then
|
||||||
|
b.bg:SetVertexColor(0.20, 0.45, 1.00, isSel and 0.55 or 0.30)
|
||||||
|
b.text:SetTextColor(0.60, 0.80, 1.00)
|
||||||
|
elseif fac == "Horde" then
|
||||||
|
b.bg:SetVertexColor(0.90, 0.15, 0.15, isSel and 0.55 or 0.30)
|
||||||
|
b.text:SetTextColor(1.00, 0.65, 0.55)
|
||||||
|
else
|
||||||
|
b.bg:SetVertexColor(1, 0.82, 0, isSel and 0.45 or 0.20)
|
||||||
|
b.text:SetTextColor(1, 0.95, 0.85)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
b.bg:SetVertexColor(1, 0.82, 0, isSel and 0.45 or 0.12)
|
||||||
|
b.text:SetTextColor(1, isSel and 0.95 or 0.85, isSel and 0.6 or 0.85)
|
||||||
|
end
|
||||||
|
b:SetScript("OnClick", function()
|
||||||
|
selectedPlayer = this.entry.key
|
||||||
|
UI:Refresh()
|
||||||
|
end)
|
||||||
|
b:Show()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:RenderRaidList()
|
||||||
|
local data = RelationshipsAttune_Data
|
||||||
|
local total = table.getn(data)
|
||||||
|
|
||||||
|
FauxScrollFrame_Update(frame.leftScroll, total, VISIBLE_RAIDS, ROW_H + 2)
|
||||||
|
local offset = FauxScrollFrame_GetOffset(frame.leftScroll) or 0
|
||||||
|
|
||||||
|
for i = 1, table.getn(raidButtons) do raidButtons[i]:Hide() end
|
||||||
|
|
||||||
|
for row = 1, VISIBLE_RAIDS do
|
||||||
|
local i = row + offset
|
||||||
|
if i <= total then
|
||||||
|
local att = data[i]
|
||||||
|
local b = raidButtons[row]
|
||||||
|
if not b then
|
||||||
|
b = CreateFrame("Button", nil, frame.leftPane)
|
||||||
|
b:SetHeight(ROW_H)
|
||||||
|
b.bg = solid(b, "BACKGROUND", 1, 1, 1, 0.08)
|
||||||
|
b.bg:SetAllPoints(b)
|
||||||
|
b.sel = solid(b, "BORDER", 1, 0.82, 0, 0.25)
|
||||||
|
b.sel:SetAllPoints(b); b.sel:Hide()
|
||||||
|
b.icon = iconTex(b, TEX_QMARK, 26)
|
||||||
|
b.icon:SetPoint("LEFT", b, "LEFT", 4, 0)
|
||||||
|
b.name = fs(b, "", 12, 1, 1, 1, "OUTLINE")
|
||||||
|
b.name:SetPoint("TOPLEFT", b.icon, "TOPRIGHT", 8, -1)
|
||||||
|
b.count = fs(b, "", 10, 0.8, 0.8, 0.8)
|
||||||
|
b.count:SetPoint("BOTTOMLEFT", b.icon, "BOTTOMRIGHT", 8, 1)
|
||||||
|
b.status = b:CreateTexture(nil, "OVERLAY")
|
||||||
|
b.status:SetWidth(16); b.status:SetHeight(16)
|
||||||
|
b.status:SetPoint("RIGHT", b, "RIGHT", -6, 0)
|
||||||
|
raidButtons[row] = b
|
||||||
|
end
|
||||||
|
b:ClearAllPoints()
|
||||||
|
b:SetPoint("TOPLEFT", frame.leftPane, "TOPLEFT", 0, -(row - 1) * (ROW_H + 2))
|
||||||
|
b:SetWidth(LEFT_W - 20)
|
||||||
|
|
||||||
|
b.icon:SetTexture(att.icon or TEX_QMARK)
|
||||||
|
b.att = att
|
||||||
|
|
||||||
|
local prog = progressFor(selectedPlayer, att.id)
|
||||||
|
local color = statusColor(prog)
|
||||||
|
b.name:SetText(att.name)
|
||||||
|
local nc = categoryColor(att.category)
|
||||||
|
b.name:SetTextColor(nc[1], nc[2], nc[3])
|
||||||
|
|
||||||
|
if prog and prog.applicable then
|
||||||
|
b.count:SetText(prog.doneCount .. " / " .. prog.total .. " steps")
|
||||||
|
b.count:SetTextColor(color[1], color[2], color[3])
|
||||||
|
if prog.done then
|
||||||
|
b.status:SetTexture(TEX_CHECK)
|
||||||
|
b.bg:SetVertexColor(0.20, 0.90, 0.30, 0.25)
|
||||||
|
elseif (prog.doneCount or 0) > 0 then
|
||||||
|
b.status:SetTexture(TEX_WAIT)
|
||||||
|
b.bg:SetVertexColor(1, 1, 1, 0.08)
|
||||||
|
else
|
||||||
|
b.status:SetTexture(TEX_CROSS)
|
||||||
|
b.bg:SetVertexColor(1, 1, 1, 0.08)
|
||||||
|
end
|
||||||
|
b.status:Show()
|
||||||
|
else
|
||||||
|
b.count:SetText("N/A for your faction")
|
||||||
|
b.count:SetTextColor(C_UNKNOWN[1], C_UNKNOWN[2], C_UNKNOWN[3])
|
||||||
|
b.status:Hide()
|
||||||
|
b.bg:SetVertexColor(1, 1, 1, 0.05)
|
||||||
|
end
|
||||||
|
|
||||||
|
if selectedRaid == att.id then b.sel:Show() else b.sel:Hide() end
|
||||||
|
b:SetScript("OnClick", function()
|
||||||
|
selectedRaid = this.att.id
|
||||||
|
frame.stepHost.page = 0
|
||||||
|
UI:Refresh()
|
||||||
|
end)
|
||||||
|
b:Show()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stepTypeLabel(kind)
|
||||||
|
if kind == "quest" then return "Quest"
|
||||||
|
elseif kind == "item" then return "Item"
|
||||||
|
elseif kind == "rep" then return "Reputation"
|
||||||
|
elseif kind == "level" then return "Level" end
|
||||||
|
return kind or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setStepTooltip(row)
|
||||||
|
row.frame:EnableMouse(true)
|
||||||
|
row.frame:SetScript("OnEnter", function()
|
||||||
|
local step = this.step
|
||||||
|
if not step then return end
|
||||||
|
GameTooltip:SetOwner(this, "ANCHOR_CURSOR")
|
||||||
|
GameTooltip:AddLine(step.name or "?", 1, 0.82, 0)
|
||||||
|
GameTooltip:AddLine(stepTypeLabel(step.type), 0.6, 0.8, 1)
|
||||||
|
if step.desc then
|
||||||
|
GameTooltip:AddLine(" ")
|
||||||
|
GameTooltip:AddLine(step.desc, 0.9, 0.9, 0.9, 1)
|
||||||
|
end
|
||||||
|
if step.start then
|
||||||
|
GameTooltip:AddLine(" ")
|
||||||
|
GameTooltip:AddLine("Where to start:", 1, 0.82, 0)
|
||||||
|
GameTooltip:AddLine(step.start, 0.9, 0.9, 0.9, 1)
|
||||||
|
end
|
||||||
|
if step.faction then
|
||||||
|
GameTooltip:AddLine(" ")
|
||||||
|
GameTooltip:AddLine("Faction: " .. step.faction, 0.7, 0.7, 0.7)
|
||||||
|
end
|
||||||
|
GameTooltip:AddLine(" ")
|
||||||
|
GameTooltip:AddLine("Click to expand / collapse.", 0.5, 0.85, 1)
|
||||||
|
GameTooltip:Show()
|
||||||
|
end)
|
||||||
|
row.frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||||
|
row.frame:RegisterForClicks("LeftButtonUp")
|
||||||
|
row.frame:SetScript("OnMouseUp", function()
|
||||||
|
local step = this.step
|
||||||
|
if not step or not this.attId then return end
|
||||||
|
toggleExpanded(this.attId, this.stepIndex)
|
||||||
|
UI:RenderDetail()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Compute a reliable width for the progress bar even before layout settles.
|
||||||
|
local function computeBarWidth()
|
||||||
|
local w = detail.barBG:GetWidth()
|
||||||
|
if not w or w < 10 then
|
||||||
|
local l, r = detail.barBG:GetLeft(), detail.barBG:GetRight()
|
||||||
|
if l and r then w = r - l end
|
||||||
|
end
|
||||||
|
if not w or w < 10 then
|
||||||
|
w = (frame.rightPane:GetWidth() or 0) - 8
|
||||||
|
end
|
||||||
|
if not w or w < 10 then
|
||||||
|
w = FRAME_W - LEFT_W - 40
|
||||||
|
end
|
||||||
|
return w
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:RenderDetail()
|
||||||
|
local data = RelationshipsAttune_Data
|
||||||
|
if not selectedRaid then selectedRaid = data[1] and data[1].id end
|
||||||
|
local att
|
||||||
|
for i = 1, table.getn(data) do
|
||||||
|
if data[i].id == selectedRaid then att = data[i]; break end
|
||||||
|
end
|
||||||
|
if not att then return end
|
||||||
|
|
||||||
|
detail.icon:SetTexture(att.icon or TEX_QMARK)
|
||||||
|
detail.name:SetText(att.name)
|
||||||
|
local cat = att.category
|
||||||
|
if cat == "Turtle" then
|
||||||
|
detail.category:SetText("Turtle / Octo custom content | " .. (att.faction or "Both"))
|
||||||
|
detail.category:SetTextColor(C_TURTLE[1], C_TURTLE[2], C_TURTLE[3])
|
||||||
|
elseif cat == "Keys & Access" then
|
||||||
|
detail.category:SetText("Keys & Access (dungeon shortcut) | " .. (att.faction or "Both"))
|
||||||
|
detail.category:SetTextColor(C_KEYS[1], C_KEYS[2], C_KEYS[3])
|
||||||
|
else
|
||||||
|
detail.category:SetText("Vanilla content | " .. (att.faction or "Both"))
|
||||||
|
detail.category:SetTextColor(0.85, 0.85, 0.85)
|
||||||
|
end
|
||||||
|
|
||||||
|
local prog = progressFor(selectedPlayer, att.id)
|
||||||
|
local done = prog and prog.doneCount or 0
|
||||||
|
local total = prog and prog.total or table.getn(att.steps)
|
||||||
|
local pct = (total > 0) and (done / total) or 0
|
||||||
|
if pct < 0 then pct = 0 elseif pct > 1 then pct = 1 end
|
||||||
|
|
||||||
|
local barW = computeBarWidth()
|
||||||
|
-- Fill uses SetWidth from the left edge. barFill is anchored to barBG's
|
||||||
|
-- TOPLEFT and BOTTOMLEFT, so its width fully controls the visible fill.
|
||||||
|
local fillW = math.max(1, (barW - 2) * pct)
|
||||||
|
detail.barFill:SetWidth(fillW)
|
||||||
|
|
||||||
|
local c = statusColor(prog)
|
||||||
|
detail.barFill:SetVertexColor(c[1], c[2], c[3], 0.95)
|
||||||
|
detail.barText:SetText(done .. " / " .. total .. " (" .. math.floor(pct * 100 + 0.5) .. "%)")
|
||||||
|
|
||||||
|
-- Build applicable step list
|
||||||
|
local applicable = {}
|
||||||
|
for i = 1, table.getn(att.steps) do
|
||||||
|
local step = att.steps[i]
|
||||||
|
if RA:StepApplies(step) then
|
||||||
|
table.insert(applicable, { idx = i, step = step })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, table.getn(stepRows) do stepRows[i].frame:Hide() end
|
||||||
|
|
||||||
|
-- Page-based layout: 5 steps per page, no partial rows.
|
||||||
|
local PER_PAGE = 5
|
||||||
|
local totalSteps = table.getn(applicable)
|
||||||
|
local pageCount = math.ceil(totalSteps / PER_PAGE)
|
||||||
|
if pageCount < 1 then pageCount = 1 end
|
||||||
|
frame.stepHost.pageCount = pageCount
|
||||||
|
local page = frame.stepHost.page or 0
|
||||||
|
if page > pageCount - 1 then page = pageCount - 1 end
|
||||||
|
if page < 0 then page = 0 end
|
||||||
|
frame.stepHost.page = page
|
||||||
|
|
||||||
|
local startI = page * PER_PAGE + 1
|
||||||
|
local endI = startI + PER_PAGE - 1
|
||||||
|
if endI > totalSteps then endI = totalSteps end
|
||||||
|
|
||||||
|
local y = 0
|
||||||
|
|
||||||
|
for slot = 1, PER_PAGE do
|
||||||
|
local si = startI + slot - 1
|
||||||
|
if si > endI then
|
||||||
|
local r = stepRows[slot]
|
||||||
|
if r then r.frame:Hide() end
|
||||||
|
else
|
||||||
|
local entry = applicable[si]
|
||||||
|
local step = entry.step
|
||||||
|
local origIdx = entry.idx
|
||||||
|
local expanded = isExpanded(att.id, origIdx)
|
||||||
|
local rowH = expanded and STEP_EXPAND_H or STEP_ROW_H
|
||||||
|
|
||||||
|
local row = stepRows[slot]
|
||||||
|
if not row then
|
||||||
|
row = {}
|
||||||
|
row.frame = CreateFrame("Button", nil, frame.stepHost)
|
||||||
|
row.bg = solid(row.frame, "BACKGROUND", 1, 1, 1, 0.06)
|
||||||
|
row.bg:SetAllPoints(row.frame)
|
||||||
|
row.icon = iconTex(row.frame, TEX_QMARK, 22)
|
||||||
|
row.icon:SetPoint("TOPLEFT", row.frame, "TOPLEFT", 4, -3)
|
||||||
|
row.type = fs(row.frame, "", 9, 0.7, 0.7, 0.7)
|
||||||
|
row.type:SetPoint("TOPLEFT", row.frame, "TOPLEFT", 36, -2)
|
||||||
|
row.name = fs(row.frame, "", 12, 1, 1, 1)
|
||||||
|
row.name:SetPoint("TOPLEFT", row.frame, "TOPLEFT", 36, -15)
|
||||||
|
row.name:SetPoint("RIGHT", row.frame, "RIGHT", -32, 0)
|
||||||
|
row.name:SetJustifyH("LEFT")
|
||||||
|
row.status = row.frame:CreateTexture(nil, "OVERLAY")
|
||||||
|
row.status:SetWidth(18); row.status:SetHeight(18)
|
||||||
|
row.status:SetPoint("TOPRIGHT", row.frame, "TOPRIGHT", -8, -4)
|
||||||
|
row.extra = fs(row.frame, "", 10, 0.90, 0.90, 0.90)
|
||||||
|
row.extra:SetPoint("TOPLEFT", row.frame, "TOPLEFT", 36, -32)
|
||||||
|
row.extra:SetPoint("TOPRIGHT", row.frame, "TOPRIGHT", -8, -32)
|
||||||
|
row.extra:SetJustifyH("LEFT")
|
||||||
|
row.extra:SetJustifyV("TOP")
|
||||||
|
setStepTooltip(row)
|
||||||
|
stepRows[slot] = row
|
||||||
|
end
|
||||||
|
row.frame.step = step
|
||||||
|
row.frame.attId = att.id
|
||||||
|
row.frame.stepIndex = origIdx
|
||||||
|
|
||||||
|
row.frame:SetHeight(rowH)
|
||||||
|
row.frame:ClearAllPoints()
|
||||||
|
row.frame:SetPoint("TOPLEFT", frame.stepHost, "TOPLEFT", 0, y)
|
||||||
|
row.frame:SetPoint("TOPRIGHT", frame.stepHost, "TOPRIGHT", 0, y)
|
||||||
|
|
||||||
|
row.icon:SetTexture(resolveStepIcon(step))
|
||||||
|
row.type:SetText(stepTypeLabel(step.type))
|
||||||
|
row.name:SetText(step.name or "?")
|
||||||
|
|
||||||
|
local ok = prog and prog.steps and prog.steps[origIdx]
|
||||||
|
if ok then
|
||||||
|
row.status:SetTexture(TEX_CHECK); row.status:Show()
|
||||||
|
row.name:SetTextColor(C_DONE[1], C_DONE[2], C_DONE[3])
|
||||||
|
row.bg:SetVertexColor(0.20, 0.90, 0.30, 0.28)
|
||||||
|
elseif prog and prog.applicable then
|
||||||
|
row.status:SetTexture(TEX_CROSS); row.status:Show()
|
||||||
|
row.name:SetTextColor(1, 1, 1)
|
||||||
|
row.bg:SetVertexColor(1, 1, 1, 0.06)
|
||||||
|
else
|
||||||
|
row.status:SetTexture(TEX_WAIT); row.status:Show()
|
||||||
|
row.name:SetTextColor(C_UNKNOWN[1], C_UNKNOWN[2], C_UNKNOWN[3])
|
||||||
|
row.bg:SetVertexColor(1, 1, 1, 0.04)
|
||||||
|
end
|
||||||
|
|
||||||
|
if expanded then
|
||||||
|
local txt = ""
|
||||||
|
if step.desc then txt = txt .. step.desc end
|
||||||
|
if step.start then
|
||||||
|
if txt ~= "" then txt = txt .. "\n\n" end
|
||||||
|
txt = txt .. "|cffffd200Where:|r " .. step.start
|
||||||
|
end
|
||||||
|
if txt == "" and step.type ~= "level" then txt = "No extra info provided." end
|
||||||
|
row.extra:SetText(txt)
|
||||||
|
row.extra:Show()
|
||||||
|
else
|
||||||
|
row.extra:SetText("")
|
||||||
|
row.extra:Hide()
|
||||||
|
end
|
||||||
|
|
||||||
|
row.frame:Show()
|
||||||
|
|
||||||
|
y = y - (rowH + 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:Refresh()
|
||||||
|
if not frame or not frame:IsShown() then return end
|
||||||
|
renderPlayerTabs()
|
||||||
|
self:RenderRaidList()
|
||||||
|
self:RenderDetail()
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:Init() self:Build() end
|
||||||
|
|
||||||
|
function UI:IsShown()
|
||||||
|
return frame and frame:IsShown()
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:Show()
|
||||||
|
self:Build()
|
||||||
|
if not selectedRaid then
|
||||||
|
selectedRaid = RelationshipsAttune_Data[1] and RelationshipsAttune_Data[1].id
|
||||||
|
end
|
||||||
|
if frame.stepHost then frame.stepHost.page = 0 end
|
||||||
|
frame:Show()
|
||||||
|
self:Refresh()
|
||||||
|
end
|
||||||
|
|
||||||
|
function UI:Hide() if frame then frame:Hide() end end
|
||||||
|
|
||||||
|
function UI:Toggle()
|
||||||
|
self:Build()
|
||||||
|
if frame:IsShown() then frame:Hide() else self:Show() end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user