Files
moon-well-ui/data/addon/ForeverUI/CharacterFrame.lua
T
Adrestie ddefd603e8 Feuille : icone dans l'ouverture du cadre, et onglets qui commandent les stats
camelot donne 42 a son Icon, soit tout le bouton, mais ses icones
portent leur propre marge transparente ; un portrait remplit sa texture
et ressortait des angles arrondis. Mesure sur UI-Character-Info-StatTab :
metal en 3..6 et 35..38, ouverture de 7 a 34, soit 28.

Les deux se comportent en onglets : le gestionnaire ouvre son panneau et
masque les statistiques, les statistiques le ferment et reviennent. Au
retour on laisse PaperDollFrame_UpdateStats refaire le travail -- lui
seul sait cacher la sixieme ligne des categories a cinq statistiques --
et poserStatistiques lit l'etat de l'onglet au lieu de tout rallumer a
chaque evenement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:25:00 +02:00

1421 lines
52 KiB
Lua

-- ForeverUI : la feuille du personnage.
--
-- Reprise a zero le 2026-09-22, avec la capture de reference sous les yeux :
-- docs/reference/camelot_feuille_personnage.png (923 x 663, echelle 4/3 --
-- le cadre y mesure 837 x 647 pour 631 x 484 logiques).
--
-- CE JALON pose le cadre, ses deux volets, les emplacements d'equipement et
-- les onglets lateraux. Le contenu du volet droit -- les statistiques et
-- leurs categories -- vient ensuite.
--
-- RELEVE DES SOURCES. Seuls camelot/ et shared/ font foi ; les trois
-- fichiers de la feuille existent en camelot/, il n'y a rien a trancher.
--
-- camelot/CharacterFrameConstants.lua
-- CHARACTER_FRAME_WIDTH 631, CHARACTER_FRAME_HEIGHT 484, replie 398.
--
-- camelot/CharacterFrame.xml
-- CharacterFrame herite de PortraitFrameBaseTemplate, dont le layoutType
-- est "PortraitFrameTemplate" : la meme mise en page que les sacs, au
-- coin haut gauche pres -- UI-Frame-PortraitMetal-CornerTopLeft, un
-- anneau plus large pour un portrait de 62. Son NineSlice et son
-- PortraitContainer (frameLevel 400) sont des CADRES FILS.
-- LeftPaneHost 398 de large, TOPLEFT (0, -20) et BOTTOMLEFT du cadre ;
-- fond UI-Character-Info-General-BG (398 x 464).
-- RightPaneHost 233, accroche au TOPRIGHT du volet gauche ; fond
-- UI-Character-Info-Stat-BG SANS ancrage -- donc il
-- remplit -- et UI-Character-Info-Stat-StoneBG (233 x 85)
-- en ARTWORK au TOPLEFT ; un common-framedivider de 11
-- pose a -6, tendu sur la hauteur.
-- ModeTabs 64 x 384, TOPLEFT sur le TOPRIGHT du cadre, y = -30.
--
-- camelot/CharacterFrame.lua
-- CHARACTER_FRAME_TAB : Character 1, Reputation 2, Skills 3, PVP 4,
-- Currency 5, Statistics 6. L'onglet du personnage porte le PORTRAIT du
-- joueur, rogne a 0,03125 (UpdateCharacterModeTabPortrait).
--
-- SidePanelTabButtonMixin (blizzard_sharedxml)
-- la taille d'un onglet vient de son art : common-sidetab, 55 x 60 dans
-- la variante camelot, moins 5 de hauteur transparente -> 55 x 55.
-- L'icone est centree a (-3, 0).
--
-- camelot/PaperDollFrame.xml et .lua
-- emplacement 40 x 40, cadre UI-Character-Info-GearSlot a sa taille ;
-- colonne gauche TOPLEFT (24, -60), colonne droite TOPRIGHT (-20, -60),
-- ecart 6 ; l'arme principale au BOTTOM (-60, 30) du volet gauche quand
-- l'emplacement de distance est montre, puis secondaire et distance a
-- +6 ; distance et munitions en 27 avec -GearSlotSmall, munitions a +19
-- de la distance ; la scene du modele occupe tout le volet gauche.
--
-- CE QUI DIFFERE, ET POURQUOI.
-- 3.3.5 montre toujours un emplacement de distance -- arc, arme de jet ou
-- relique selon la classe : seule la variante a -60 sert.
-- Les icones d'onglet INV_SideTab_*_c60 que la source demande N'EXISTENT
-- PAS dans l'export du client : seul l'art commun des onglets y est. Les
-- onglets gardent donc le texte du client, sauf celui du personnage qui
-- porte son portrait comme dans la source.
-- Les emplacements, les onglets et le modele sont des cadres du client :
-- ils sont redimensionnes et reposes, jamais recrees, et jamais en
-- combat.
local LARGEUR, HAUTEUR = 631, 484
local VOLET_GAUCHE, VOLET_DROIT = 398, 233
local COMBLE = 20 -- les volets commencent sous le titre
local NIVEAU_ART = 5 -- l'art passe au-dessus des volets
-- L'ENCADREMENT DE METAL, ET CE QUI MANQUAIT.
--
-- RELEVE -- NineSliceLayouts.PortraitFrameTemplate donne les memes
-- decalages que HeldBagLayout : haut gauche (-13, 16), haut droit (4, 16),
-- bas gauche (-13, -3), bas droit (4, -3). Seul l'atlas du coin haut gauche
-- differe. Mais la source NE S'ARRETE PAS LA : camelot repasse ensuite sur
-- TOUTES les mises en page (camelot/NineSliceLayoutOverrides.lua), parce que
-- "l'art fait pour Camelot ne fait pas la taille exacte de l'art standard et
-- doit etre decale autrement" -- ce sont ses mots.
--
-- coin haut droit (UI-Frame-Metal-CornerTopRight) x += -2 -> 2
-- coin bas gauche (UI-Frame-Metal-CornerBottomLeft) y = -8
-- coin bas droit (UI-Frame-Metal-CornerBottomRight) x += -2, y = -8
--
-- Nous prenions les valeurs NON corrigees : les deux coins du bas etaient
-- 5 px trop haut, d'ou un encadrement qui n'allait pas jusqu'en bas de la
-- fenetre, et les coins de droite 2 px trop a droite.
--
-- ET CE QUI MANQUAIT ENCORE : ces decalages, corriges ou non, posent le
-- CONTOUR EXTERIEUR de l'image. Le filet interieur du metal tombe alors a
-- 6 px DANS la fenetre, et ces 6 px de fond restent visibles sous lui --
-- c'est ce qui se lit comme un encadrement qui n'atteint pas le bas.
--
-- MESURE SUR L'ART, et non sur une capture : une capture a sa propre
-- echelle, l'atlas non. uiframemetal2xc60 est en double densite, donc les
-- mesures sont divisees par deux. Distance du filet interieur au bord de
-- l'image, en pixels d'affichage :
--
-- bande gauche 18,5 (coin portrait et coin bas gauche, identiques)
-- bande droite 8,5
-- bande basse 14
-- bande haute 41,5 -- c'est la barre de titre, interieure par
-- conception : le haut ne se recalcule pas.
--
-- Le decalage vaut donc cette distance : l'image deborde d'autant, et son
-- filet tombe pile sur le bord de la fenetre.
--
-- PANNEAU_MONTEE est en plus, et n'est PAS de la source : 1 px de montee de
-- tout l'encadrement, juge a l'ecran, a la demande.
local PANNEAU_MONTEE = 1
local PANNEAU_COINS = {
coinHautGauche = { x = -18.5, y = 16 + PANNEAU_MONTEE },
coinHautDroit = { x = 8.5, y = 16 + PANNEAU_MONTEE },
coinBasGauche = { x = -18.5, y = -14 + PANNEAU_MONTEE },
coinBasDroit = { x = 8.5, y = -14 + PANNEAU_MONTEE },
}
local EMPLACEMENT = 40
-- L'ECART entre deux emplacements. La source n'en donne qu'un, 6, pour les
-- deux sens. ECART ASSUME, sur demande : les colonnes sont resserrees de 2
-- px, la rangee des armes garde le 6 de la source.
local ECART = 6
local ECART_VERTICAL = ECART - 2
local GAUCHE_X, GAUCHE_Y = 24, -60
local DROITE_X, DROITE_Y = -20, -60
local ARME_X, ARME_Y = -60, 30
local PETIT = 27 -- ne sert plus qu'aux munitions
local MUNITIONS_ECART = 19
-- ECART ASSUME, sur demande. camelot pose l'emplacement de distance en 27
-- avec UI-Character-Info-GearSlotSmall, comme les munitions. Il porte
-- pourtant, selon la classe, une arme a distance OU une relique -- des
-- objets de meme rang que les deux armes de melee. Les quatre emplacements
-- de la rangee prennent donc la meme taille que les autres.
-- LES DEUX ONGLETS DU VOLET DROIT.
--
-- RELEVE -- camelot/PaperDollFrame.xml. PaperDollSidebarTabs est un cadre de
-- 233 x 85 ancre au TOP du volet droit, y = -4 ; il tient des CheckButton de
-- 42 x 42 -- PaperDollSidebarTabTemplate -- dont le premier est au TOP
-- (0, -5) et les autres colles a sa gauche et a sa droite. Chacun porte une
-- Icon de 42 en BACKGROUND, le cadre UI-Character-Info-StatTab en BORDER a
-- sa taille d'atlas, et UI-Character-Info-StatTab-Selected quand il est
-- choisi.
--
-- RELEVE -- mainline/PaperDollFrameConstants.lua, la table que camelot
-- charge (PAPERDOLL_SIDEBARS) :
-- STATS icon = nil, "Uses the character portrait", rogne a
-- 0,109375 / 0,890625 / 0,09375 / 0,90625
-- EQUIPMENTMANAGER Interface\PaperDollInfoFrame\PaperDollSidebarTabs
-- rogne a 0,015625 / 0,53125 / 0,46875 / 0,60546875
-- PET / TITLES les autres onglets, hors sujet ici
--
-- CE QUI DIFFERE, ET POURQUOI.
-- PaperDollSidebarTabs.blp N'EXISTE PAS dans ce client : l'onglet du
-- gestionnaire prend donc UI-GearManager-Button, qui y est, et qui est
-- justement l'icone de son bouton d'origine.
-- Les chaines PAPERDOLL_SIDEBAR_STATS et PAPERDOLL_EQUIPMENTMANAGER n'y
-- sont pas non plus. L'infobulle du premier onglet prend CHARACTER_INFO,
-- la plus proche que ce client porte ; le second a EQUIPMENT_MANAGER, qui
-- existe tel quel.
-- Le client n'a pas d'onglets lateraux : ces deux-la sont crees. Le
-- gestionnaire, lui, n'est pas recree -- l'onglet ouvre et ferme le
-- GearManagerDialog du client, exactement comme le bouton d'origine, qui
-- est masque.
local ONGLET_VOLET = 42
local ONGLET_VOLET_Y = -9 -- -4 du cadre des onglets, -5 du premier
-- L'ICONE TIENT DANS L'OUVERTURE DU CADRE, PAS DANS LE BOUTON.
--
-- camelot donne 42 a son Icon, soit tout le bouton. Ses icones a lui sont
-- des rognages de PaperDollSidebarTabs, qui portent leur propre marge
-- transparente ; un portrait, lui, remplit sa texture d'un bord a l'autre et
-- ressort donc des angles arrondis du cadre.
--
-- Mesure sur UI-Character-Info-StatTab : sur ses 42, la bande de metal
-- occupe 3..6 et 35..38, l'ouverture va donc de 7 a 34 -- 28 px, centres.
local ONGLET_ICONE = 28
local ATLAS_ONGLET_VOLET = "ui-character-info-stattab"
local ATLAS_ONGLET_VOLET_CHOISI = "ui-character-info-stattab-selected"
local ONGLET_PORTRAIT_COORD = { 0.109375, 0.890625, 0.09375, 0.90625 }
local ICONE_GESTIONNAIRE = "Interface\\PaperDollInfoFrame\\UI-GearManager-Button"
local PIERRE_HAUTEUR = 85 -- UI-Character-Info-Stat-StoneBG
local SEPARATEUR = 11
local SEPARATEUR_EMBOUT = 4 -- mesure sur l'art : 11 x 50, deux embouts
local ONGLETS_L, ONGLETS_H = 64, 384 -- CharacterFrameModeTabs
local ONGLETS_Y = -30
local ONGLET_L, ONGLET_H = 55, 55 -- 55 x 60 moins 5 de transparent
local ONGLET_ICONE = 32
local ONGLET_ICONE_X = -3 -- GetIconAnchorOffsetsForTabArt
local PORTRAIT_ONGLET = 0.03125 -- UpdateCharacterModeTabPortrait
local PORTRAIT = 48 -- voir le calcul plus bas
-- LE PORTRAIT SUIT SON ANNEAU. L'anneau est porte par le coin haut gauche
-- de l'encadrement : deplacer ce coin deplace le trou, et un portrait pose
-- en dur se retrouve a cote. Sa place se calcule donc DEPUIS le coin, et
-- non plus en absolu -- c'est ce qui l'avait desaxe quand l'encadrement est
-- passe au calcul sur le filet interieur.
--
-- Centre du trou dans l'image du coin, en pixels d'affichage : (38 ; -38,5)
-- du coin haut gauche de l'image. Mesure sur l'art -- l'anneau y occupe les
-- plages 13..39 et 113..138 de la feuille, en double densite, soit un trou
-- centre a 76 px de feuille = 38 affiches -- et c'est aussi ce que donnait
-- la place validee en jeu quand le coin etait a (-13, 16).
local PORTRAIT_TROU_X, PORTRAIT_TROU_Y = 38, -38.5
local PORTRAIT_X = PANNEAU_COINS.coinHautGauche.x + PORTRAIT_TROU_X
local PORTRAIT_Y = PANNEAU_COINS.coinHautGauche.y + PORTRAIT_TROU_Y
-- LE TITRE. TitledPanelMixin:SetTitleOffsets pose le conteneur du titre en
-- TOPLEFT (gauche, -1) et TOPRIGHT (droite, -1), avec son texte a TOP
-- (0, -5). ContainerFrame l'appelle avec 35 ; CharacterFrame ne l'appelle
-- PAS et garde donc les valeurs par defaut, 58 et -24. Le titre n'est donc
-- pas centre sur la fenetre mais entre le portrait et le bouton de
-- fermeture -- son milieu tombe a 332,5 sur 631, ce que la capture
-- confirme. CharacterFrameMixin:OnLoad ajoute SetTitleMaxLinesAndHeight(1,
-- 13) : une seule ligne.
local TITRE_GAUCHE, TITRE_DROITE = 58, -24
local TITRE_CONTENEUR, TITRE_TEXTE = -1, -5
local TITRE_BANDE = 20
-- ECART ASSUME, sur demande. characterFrameDisplayInfo fait suivre le
-- titre au sous-cadre affiche : le nom du joueur par defaut, puis
-- REPUTATION, CURRENCY, PVP, SKILLS ou STATISTICS selon le panneau, en
-- jaune. Ici la barre du haut porte TOUJOURS le nom du personnage et son
-- titre, quel que soit le panneau.
-- LE NIVEAU, LA RACE ET LA CLASSE, dans le volet DROIT.
--
-- RELEVE -- PaperDollLevelInfo : une bande de 220 x 20 posee au TOP
-- (0, -50) de PaperDollSidebarTabs, qui est elle-meme au TOP (0, -4) du
-- volet droit. La ligne tombe donc a -54 sous le haut du volet, centree.
-- C'est le "Druidesse de niveau 3" de la capture. 3.3.5 n'a qu'une ligne
-- pour les trois ; elle prend cette place.
local NIVEAU_Y = -54
local NIVEAU_LARGEUR, NIVEAU_HAUTEUR = 220, 20
-- LES FLECHES DU MODELE, centrees dans le volet gauche : leur milieu tombe
-- sur celui du volet, mesure a 276 sur la capture contre 275 pour le volet.
-- Leur hauteur est un REGLAGE, pas un releve -- la capture montre trois
-- boutons la ou 3.3.5 en a deux, et ils n'ont pas la meme taille.
local ROTATION_Y = -12
local ROTATION_ECART = 4
-- LE MODELE remonte d'autant dans son volet. Reglage lui aussi : la source
-- fait remplir tout le volet a sa scene, mais sa camera n'est pas celle de
-- 3.3.5, qui cadre le personnage plus bas.
local MODELE_Y = 24
-- L'ECHELLE DU MODELE. Camelot cadre son personnage par une scene
-- (CharacterModelScene) et la position de son acteur ; 3.3.5 n'a pas de
-- scene, et ce client n'a ni SetCamDistanceScale ni SetPortraitZoom --
-- verifie dans Wow.exe, seul SetModelScale y figure. C'est donc par lui
-- qu'on recule le personnage. Valeur choisie a l'oeil, a la demande.
-- CE QUE CE CLIENT PORTE, releve dans Wow.exe : SetModelScale et
-- GetModelScale, SetPosition et GetPosition, SetCamera, SetFacing. PAS de
-- SetCameraDistance, SetCameraPosition, SetCameraTarget, SetCustomCamera ni
-- SetPortraitZoom. Deux leviers, donc, et deux seulement :
--
-- ECHELLE SetModelScale reduit le MODELE. La camera ne bouge pas, et le
-- modele se reduit autour de son origine -- il parait donc aussi
-- glisser vers le bas du cadre.
-- POSITION SetPosition(profondeur, lateral, hauteur) deplace le modele
-- devant la camera. L'eloigner le rapetisse SANS changer son
-- cadrage : c'est le vrai recul.
--
-- Reglable en jeu par /fui modele, pour juger a l'oeil ; ce qui est ici est
-- ce qui s'applique au chargement.
-- VALEURS RETENUES, jugees a l'ecran. C'est la POSITION qui cadre : le
-- personnage recule de 6,5 devant la camera, ce qui le rapetisse sans
-- deplacer son cadrage. L'echelle reste celle du client -- nil veut dire
-- qu'on n'y touche pas, et non qu'on la remet a 1.
local MODELE_ECHELLE = nil
local MODELE_POSITION = { -6.5, 0, 0 } -- profondeur, lateral, hauteur
-- LE PANNEAU DES RESISTANCES se decale vers la droite. On garde son
-- ancrage d'origine et on n'y ajoute que ce decalage, sinon chaque passage
-- le pousserait un peu plus loin.
local RESISTANCES_X = 30
-- LES STATISTIQUES.
--
-- D'OU VIENT LA SOURCE. Le client charge son PaperDollFrame depuis
-- patch-enUS-2 (le .xml) et patch-enUS-3 (le .lua) : c'est le FrameXML
-- d'origine. Le patch-enUS-9, lui, porte l'extension .disabled -- le jeu
-- ne l'ouvre pas, et ses noms (MostrarStatPaperDoll*DropDown,
-- PStatFrameTemplate) n'existent nulle part a l'ecran. Tout ce qui suit
-- est releve sur ce que le client charge VRAIMENT : l'addon ne depend
-- d'aucune archive ajoutee.
--
-- CE QUE LE CLIENT PORTE. CharacterAttributesFrame, 230 x 78, tient deux
-- groupes de six lignes poses COTE A COTE -- PlayerStatFrameLeft1..6 et
-- PlayerStatFrameRight1..6 -- chacun coiffe de son menu de categorie,
-- PlayerStatFrameLeftDropDown et PlayerStatFrameRightDropDown. Le modele
-- d'une ligne, StatFrameTemplate, fait 104 x 13 : un intitule a gauche,
-- sa valeur dans un cadre fils cale a droite. Les lignes s'enchainent
-- sans ecart, d'ou un pas de 13.
--
-- L'addon n'en recree aucun : le calcul des valeurs, les infobulles et le
-- menu des categories restent au client. Il les repose l'un SOUS l'autre
-- dans le volet droit -- une seule colonne, comme camelot -- et les
-- elargit : le volet fait 233 et le modele 104, et une ligne dont la
-- valeur est calee a droite gagne a occuper toute la largeur.
local STAT_PAS = 13 -- la hauteur de StatFrameTemplate
local STAT_MARGE = 20 -- de chaque cote du volet
local STAT_HAUT = 10 -- sous la bande de pierre
local STAT_ENTRE_GROUPES = 16
local STAT_PAR_GROUPE = 6
-- LES SELECTEURS DE CATEGORIE. Ce sont des UIDropDownMenuTemplate : un
-- cadre de 40 x 32 dont l'art -- $parentLeft 25, $parentMiddle 115,
-- $parentRight 25, sur 64 de haut -- DEBORDE tres largement et ne suit pas
-- la taille du cadre. Il s'efface, et l'en-tete moderne prend sa place ;
-- le clic, lui, reste celui du client ($parentButton ouvre le menu des
-- cinq PLAYERSTAT_DROPDOWN_OPTIONS).
--
-- RELEVE -- CharacterStatFrameCategoryTemplate de camelot : 197 x 40, fond
-- UI-Character-Info-Title tendu du TOPLEFT au BOTTOMRIGHT, intitule
-- GameFontHighlight centre a (0, 1). Ses lignes font 187 : l'en-tete
-- deborde donc de 5 de chaque cote.
--
-- L'intitule ne se lit pas sur le bouton : la categorie choisie vit dans
-- une CVar qui porte une CLE, et le texte est la globale du meme nom. Le
-- client rappelle UpdatePaperdollStats(prefixe, cle) a chaque changement.
local STAT_ENTETE = 40
local STAT_ENTETE_DEBORD = 5
local STAT_FLECHE = -6 -- le bouton du client, contre le bord
local ATLAS_ENTETE = "ui-character-info-title"
local SELECTEUR_PIECES = { "Left", "Middle", "Right", "Text" }
local STAT_GROUPES = {
{
selecteur = "PlayerStatFrameLeftDropDown",
prefixe = "PlayerStatFrameLeft",
cvar = "playerStatLeftDropdown",
},
{
selecteur = "PlayerStatFrameRightDropDown",
prefixe = "PlayerStatFrameRight",
cvar = "playerStatRightDropdown",
},
}
local FERMETURE = 24
local FERMETURE_X, FERMETURE_Y = 1, 0
local FERMETURE_ATLAS = {
{ atlas = "redbutton-exit", methode = "GetNormalTexture" },
{ atlas = "redbutton-exit-pressed", methode = "GetPushedTexture" },
{ atlas = "redbutton-exit-disabled", methode = "GetDisabledTexture" },
{ atlas = "redbutton-highlight", methode = "GetHighlightTexture" },
}
local COIN_PORTRAIT = "ui-frame-portraitmetal-cornertopleft"
local ATLAS = {
fondGauche = "ui-character-info-general-bg",
fondDroit = "ui-character-info-stat-bg",
pierre = "ui-character-info-stat-stonebg",
separateur = "common-framedivider",
emplacement = "ui-character-info-gearslot",
petitEmplacement = "ui-character-info-gearslotsmall",
onglet = "common-sidetab",
ongletSurvol = "common-sidetab-hover",
ongletChoisi = "common-sidetab-selected",
}
local COLONNE_GAUCHE = {
"Head", "Neck", "Shoulder", "Back", "Chest", "Shirt", "Tabard", "Wrist",
}
local COLONNE_DROITE = {
"Hands", "Waist", "Legs", "Feet", "Finger0", "Finger1", "Trinket0", "Trinket1",
}
local RANGEE_ARMES = { "MainHand", "SecondaryHand", "Ranged" }
-- ECART ASSUME, sur demande : la rangee du bas se resserre vers la gauche.
-- Les valeurs sont RELATIVES au voisin de gauche, et s'additionnent le long
-- de la chaine -- decaler la main gauche de 2 emporte la distance avec elle.
-- Sur l'ecran : main droite 0, main gauche -2, distance -4, et les munitions
-- -4 puisqu'elles s'accrochent a la distance.
--
-- La fleche des munitions n'a pas de reglage : c'est une region de
-- l'emplacement lui-meme -- une texture OVERLAY de 23 x 41 centree a
-- (-22, 0) sur CharacterAmmoSlot -- donc elle le suit.
local RANGEE_DECALAGE = { 0, -2, -2 }
-- L'art d'epoque ne tient pas qu'au cadre : 3.3.5 le repartit sur ses
-- sous-cadres, qui sont des cadres fils et echappent a un balayage du seul
-- CharacterFrame.
local ANCIENS_CADRES = {
"CharacterFrame", "PaperDollFrame", "PetPaperDollFrame", "SkillFrame",
"ReputationFrame", "HonorFrame", "TokenFrame", "PaperDollItemsFrame",
"CharacterAttributesFrame", "CharacterAttributesFrameer",
"CharacterResistanceFrame",
}
-- LA FENETRE SE DEPLACE PAR SA BARRE DU HAUT. Le cadre du client est
-- range par le systeme de panneaux : il le repose a chaque ouverture. On
-- retient donc la place choisie et on la repose apres lui, a chaque
-- passage de l'habillage -- qui tourne justement a l'ouverture.
local function placeRetenue()
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.positions = ForeverUIDB.positions or {}
return ForeverUIDB.positions
end
local voletGauche, voletDroit, barreOnglets
local onglets = {}
local function balayerTextures(cadre)
if not cadre or not cadre.GetRegions then
return
end
local regions = { cadre:GetRegions() }
for _, region in ipairs(regions) do
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetAlpha(0)
end
end
end
local function effacerArtDepoque()
for _, nom in ipairs(ANCIENS_CADRES) do
balayerTextures(_G[nom])
end
end
local function monterVolets(cadre)
if voletGauche then
return
end
voletGauche = CreateFrame("Frame", "ForeverUICharacterLeftPane", cadre)
voletGauche:SetWidth(VOLET_GAUCHE)
voletGauche:SetPoint("TOPLEFT", cadre, "TOPLEFT", 0, -COMBLE)
voletGauche:SetPoint("BOTTOMLEFT", cadre, "BOTTOMLEFT", 0, 0)
local fondGauche = voletGauche:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(fondGauche, ATLAS.fondGauche)
fondGauche:SetPoint("TOPLEFT", voletGauche, "TOPLEFT", 0, 0)
voletDroit = CreateFrame("Frame", "ForeverUICharacterRightPane", cadre)
voletDroit:SetWidth(VOLET_DROIT)
voletDroit:SetPoint("TOPLEFT", voletGauche, "TOPRIGHT", 0, 0)
voletDroit:SetPoint("BOTTOMLEFT", voletGauche, "BOTTOMRIGHT", 0, 0)
-- La source declare ce fond sans ancrage : il remplit son volet. A sa
-- taille d'atlas (233 x 383) il laisserait 81 px nus en bas.
local fondDroit = voletDroit:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(fondDroit, ATLAS.fondDroit, true)
fondDroit:SetAllPoints(voletDroit)
local pierre = voletDroit:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(pierre, ATLAS.pierre)
pierre:SetPoint("TOPLEFT", voletDroit, "TOPLEFT", 0, 0)
voletDroit.pierre = pierre
-- Le separateur porte un embout a chaque bout : tendu tel quel sur la
-- hauteur du volet, ils s'etalent. Trois tranches.
-- Il passe DEVANT l'encadrement de la fenetre. L'habillage vit dans un
-- cadre fils a NIVEAU_ART ; le separateur, pose au niveau du volet,
-- passait dessous et disparaissait sous le metal. Il prend donc un cran
-- de plus que l'habillage -- qui n'existe pas encore quand les volets se
-- montent, d'ou le calcul depuis la constante et non depuis le cadre.
local separateur = ForeverUI.CreateVerticalDivider(voletDroit, ATLAS.separateur,
SEPARATEUR_EMBOUT, cadre:GetFrameLevel() + NIVEAU_ART + 1)
separateur:SetWidth(SEPARATEUR)
separateur:SetPoint("TOPLEFT", voletDroit, "TOPLEFT", -6, -1)
separateur:SetPoint("BOTTOMLEFT", voletDroit, "BOTTOMLEFT", -6, 0)
voletDroit.separateur = separateur
ForeverUI.CharacterPanes = { gauche = voletGauche, droit = voletDroit }
end
-- LE PORTRAIT. La source le pose en 62 dans PortraitContainer et l'arrondit
-- par un masque ; ici c'est le trou de l'anneau qui decoupe, comme sur les
-- sacs.
--
-- PROFIL DE L'ANNEAU, mesure sur l'art a la taille ou il est dessine --
-- rayon en pixels d'affichage, contre l'opacite :
--
-- 0 a 12 transparent, c'est le trou
-- 13 a 25 degrade
-- 26 a 29 opaque
-- 30 a 32 retombee, rien au-dela
--
-- Un carre de cote C touche le rayon C/2 sur ses axes et C/2 x racine(2)
-- dans ses angles. Aucune taille ne couvre le degrade tout en cachant ses
-- angles : il faudrait C >= 52 pour les uns et C <= 41 pour les autres.
--
-- C = 48, A LA DEMANDE : sur les axes il atteint 24, la ou le metal est
-- deja opaque a 86 % -- c'etait les "2 px de chaque cote" qui manquaient
-- avec 44, qui s'arretait a 22, dans le degrade a moitie transparent. En
-- contrepartie ses angles tombent a 33,9, au-dela de l'anneau : si leurs
-- pointes se voient, 46 est le compromis (axes a 23, angles a 32,5, dans
-- la retombee).
local function poserPortrait(cadre)
local hote = cadre.foreverHabillage or cadre
if not cadre.foreverPortrait then
local portrait = hote:CreateTexture(nil, "BACKGROUND")
portrait:SetWidth(PORTRAIT)
portrait:SetHeight(PORTRAIT)
portrait:SetPoint("CENTER", hote, "TOPLEFT", PORTRAIT_X, PORTRAIT_Y)
cadre.foreverPortrait = portrait
end
if SetPortraitTexture then
SetPortraitTexture(cadre.foreverPortrait, "player")
end
end
-- LE TITRE, dans un cadre fils. La source range le sien dans
-- TitleContainer, a frameLevel 510 : au-dessus du NineSlice, qui est a 400.
-- Il le faut, le metal etant en OVERLAY sur son propre cadre.
local function poserTitre(cadre)
if not cadre.foreverBandeTitre then
local ancien = _G["CharacterNameText"]
if ancien then
ancien:Hide()
end
local bande = CreateFrame("Frame", "ForeverUICharacterTitle", cadre)
bande:SetFrameLevel(cadre:GetFrameLevel() + NIVEAU_ART + 2)
bande:SetHeight(TITRE_BANDE)
bande:SetPoint("TOPLEFT", cadre, "TOPLEFT", TITRE_GAUCHE, TITRE_CONTENEUR)
bande:SetPoint("TOPRIGHT", cadre, "TOPRIGHT", TITRE_DROITE, TITRE_CONTENEUR)
local texte = bande:CreateFontString(nil, "OVERLAY", "GameFontNormal")
texte:SetPoint("TOP", bande, "TOP", 0, TITRE_TEXTE)
texte:SetPoint("LEFT", bande, "LEFT")
texte:SetPoint("RIGHT", bande, "RIGHT")
texte:SetJustifyH("CENTER")
cadre.foreverBandeTitre = bande
cadre.foreverTitre = texte
-- La barre du haut sert de poignee : clic maintenu, la fenetre suit.
cadre:SetMovable(true)
cadre:SetClampedToScreen(true)
bande:EnableMouse(true)
bande:RegisterForDrag("LeftButton")
bande:SetScript("OnDragStart", function()
if not InCombatLockdown() then
cadre:StartMoving()
end
end)
bande:SetScript("OnDragStop", function()
cadre:StopMovingOrSizing()
local point, _, pointRelatif, x, y = cadre:GetPoint(1)
if point then
placeRetenue()["feuille"] = {
point = point, relativePoint = pointRelatif, x = x, y = y,
}
end
end)
end
-- UnitPVPName rend le nom ACCOMPAGNE de son titre quand le joueur en
-- porte un ; sans titre, c'est le nom seul.
local texte = cadre.foreverTitre
local nom = (UnitPVPName and UnitPVPName("player")) or UnitName("player")
-- (le deplacement est monte plus bas, une seule fois)
texte:SetText(nom or "")
if HIGHLIGHT_FONT_COLOR then
texte:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g,
HIGHLIGHT_FONT_COLOR.b)
end
end
-- Le bouton de fermeture : celui du client, rhabille du X rouge des
-- panneaux modernes et remis au coin haut droit.
local function poserFermeture(cadre)
local fermer = _G["CharacterFrameCloseButton"]
if not fermer then
return
end
fermer:SetWidth(FERMETURE)
fermer:SetHeight(FERMETURE)
fermer:ClearAllPoints()
fermer:SetPoint("TOPRIGHT", cadre, "TOPRIGHT", FERMETURE_X, FERMETURE_Y)
fermer:SetFrameLevel(cadre:GetFrameLevel() + NIVEAU_ART + 2)
if cadre.foreverFermetureHabillee then
return
end
for _, entree in ipairs(FERMETURE_ATLAS) do
local texture = fermer[entree.methode] and fermer[entree.methode](fermer)
if texture then
ForeverUI.SetAtlas(texture, entree.atlas, true)
texture:ClearAllPoints()
texture:SetAllPoints(fermer)
if entree.atlas == "redbutton-highlight" then
texture:SetBlendMode("ADD")
end
end
end
cadre.foreverFermetureHabillee = true
end
local function habillerEmplacement(nom, cote, atlasCadre)
local bouton = _G["Character" .. nom .. "Slot"]
if not bouton then
return nil
end
bouton:SetWidth(cote)
bouton:SetHeight(cote)
-- LE MODELE PREND LA SOURIS, ET IL COUVRE TOUT LE VOLET.
--
-- Chez le client, la scene du modele tient entre les deux colonnes
-- d'emplacements (233 x 215) : rien ne se recouvre. Camelot lui donne
-- tout le volet gauche et pose les emplacements PAR-DESSUS, ce qu'on a
-- repris -- mais le modele, lui, est sensible a la souris, et il nait
-- au meme niveau de cadre que les emplacements, tous enfants de
-- PaperDollFrame.
--
-- MESURE EN JEU (/fui perso) : emplacement et modele tous deux a 28,
-- et GetMouseFocus rend CharacterModelFrame. A NIVEAU EGAL, C'EST LE
-- MODELE QUI RECOIT LE CLIC -- plus de deseequipement, plus
-- d'infobulle. L'emplacement passe donc un cran au-dessus.
--
-- Le test est un "au moins" : chaque passage de l'habillage le rejoue,
-- et une augmentation seche ferait monter le niveau sans fin.
local modele = _G["CharacterModelFrame"]
if modele and modele.GetFrameLevel
and bouton:GetFrameLevel() <= modele:GetFrameLevel() then
bouton:SetFrameLevel(modele:GetFrameLevel() + 1)
end
if not bouton.foreverCadre then
local normale = bouton:GetNormalTexture()
if normale then
normale:SetAlpha(0)
end
local cadre = bouton:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(cadre, atlasCadre)
cadre:SetPoint("CENTER", bouton, "CENTER", 0, 0)
bouton.foreverCadre = cadre
end
local icone = _G["Character" .. nom .. "SlotIconTexture"]
if icone then
icone:ClearAllPoints()
icone:SetAllPoints(bouton)
icone:SetTexCoord(0, 1, 0, 1)
end
return bouton
end
local function poserEmplacements()
local precedent
for index, nom in ipairs(COLONNE_GAUCHE) do
local bouton = habillerEmplacement(nom, EMPLACEMENT, ATLAS.emplacement)
if bouton then
bouton:ClearAllPoints()
if index == 1 then
bouton:SetPoint("TOPLEFT", voletGauche, "TOPLEFT", GAUCHE_X, GAUCHE_Y)
else
bouton:SetPoint("TOPLEFT", precedent, "BOTTOMLEFT", 0, -ECART_VERTICAL)
end
precedent = bouton
end
end
precedent = nil
for index, nom in ipairs(COLONNE_DROITE) do
local bouton = habillerEmplacement(nom, EMPLACEMENT, ATLAS.emplacement)
if bouton then
bouton:ClearAllPoints()
if index == 1 then
bouton:SetPoint("TOPRIGHT", voletGauche, "TOPRIGHT", DROITE_X, DROITE_Y)
else
bouton:SetPoint("TOPLEFT", precedent, "BOTTOMLEFT", 0, -ECART_VERTICAL)
end
precedent = bouton
end
end
precedent = nil
for index, nom in ipairs(RANGEE_ARMES) do
local bouton = habillerEmplacement(nom, EMPLACEMENT, ATLAS.emplacement)
if bouton then
bouton:ClearAllPoints()
if index == 1 then
bouton:SetPoint("BOTTOM", voletGauche, "BOTTOM", ARME_X, ARME_Y)
else
bouton:SetPoint("TOPLEFT", precedent, "TOPRIGHT",
ECART + (RANGEE_DECALAGE[index] or 0), 0)
end
precedent = bouton
end
end
local munitions = habillerEmplacement("Ammo", PETIT, ATLAS.petitEmplacement)
if munitions and precedent then
munitions:ClearAllPoints()
munitions:SetPoint("LEFT", precedent, "RIGHT", MUNITIONS_ECART, 0)
end
end
ForeverUI.ModeleReglage = { echelle = MODELE_ECHELLE, position = MODELE_POSITION }
-- LE MODELE SE CHARGE APRES COUP, ET REPART A ZERO.
--
-- PaperDollFrame_OnShow appelle CharacterModelFrame:SetUnit("player"), qui
-- lance un chargement ASYNCHRONE. Notre reglage, pose juste apres, s'applique
-- a un modele qui n'est pas encore la : la fin du chargement remet la
-- transformation a zero et le personnage ressort du cadre. La meme commande
-- tapee a la main tient, elle, parce que le modele est deja charge -- c'est
-- exactement ce qu'on observait.
--
-- On repose donc le reglage pendant une seconde et demie apres chaque
-- passage, a chaque image. Sans condition : rien ne garantit que GetPosition
-- rende ce que le moteur dessine vraiment, donc on ne compare pas, on
-- repose. C'est le meme rattrapage que pour la hauteur des sacs.
local MODELE_RATTRAPAGE = 1.5
local rattrapageModele = CreateFrame("Frame", "ForeverUICharacterModelRecheck")
rattrapageModele:Hide()
rattrapageModele.reste = 0
local function appliquerReglageModele()
local modele = _G["CharacterModelFrame"]
if not modele then
return
end
local reglage = ForeverUI.ModeleReglage
if modele.SetModelScale and reglage.echelle then
modele:SetModelScale(reglage.echelle)
end
if modele.SetPosition and reglage.position then
modele:SetPosition(reglage.position[1], reglage.position[2], reglage.position[3])
end
end
rattrapageModele:SetScript("OnUpdate", function(self, ecoule)
appliquerReglageModele()
self.reste = self.reste - (ecoule or 0)
if self.reste <= 0 then
self:Hide()
end
end)
ForeverUI.ModeleRattrapage = rattrapageModele
local function poserModele()
local modele = CharacterModelFrame
if not modele then
return
end
modele:ClearAllPoints()
modele:SetPoint("TOPLEFT", voletGauche, "TOPLEFT", 0, MODELE_Y)
modele:SetPoint("BOTTOMRIGHT", voletGauche, "BOTTOMRIGHT", 0, MODELE_Y)
-- Tout de suite, puis a chaque image le temps que le modele finisse
-- d'arriver.
appliquerReglageModele()
rattrapageModele.reste = MODELE_RATTRAPAGE
rattrapageModele:Show()
-- Les fleches de rotation : centrees sur le volet, cote a cote.
local gauche = _G["CharacterModelFrameRotateLeftButton"]
local droite = _G["CharacterModelFrameRotateRightButton"]
if gauche and droite then
local demi = gauche:GetWidth() / 2 + ROTATION_ECART / 2
gauche:ClearAllPoints()
gauche:SetPoint("TOP", voletGauche, "TOP", -demi, ROTATION_Y)
gauche:SetFrameLevel(modele:GetFrameLevel() + 2)
droite:ClearAllPoints()
droite:SetPoint("TOP", voletGauche, "TOP", demi, ROTATION_Y)
droite:SetFrameLevel(modele:GetFrameLevel() + 2)
end
end
-- Le panneau des resistances, decale vers la droite. Son ancrage d'origine
-- est garde a part : on repart toujours de lui, jamais de la position
-- courante, qui derive a chaque passage.
local function poserResistances()
local cadre = _G["CharacterResistanceFrame"]
if not cadre or not cadre.GetPoint or cadre:GetNumPoints() == 0 then
return
end
if not cadre.foreverAncrage then
local point, cible, pointCible, x, y = cadre:GetPoint(1)
cadre.foreverAncrage = { point, cible, pointCible, x or 0, y or 0 }
end
local a = cadre.foreverAncrage
cadre:ClearAllPoints()
cadre:SetPoint(a[1], a[2], a[3], a[4] + RESISTANCES_X, a[5])
end
-- Un selecteur prend le fond et l'intitule de l'en-tete moderne. Le menu
-- reste celui du client : c'est lui qui porte les categories et le calcul.
local function habillerSelecteur(selecteur)
if selecteur.foreverIntitule then
return
end
local nom = selecteur:GetName()
-- Le cadre dore du menu deroulant s'en va : il deborde du cadre et ne
-- se redimensionne pas avec lui. Son texte part avec, l'en-tete porte
-- le sien.
for _, piece in ipairs(SELECTEUR_PIECES) do
local region = nom and _G[nom .. piece]
if region then
region:Hide()
end
end
local fond = selecteur:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(fond, ATLAS_ENTETE, true)
fond:SetPoint("TOPLEFT", selecteur, "TOPLEFT")
fond:SetPoint("BOTTOMRIGHT", selecteur, "BOTTOMRIGHT")
selecteur.foreverFond = fond
local intitule = selecteur:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
intitule:SetPoint("CENTER", selecteur, "CENTER", 0, 1)
selecteur.foreverIntitule = intitule
-- Le bouton du client garde le clic ; il revient contre le bord droit.
-- Et la barre entiere ouvre le menu, comme un en-tete de camelot : une
-- fleche de 24 sur une barre de 203 se chercherait.
local bouton = nom and _G[nom .. "Button"]
if bouton then
bouton:ClearAllPoints()
bouton:SetPoint("RIGHT", selecteur, "RIGHT", STAT_FLECHE, 0)
end
selecteur:EnableMouse(true)
selecteur:SetScript("OnMouseUp", function(self)
ToggleDropDownMenu(nil, nil, self)
if PlaySound then
PlaySound("igMainMenuOptionCheckBoxOn")
end
end)
end
-- L'intitule d'une categorie : la CVar porte une CLE (PLAYERSTAT_BASE_STATS),
-- le texte affichable est la globale du meme nom.
local function ecrireCategorie(selecteur, cle)
if selecteur and selecteur.foreverIntitule then
selecteur.foreverIntitule:SetText((cle and _G[cle]) or "")
end
end
-- LES STATISTIQUES SE MONTRENT ET SE CACHENT, au gre des deux onglets.
--
-- Au retour on ne rallume pas les lignes une a une : on les montre puis on
-- laisse le client refaire son travail. UpdatePaperdollStats decide seul de
-- la sixieme ligne -- il la montre, et la cache pour les categories qui
-- n'ont que cinq statistiques -- et forcer la notre par-dessus ferait
-- apparaitre une ligne vide.
local function montrerStatistiques(afficher)
if not voletDroit then
return
end
voletDroit.statsMontrees = afficher and true or false
for _, groupe in ipairs(STAT_GROUPES) do
local selecteur = _G[groupe.selecteur]
if selecteur then
if afficher then selecteur:Show() else selecteur:Hide() end
end
for index = 1, STAT_PAR_GROUPE do
local ligne = _G[groupe.prefixe .. index]
if ligne then
if afficher then ligne:Show() else ligne:Hide() end
end
end
end
if afficher and PaperDollFrame_UpdateStats then
PaperDollFrame_UpdateStats()
end
end
ForeverUI.CharacterShowStats = montrerStatistiques
-- Les statistiques, reposees dans le volet droit. Ce sont des cadres du
-- client : on les deplace et on les elargit, on ne les recree pas -- ce
-- sont eux qui portent les infobulles et les menus de categorie.
local function poserStatistiques()
if not voletDroit then
return
end
local largeur = VOLET_DROIT - 2 * STAT_MARGE
local niveau = voletDroit:GetFrameLevel() + 3
local y = -(PIERRE_HAUTEUR + STAT_HAUT)
for _, groupe in ipairs(STAT_GROUPES) do
local selecteur = _G[groupe.selecteur]
if selecteur then
habillerSelecteur(selecteur)
ecrireCategorie(selecteur, GetCVar and GetCVar(groupe.cvar))
selecteur:SetFrameLevel(niveau)
selecteur:SetWidth(largeur + 2 * STAT_ENTETE_DEBORD)
selecteur:SetHeight(STAT_ENTETE)
selecteur:ClearAllPoints()
selecteur:SetPoint("TOPLEFT", voletDroit, "TOPLEFT",
STAT_MARGE - STAT_ENTETE_DEBORD, y)
-- Montre seulement si l'onglet des statistiques est celui
-- qui est ouvert : chaque passage de l'habillage repasse ici.
if voletDroit.statsMontrees == false then
selecteur:Hide()
else
selecteur:Show()
end
y = y - STAT_ENTETE
end
for index = 1, STAT_PAR_GROUPE do
local ligne = _G[groupe.prefixe .. index]
if ligne then
ligne:SetFrameLevel(niveau)
ligne:SetWidth(largeur)
ligne:ClearAllPoints()
ligne:SetPoint("TOPLEFT", voletDroit, "TOPLEFT", STAT_MARGE, y)
y = y - STAT_PAS
end
end
y = y - STAT_ENTRE_GROUPES
end
end
-- La ligne du niveau, de la race et de la classe.
--
-- PIEGE 3.3.5. Une region ne se REPARENTE pas : SetParent n'est pas dans la
-- table des methodes de FontString de ce client. Ancrer celle du client au
-- volet ne suffirait pas non plus -- elle appartient au cadre, donc elle se
-- dessinerait SOUS les volets, qui sont des cadres fils. On efface donc la
-- sienne et on pose la notre dans le volet, en recopiant son texte : c'est
-- le client qui le compose, nous ne faisons que l'afficher.
local function poserNiveau()
local source = _G["CharacterLevelText"]
if not voletDroit.ligneNiveau then
local ligne = voletDroit:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
ligne:SetPoint("TOP", voletDroit, "TOP", 0, NIVEAU_Y)
ligne:SetWidth(NIVEAU_LARGEUR)
ligne:SetHeight(NIVEAU_HAUTEUR)
ligne:SetJustifyH("CENTER")
ligne:SetJustifyV("MIDDLE")
voletDroit.ligneNiveau = ligne
end
if source then
source:Hide()
end
-- SANS LA RACE, a la demande -- et c'est aussi ce que fait camelot :
-- son PaperDollFrame_SetLevel emploie PLAYER_LEVEL_NO_SPEC, qui ne
-- porte que le niveau et la classe. Cette chaine n'existe pas dans ce
-- client, dont le PLAYER_LEVEL vaut "Level %s %s %s" -- niveau, RACE,
-- classe. La ligne est donc recomposee a partir de deux chaines que le
-- client porte, UNIT_LEVEL_TEMPLATE et le nom de classe, plutot que
-- d'ecrire un format en dur qui ne tiendrait que dans une langue.
local niveau = UnitLevel and UnitLevel("player")
local classe = UnitClass and UnitClass("player")
local texte = ""
if niveau and classe then
texte = string.format(UNIT_LEVEL_TEMPLATE or "Level %d", niveau) .. " " .. classe
elseif source then
texte = source:GetText() or ""
end
voletDroit.ligneNiveau:SetText(texte)
end
-- Un onglet du volet droit : le cadre de camelot, une icone dessous, et une
-- marque quand il est choisi. SetCheckedTexture ne prend qu'un CHEMIN dans
-- ce client, jamais un rectangle d'atlas : la marque est donc une texture a
-- nous, montree et cachee a la main.
local function creerOngletVolet(nom, infobulle, clic)
local onglet = CreateFrame("Button", nom, voletDroit)
onglet:SetWidth(ONGLET_VOLET)
onglet:SetHeight(ONGLET_VOLET)
onglet:SetFrameLevel(voletDroit:GetFrameLevel() + 3)
local icone = onglet:CreateTexture(nil, "BACKGROUND")
icone:SetWidth(ONGLET_ICONE)
icone:SetHeight(ONGLET_ICONE)
icone:SetPoint("CENTER", onglet, "CENTER", 0, 0)
onglet.icone = icone
local choisi = onglet:CreateTexture(nil, "BORDER")
ForeverUI.SetAtlas(choisi, ATLAS_ONGLET_VOLET_CHOISI)
choisi:SetPoint("CENTER", onglet, "CENTER", 0, 0)
choisi:Hide()
onglet.choisi = choisi
local cadre = onglet:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(cadre, ATLAS_ONGLET_VOLET)
cadre:SetPoint("CENTER", onglet, "CENTER", 0, 0)
onglet:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(infobulle)
GameTooltip:Show()
end)
onglet:SetScript("OnLeave", function() GameTooltip:Hide() end)
if clic then
onglet:SetScript("OnClick", clic)
end
return onglet
end
local function poserOngletsVolet()
if not voletDroit then
return
end
if not voletDroit.ongletStats then
-- Les deux se comportent en onglets : celui qu'on ouvre ferme
-- l'autre. Le gestionnaire reste celui du client, on ne fait que
-- montrer et cacher son panneau.
local function choisir(statistiques)
if voletDroit.ongletStats then
if statistiques then
voletDroit.ongletStats.choisi:Show()
voletDroit.ongletEquipement.choisi:Hide()
else
voletDroit.ongletStats.choisi:Hide()
voletDroit.ongletEquipement.choisi:Show()
end
end
montrerStatistiques(statistiques)
if GearManagerDialog then
if statistiques then
GearManagerDialog:Hide()
else
GearManagerDialog:Show()
end
end
end
voletDroit.ongletStats = creerOngletVolet("ForeverUICharacterStatsTab",
CHARACTER_INFO or "Character Info",
function() choisir(true) end)
voletDroit.ongletStats.choisi:Show()
voletDroit.ongletEquipement = creerOngletVolet(
"ForeverUICharacterGearTab", EQUIPMENT_MANAGER or "Equipment Manager",
function() choisir(false) end)
voletDroit.ongletEquipement.icone:SetTexture(ICONE_GESTIONNAIRE)
-- Les deux se touchent, comme chez camelot, et la paire est centree.
voletDroit.ongletStats:SetPoint("TOP", voletDroit, "TOP",
-ONGLET_VOLET / 2, ONGLET_VOLET_Y)
voletDroit.ongletEquipement:SetPoint("LEFT", voletDroit.ongletStats,
"RIGHT", 0, 0)
end
-- Le portrait, rogne comme la source le demande. A reposer : le client
-- refait la texture a chaque changement d'apparence.
if SetPortraitTexture then
SetPortraitTexture(voletDroit.ongletStats.icone, "player")
voletDroit.ongletStats.icone:SetTexCoord(ONGLET_PORTRAIT_COORD[1],
ONGLET_PORTRAIT_COORD[2], ONGLET_PORTRAIT_COORD[3], ONGLET_PORTRAIT_COORD[4])
end
-- Le bouton d'origine du gestionnaire s'efface : c'est l'onglet qui
-- ouvre desormais son panneau.
local ancien = _G["GearManagerToggleButton"]
if ancien then
ancien:Hide()
end
end
-- LES ONGLETS LATERAUX. camelot les met en colonne A DROITE, dehors : une
-- barre de 64 x 384 ancree au TOPRIGHT du cadre, 30 px sous son haut. Les
-- onglets du bas de 3.3.5 sont donc reposes la, empiles.
local function poserOnglets(cadre)
if not barreOnglets then
barreOnglets = CreateFrame("Frame", "ForeverUICharacterModeTabs", cadre)
barreOnglets:SetWidth(ONGLETS_L)
barreOnglets:SetHeight(ONGLETS_H)
barreOnglets:SetPoint("TOPLEFT", cadre, "TOPRIGHT", 0, ONGLETS_Y)
end
local precedent
local index = 1
while true do
local onglet = _G["CharacterFrameTab" .. index]
if not onglet then
break
end
if not onglet.foreverSkinned then
balayerTextures(onglet)
for _, methode in ipairs({ "GetNormalTexture", "GetPushedTexture",
"GetDisabledTexture", "GetHighlightTexture" }) do
local texture = onglet[methode] and onglet[methode](onglet)
if texture then
texture:SetAlpha(0)
end
end
local fond = onglet:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(fond, ATLAS.onglet, true)
fond:SetAllPoints(onglet)
onglet.foreverFond = fond
local survol = onglet:CreateTexture(nil, "HIGHLIGHT")
ForeverUI.SetAtlas(survol, ATLAS.ongletSurvol, true)
survol:SetAllPoints(onglet)
local icone = onglet:CreateTexture(nil, "ARTWORK")
icone:SetWidth(ONGLET_ICONE)
icone:SetHeight(ONGLET_ICONE)
icone:SetPoint("CENTER", onglet, "CENTER", ONGLET_ICONE_X, 0)
icone:Hide()
onglet.foreverIcone = icone
-- Le texte du client reste : les icones que la source demande
-- pour les autres onglets n'existent pas dans l'export.
local texte = _G["CharacterFrameTab" .. index .. "Text"]
if texte then
texte:ClearAllPoints()
texte:SetPoint("CENTER", onglet, "CENTER", ONGLET_ICONE_X, 0)
texte:SetWidth(ONGLET_L - 10)
end
onglet.foreverSkinned = true
onglets[index] = onglet
end
onglet:SetWidth(ONGLET_L)
onglet:SetHeight(ONGLET_H)
onglet:ClearAllPoints()
if precedent then
onglet:SetPoint("TOPLEFT", precedent, "BOTTOMLEFT", 0, 0)
else
onglet:SetPoint("TOPLEFT", barreOnglets, "TOPLEFT", 0, 0)
end
precedent = onglet
index = index + 1
end
-- L'onglet du personnage porte le portrait du joueur, rogne comme le
-- fait UpdateCharacterModeTabPortrait.
local premier = onglets[1]
if premier and premier.foreverIcone and SetPortraitTexture then
SetPortraitTexture(premier.foreverIcone, "player")
premier.foreverIcone:SetTexCoord(PORTRAIT_ONGLET, 1 - PORTRAIT_ONGLET,
PORTRAIT_ONGLET, 1 - PORTRAIT_ONGLET)
premier.foreverIcone:Show()
local texte = _G["CharacterFrameTab1Text"]
if texte then
texte:Hide()
end
end
end
-- Le systeme de panneaux du client repose la fenetre a chaque ouverture :
-- on remet la place retenue apres lui.
local function reposerLaPlace(cadre)
local place = placeRetenue()["feuille"]
if not place or InCombatLockdown() then
return
end
cadre:ClearAllPoints()
cadre:SetPoint(place.point, UIParent, place.relativePoint or place.point,
place.x or 0, place.y or 0)
end
local function habiller()
local cadre = CharacterFrame
if not cadre or InCombatLockdown() then
return
end
cadre:SetWidth(LARGEUR)
cadre:SetHeight(HAUTEUR)
if not cadre.foreverSkinned then
-- L'ORDRE COMPTE. Les volets sont des cadres fils : ils recouvrent
-- toute region de leur parent. L'art du panneau doit donc vivre
-- dans un cadre fils de niveau superieur, comme le NineSlice de la
-- source, sinon les fonds le recouvrent.
effacerArtDepoque()
monterVolets(cadre)
ForeverUI.SetPanelArt(cadre, {
coinHautGauche = COIN_PORTRAIT,
coins = PANNEAU_COINS,
niveau = NIVEAU_ART,
})
cadre.foreverSkinned = true
end
effacerArtDepoque()
if ForeverUI.UpdatePanelCorners then
ForeverUI.UpdatePanelCorners(cadre)
end
poserPortrait(cadre)
poserTitre(cadre)
reposerLaPlace(cadre)
poserFermeture(cadre)
poserModele()
poserResistances()
poserStatistiques()
poserNiveau()
poserOngletsVolet()
poserEmplacements()
poserOnglets(cadre)
end
-- Le client rappelle UpdatePaperdollStats(prefixe, cle) des que la
-- categorie change : l'intitule de l'en-tete correspondant se remet a jour.
local function suivreCategorie(prefixe, cle)
for _, groupe in ipairs(STAT_GROUPES) do
if groupe.prefixe == prefixe then
ecrireCategorie(_G[groupe.selecteur], cle)
return
end
end
end
ForeverUI.CharacterSheet = { Apply = habiller, Tabs = onglets }
if hooksecurefunc and type(_G["UpdatePaperdollStats"]) == "function" then
hooksecurefunc("UpdatePaperdollStats", suivreCategorie)
end
if hooksecurefunc then
for _, nomFonction in ipairs({ "CharacterFrame_ShowSubFrame", "PaperDollFrame_OnShow",
"PaperDollFrame_SetLevel", "CharacterFrame_Collapse", "CharacterFrame_Expand" }) do
if type(_G[nomFonction]) == "function" then
hooksecurefunc(nomFonction, habiller)
end
end
end
local veilleur = CreateFrame("Frame", "ForeverUICharacterWatcher")
veilleur:RegisterEvent("PLAYER_ENTERING_WORLD")
veilleur:RegisterEvent("PLAYER_REGEN_ENABLED")
veilleur:RegisterEvent("UNIT_INVENTORY_CHANGED")
veilleur:RegisterEvent("UNIT_PORTRAIT_UPDATE")
veilleur:RegisterEvent("UNIT_MODEL_CHANGED")
veilleur:SetScript("OnEvent", habiller)
if CharacterFrame then
CharacterFrame:HookScript("OnShow", habiller)
end
habiller()
-- LE TEMOIN. Un bouton d'equipement qui ne repond pas au clic est recouvert
-- par un cadre qui prend la souris, ou bien il ne la prend plus lui-meme :
-- rien de tout cela ne se voit a l'ecran. On demande donc au jeu.
--
-- Deux reponses. D'abord l'etat du bouton et la liste de TOUT ce qui, visible
-- et sensible a la souris, couvre son centre -- avec sa strate et son niveau,
-- qui decident lequel recoit le clic. Ensuite, pendant cinq secondes, ce que
-- GetMouseFocus rend : il suffit de promener le curseur sur un emplacement
-- pour lire le nom du cadre qui l'intercepte vraiment.
local function nomDe(cadre)
if not cadre then
return "?"
end
return (cadre.GetName and cadre:GetName()) or "(sans nom)"
end
local function couvre(cadre, x, y)
if not cadre.GetLeft then
return false
end
local g, d, h, b = cadre:GetLeft(), cadre:GetRight(), cadre:GetTop(), cadre:GetBottom()
return g and d and h and b and x >= g and x <= d and y >= b and y <= h
end
local function parcourir(cadre, x, y, trouves, profondeur)
if not cadre or profondeur > 6 then
return
end
if cadre.IsVisible and cadre:IsVisible() and cadre.IsMouseEnabled
and cadre:IsMouseEnabled() and couvre(cadre, x, y) then
trouves[#trouves + 1] = cadre
end
if cadre.GetChildren then
local enfants = { cadre:GetChildren() }
for _, enfant in ipairs(enfants) do
parcourir(enfant, x, y, trouves, profondeur + 1)
end
end
end
function ForeverUI.CharacterSheetDebug()
local dire = function(texte)
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. texte)
end
local bouton = _G["CharacterHeadSlot"]
if not bouton or not bouton:GetLeft() then
dire("feuille : ouvrez-la d'abord, puis refaites /fui perso")
return
end
dire(string.format("tete : %dx%d strate=%s niveau=%d souris=%s montre=%s",
bouton:GetWidth(), bouton:GetHeight(), tostring(bouton:GetFrameStrata()),
bouton:GetFrameLevel(), tostring(bouton:IsMouseEnabled()),
tostring(bouton:IsVisible())))
-- Un bouton peut aussi avoir perdu ses scripts ou avoir ete desactive :
-- cela ne se voit pas davantage a l'ecran.
DEFAULT_CHAT_FRAME:AddMessage(string.format(
" clic=%s glisser=%s recoit=%s actif=%s",
tostring(bouton:GetScript("OnClick") ~= nil),
tostring(bouton:GetScript("OnDragStart") ~= nil),
tostring(bouton:GetScript("OnReceiveDrag") ~= nil),
tostring(not bouton.IsEnabled or bouton:IsEnabled() and true or false)))
local x = (bouton:GetLeft() + bouton:GetRight()) / 2
local y = (bouton:GetTop() + bouton:GetBottom()) / 2
local trouves = {}
parcourir(CharacterFrame, x, y, trouves, 0)
DEFAULT_CHAT_FRAME:AddMessage(" ce qui couvre son centre et prend la souris :")
for _, cadre in ipairs(trouves) do
DEFAULT_CHAT_FRAME:AddMessage(string.format(" %-34s strate=%-16s niveau=%d",
nomDe(cadre), tostring(cadre:GetFrameStrata()), cadre:GetFrameLevel()))
end
local guetteur = ForeverUI.guetteurSouris
if not guetteur then
guetteur = CreateFrame("Frame", "ForeverUICharacterMouseWatch")
guetteur:Hide()
ForeverUI.guetteurSouris = guetteur
end
guetteur.reste = 5
guetteur.dernier = nil
guetteur:SetScript("OnUpdate", function(self, ecoule)
self.reste = self.reste - ecoule
if self.reste <= 0 then
self:Hide()
self:SetScript("OnUpdate", nil)
DEFAULT_CHAT_FRAME:AddMessage(" (fin de la veille)")
return
end
local sous = GetMouseFocus and GetMouseFocus()
local nom = nomDe(sous)
if nom ~= self.dernier then
self.dernier = nom
DEFAULT_CHAT_FRAME:AddMessage(" sous le curseur : " .. nom)
end
end)
guetteur:Show()
dire("promenez le curseur sur un emplacement pendant cinq secondes.")
end
-- LE REGLAGE DU MODELE EN JEU. Aucune des deux valeurs ne se releve dans la
-- source -- camelot cadre par une scene que 3.3.5 n'a pas -- elles se jugent
-- donc a l'oeil. La commande les pose et les rend, pour pouvoir comparer les
-- deux leviers sans recharger l'interface.
--
-- /fui modele ce que porte le modele
-- /fui modele echelle 0.8 SetModelScale
-- /fui modele position -5 0 0 SetPosition(profondeur, lateral, hauteur)
-- /fui modele position defaut rend la position au client
function ForeverUI.CharacterModelTune(argument)
local dire = function(texte)
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. texte)
end
local modele = _G["CharacterModelFrame"]
if not modele then
return
end
local cle, reste = string.match(argument or "", "^(%S*)%s*(.*)$")
cle = string.lower(cle or "")
local reglage = ForeverUI.ModeleReglage
if cle == "echelle" then
reglage.echelle = tonumber(reste) or reglage.echelle
elseif cle == "position" then
if string.lower(reste) == "defaut" then
reglage.position = nil
modele:RefreshUnit()
else
local x, y, z = string.match(reste, "^(%-?[%d%.]+)%s+(%-?[%d%.]+)%s+(%-?[%d%.]+)$")
if x then
reglage.position = { tonumber(x), tonumber(y), tonumber(z) }
else
dire("modele : /fui modele position <profondeur> <lateral> <hauteur>")
return
end
end
elseif cle ~= "" then
dire("modele : echelle <n> | position <x> <y> <z> | position defaut")
return
end
if ForeverUI.CharacterSheet then
ForeverUI.CharacterSheet.Apply()
end
local echelle = modele.GetModelScale and modele:GetModelScale()
local x, y, z = nil, nil, nil
if modele.GetPosition then
x, y, z = modele:GetPosition()
end
dire(string.format("modele : echelle=%s position=(%s, %s, %s)",
tostring(echelle), tostring(x), tostring(y), tostring(z)))
end