local _G = ShaguTweaks.GetGlobalEnv() local T = ShaguTweaks.T local API = ShaguTweaks.API local module = ShaguTweaks:register({ title = T["Standalone Player Frame"], description = T["Separates the existing player portrait, name, health and power bars. Hold Ctrl+Shift+Alt to drag each piece; right-click with those keys held to reset it. Big Health also enlarges the power bar."], expansions = { ["vanilla"] = true }, category = T["Unit Frames"], enabled = nil, }) local function Unlocked() return API.IsShiftKeyDown() and API.IsControlKeyDown() and API.IsAltKeyDown() end local function Position(holder, point) holder:ClearAllPoints() holder:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", point.x, point.y) end local function StopDrag(holder) if not holder.dragging then return end holder:StopMovingOrSizing() holder:SetUserPlaced(false) holder.dragging = nil holder.ignoreClick = true local x, y = holder:GetLeft(), holder:GetTop() if x and y then holder.positions[holder.key] = { x = x, y = y } Position(holder, holder.positions[holder.key]) end end local function MouseDown() local holder = this.standaloneHolder holder.ignoreClick = nil if Unlocked() and arg1 == "RightButton" then StopDrag(holder) holder.positions[holder.key] = nil Position(holder, holder.defaultPosition) holder.ignoreClick = true elseif not Unlocked() and this.standaloneMouseDown then this.standaloneMouseDown() end end local function DragStart() if not Unlocked() then return end local holder = this.standaloneHolder holder.dragging = true holder:StartMoving() end local function DragStop() StopDrag(this.standaloneHolder) end local function UnitClick() local holder = this.standaloneHolder if holder.ignoreClick or holder.dragging or Unlocked() then return end if arg1 == "RightButton" and not SpellIsTargeting() then ToggleDropDownMenu(1, nil, PlayerFrameDropDown, holder:GetName(), 0, 0) elseif this.standaloneMouseUp then this.standaloneMouseUp() else PlayerFrame_OnClick(arg1) end end local function BindHandle(handle, holder) handle.standaloneHolder = holder handle.standaloneMouseDown = handle:GetScript("OnMouseDown") handle:RegisterForDrag("LeftButton") handle:EnableMouse(true) handle:SetScript("OnMouseDown", MouseDown) handle:SetScript("OnDragStart", DragStart) handle:SetScript("OnDragStop", DragStop) if handle:GetObjectType() == "Button" then handle:RegisterForClicks("LeftButtonUp", "RightButtonUp") handle:SetScript("OnClick", UnitClick) else handle.standaloneMouseUp = handle:GetScript("OnMouseUp") handle:SetScript("OnMouseUp", UnitClick) end end local function MoveRegion(region, holder, x, y) if not region then return end region:SetParent(holder) region:ClearAllPoints() region:SetPoint("TOPLEFT", holder, "TOPLEFT", x, y) end module.enable = function(self) if self.scheduled then return end self.scheduled = true local function Setup() if self.installed then return end if not PlayerFrame or not PlayerPortrait or not PlayerFrameTexture or not PlayerFrameHealthBar or not PlayerFrameManaBar then return end if not PlayerFrameHealthBar:GetLeft() or not PlayerFrameHealthBar:GetTop() or not PlayerPortrait:GetLeft() or not PlayerPortrait:GetTop() then return end self.installed = true ShaguTweaks_config.StandalonePlayerFrame = ShaguTweaks_config.StandalonePlayerFrame or {} local positions = ShaguTweaks_config.StandalonePlayerFrame local scale = PlayerFrame:GetEffectiveScale() / UIParent:GetEffectiveScale() local big = ShaguTweaks_config[T["Unit Frame Big Health for Player"]] == 1 local width = PlayerFrameHealthBar:GetWidth() local healthHeight = PlayerFrameHealthBar:GetHeight() local manaHeight = big and healthHeight or PlayerFrameManaBar:GetHeight() local left = PlayerFrameHealthBar:GetLeft() - 4 local top = PlayerFrameHealthBar:GetTop() + 3 local portraitLeft, portraitTop = PlayerPortrait:GetLeft() - 8, PlayerPortrait:GetTop() + 4 local function Holder(key, w, h, x, y) local holder = CreateFrame("Button", "ShaguTweaksPlayer" .. key, UIParent) holder:SetScale(scale) holder:SetWidth(w) holder:SetHeight(h) holder:SetMovable(true) holder:SetClampedToScreen(true) holder:SetFrameStrata(PlayerFrame:GetFrameStrata()) holder:SetFrameLevel(PlayerFrame:GetFrameLevel() + 3) holder.unit = "player" holder.key, holder.positions = key, positions holder.defaultPosition = { x = x, y = y } Position(holder, positions[key] or holder.defaultPosition) BindHandle(holder, holder) holder:SetScript("OnHide", function() StopDrag(this) end) holder:SetScript("OnReceiveDrag", PlayerFrame_OnReceiveDrag) if UnitFrame_OnEnter and UnitFrame_OnLeave and UnitFrame_OnUpdate then holder:SetScript("OnEnter", function() if Unlocked() then return end UnitFrame_OnEnter() if this.updateTooltip then this:SetScript("OnUpdate", UnitFrame_OnUpdate) end end) holder:SetScript("OnLeave", function() UnitFrame_OnLeave() this:SetScript("OnUpdate", nil) end) end return holder end local portrait = Holder("Portrait", 80, 80, portraitLeft, portraitTop) local name = Holder("Name", width + 8, 23, left, top + 25) local health = Holder("Health", width + 8, healthHeight + 6, left, top) local mana = Holder("Power", width + 8, manaHeight + 6, left, top - healthHeight - 8) ShaguTweaks.StandalonePlayerFrame = { portrait = portrait, name = name, health = health, power = mana } -- Split the native artwork before changing its source texture's anchors. -- Preserve its UV orientation (the player's stock artwork is mirrored). local art = PlayerFrameTexture local path = art:GetTexture() local u, v, unused, bottom, right = art:GetTexCoord() local artWidth, artHeight = PlayerFrame:GetWidth(), PlayerFrame:GetHeight() local r, g, b, a = art:GetVertexColor() local function Crop(holder, x, y, w, h) local texture = holder:CreateTexture(nil, "OVERLAY") texture:SetTexture(path) texture:SetTexCoord(u + (right-u)*x/artWidth, u + (right-u)*(x+w)/artWidth, v + (bottom-v)*y/artHeight, v + (bottom-v)*(y+h)/artHeight) texture:SetAllPoints(holder) texture:SetVertexColor(r, g, b, a) return texture end -- Keep the native level badge on the left. Complete the right side from -- the clean upper-left quadrant, leaving space for the live resting icon -- without mirroring a second level badge into the artwork. The native -- resting/combat icon shares the original left badge's position. portrait.art = Crop(portrait, 34, 8, 40, 72) portrait.artRight = Crop(portrait, 74, 8, -40, 36) portrait.artBottomRight = Crop(portrait, 74, 44, -40, -36) local function Piece(texture, x, y, height) texture:ClearAllPoints() texture:SetPoint("TOPLEFT", portrait, "TOPLEFT", x, y) texture:SetWidth(40) texture:SetHeight(height) end Piece(portrait.art, 0, 0, 72) Piece(portrait.artRight, 40, 0, 36) Piece(portrait.artBottomRight, 40, -36, 36) -- The native lower-left quadrant includes a dark rectangular shadow -- beside the level badge. Use the same clean quadrant as the lower right, -- then restore only the badge circle, not its surrounding shadow. portrait.badge = Crop(portrait, 37, 50, 32, 32) portrait.badge:ClearAllPoints() portrait.badge:SetPoint("TOPLEFT", portrait, "TOPLEFT", 3, -42) portrait.badge:SetWidth(32) portrait.badge:SetHeight(32) if type(portrait.badge.SetMask) == "function" then portrait.art:SetTexCoord(u + (right-u)*34/artWidth, u + (right-u)*74/artWidth, v + (bottom-v)*8/artHeight, v + (bottom-v)*44/artHeight) Piece(portrait.art, 0, 0, 36) portrait.artBottomLeft = Crop(portrait, 34, 44, 40, -36) Piece(portrait.artBottomLeft, 0, -36, 36) portrait.badge:SetMask("Interface\\CharacterFrame\\TempPortraitAlphaMask") -- Keep the badge above every ring piece but below the number/state icons. portrait.badgeOverlay = CreateFrame("Frame", nil, portrait) portrait.badgeOverlay:SetAllPoints(portrait) portrait.badgeOverlay:SetFrameLevel(portrait:GetFrameLevel() + 1) portrait.badge:SetParent(portrait.badgeOverlay) else -- Older clients retain the original left half and its native badge. portrait.badge:Hide() end -- The combined frame texture has a portrait arc at the left ends of its -- bars. A separate Blizzard nine-slice edge gives clean rounded corners -- at both normal and Big Health heights, without clipping the bar fill. local border = { edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 12, tile = true, tileSize = 8, insets = { left = 3, right = 3, top = 3, bottom = 3 }, } local color = ShaguTweaks.UIBorder for _, holder in ipairs({ name, health, mana }) do holder:SetBackdrop(border) holder:SetBackdropBorderColor(color and color.r or 1, color and color.g or 1, color and color.b or 1, 1) end -- Keep status/role indicators with the portrait. Their native event -- handlers and parent visibility continue running unchanged. local indicators = { "PlayerHitIndicator", "PlayerPVPIcon", "PlayerPVPIconHitArea", "PlayerLeaderIcon", "PlayerMasterIcon", "PlayerRestIcon", "PlayerAttackIcon", "PlayerAttackBackground", "PlayerStatusGlow", "PlayerFrameGroupIndicator" } local indicatorPoints = {} for i = 1, table.getn(indicators) do local region = _G[indicators[i]] if region and region:GetLeft() and region:GetTop() then indicatorPoints[i] = { region:GetLeft() - portraitLeft, region:GetTop() - portraitTop } end end -- Snapshot first: some indicators are anchored to other indicators. for i = 1, table.getn(indicators) do local point = indicatorPoints[i] if point then MoveRegion(_G[indicators[i]], portrait, point[1], point[2]) end end MoveRegion(PlayerPortrait, portrait, 8, -4) if _G.PlayerLevelText then -- Put the native text above the ring and portrait, centered over the -- native left-hand badge. The stock level-update code keeps owning it. local levelX, levelY = 19, -58 if PlayerLevelText.GetCenter then local x, y = PlayerLevelText:GetCenter() if x and y then levelX, levelY = x - portraitLeft, y - portraitTop end end portrait.levelOverlay = CreateFrame("Frame", nil, portrait) portrait.levelOverlay:SetAllPoints(portrait) portrait.levelOverlay:SetFrameLevel(portrait:GetFrameLevel() + 5) PlayerLevelText:SetParent(portrait.levelOverlay) PlayerLevelText:ClearAllPoints() PlayerLevelText:SetPoint("CENTER", portrait.levelOverlay, "TOPLEFT", levelX, levelY) PlayerLevelText:SetDrawLayer("BACKGROUND") PlayerLevelText:SetAlpha(1) PlayerLevelText:SetText(UnitLevel("player")) PlayerLevelText:Show() -- Keep the level above the ring but BELOW its native state icons. -- Their existing Show/Hide logic then covers the number while resting -- or in combat and reveals it again without adding another event loop. for _, key in ipairs({ "PlayerRestIcon", "PlayerAttackIcon", "PlayerAttackBackground", "PlayerStatusGlow" }) do local indicator = _G[key] if indicator then indicator:SetParent(portrait.levelOverlay) end end end local playerName = PlayerFrame.name or _G.PlayerName if playerName then playerName:SetParent(name) playerName:ClearAllPoints() playerName:SetPoint("CENTER", name, "CENTER", 0, 0) end MoveRegion(_G.PlayerFrameNameBackground, name, 4, -2) -- Big Health deliberately hides the stock name background. Keep its -- color source for that module, but draw a separate class strip here. name.classBackground = name:CreateTexture(nil, "BACKGROUND") name.classBackground:SetPoint("TOPLEFT", name, "TOPLEFT", 4, -3) name.classBackground:SetPoint("BOTTOMRIGHT", name, "BOTTOMRIGHT", -4, 3) local healthTexture = PlayerFrameHealthBar:GetStatusBarTexture() name.classBackground:SetTexture(healthTexture and healthTexture:GetTexture() or "Interface\\TargetingFrame\\UI-StatusBar") local _, class = UnitClass("player") local palette = ShaguTweaks.ModernClassColors or RAID_CLASS_COLORS local classColor = palette and palette[class] local cr, cg, cb = PlayerFrameHealthBar:GetStatusBarColor() name.classBackground:SetVertexColor(classColor and classColor.r or cr, classColor and classColor.g or cg, classColor and classColor.b or cb, 1) if ShaguTweaks_config[T["Hide Player Name"]] == 1 then name:Hide() end local function DetachBar(bar, holder, h) -- Vanilla's UnitFrameManaBar_Update calls UpdateManaType(GetParent()). -- Keep that parent contract as well as the bar's own unit/events. holder.healthbar = PlayerFrameHealthBar holder.manabar = PlayerFrameManaBar bar:SetParent(holder) bar:ClearAllPoints() bar:SetPoint("TOPLEFT", holder, "TOPLEFT", 4, -3) bar:SetWidth(width) bar:SetHeight(h) BindHandle(bar, holder) local background = bar:CreateTexture(nil, "BACKGROUND") background:SetTexture(0, 0, 0, .5) background:SetAllPoints(bar) if bar.TextString then bar.TextString:SetParent(bar) bar.TextString:ClearAllPoints() bar.TextString:SetPoint("CENTER", bar, "CENTER", 0, 0) end end DetachBar(PlayerFrameHealthBar, health, healthHeight) DetachBar(PlayerFrameManaBar, mana, manaHeight) if _G.PlayerFrameAlternatePowerBar then local alternate = _G.PlayerFrameAlternatePowerBar alternate:SetParent(mana) alternate:ClearAllPoints() alternate:SetPoint("TOPLEFT", mana, "BOTTOMLEFT", 4, -2) alternate:SetWidth(width) end -- Hide the now-redundant shared backing, without hiding PlayerFrame: its -- native events, pet frame, menu and status scripts still need to run. local function Suppress(region) if not region then return end local setAlpha = region.SetAlpha setAlpha(region, 0) ShaguTweaks.hooksecurefunc(region, "SetAlpha", function(object) setAlpha(object, 0) end) end Suppress(PlayerFrameTexture) Suppress(_G.PlayerFrameBackground) Suppress(_G.PlayerFrameNameBackground) PlayerFrame:EnableMouse(false) if PlayerStatusTexture then MoveRegion(PlayerStatusTexture, portrait, 1, 0) local function CropStatus() PlayerStatusTexture:SetTexCoord(0, 72/256, 0, 66/128) PlayerStatusTexture:SetWidth(72) PlayerStatusTexture:SetHeight(66) end if type(PlayerStatusTexture.SetMask) == "function" then PlayerStatusTexture:SetMask("Interface\\CharacterFrame\\TempPortraitAlphaMask") end ShaguTweaks.hooksecurefunc("PlayerFrame_UpdateStatus", CropStatus) CropStatus() end end -- Wait until all modules and their one-shot OnUpdates have initialized. -- This also captures the player's restored layout-cache position. local ready = CreateFrame("Frame") ready:RegisterEvent("PLAYER_ENTERING_WORLD") ready:SetScript("OnEvent", function() this:UnregisterAllEvents() this:SetScript("OnUpdate", function() this:SetScript("OnUpdate", nil) this:Hide() if API.timer then API.Defer(Setup) else -- A second frame lets the other modules' one-shot updates finish. this:SetScript("OnUpdate", function() this:SetScript("OnUpdate", nil) Setup() end) this:Show() end end) end) end