-- Player unit frame. Geometry from Blizzard_UnitFrame/mainline/PlayerFrame.xml, as camelot -- PlayerFrame.lua overrides it since 1.60.1.70450 (PlayerFrame_Update*Visuals). -- The art sits above the square portrait and its ring covers the corners: 3.3.5 has no -- MaskTexture to make the portrait round. -- Position is decided and saved by ForeverUI.Layout (see Layout.lua). local FRAME_WIDTH, FRAME_HEIGHT = 232, 100 local L = ForeverUI.L local ART_NORMAL = "ui-hud-unitframe-player-portraiton" -- Threat glow, not an alternate frame art: both clients pass this image to UnitFrame_Initialize -- as threatIndicator (modern FrameFlash, 3.3.5 PlayerFrameFlash). local THREAT_GLOW = "ui-hud-unitframe-player-portraiton-incombat" local HEALTH_FILL = "ui-hud-unitframe-player-portraiton-bar-health" -- The atlas has one fill per power type: it gives the color, nothing is tinted by hand. local POWER_FILL = { MANA = "ui-hud-unitframe-player-portraiton-bar-mana", RAGE = "ui-hud-unitframe-player-portraiton-bar-rage", FOCUS = "ui-hud-unitframe-player-portraiton-bar-focus", ENERGY = "ui-hud-unitframe-player-portraiton-bar-energy", RUNIC_POWER = "ui-hud-unitframe-player-portraiton-bar-runicpower", } local frame = CreateFrame("Button", "ForeverUIPlayerFrame", UIParent, "SecureUnitButtonTemplate") frame:SetWidth(FRAME_WIDTH) frame:SetHeight(FRAME_HEIGHT) frame:SetFrameStrata("LOW") -- The clickable area follows the art, not the frame: (232-198)/2 on each side. frame:SetHitRectInsets(17, 17, 14, 15) frame.unit = "player" frame:SetAttribute("unit", "player") frame:SetAttribute("*type1", "target") -- 3.3.5 uses "menu", not "togglemenu" as recent clients do: -- SecureActionButton_OnClick then calls self.menu. frame:SetAttribute("*type2", "menu") frame:RegisterForClicks("AnyUp") frame.menu = function(self) ForeverUI.UnitMenu.open(ToggleDropDownMenu, 1, nil, PlayerFrameDropDown, self, 106, 27) end local portrait = frame:CreateTexture(nil, "BACKGROUND") portrait:SetWidth(60) portrait:SetHeight(60) portrait:SetPoint("TOPLEFT", 24, -19) -- camelot: health container 124 x 15 at (87, -46), the 124 x 20 bar 3 px above it; mana bar at -- (87, -61). The mana bar is created after the health bar, so it covers the 2 px the health art -- reaches below -61 (camelot's mask cuts them; 3.3.5 has none). -- OVERLAY, above the art: camelot's bars are StatusBars, child frames drawn over the frame -- texture, whose bar slots are a dark veil (74 % opaque since 70450, 40 % before). local healthFill = frame:CreateTexture(nil, "OVERLAY") healthFill:SetPoint("TOPLEFT", 87, -43) local powerFill = frame:CreateTexture(nil, "OVERLAY") powerFill:SetPoint("TOPLEFT", 87, -61) local art = frame:CreateTexture(nil, "ARTWORK") art:SetPoint("CENTER", 0, 0) ForeverUI.SetAtlas(art, ART_NORMAL) -- camelot PlayerFrame_UpdatePlayerNameTextAnchor: centered, 116 wide, center at (149, -37) local nameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") nameText:SetWidth(116) nameText:SetHeight(12) nameText:SetJustifyH("CENTER") nameText:SetPoint("CENTER", frame, "TOPLEFT", 149, -37) -- The level lives in a child frame one level up, not in a layer of the main frame. Textures -- in one layer are ordered only by creation, and the art is re-set during play, which put it -- in front of the circle. A child frame does not depend on that order. local overlayHolder = CreateFrame("Frame", nil, frame) overlayHolder:SetAllPoints(frame) overlayHolder:SetFrameLevel(frame:GetFrameLevel() + 1) local levelCircle = overlayHolder:CreateTexture(nil, "ARTWORK") levelCircle:SetPoint("BOTTOMLEFT", 13, 7) ForeverUI.SetAtlas(levelCircle, "ui-hud-unitframe-smallcircle") local levelText = overlayHolder:CreateFontString(nil, "OVERLAY", "GameFontNormal") levelText:SetPoint("CENTER", levelCircle, "CENTER", 0, 0) -- ------------------------------------------------------------- Player states -- Everything below lives in overlayHolder, above the art, sorted by layer: status veil at the -- back, icons above, texts last. These textures are never re-set in play (only color and -- alpha change), so their order cannot drift. local STATUS_ATLAS = "ui-hud-unitframe-player-portraiton-status" local COMBAT_ICON = "ui-hud-unitframe-player-combaticon" local CORNER_ATLAS = "ui-hud-unitframe-player-portraiton-cornerembellishment" local LEADER_ICON = "ui-hud-unitframe-player-group-leadericon" -- camelot has no assistant nor master looter icon: the 3.3.5 ones (16 x 16) local GROUP_ICONS = "Interface" .. string.char(92) .. "GroupFrame" .. string.char(92) local ASSISTANT_ICON = GROUP_ICONS .. "UI-Group-AssistantIcon" local MASTER_ICON = GROUP_ICONS .. "UI-Group-MasterLooter" local REST_ATLAS = "ui-hud-unitframe-player-rest-flipbook" -- Rest sprite sheet: 7 rows of 6 cells, 42 frames played in 1.5 s. 3.3.5 has no FlipBook -- animation, so OnUpdate moves the tex coords by hand. local REST_COLUMNS, REST_ROWS, REST_FRAMES, REST_DURATION = 6, 7, 42, 1.5 -- ADD blend, as the 3.3.5 PlayerStatusTexture: the light adds up. A normal blend with a red -- tint at half alpha washes the whole frame pink. local statusTexture = overlayHolder:CreateTexture(nil, "BACKGROUND") statusTexture:SetPoint("TOPLEFT", 17, -14) ForeverUI.SetAtlas(statusTexture, STATUS_ATLAS) statusTexture:SetBlendMode("ADD") statusTexture:Hide() local threatGlow = overlayHolder:CreateTexture(nil, "BORDER") threatGlow:SetPoint("CENTER", frame, "CENTER", 0, 0) ForeverUI.SetAtlas(threatGlow, THREAT_GLOW) threatGlow:Hide() local cornerIcon = overlayHolder:CreateTexture(nil, "ARTWORK") cornerIcon:SetPoint("TOPLEFT", 58, -53) ForeverUI.SetAtlas(cornerIcon, CORNER_ATLAS) local combatIcon = overlayHolder:CreateTexture(nil, "OVERLAY") combatIcon:SetPoint("TOPLEFT", 64, -62) ForeverUI.SetAtlas(combatIcon, COMBAT_ICON) combatIcon:Hide() local leaderIcon = overlayHolder:CreateTexture(nil, "OVERLAY") leaderIcon:SetPoint("TOPLEFT", 86, -10) ForeverUI.SetAtlas(leaderIcon, LEADER_ICON) leaderIcon:Hide() -- Master looter, 40 right and 2 up from the leader icon, as PlayerFrame_ToPlayerArt places -- PlayerMasterIcon next to PlayerLeaderIcon local masterIcon = overlayHolder:CreateTexture(nil, "OVERLAY") masterIcon:SetTexture(MASTER_ICON) masterIcon:SetWidth(16) masterIcon:SetHeight(16) masterIcon:SetPoint("TOPLEFT", leaderIcon, "TOPLEFT", 40, 2) masterIcon:Hide() local restTexture = overlayHolder:CreateTexture(nil, "OVERLAY") restTexture:SetPoint("TOPLEFT", 59, -1) restTexture:SetWidth(30) restTexture:SetHeight(30) restTexture:Hide() local healthText = overlayHolder:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") healthText:SetPoint("CENTER", frame, "TOPLEFT", 87 + 62, -43 - 10) healthText:Hide() local powerText = overlayHolder:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") powerText:SetPoint("CENTER", frame, "TOPLEFT", 87 + 62, -61 - 5) powerText:Hide() frame.overlayHolder = overlayHolder frame.portrait = portrait frame.healthFill = healthFill frame.powerFill = powerFill frame.art = art frame.threatGlow = threatGlow frame.nameText = nameText frame.levelText = levelText frame.statusTexture = statusTexture frame.combatIcon = combatIcon frame.cornerIcon = cornerIcon frame.leaderIcon = leaderIcon frame.masterIcon = masterIcon frame.restTexture = restTexture frame.healthText = healthText frame.powerText = powerText local VEHICLE_ART = "ui-hud-unitframe-player-portraiton-vehicle" local VEHICLE_HEALTH_WIDTH = 118 -- Unit shown by the frame: the vehicle while in one, as PlayerFrame_ToVehicleArt in 3.3.5. -- The modern client also swaps the art for the vehicle variant, a few pixels wider. local function displayedUnit() return frame.displayUnit or "player" end local function updateVehicleArt() local inVehicle = (UnitHasVehicleUI and UnitHasVehicleUI("player") and UnitExists("vehicle")) and true or false frame.inVehicle = inVehicle frame.displayUnit = inVehicle and "vehicle" or "player" art:ClearAllPoints() if inVehicle then ForeverUI.SetAtlas(art, VEHICLE_ART) art:SetPoint("CENTER", -2, 0) else ForeverUI.SetAtlas(art, ART_NORMAL) art:SetPoint("CENTER", 0, 0) end end local function updateHealth() local unit = displayedUnit() local maximum = UnitHealthMax(unit) local fraction = 0 if maximum and maximum > 0 then fraction = UnitHealth(unit) / maximum end ForeverUI.SetAtlasFill(healthFill, HEALTH_FILL, fraction, frame.inVehicle and VEHICLE_HEALTH_WIDTH or nil) end local function updatePower() local unit = displayedUnit() local powerType, powerToken = UnitPowerType(unit) local atlas = POWER_FILL[powerToken or ""] or POWER_FILL.MANA local maximum = UnitPowerMax(unit, powerType) local fraction = 0 if maximum and maximum > 0 then fraction = UnitPower(unit, powerType) / maximum end ForeverUI.SetAtlasFill(powerFill, atlas, fraction, frame.inVehicle and VEHICLE_HEALTH_WIDTH or nil) end local function updateName() nameText:SetText(UnitName(displayedUnit())) end local function updateLevel() levelText:SetText(UnitLevel("player")) end local function updatePortrait() SetPortraitTexture(portrait, displayedUnit()) end -- The glow follows camelot's observed behavior, not UnitFrame_UpdateThreatIndicator (lit above -- zero, tinted by GetThreatStatusColor): it is always red and only its alpha changes, faint -- once engaged, full when an enemy targets you. local THREAT_ALPHA_ENGAGED, THREAT_ALPHA_TARGETED = 0.45, 1.0 -- Being targeted cannot rely on the threat table alone: on a private server -- UnitThreatSituation("player") often returns nil and UNIT_THREAT_SITUATION_UPDATE never -- fires. So also check whether the hostile target targets us. local function playerIsTargeted() local status = UnitThreatSituation and UnitThreatSituation("player") if status and status >= 2 then return true end if UnitExists("target") and UnitCanAttack("player", "target") and UnitIsUnit("targettarget", "player") then return true end return false end local updateThreat updateThreat = function() if UnitIsDeadOrGhost and UnitIsDeadOrGhost("player") then threatGlow:Hide() return end local status = UnitThreatSituation and UnitThreatSituation("player") local targeted = playerIsTargeted() local engaged = frame.inCombat or frame.onHateList or (status and status > 0) if targeted or engaged then threatGlow:SetVertexColor(1.0, 0.0, 0.0) threatGlow:SetAlpha(targeted and THREAT_ALPHA_TARGETED or THREAT_ALPHA_ENGAGED) threatGlow:Show() else threatGlow:Hide() end end -- Player state, in the order of the modern PlayerFrame_UpdateStatus (camelot 70450): nothing in -- a vehicle, otherwise combat, then the hate list, then rest. 3.3.5 put rest first: in an inn, -- the veil was gold even in combat. local restElapsed = 0 local function updateStatus() if UnitHasVehicleUI and UnitHasVehicleUI("player") then statusTexture:Hide() combatIcon:Hide() restTexture:Hide() frame.resting = false return end -- The client separates two states that UnitAffectingCombat merges: -- inCombat -- PLAYER_ENTER_COMBAT: you attack (right-click on an enemy). -- Red veil and combat icon. -- onHateList -- PLAYER_REGEN_DISABLED: you are on someone's hate list. Icon only. -- The onHateList branch leaves the veil alone, as both clients do: it keeps its color until -- combat really ends. if frame.inCombat then statusTexture:SetVertexColor(1.0, 0.0, 0.0) statusTexture:Show() combatIcon:Show() cornerIcon:Hide() restTexture:Hide() frame.resting = false elseif frame.onHateList then combatIcon:Show() cornerIcon:Hide() restTexture:Hide() frame.resting = false elseif IsResting() then statusTexture:SetVertexColor(1.0, 0.88, 0.25) statusTexture:Show() restTexture:Show() combatIcon:Hide() cornerIcon:Show() frame.resting = true restElapsed = 0 else statusTexture:Hide() combatIcon:Hide() cornerIcon:Show() restTexture:Hide() frame.resting = false end end -- Leader, or raid assistant in its place (the raid list's rank icon); master looter as -- PlayerFrame_UpdatePartyLeader shows it local function updateLeader() if IsPartyLeader and IsPartyLeader() then ForeverUI.SetAtlas(leaderIcon, LEADER_ICON) leaderIcon:Show() elseif GetNumRaidMembers() > 0 and IsRaidOfficer() then leaderIcon:SetTexture(ASSISTANT_ICON) leaderIcon:SetTexCoord(0, 1, 0, 1) leaderIcon:SetWidth(16) leaderIcon:SetHeight(16) leaderIcon:Show() else leaderIcon:Hide() end local _, master = GetLootMethod() if master == 0 and (GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0) then masterIcon:Show() else masterIcon:Hide() end end -- Bar texts follow the game settings: playerStatusText "1" means always shown, and -- statusTextPercentage picks a percentage or raw values. On hover they always show. local function formatValue(value, maximum) if not maximum or maximum <= 0 then return "" end if GetCVarBool and GetCVarBool("statusTextPercentage") then return tostring(math.ceil(value / maximum * 100)) .. "%" end return value .. " / " .. maximum end local function updateTexts() local always = GetCVar and GetCVar("playerStatusText") == "1" if not (always or frame.hovered) then healthText:Hide() powerText:Hide() return end -- the values of the bars: the vehicle's while in one local unit = displayedUnit() local powerType = UnitPowerType(unit) healthText:SetText(formatValue(UnitHealth(unit), UnitHealthMax(unit))) powerText:SetText(formatValue(UnitPower(unit, powerType), UnitPowerMax(unit, powerType))) healthText:Show() powerText:Show() end ForeverUI.PlayerFrameUpdateThreat = function() updateThreat() end -- Some classes change the frame art: camelot switches to the "ClassResource" variant when a -- class resource shows under the bars. normalAtlas: atlas used outside a vehicle. ForeverUI.PlayerFrameSetArt = function(normalAtlas) if normalAtlas then ART_NORMAL = normalAtlas updateVehicleArt() end end -- /fui debug: prints what the client really returns, to tune the glow on observed values. ForeverUI.PlayerFrameDebug = function() local status = UnitThreatSituation and UnitThreatSituation("player") local target = UnitExists("target") and UnitName("target") or L.PLAYERFRAME_DEBUG_NO_TARGET DEFAULT_CHAT_FRAME:AddMessage(string.format( "|cff66ccffForeverUI|r " .. L.PLAYERFRAME_DEBUG, tostring(frame.inCombat), tostring(frame.onHateList), tostring(status), target, tostring(UnitExists("target") and UnitCanAttack("player", "target") or false), tostring(UnitIsUnit("targettarget", "player")), tostring(threatGlow:IsShown()), threatGlow:GetAlpha())) end frame:SetScript("OnEnter", function(self) self.hovered = true updateTexts() end) frame:SetScript("OnLeave", function(self) self.hovered = false updateTexts() end) -- One OnUpdate for two animations: the rest sprite sheet, and the status veil that pulses -- like the original frame's. ForeverUI.SetAtlas(restTexture, REST_ATLAS, true) frame:SetScript("OnUpdate", function(self, elapsed) if self.resting then restElapsed = restElapsed + elapsed local entry = ForeverUI.AtlasEntry(REST_ATLAS) if entry then local index = math.floor(restElapsed / (REST_DURATION / REST_FRAMES)) % REST_FRAMES local column = index - math.floor(index / REST_COLUMNS) * REST_COLUMNS local row = math.floor(index / REST_COLUMNS) local du = (entry[3] - entry[2]) / REST_COLUMNS local dv = (entry[5] - entry[4]) / REST_ROWS restTexture:SetTexCoord(entry[2] + column * du, entry[2] + (column + 1) * du, entry[4] + row * dv, entry[4] + (row + 1) * dv) end end if statusTexture:IsShown() then local pulse = 0.35 + 0.25 * math.sin(GetTime() * 3) statusTexture:SetAlpha(pulse) end -- "Targeted" is read from the target, not from an event: re-check it three times per second -- while engaged. self.threatElapsed = (self.threatElapsed or 0) + elapsed if self.threatElapsed > 0.3 then self.threatElapsed = 0 if frame.inCombat or frame.onHateList or threatGlow:IsShown() then ForeverUI.PlayerFrameUpdateThreat() end end end) -- The default frame is disabled, not just hidden: its own events (mounts, vehicles, group) -- would show it again. local function hideDefaultPlayerFrame() if InCombatLockdown() or not PlayerFrame then return end ForeverUI.Suppress(PlayerFrame) end local function updateAll() updateVehicleArt() updateHealth() updatePower() updateName() updateLevel() updatePortrait() updateThreat() updateStatus() updateLeader() updateTexts() end frame:SetScript("OnEvent", function(self, event, unit) if event == "PLAYER_ENTERING_WORLD" then self.onHateList = UnitAffectingCombat("player") hideDefaultPlayerFrame() updateAll() return end if event == "PLAYER_ENTER_COMBAT" then self.inCombat = true updateStatus() updateThreat() return end if event == "PLAYER_LEAVE_COMBAT" then self.inCombat = false updateStatus() updateThreat() return end if event == "PLAYER_REGEN_DISABLED" or event == "PLAYER_REGEN_ENABLED" then self.onHateList = (event == "PLAYER_REGEN_DISABLED") -- Skipped when PLAYER_ENTERING_WORLD came in combat (reload, reconnect): done now if event == "PLAYER_REGEN_ENABLED" then hideDefaultPlayerFrame() end updateStatus() updateThreat() return end if event == "UNIT_THREAT_SITUATION_UPDATE" or event == "PLAYER_TARGET_CHANGED" or event == "UNIT_TARGET" then updateThreat() return end if event == "PLAYER_UPDATE_RESTING" then updateStatus() return end if event == "PARTY_LEADER_CHANGED" or event == "PARTY_MEMBERS_CHANGED" or event == "PARTY_LOOT_METHOD_CHANGED" or event == "RAID_ROSTER_UPDATE" then updateLeader() return end if event == "CVAR_UPDATE" then updateTexts() return end if event == "PLAYER_LEVEL_UP" then updateLevel() return end -- In a vehicle the bars show "vehicle": its health and power events carry that token if unit ~= "player" and unit ~= displayedUnit() then return end if event == "UNIT_ENTERED_VEHICLE" or event == "UNIT_EXITED_VEHICLE" then updateVehicleArt() updateHealth() updatePower() updateName() updatePortrait() updateStatus() elseif event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then updateHealth() updateTexts() elseif event == "UNIT_NAME_UPDATE" then updateName() elseif event == "UNIT_PORTRAIT_UPDATE" then updatePortrait() elseif event == "UNIT_LEVEL" then updateLevel() else updatePower() updateTexts() end end) frame:RegisterEvent("PLAYER_ENTERING_WORLD") frame:RegisterEvent("PLAYER_REGEN_DISABLED") frame:RegisterEvent("PLAYER_ENTER_COMBAT") frame:RegisterEvent("PLAYER_LEAVE_COMBAT") frame:RegisterEvent("PLAYER_REGEN_ENABLED") frame:RegisterEvent("PLAYER_LEVEL_UP") frame:RegisterEvent("UNIT_HEALTH") frame:RegisterEvent("UNIT_MAXHEALTH") frame:RegisterEvent("UNIT_NAME_UPDATE") frame:RegisterEvent("UNIT_PORTRAIT_UPDATE") frame:RegisterEvent("UNIT_LEVEL") frame:RegisterEvent("UNIT_DISPLAYPOWER") -- 3.3.5 has no UNIT_POWER: each power type has its own event. frame:RegisterEvent("UNIT_MANA") frame:RegisterEvent("UNIT_RAGE") frame:RegisterEvent("UNIT_FOCUS") frame:RegisterEvent("UNIT_ENERGY") frame:RegisterEvent("UNIT_RUNIC_POWER") frame:RegisterEvent("UNIT_MAXMANA") frame:RegisterEvent("UNIT_MAXRAGE") frame:RegisterEvent("UNIT_MAXFOCUS") frame:RegisterEvent("UNIT_MAXENERGY") frame:RegisterEvent("UNIT_MAXRUNIC_POWER") frame:RegisterEvent("PLAYER_UPDATE_RESTING") frame:RegisterEvent("PARTY_LEADER_CHANGED") frame:RegisterEvent("PARTY_MEMBERS_CHANGED") frame:RegisterEvent("PARTY_LOOT_METHOD_CHANGED") frame:RegisterEvent("RAID_ROSTER_UPDATE") frame:RegisterEvent("UNIT_ENTERED_VEHICLE") frame:RegisterEvent("UNIT_EXITED_VEHICLE") frame:RegisterEvent("CVAR_UPDATE") frame:RegisterEvent("UNIT_THREAT_SITUATION_UPDATE") frame:RegisterEvent("PLAYER_TARGET_CHANGED") frame:RegisterEvent("UNIT_TARGET") ForeverUI.PlayerFrame = frame ForeverUI.Layout.Register(frame, "playerframe", L.PLAYERFRAME_EDIT_LABEL, "TOPLEFT", "TOPLEFT", 10, -10)