local AddOnName = "Nanami-UI" SFrames = SFrames or {} -- ============================================================ -- Roll Tracker Data -- ============================================================ local currentRolls = {} -- [itemName] = { [playerName] = { action, roll } } local _A = SFrames.ActiveTheme local _bg = _A and _A.panelBg or { 0.08, 0.08, 0.10, 0.95 } local _bd = _A and _A.panelBorder or { 0.3, 0.3, 0.35, 1 } local RollUI = CreateFrame("Frame") RollUI:RegisterEvent("CHAT_MSG_LOOT") RollUI:RegisterEvent("CHAT_MSG_SYSTEM") -- ============================================================ -- String helpers (Lua 5.0 only - NO string.match!) -- ============================================================ local function StripLinks(msg) return string.gsub(msg, "|c%x%x%x%x%x%x%x%x|H.-|h(.-)|h|r", "%1") end local function GetBracketItem(text) local _, _, name = string.find(text, "%[(.-)%]") if name then return name end text = string.gsub(text, "^%s+", "") text = string.gsub(text, "%s+$", "") text = string.gsub(text, "%.$", "") return text end local function TrimStr(s) s = string.gsub(s, "^%s+", "") s = string.gsub(s, "%s+$", "") return s end -- ============================================================ -- Parse loot roll chat messages -- ============================================================ local function TrackRollEvent(msg) if not msg or msg == "" then return nil end local clean = StripLinks(msg) local player, rawItem, rollType, rollNum -- Patterns based on ACTUAL Turtle WoW Chinese message format: -- "Buis选择了贪婪取向:[物品名]" -- "Buis选择了需求取向:[物品名]" local _, _, p, i -- === PRIMARY: Turtle WoW Chinese format "选择了需求取向" / "选择了贪婪取向" === _, _, p, i = string.find(clean, "^(.+)选择了需求取向:(.+)$") if p and i then player = p; rawItem = i; rollType = "Need" end if not player then _, _, p, i = string.find(clean, "^(.+)选择了需求取向: (.+)$") if p and i then player = p; rawItem = i; rollType = "Need" end end if not player then _, _, p, i = string.find(clean, "^(.+)选择了贪婪取向:(.+)$") if p and i then player = p; rawItem = i; rollType = "Greed" end end if not player then _, _, p, i = string.find(clean, "^(.+)选择了贪婪取向: (.+)$") if p and i then player = p; rawItem = i; rollType = "Greed" end end -- Pass: "放弃了" format if not player then _, _, p, i = string.find(clean, "^(.+)放弃了:(.+)$") if p and i then player = p; rawItem = i; rollType = "Pass" end end if not player then _, _, p, i = string.find(clean, "^(.+)放弃了: (.+)$") if p and i then player = p; rawItem = i; rollType = "Pass" end end if not player then _, _, p, i = string.find(clean, "^(.+)放弃了(.+)$") if p and i then player = p; rawItem = i; rollType = "Pass" end end -- Roll (Chinese): "掷出 85 (1-100)" if not player then local r _, _, p, r, i = string.find(clean, "^(.+)掷出%s*(%d+).-:(.+)$") if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end end if not player then local r _, _, p, r, i = string.find(clean, "^(.+)掷出%s*(%d+).-: (.+)$") if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end end -- === FALLBACK: Other Chinese formats === if not player then _, _, p, i = string.find(clean, "^(.+)需求了:(.+)$") if p and i then player = p; rawItem = i; rollType = "Need" end end if not player then _, _, p, i = string.find(clean, "^(.+)贪婪了:(.+)$") if p and i then player = p; rawItem = i; rollType = "Greed" end end -- === ENGLISH patterns === if not player then _, _, p, i = string.find(clean, "^(.+) selected Need for: (.+)$") if p and i then player = p; rawItem = i; rollType = "Need" end end if not player then _, _, p, i = string.find(clean, "^(.+) selected Greed for: (.+)$") if p and i then player = p; rawItem = i; rollType = "Greed" end end if not player then _, _, p, i = string.find(clean, "^(.+) passed on: (.+)$") if p and i then player = p; rawItem = i; rollType = "Pass" end end if not player then local r _, _, p, r, i = string.find(clean, "^(.+) rolls (%d+) .+for: (.+)$") if p and r and i then player = p; rawItem = i; rollType = "Roll"; rollNum = r end end if not player then _, _, p, i = string.find(clean, "^(.+) passes on (.+)$") if p and i then player = p; rawItem = i; rollType = "Pass" end end if player and rawItem then player = TrimStr(player) local itemName = GetBracketItem(rawItem) if itemName == "" or player == "" then return nil end if not currentRolls[itemName] then currentRolls[itemName] = {} end if not currentRolls[itemName][player] then currentRolls[itemName][player] = {} end if rollType == "Roll" then currentRolls[itemName][player].roll = rollNum else currentRolls[itemName][player].action = rollType end return itemName end return nil end -- ============================================================ -- Update the tracker text on visible roll frames -- ============================================================ local function UpdateRollTrackers() for idx = 1, 4 do local frame = _G["GroupLootFrame"..idx] if frame and frame:IsVisible() and frame.rollID then local _, itemName = GetLootRollItemInfo(frame.rollID) if itemName and frame.sfNeedFS then local data = currentRolls[itemName] local needText, greedText, passText = "", "", "" if data then local needs, greeds, passes = {}, {}, {} for pl, info in pairs(data) do local hex = SFrames and SFrames:GetClassHexForName(pl) local coloredPl = hex and ("|cff" .. hex .. pl .. "|r") or pl local rollStr = "" if info.roll then rollStr = "|cffaaaaaa(" .. info.roll .. ")|r" end if info.action == "Need" then table.insert(needs, coloredPl .. rollStr) elseif info.action == "Greed" then table.insert(greeds, coloredPl .. rollStr) elseif info.action == "Pass" then table.insert(passes, coloredPl) end end if table.getn(needs) > 0 then needText = "|cffff5555需求|r " .. table.concat(needs, " ") end if table.getn(greeds) > 0 then greedText = "|cff55ff55贪婪|r " .. table.concat(greeds, " ") end if table.getn(passes) > 0 then passText = "|cff888888放弃|r " .. table.concat(passes, " ") end end frame.sfNeedFS:SetText(needText) frame.sfGreedFS:SetText(greedText) frame.sfPassFS:SetText(passText) end end end end -- ============================================================ -- Event Handler -- ============================================================ RollUI:SetScript("OnEvent", function() if event == "CHAT_MSG_LOOT" or event == "CHAT_MSG_SYSTEM" then local matched = TrackRollEvent(arg1) if matched then UpdateRollTrackers() end end end) -- ============================================================ -- Kill ALL Blizzard textures on the main frame only -- ============================================================ local function NukeBlizzTextures(frame) local regions = {frame:GetRegions()} for _, r in ipairs(regions) do if r:IsObjectType("Texture") and not r.sfKeep then r:SetTexture(nil) r:SetAlpha(0) r:Hide() -- Don't override Show! SetBackdrop needs it. -- Instead mark as nuked and move off-screen if not r.sfNuked then r.sfNuked = true r:ClearAllPoints() r:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999) end end end end -- ============================================================ -- Style the GroupLootFrame -- ============================================================ local function StyleGroupLootFrame(frame) if frame.sfSkinned then return end frame.sfSkinned = true local fname = frame:GetName() -- 1) Kill all Blizz textures on main frame NukeBlizzTextures(frame) -- 2) Alt-Drag frame:SetMovable(true) frame:EnableMouse(true) frame:RegisterForDrag("LeftButton") frame:SetScript("OnDragStart", function() if IsAltKeyDown() then this:StartMoving() end end) frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) -- 3) Rounded backdrop frame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) frame:SetBackdropColor(_bg[1], _bg[2], _bg[3], _bg[4]) frame:SetBackdropBorderColor(_bd[1], _bd[2], _bd[3], _bd[4]) -- Tag all backdrop regions so NukeBlizzTextures won't kill them local bdRegions = {frame:GetRegions()} for _, r in ipairs(bdRegions) do r.sfKeep = true end -- 5) Frame size (taller for tracker area) frame:SetWidth(300) frame:SetHeight(100) -- 6) Icon local iconFrame = _G[fname.."IconFrame"] if iconFrame then iconFrame:ClearAllPoints() iconFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -10) iconFrame:SetWidth(36) iconFrame:SetHeight(36) -- Kill IconFrame's own textures local iRegions = {iconFrame:GetRegions()} for _, r in ipairs(iRegions) do if r:IsObjectType("Texture") then local tName = r:GetName() or "" if string.find(tName, "Icon") then r:SetTexCoord(0.08, 0.92, 0.08, 0.92) r:ClearAllPoints() r:SetAllPoints(iconFrame) r.sfKeep = true else r:SetTexture(nil) r:SetAlpha(0) r:Hide() r.sfNuked = true r:ClearAllPoints() r:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999) end end end -- Clean icon border local iconBorder = frame:CreateTexture(nil, "ARTWORK") iconBorder:SetTexture("Interface\\Buttons\\WHITE8X8") iconBorder:SetVertexColor(0.4, 0.4, 0.45, 1) iconBorder:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", -2, 2) iconBorder:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", 2, -2) iconBorder.sfKeep = true local qualGlow = frame:CreateTexture(nil, "OVERLAY") qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") qualGlow:SetBlendMode("ADD") qualGlow:SetAlpha(0.8) qualGlow:SetWidth(68) qualGlow:SetHeight(68) qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) qualGlow:Hide() qualGlow.sfKeep = true frame.sfQualGlow = qualGlow end -- 7) Item Name (shorter width to make room for buttons) local itemNameFS = _G[fname.."Name"] if itemNameFS then itemNameFS:ClearAllPoints() itemNameFS:SetPoint("LEFT", iconFrame, "RIGHT", 10, 0) itemNameFS:SetWidth(145) itemNameFS:SetHeight(36) itemNameFS:SetJustifyH("LEFT") itemNameFS:SetJustifyV("MIDDLE") end -- 8) Buttons: right side, vertically centered with icon local need = _G[fname.."NeedButton"] local greed = _G[fname.."GreedButton"] local pass = _G[fname.."PassButton"] -- Create our own close/pass button since Blizz keeps hiding PassButton local closeBtn = CreateFrame("Button", nil, frame) closeBtn:SetWidth(20) closeBtn:SetHeight(20) closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6) closeBtn:SetFrameLevel(frame:GetFrameLevel() + 10) -- Rounded backdrop matching UI closeBtn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 8, edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 } }) local _cbg = _A and _A.closeBtnBg or { 0.5, 0.1, 0.1, 0.85 } local _cbd = _A and _A.closeBtnBorder or { 0.6, 0.2, 0.2, 0.8 } local _cbgH = _A and _A.closeBtnHoverBg or { 0.7, 0.15, 0.15, 1 } local _cbdH = _A and _A.closeBtnHoverBorder or { 0.9, 0.3, 0.3, 1 } closeBtn:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4]) closeBtn:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4]) -- "X" text local closeText = closeBtn:CreateFontString(nil, "OVERLAY") closeText:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 11, "OUTLINE") closeText:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) closeText:SetText("X") closeText:SetTextColor(0.9, 0.8, 0.8) closeBtn:SetScript("OnClick", function() local parent = this:GetParent() if parent and parent.rollID then ConfirmLootRoll(parent.rollID, 0) end end) closeBtn:SetScript("OnEnter", function() this:SetBackdropColor(_cbgH[1], _cbgH[2], _cbgH[3], _cbgH[4]) this:SetBackdropBorderColor(_cbdH[1], _cbdH[2], _cbdH[3], _cbdH[4]) GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText("放弃 / Pass") GameTooltip:Show() end) closeBtn:SetScript("OnLeave", function() this:SetBackdropColor(_cbg[1], _cbg[2], _cbg[3], _cbg[4]) this:SetBackdropBorderColor(_cbd[1], _cbd[2], _cbd[3], _cbd[4]) GameTooltip:Hide() end) -- Position need/greed to the left of close button if need and greed then greed:ClearAllPoints() greed:SetPoint("RIGHT", closeBtn, "LEFT", -6, 0) greed:SetWidth(22); greed:SetHeight(22) greed:Show() need:ClearAllPoints() need:SetPoint("RIGHT", greed, "LEFT", -4, 0) need:SetWidth(22); need:SetHeight(22) need:Show() end -- Hide Blizz pass button if pass then pass:ClearAllPoints() pass:SetPoint("CENTER", UIParent, "CENTER", 9999, 9999) pass:SetAlpha(0) end -- 9) HIDE Blizz timer completely local blizzTimer = _G[fname.."Timer"] if blizzTimer then blizzTimer:SetAlpha(0) blizzTimer:ClearAllPoints() blizzTimer:SetPoint("BOTTOMLEFT", frame, "TOPLEFT", 0, 500) end -- 10) Our own timer bar local myTimer = CreateFrame("StatusBar", nil, frame) myTimer:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 8) myTimer:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 8) myTimer:SetHeight(6) myTimer:SetMinMaxValues(0, 1) myTimer:SetValue(1) myTimer:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") myTimer:SetStatusBarColor(0.9, 0.7, 0.15) local myTimerBg = myTimer:CreateTexture(nil, "BACKGROUND") myTimerBg:SetTexture("Interface\\Buttons\\WHITE8X8") myTimerBg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1) myTimerBg:SetAllPoints(myTimer) frame.sfTimer = myTimer frame.sfTimerMax = nil -- will be set on first OnUpdate -- 11) OnUpdate: record max time on first tick, then animate frame:SetScript("OnUpdate", function() if this.rollID and this.sfTimer then local timeLeft = GetLootRollTimeLeft(this.rollID) if timeLeft and timeLeft > 0 then if not this.sfTimerMax or this.sfTimerMax < timeLeft then this.sfTimerMax = timeLeft end this.sfTimer:SetMinMaxValues(0, this.sfTimerMax) this.sfTimer:SetValue(timeLeft) else this.sfTimer:SetValue(0) end end end) -- 12) Clean button textures so they don't extend beyond bounds if need then local nRegions = {need:GetRegions()} for _, r in ipairs(nRegions) do if r:IsObjectType("Texture") then r:ClearAllPoints() r:SetAllPoints(need) end end end if greed then local gRegions = {greed:GetRegions()} for _, r in ipairs(gRegions) do if r:IsObjectType("Texture") then r:ClearAllPoints() r:SetAllPoints(greed) end end end -- 13) Three FontStrings for Need / Greed / Pass (vertical stack, full width) local textWidth = 276 local needFS = frame:CreateFontString(nil, "OVERLAY") needFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE") needFS:SetPoint("TOPLEFT", iconFrame, "BOTTOMLEFT", 0, -4) needFS:SetWidth(textWidth) needFS:SetJustifyH("LEFT") needFS:SetJustifyV("TOP") needFS:SetTextColor(1, 1, 1) frame.sfNeedFS = needFS local greedFS = frame:CreateFontString(nil, "OVERLAY") greedFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE") greedFS:SetPoint("TOPLEFT", needFS, "BOTTOMLEFT", 0, -1) greedFS:SetWidth(textWidth) greedFS:SetJustifyH("LEFT") greedFS:SetJustifyV("TOP") greedFS:SetTextColor(1, 1, 1) frame.sfGreedFS = greedFS local passFS = frame:CreateFontString(nil, "OVERLAY") passFS:SetFont(SFrames:GetFont() or "Fonts\\ARKai_T.ttf", 10, "OUTLINE") passFS:SetPoint("TOPLEFT", greedFS, "BOTTOMLEFT", 0, -1) passFS:SetWidth(textWidth) passFS:SetJustifyH("LEFT") passFS:SetJustifyV("TOP") passFS:SetTextColor(1, 1, 1) frame.sfPassFS = passFS end -- ============================================================ -- Hooks -- ============================================================ local function RestoreBackdrop(frame) if not frame.sfSkinned then return end frame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) frame:SetBackdropColor(_bg[1], _bg[2], _bg[3], _bg[4]) frame:SetBackdropBorderColor(_bd[1], _bd[2], _bd[3], _bd[4]) end local function UpdateRollQualityGlow(frame) if frame.sfQualGlow and frame.rollID then local _, _, _, quality = GetLootRollItemInfo(frame.rollID) if quality and quality > 1 then local r, g, b = GetItemQualityColor(quality) frame.sfQualGlow:SetVertexColor(r, g, b) frame.sfQualGlow:Show() else frame.sfQualGlow:Hide() end end end local Hook_GroupLootFrame_OnShow = GroupLootFrame_OnShow function GroupLootFrame_OnShow() if Hook_GroupLootFrame_OnShow then Hook_GroupLootFrame_OnShow() end StyleGroupLootFrame(this) NukeBlizzTextures(this) RestoreBackdrop(this) UpdateRollQualityGlow(this) UpdateRollTrackers() end local Hook_GroupLootFrame_Update = GroupLootFrame_Update function GroupLootFrame_Update() if Hook_GroupLootFrame_Update then Hook_GroupLootFrame_Update() end for idx = 1, 4 do local f = _G["GroupLootFrame"..idx] if f and f:IsVisible() and f.sfSkinned then NukeBlizzTextures(f) RestoreBackdrop(f) UpdateRollQualityGlow(f) end end UpdateRollTrackers() end