Files
moon-well-ui/data/addon/ForeverUI/PlayerFrame.lua
T
Adrestie eb681aa9cd Aucun texte en dur : anglais en base, francais, langues extensibles
- socle : Textes.lua (ForeverUI.L) et ForeverUIGlueTextes.lua (G.L) ; les
  textes dans Textes_enUS / Textes_frFR (325 cles en jeu) et
  ForeverUIGlueTextes_enUS / _frFR (41 cles aux ecrans d'accueil) ; une cle
  absente prend l'anglais, une langue sans fichier lit l'anglais ; ajouter
  une langue = un fichier de plus
- les textes que le client 3.3.5 connait se lisent dans ses globales
  (GlobalStrings, GlueStrings en glue) ; les textes de secours sont retires
- tous les fichiers repris : libelles, infobulles, mode edition, messages et
  aide de /fui, erreurs internes ; .toc : Notes + Notes-frFR
- selection des personnages : race et classe traduites ramenees a leur jeton
  par les tables de langue (GetAvailableRaces / GetAvailableClasses font
  planter le client hors de la creation)
- tools/cartes_instances.py : noms des instances dans toutes les langues de
  la LibBabble-Zone de WDM (portails de la carte sur tout client)
- banc : toute cle employee existe en anglais et en francais, memes cles dans
  les deux, bascule frFR / deDE essayee ; chaines 3.3.5 ajoutees au faux client
- README : etat des sections mis a jour, langues, glue, deploiement --glue

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 03:22:30 +02:00

576 lines
19 KiB
Lua

-- ForeverUI : le cadre joueur.
--
-- GEOMETRIE. Reprise telle quelle de Blizzard_UnitFrame/mainline/PlayerFrame.xml
-- (la saveur [Family] que charge camelot) :
-- cadre 232 x 100
-- portrait 60 x 60 ancre TOPLEFT (24, -19)
-- art du cadre 198 x 71 centre
-- barre de vie 124 x 20 ancre TOPLEFT (85, -40)
-- barre de ress.124 x 10 ancre TOPLEFT (85, -61)
-- nom 96 x 12 ancre TOPLEFT (88, -27)
-- cercle niveau 39 x 39 ancre BOTTOMLEFT (13, 7)
--
-- L'art est pose au-dessus du portrait, comme dans l'original : le trou du
-- cadre est rond, le portrait est carre, et c'est l'anneau qui recouvre les
-- coins -- un client 3.3.5 n'a pas de MaskTexture pour faire autrement.
--
-- POSITION. Le cadre ne se pose pas lui-meme : il s'enregistre aupres de
-- ForeverUI.Layout, qui decide et retient. Voir Layout.lua.
local FRAME_WIDTH, FRAME_HEIGHT = 232, 100
local L = ForeverUI.L
local ART_NORMAL = "ui-hud-unitframe-player-portraiton"
-- "-incombat" n'est PAS un art de cadre de rechange : dans les deux clients,
-- cette image est passee a UnitFrame_Initialize en position threatIndicator
-- (FrameFlash cote moderne, PlayerFrameFlash en 3.3.5). C'est la lueur de
-- menace, montree seulement quand le joueur tient l'aggro, et teintee selon le
-- niveau de menace. L'art du cadre, lui, ne change jamais.
local THREAT_GLOW = "ui-hud-unitframe-player-portraiton-incombat"
local HEALTH_FILL = "ui-hud-unitframe-player-portraiton-bar-health"
-- L'atlas porte un remplissage distinct par type de ressource : c'est lui qui
-- donne la couleur, on ne teinte rien a la main.
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")
-- La zone cliquable suit l'art, pas le cadre : (232-198)/2 de chaque cote.
frame:SetHitRectInsets(17, 17, 14, 15)
frame.unit = "player"
frame:SetAttribute("unit", "player")
frame:SetAttribute("*type1", "target")
-- En 3.3.5 l'attribut vaut "menu", pas "togglemenu" comme sur les clients
-- recents : SecureActionButton_OnClick appelle alors self.menu.
frame:SetAttribute("*type2", "menu")
frame:RegisterForClicks("AnyUp")
frame.menu = function(self)
ForeverUI.MenuUnite.ouvrir(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)
local healthFill = frame:CreateTexture(nil, "BORDER")
healthFill:SetPoint("TOPLEFT", 85, -40)
local powerFill = frame:CreateTexture(nil, "BORDER")
powerFill:SetPoint("TOPLEFT", 85, -61)
local art = frame:CreateTexture(nil, "ARTWORK")
art:SetPoint("CENTER", 0, 0)
ForeverUI.SetAtlas(art, ART_NORMAL)
local nameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
nameText:SetWidth(96)
nameText:SetHeight(12)
nameText:SetJustifyH("LEFT")
nameText:SetPoint("TOPLEFT", 88, -27)
-- Le niveau vit dans un cadre fils d'un niveau au-dessus, pas dans un calque
-- du cadre principal. Deux textures d'un meme calque ne sont ordonnees que par
-- leur ordre de creation, et l'art du cadre est re-affecte en cours de partie
-- (bascule combat) : il repassait alors devant le cercle, et seul un
-- rechargement remettait les choses dans l'ordre. Un cadre fils ne depend pas
-- de cet ordre.
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)
-- ------------------------------------------------------------- etats du joueur
-- Tout ce qui suit vit dans overlayHolder, donc au-dessus de l'art, et chaque
-- element est range par calque : le voile d'etat en fond, les icones au-dessus,
-- les textes en dernier. Aucune de ces textures n'est reaffectee en jeu (on ne
-- change que couleur et transparence), donc l'ordre ne peut plus glisser.
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"
local REST_ATLAS = "ui-hud-unitframe-player-rest-flipbook"
-- Le repos est une planche d'images : 7 rangees de 6 vignettes, 42 en tout,
-- parcourues en 1,5 s. Le client 3.3.5 n'a pas d'animation FlipBook, donc on
-- deplace le rectangle a la main dans un OnUpdate.
local REST_COLUMNS, REST_ROWS, REST_FRAMES, REST_DURATION = 6, 7, 42, 1.5
-- Le voile est une image claire : melangee normalement, teintee en rouge et
-- posee a demi-transparence, elle delave tout le cadre en rose. Le client
-- 3.3.5 declare son propre voile en alphaMode="ADD" (PlayerStatusTexture), et
-- c'est bien ainsi qu'il faut le poser : la lumiere s'ajoute au lieu de laver.
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", -1.5, 1)
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()
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", 85 + 62, -40 - 10)
healthText:Hide()
local powerText = overlayHolder:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
powerText:SetPoint("CENTER", frame, "TOPLEFT", 85 + 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.restTexture = restTexture
frame.healthText = healthText
frame.powerText = powerText
local VEHICLE_ART = "ui-hud-unitframe-player-portraiton-vehicle"
local VEHICLE_HEALTH_WIDTH = 118
-- En vehicule, le cadre du joueur montre le vehicule : c'est ce que fait
-- PlayerFrame_ToVehicleArt en 3.3.5, et le moderne echange en plus l'art du
-- cadre pour la variante vehicule, plus large de quelques pixels.
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
-- La lueur suit le comportement observe sur camelot, et non la lettre de
-- UnitFrame_UpdateThreatIndicator : le client de reference n'allume la lueur
-- qu'au-dessus de zero et la teinte par GetThreatStatusColor, alors qu'en jeu
-- la lueur est rouge dans les deux cas et ne change que d'intensite --
-- discrete des l'engagement, franche quand un ennemi vous prend pour cible.
-- Les deux niveaux sont ici, a regler d'un chiffre si besoin.
local THREAT_ALPHA_ENGAGED, THREAT_ALPHA_TARGETED = 0.45, 1.0
-- Etre pris pour cible ne peut pas reposer sur la seule table de menace : sur
-- un serveur prive, UnitThreatSituation("player") renvoie souvent rien et
-- UNIT_THREAT_SITUATION_UPDATE ne part jamais. On regarde donc aussi, tout
-- simplement, si l'ennemi en face nous vise.
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
-- L'ETAT DU JOUEUR, dans l'ordre que le client 3.3.5 applique lui-meme
-- (PlayerFrame_UpdateStatus) : en vehicule rien ne s'affiche, sinon le repos
-- passe avant le combat.
local restElapsed = 0
local function updateStatus()
if UnitHasVehicleUI and UnitHasVehicleUI("player") then
statusTexture:Hide()
combatIcon:Hide()
restTexture:Hide()
frame.resting = false
return
end
-- Le client distingue deux choses que UnitAffectingCombat confond :
-- inCombat -- PLAYER_ENTER_COMBAT : vous frappez, des le clic droit sur
-- un ennemi. Voile rouge et icone de combat, immediatement.
-- onHateList -- PLAYER_REGEN_DISABLED : quelqu'un vous a sur sa liste de
-- haine. L'icone seule.
-- Le voile n'est pas touche dans la branche onHateList, exactement comme
-- dans les deux clients : il garde sa couleur tant que le combat dure et ne
-- s'eteint qu'une fois vraiment sorti.
if IsResting() then
statusTexture:SetVertexColor(1.0, 0.88, 0.25)
statusTexture:Show()
restTexture:Show()
combatIcon:Hide()
cornerIcon:Show()
frame.resting = true
restElapsed = 0
elseif 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
else
statusTexture:Hide()
combatIcon:Hide()
cornerIcon:Show()
restTexture:Hide()
frame.resting = false
end
end
local function updateLeader()
if IsPartyLeader and IsPartyLeader() then
leaderIcon:Show()
else
leaderIcon:Hide()
end
end
-- Le texte des barres suit le reglage du jeu plutot qu'un reglage a nous :
-- playerStatusText a "1" veut dire "toujours affiche", et statusTextPercentage
-- decide entre un pourcentage et les valeurs brutes. Au survol, il s'affiche
-- quel que soit le reglage.
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
local powerType = UnitPowerType("player")
healthText:SetText(formatValue(UnitHealth("player"), UnitHealthMax("player")))
powerText:SetText(formatValue(UnitPower("player", powerType), UnitPowerMax("player", powerType)))
healthText:Show()
powerText:Show()
end
ForeverUI.PlayerFrameUpdateThreat = function() updateThreat() end
-- Certaines classes changent l'art du cadre : camelot passe a la variante
-- "ClassResource" quand une ressource de classe s'affiche sous les barres.
ForeverUI.PlayerFrameSetArt = function(normalAtlas)
if normalAtlas then
ART_NORMAL = normalAtlas
updateVehicleArt()
end
end
-- /fui debug : ce que le client repond vraiment, pour regler la lueur sur des
-- valeurs constatees plutot que supposees.
ForeverUI.PlayerFrameDebug = function()
local status = UnitThreatSituation and UnitThreatSituation("player")
local cible = 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), cible,
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)
-- Deux animations, un seul OnUpdate : le sommeil parcourt sa planche d'images,
-- et le voile d'etat respire comme le fait celui du cadre d'origine.
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
-- La bascule "on me vise" se lit sur la cible, pas sur un evenement :
-- on la relit trois fois par seconde tant que le joueur est engage.
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)
-- Le cadre d'origine est desactive plutot que simplement masque : sans cela
-- ses propres evenements le reafficheraient (montures, vehicules, groupe).
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")
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" then
updateLeader()
return
end
if event == "CVAR_UPDATE" then
updateTexts()
return
end
if event == "PLAYER_LEVEL_UP" then
updateLevel()
return
end
if unit ~= "player" 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 n'a pas UNIT_POWER : chaque ressource a son propre evenement.
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("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)