Files
moon-well-ui/data/addon/ForeverUI/ProfessionsBook.lua
T
Adrestie b1da47c3eb PNJ etapes 3-4 : maitre, vol, etable, sertissage, dialogues, metiers
Etape 3
- Maitre (Trainer.lua, nouveau) : liste de camelot (categories
  professions-recipe-header, lignes TrainerTextures), filtre, rang de
  metier lu dans les competences.
- Maitre de vol (Taxi.lua, nouveau) : TaxiFrame de camelot 590 x 608,
  cadre bronze, images de vol 1463 / 1464 (positions reportees du carre
  3.3.5), boutons et traits de camelot ; zones BC, Outreterre et Norfendre
  sur l'image de 3.3.5.
- Etable (Stable.lua, nouveau) : scene de camelot, fond selon la
  specialisation, rangee de 5 centree.
- Sertissage (Socketing.lua, nouveau) : decor et chasses de camelot.
- Boites de dialogue (Dialogues.lua, nouveau) : StaticPopup1-4 aux
  contours de camelot (fond sombre, bordure en losanges, croix).
- Gabarits : Gb.CadreSimple (repris de la banque de guilde), Gb.MenuFiltre.

Etape 4 (metiers, disposition de camelot)
- Fabrication (TradeSkill.lua, nouveau) : 673 x 594, liste en arbre,
  filtre, recherche, rang, fiche (carte du metier, resultat, reactifs),
  boutons ; noms mesures sur un texte a part (une ligne reutilisee coupait
  le nom suivant) ; eclat du rang rogne a la largeur du masque.
- Micro-bouton Professions (BottomBar.lua) apres la feuille de personnage.
- Livre des metiers (ProfessionsBook.lua, nouveau) : cartes de camelot,
  rangs, sorts securises, oubli ; plus de deux metiers principaux (zone a
  defilement, colonnes reduites) ; portrait cuit rond ; onglets lateraux
  (ensemble + metiers a fabrication, retrecis s'ils sont trop nombreux) ;
  fenetre deplacable (place commune livre / fabrication).
- DropDown : liste du filtre des metiers elargie a ses entrees.
- Art : feuilles et elements des metiers, icones d'onglets cuites
  (cuire_masque.py), portrait rond (cuire_portrait.py, nouveau).
- Banc : essais des etapes 3 et 4 ; micro-menu a neuf boutons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:02:35 +02:00

1312 lines
50 KiB
Lua

-- ForeverUI : le livre des metiers de camelot -- la page d'ensemble de sa
-- fenetre des metiers (ProfessionsFrame.BookPage), que le micro-bouton des
-- metiers ouvre (BottomBar.lua). Chantier des PNJ, etape 4 (demande du
-- 2026-09-28 : « ajouter le micro bouton "Professions" a cote de la feuille
-- de personnage et faire le menu »), puis ses onglets lateraux (« fais la
-- suite de l'etape 4 », voir la section des onglets).
--
-- RELEVE -- CAMELOT (blizzard_professionsbook : camelot/blizzard_professions-
-- book.lua, camelot/blizzard_professionsbooktemplates.xml, blizzard_-
-- professionsbook.lua / templates.xml / _bootstrap.lua ; blizzard_-
-- professions : camelot/blizzard_professionsframe.xml / .lua ; blizzard_-
-- professionstemplates : rankbar ; blizzard_micromenu : mainline/mainmenubar-
-- microbuttons.lua ; tables DB2 GlobalStrings, GlobalColor, UiTextureAtlas-
-- Member / ElementSliceData) :
-- fenetre ProfessionsFrame 673 x 594, celle de la page de fabrication
-- (TradeSkill.lua) ; SelectBookPage : portrait INV_SideTab_-
-- Professions_c60, titre TRADE_SKILL_TITLE de TRADE_SKILLS ;
-- fond Profession-Background-Overview sans stries ; la page
-- couvre toute la fenetre
-- metiers GetProfessions : deux principaux, puis cuisine, peche,
-- secourisme
-- principal 664 x 142 : le premier a (5, -41), le second sous lui a
-- (0, 5) ; carte Profession-overview-Card-<metier> (sinon
-- Profession-overview-Card) sur toute la carte ; nom
-- GameFontNormal a (20, -24) ; rang 441, RIGHT (-40, 0) ;
-- oubli 20 x 20 a droite du rang (1, -4) : Profession-button-
-- red-crossmark a sa taille (-pressed enfonce), infobulle
-- UNLEARN_SKILL_TOOLTIP, clic : boite UNLEARN_SKILL ; sorts a
-- BOTTOMLEFT (15, 46) s'il n'y en a qu'un, sinon (15, 60) et
-- (15, 10) ; absent : PROFESSIONS_FIRST_ / _SECOND_PROFESSION
-- a (20, -24), PROFESSIONS_MISSING_PROFESSION GameFontHighlight-
-- Small2 485 au centre
-- secondaire 225 x 275, le premier sous le second principal (0, 4), les
-- autres a sa droite (-6, 0) : cuisine, peche, secourisme ;
-- carte Profession-overview-card-generic-<metier> ; nom TOP
-- (0, -25) ; rang 190, TOP (0, -47) ; jusqu'a quatre sorts
-- empiles depuis BOTTOMLEFT (20, 25) ; absent : le nom du
-- metier et PROFESSIONS_<METIER>_MISSING (175, TOP sous le
-- nom (5, -13))
-- rang ProfessionsRankBarTemplate, 18 de haut : fond Profession-
-- ProgressBar-BG et cadre Profession-ProgressBar-frame
-- (decoupe 30 / 0 / 30 / 0) a la largeur de la barre et a la
-- hauteur de l'atlas ; remplissage 441 x 18 a (2, -3), vu de
-- 1 apres son debut sur largeur x part + decalage (-7 pour
-- un principal, -5 pour un secondaire) ; eclat au bout,
-- eteint si plein ; texte TRADESKILL_NAME_RANK Number12Font-
-- Outline au centre (-3)
-- sort 40 x 40 : icone sous un masque carre rentre de 3, cadre
-- Profession-square-frame (48) au centre ; nom GameFontNormal
-- 100 a RIGHT (5, 7), dore (PASSIVE_SPELL_FONT_COLOR pour un
-- passif), rang NewSubSpellFont 95 x 28 dessous (0, -1) ;
-- icone a 0,4 si le sort n'est pas utilisable ; survol
-- ButtonHilight-Square (UI-PassiveHighlight pour un passif),
-- enfonce UI-Quickslot-Depress ; jamais coche
-- ouverture ToggleProfessionsBook : la fenetre s'ouvre ou se ferme ;
-- sons IG_SPELLBOOK_OPEN / IG_ABILITY_CLOSE ; UNLEARN_SKILL
-- se ferme avec elle
--
-- RELEVE -- 3.3.5 : ni GetProfessions ni GetProfessionInfo. Les metiers se
-- lisent dans les competences (GetSkillLineInfo : nom, rang, bonus, maximum,
-- abandonnable ; l'oubli par la boite UNLEARN_SKILL et AbandonSkill, comme
-- SkillFrame), leurs sorts dans le grimoire (GetSpellLink de chaque case
-- donne l'identifiant du sort).
--
-- CE QUI DIFFERE, ET POURQUOI.
-- * La fenetre du livre est a nous : le TradeSkillFrame de 3.3.5 n'existe
-- que pendant une session de metier. Meme taille, meme place (panneau
-- "left", pushable 3, comme lui) ; elle se declare par ses attributs
-- UIPanelLayout-*, sans toucher a la table UIPanelWindows du client.
-- Ouvrir un metier (clic sur son sort) ferme le livre : la page de
-- fabrication prend sa place, comme chez camelot.
-- * Le metier est reconnu a son nom, compare a celui d'un sort qui porte
-- le meme (GetSpellInfo : la langue du client) : il choisit la carte et
-- la bande du rang.
-- * Les sorts d'une carte : ceux du grimoire que SkillLineAbility.dbc range
-- sous le metier (table SORTS), actifs seulement, dans un ordre fixe --
-- celui qui ouvre la fabrication d'abord. Minage : Fondre et Decouverte
-- de gisements ; herboristerie : Decouverte d'herbes et Sang-de-vie ;
-- depecage : son sort de rang. Camelot les tient de GetProfessionInfo.
-- * LANCER UN SORT EST PROTEGE : les boutons de sort sont securises, et la
-- fenetre qui les porte devient protegee. Elle se ferme donc a l'entree
-- en combat et ne s'ouvre pas pendant (ERR_NOT_IN_COMBAT) ; camelot
-- l'ouvre en combat.
-- * Une categorie de competences repliee est depliee le temps de la
-- lecture, puis repliee a nouveau.
-- * Les barres de rang restent dans la strate de la fenetre (camelot les
-- met en HIGH) ; leur bande est fixe (la premiere image, voir
-- TradeSkill.lua) ; pas de menu d'extension.
-- * Pas d'emplacements de barre montres a l'ouverture (MultiActionBar_-
-- ShowAllGrids toucherait aux boutons d'action proteges), pas de
-- clignotement de rappel de specialisation, pas d'aide.
-- * Joaillerie et calligraphie n'ont pas de carte chez camelot : la carte
-- generique, comme son code le prevoit.
-- * Le micro-bouton reste enfonce tant que les metiers sont ouverts (livre
-- ou fabrication), comme ses voisins ; chez camelot il ne l'est jamais
-- (il regarde ProfessionsBookFrame, que camelot ne charge pas).
-- * Le portrait est CUIT rond (tools/cuire_portrait.py) : SetPortraitTo-
-- Texture le laissait carre, hors de l'anneau (demande du 2026-09-28).
--
-- PLUS DE DEUX METIERS PRINCIPAUX (demande du 2026-09-28 : un serveur prive
-- peut en donner davantage ; camelot n'en connait que deux). Les rangees
-- sont dans une zone a defilement, au pas de camelot (142 de haut, 5 de
-- recouvrement : 137) :
-- * trois metiers : une troisieme rangee ; les colonnes cuisine, peche,
-- secourisme perdent sa hauteur (275 -> 138). Leurs sorts ne tiennent
-- plus empiles : ils passent en ligne, icones seules (le nom et le rang
-- restent dans l'infobulle) ;
-- * au-dela : la zone garde trois rangees, une barre de defilement parait
-- a droite, les rangees perdent sa place en largeur (664 -> 646), et les
-- suivantes, dessous, passent derriere les colonnes, invisibles ;
-- * une carte plus petite que son art n'est pas ecrasee : ses bords sont
-- gardes et une bande est rognee (le bord gauche d'une rangee, le bord
-- haut d'une colonne, 20).
local ForeverUI = ForeverUI or {}
_G.ForeverUI = ForeverUI
local Gb = ForeverUI.Gabarits
local L = ForeverUI.L
local LM = {}
ForeverUI.LivreMetiers = LM
local SEP = string.char(92)
local POLICE = "Fonts" .. SEP .. "FRIZQT__.TTF"
local POLICE_CHIFFRES = "Fonts" .. SEP .. "ARIALN.TTF"
local BOUTONS = "Interface" .. SEP .. "Buttons" .. SEP
local ICONE_LIVRE = "Interface" .. SEP .. "ForeverUI" .. SEP .. "icons" .. SEP .. "inv_sidetab_professions_c60-rond"
local N = {
fenetre = { 673, 594 },
portrait = { cote = 48, x = 1, y = 1.5 },
principal = { 664, 142, x = 5, y = -41, ecart = 5 },
secondaire = { 225, 275, ecart = 4, pas = -6 },
-- plus de deux principaux : trois rangees visibles au plus, la barre
-- (8, fleches de 17) dans une gouttiere de 18 a droite des rangees
rangees = 3,
gouttiere = 18,
rogne = 20,
enLigne = 5,
nom = { 20, -24 },
absent = 485,
sortsPrincipal = { x = 15, seul = 46, haut = 60, bas = 10 },
sortsSecondaire = { x = 20, y = 25 },
sort = { cote = 40, masque = 3, texte = { 100, 5, 7 }, sous = { 95, 28, -1 } },
rangPrincipal = { 441, x = -40, decalage = -7 },
rangSecondaire = { 190, y = -47, decalage = -5 },
rang = { h = 18, fond = 23, tranche = 30, rempli = { 441, 18, 2, -3 }, masque = 1, eclat = { 53, 16 }, texte = -3 },
oubli = { 20, 1, -4 },
secondaireNom = -25,
secondaireTexte = { 175, 5, -13 },
niveaux = { contenu = 1, zone = 1, rangees = 2, colonnes = 10, croix = 22 },
}
-- NORMAL_FONT_COLOR ; PASSIVE_SPELL_FONT_COLOR (GlobalColor de camelot,
-- 0xffc4a300)
local COULEURS = {
normal = { 1, 0.82, 0 },
passif = { 0.7686, 0.6392, 0 },
}
-- UIPanelWindows["TradeSkillFrame"] (Blizzard_TradeSkillUI.lua de 3.3.5),
-- plus whileDead, comme un grimoire ; la largeur de camelot, onglets
-- compris (professionsFrameWidthOverride), donnee aussi au TradeSkillFrame
local LARGEUR_PANNEAU = 750
local PANNEAU = { area = "left", pushable = 3, whileDead = 1, width = LARGEUR_PANNEAU }
-- LES METIERS. nom : un sort qui porte le nom du metier (Spell.dbc,
-- SkillLine.dbc) ; bande : Skillbar_Fill_Flipbook_<bande> ; secondaire :
-- sa place parmi les trois cartes du bas ; onglet : l'icone de son onglet
-- lateral (tabicons/), pour ceux qui ont une page de fabrication. SORTS : par groupe, les rangs d'un
-- meme sort (SkillLineAbility.dbc, sorts ni recettes ni caches), dans
-- l'ordre des cartes.
local METIERS = {
alchemy = { nom = 2259, bande = "alchemy_c60", onglet = "trade_alchemy" },
blacksmithing = { nom = 2018, bande = "blacksmithing", onglet = "trade_blacksmithing" },
enchanting = { nom = 7411, bande = "enchanting_c60", onglet = "trade_engraving" },
engineering = { nom = 4036, bande = "engineering", onglet = "trade_engineering" },
herbalism = { nom = 9134, bande = "herbalism" },
inscription = { nom = 45357, bande = "inscription", onglet = "inv_inscription_tradeskill01" },
jewelcrafting = { nom = 25229, bande = "jewelcrafting", onglet = "inv_misc_gem_01" },
leatherworking = { nom = 2108, bande = "leatherworking", onglet = "trade_leatherworking" },
mining = { nom = 2575, bande = "mining", onglet = "trade_mining" },
skinning = { nom = 8613, bande = "skinning_c60" },
tailoring = { nom = 3908, bande = "tailoring", onglet = "trade_tailoring" },
cooking = { nom = 2550, bande = "cooking", secondaire = 1, absent = L.PROFESSIONSBOOK_COOKING_MISSING,
onglet = "inv_misc_food_15" },
fishing = { nom = 7620, bande = "fishing", secondaire = 2, absent = L.PROFESSIONSBOOK_FISHING_MISSING },
firstaid = { nom = 3273, bande = "firstaid_c60", secondaire = 3, absent = L.PROFESSIONSBOOK_FIRST_AID_MISSING,
onglet = "spell_holy_sealofsacrifice" },
}
local SECONDAIRES = { "cooking", "fishing", "firstaid" }
local SORTS = {
alchemy = { { 2259, 3101, 3464, 11611, 28596, 51304 } },
blacksmithing = { { 2018, 3100, 3538, 9785, 29844, 51300 } },
enchanting = { { 7411, 7412, 7413, 13920, 28029, 51313 }, { 13262 } },
engineering = { { 4036, 4037, 4038, 12656, 30350, 51306 } },
herbalism = { { 2383, 8387 }, { 55428, 55480, 55500, 55501, 55502, 55503 } },
inscription = { { 45357, 45358, 45359, 45360, 45361, 45363 }, { 51005 } },
jewelcrafting = { { 25229, 25230, 28894, 28895, 28897, 51311 }, { 31252 } },
leatherworking = { { 2108, 3104, 3811, 10662, 32549, 51302 } },
mining = { { 2656 }, { 2580, 8388 } },
skinning = { { 8613, 8617, 8618, 10768, 32678, 50305 } },
tailoring = { { 3908, 3909, 3910, 12180, 26790, 51309 } },
cooking = { { 2550, 3102, 3413, 18260, 33359, 51296 }, { 818 } },
fishing = { { 62734, 7620, 7731, 7732, 18248, 33095, 51294 }, { 43308 } },
firstaid = { { 3273, 3274, 7924, 10846, 27028, 45542 } },
}
local function poser(r, ...)
r:ClearAllPoints()
r:SetPoint(...)
end
local function atlas(t, nom, taille)
return ForeverUI.SetAtlas(t, nom, not taille)
end
local vrai = Gb.Vrai
-- les polices de camelot absentes de 3.3.5 (les memes objets que
-- TradeSkill.lua)
local function police(nom, chemin, taille, contour, ombre, r, g, b)
local p = _G[nom] or CreateFont(nom)
p:SetFont(chemin, taille, contour or "")
if ombre then
p:SetShadowOffset(1, -1)
p:SetShadowColor(0, 0, 0, 1)
else
p:SetShadowOffset(0, 0)
p:SetShadowColor(0, 0, 0, 0)
end
p:SetTextColor(r or 1, g or 1, b or 1)
return p
end
local POLICES = {
small2 = police("ForeverUIFontHighlightSmall2", POLICE, 11), -- GameFontHighlightSmall2
sous = police("ForeverUIFontNewSubSpell", POLICE, 10, nil, true, 0.82, 0.7, 0.54), -- NewSubSpellFont
rang = police("ForeverUIFontNumber12Outline", POLICE_CHIFFRES, 12, "OUTLINE"), -- Number12FontOutline
}
-- ------------------------------------------------------------ la lecture
-- DEPLIER, LIRE, REPLIER. Replier et deplier annoncent SKILL_LINES_CHANGED :
-- le livre ne relit pas les annonces qui suivent sa propre lecture.
local silence = 0
local function deplier()
local replies = {}
local i = 1
while i <= (GetNumSkillLines() or 0) do
local nom, entete, deplie = GetSkillLineInfo(i)
if vrai(entete) and not vrai(deplie) then
replies[nom] = true
ExpandSkillHeader(i)
end
i = i + 1
end
return replies
end
-- le nom de chaque metier dans la langue du client -> sa cle
local function nomsDesMetiers()
local parNom = {}
for cle, m in pairs(METIERS) do
local nom = GetSpellInfo(m.nom)
if nom then parNom[nom] = cle end
end
return parNom
end
-- les metiers du joueur : les principaux (deux chez camelot, davantage sur
-- un serveur prive), et les secondaires par cle
function LM.Lire()
local parNom = nomsDesMetiers()
local replies = deplier()
local principaux, secondaires = {}, {}
for i = 1, GetNumSkillLines() or 0 do
local nom, entete, _, rang, _, bonus, maxi, abandon = GetSkillLineInfo(i)
local cle = not vrai(entete) and parNom[nom]
if cle then
local m = { cle = cle, nom = nom, rang = rang or 0, maxi = maxi or 0, bonus = bonus or 0, abandon = vrai(abandon) }
if METIERS[cle].secondaire then
secondaires[cle] = m
else
principaux[#principaux + 1] = m
end
end
end
if next(replies) then
for i = GetNumSkillLines() or 0, 1, -1 do
local nom, entete, deplie = GetSkillLineInfo(i)
if vrai(entete) and vrai(deplie) and replies[nom] then
CollapseSkillHeader(i)
end
end
silence = GetTime() + 0.5
end
return principaux, secondaires
end
-- le grimoire : identifiant du sort -> case
local function lireGrimoire()
local cases = {}
for onglet = 1, GetNumSpellTabs() or 0 do
local _, _, decalage, nombre = GetSpellTabInfo(onglet)
decalage = decalage or 0
for slot = decalage + 1, decalage + (nombre or 0) do
local lien = GetSpellLink(slot, BOOKTYPE_SPELL)
local id = lien and tonumber(string.match(lien, "spell:(%d+)"))
if id and not cases[id] then cases[id] = slot end
end
end
return cases
end
-- les cases des sorts d'un metier : un par groupe, le plus haut rang present
local function sortsDu(cle, cases, maxi)
local liste = {}
for _, groupe in ipairs(SORTS[cle]) do
for k = #groupe, 1, -1 do
local slot = cases[groupe[k]]
if slot then
liste[#liste + 1] = slot
break
end
end
if #liste >= maxi then break end
end
return liste
end
-- l'indice d'une competence, pour AbandonSkill : sa categorie est depliee
-- s'il le faut (et le reste, la boite ouverte)
local function indiceDe(nom)
for passe = 1, 2 do
for i = 1, GetNumSkillLines() or 0 do
local n, entete = GetSkillLineInfo(i)
if n == nom and not vrai(entete) then return i end
end
if passe == 1 then deplier() end
end
end
-- ------------------------------------------------------------ le rang
-- un element a decoupe 30 / 30 : ses bouts a leur taille, le milieu etire
local function troisTranches(hote, nom, largeur, hauteur, bout, couche)
local e = ForeverUI.AtlasEntry(nom)
if not e then return end
local du = (e[3] - e[2]) * bout / e[6]
local morceaux = {
{ e[2], e[2] + du, 0, bout },
{ e[2] + du, e[3] - du, bout, largeur - 2 * bout },
{ e[3] - du, e[3], largeur - bout, bout },
}
for _, m in ipairs(morceaux) do
local t = hote:CreateTexture(nil, couche)
t:SetTexture(e[1])
t:SetTexCoord(m[1], m[2], e[4], e[5])
t:SetWidth(m[4])
t:SetHeight(hauteur)
t:SetPoint("TOPLEFT", hote, "TOPLEFT", m[3], 0)
end
end
local function creerRang(carte, largeur, decalage)
local R = N.rang
local r = CreateFrame("Frame", nil, carte)
r:SetWidth(largeur)
r:SetHeight(R.h)
r.largeur, r.decalage = largeur, decalage
local fond = r:CreateTexture(nil, "BACKGROUND")
atlas(fond, "profession-progressbar-bg")
fond:SetWidth(largeur)
fond:SetHeight(R.fond)
fond:SetPoint("TOPLEFT", r, "TOPLEFT", 0, 0)
local rempli = r:CreateTexture(nil, "BORDER")
rempli:SetHeight(R.rempli[2])
rempli:SetPoint("TOPLEFT", r, "TOPLEFT", R.rempli[3] + R.masque, R.rempli[4])
local eclat = r:CreateTexture(nil, "ARTWORK")
eclat:SetWidth(R.eclat[1])
eclat:SetHeight(R.eclat[2])
eclat:SetBlendMode("ADD")
troisTranches(r, "profession-progressbar-frame", largeur, R.fond, R.tranche, "OVERLAY")
local cadreTexte = CreateFrame("Frame", nil, r)
cadreTexte:SetHeight(R.h)
cadreTexte:SetPoint("LEFT", r, "LEFT", 0, R.texte)
cadreTexte:SetPoint("RIGHT", r, "RIGHT", 0, R.texte)
cadreTexte:SetFrameLevel(r:GetFrameLevel() + 1)
local texte = cadreTexte:CreateFontString(nil, "ARTWORK")
texte:SetFontObject(POLICES.rang)
texte:SetPoint("CENTER", cadreTexte, "CENTER", 0, 0)
r.fond, r.rempli, r.eclat, r.texte = fond, rempli, eclat, texte
return r
end
-- ProfessionsRankBarMixin:Update, sans animation : texte, bande du metier
-- (sinon DefaultBlue) vue sur largeur x part + decalage, eclat au bout
local function majRang(r, m)
local R = N.rang
if m.bonus > 0 then
r.texte:SetFormattedText(L.TRADESKILL_NAME_RANK_MODIFIER, m.nom, m.rang, m.bonus, m.maxi)
else
r.texte:SetFormattedText(L.TRADESKILL_NAME_RANK, m.nom, m.rang, m.maxi)
end
local bande = METIERS[m.cle].bande
local e = ForeverUI.AtlasEntry("skillbar_fill_flipbook_" .. bande) or ForeverUI.AtlasEntry("skillbar_fill_flipbook_defaultblue")
local eclat = ForeverUI.AtlasEntry("skillbar_flare_" .. bande)
local part = m.maxi > 0 and math.min(m.rang / m.maxi, 1) or 0
local vu = math.min(R.rempli[1] - R.masque, r.largeur * part + r.decalage)
if e and vu >= 1 then
r.rempli:SetTexture(e[1])
local du = (e[3] - e[2]) / R.rempli[1]
r.rempli:SetTexCoord(e[2] + du * R.masque, e[2] + du * (R.masque + vu), e[4], e[5])
r.rempli:SetWidth(vu)
r.rempli:Show()
else
r.rempli:Hide()
end
-- l'eclat, masque comme chez camelot : sa partie droite, a la largeur
-- visible (voir TradeSkill.lua)
if eclat and vu >= 1 then
local l = math.min(R.eclat[1], vu)
local du = (eclat[3] - eclat[2]) / R.eclat[1]
r.eclat:SetTexture(eclat[1])
r.eclat:SetTexCoord(eclat[3] - du * l, eclat[3], eclat[4], eclat[5])
r.eclat:SetWidth(l)
poser(r.eclat, "RIGHT", r, "TOPLEFT", R.rempli[3] + R.masque + vu, R.rempli[4] - R.rempli[2] / 2)
r.eclat:SetAlpha(m.maxi > 0 and m.rang >= m.maxi and 0 or 1)
r.eclat:Show()
else
r.eclat:Hide()
end
end
-- ------------------------------------------------------------ les sorts
local nombreSorts = 0
-- ProfessionButtonTemplate, en bouton SECURISE (type "spell") : lancer un
-- sort est protege
local function creerSort(carte)
nombreSorts = nombreSorts + 1
local S = N.sort
local nom = "ForeverUIProfessionsBookSpell" .. nombreSorts
local b = CreateFrame("Button", nom, carte, "SecureActionButtonTemplate")
b:SetWidth(S.cote)
b:SetHeight(S.cote)
b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
b:RegisterForDrag("LeftButton")
b:SetAttribute("type", "spell")
-- le clic modifie donne le lien dans la discussion, pas le sort
b:SetAttribute("shift-type1", "lien")
b:SetAttribute("shift-type2", "lien")
local icone = b:CreateTexture(nil, "BORDER")
icone:SetPoint("TOPLEFT", b, "TOPLEFT", S.masque, -S.masque)
icone:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -S.masque, S.masque)
local bord = S.masque / S.cote
icone:SetTexCoord(bord, 1 - bord, bord, 1 - bord)
local cadre = b:CreateTexture(nil, "OVERLAY")
atlas(cadre, "profession-square-frame", true)
cadre:SetPoint("CENTER", icone, "CENTER", 0, 0)
b:SetPushedTexture(BOUTONS .. "UI-Quickslot-Depress")
b:SetHighlightTexture(BOUTONS .. "ButtonHilight-Square")
b:GetHighlightTexture():SetBlendMode("ADD")
local recharge = CreateFrame("Cooldown", nom .. "Cooldown", b, "CooldownFrameTemplate")
recharge:SetAllPoints(b)
local texte = b:CreateFontString(nil, "BORDER", "GameFontNormal")
texte:SetWidth(S.texte[1])
texte:SetJustifyH("LEFT")
texte:SetPoint("LEFT", b, "RIGHT", S.texte[2], S.texte[3])
local sous = b:CreateFontString(nil, "BORDER")
sous:SetFontObject(POLICES.sous)
sous:SetWidth(S.sous[1])
sous:SetHeight(S.sous[2])
sous:SetJustifyH("LEFT")
sous:SetJustifyV("TOP")
sous:SetPoint("TOPLEFT", texte, "BOTTOMLEFT", 0, S.sous[3])
b.icone, b.cadre, b.recharge, b.texte, b.sous = icone, cadre, recharge, texte, sous
b:SetScript("OnEnter", function(self)
if not self.slot then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetSpell(self.slot, BOOKTYPE_SPELL)
GameTooltip:Show()
end)
b:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
b:SetScript("OnDragStart", function(self)
if self.slot then PickupSpell(self.slot, BOOKTYPE_SPELL) end
end)
-- le lien : celui du metier s'il y en a un, sinon celui du sort
b:SetScript("PostClick", function(self)
if self.slot and IsModifiedClick("CHATLINK") then
local lien, lienMetier = GetSpellLink(self.slot, BOOKTYPE_SPELL)
if lienMetier or lien then ChatEdit_InsertLink(lienMetier or lien) end
end
end)
b:Hide()
return b
end
local function majRecharge(b)
local debut, duree, actif = GetSpellCooldown(b.slot, BOOKTYPE_SPELL)
CooldownFrame_SetTimer(b.recharge, debut or 0, duree or 0, actif or 0)
if vrai(actif) then
b.icone:SetVertexColor(1, 1, 1)
else
b.icone:SetVertexColor(0.4, 0.4, 0.4)
end
end
-- UpdateButton : icone, nom, rang, recharge ; le sort que le bouton lance
-- ("Nom(Rang)", ce que SecureActionButton passe a CastSpellByName)
local function remplirSort(b, slot)
b.slot = slot
local nom, rang = GetSpellName(slot, BOOKTYPE_SPELL)
local passif = IsPassiveSpell(slot, BOOKTYPE_SPELL)
b:GetHighlightTexture():SetTexture(BOUTONS .. (passif and "UI-PassiveHighlight" or "ButtonHilight-Square"))
local c = passif and COULEURS.passif or COULEURS.normal
b.texte:SetTextColor(c[1], c[2], c[3])
b.texte:SetText(nom)
b.sous:SetText(rang or "")
b.icone:SetTexture(GetSpellTexture(slot, BOOKTYPE_SPELL))
majRecharge(b)
if rang and rang ~= "" then
b:SetAttribute("spell", nom .. "(" .. rang .. ")")
else
b:SetAttribute("spell", nom)
end
end
-- ------------------------------------------------------------ l'oubli
function LM.Oublier(nom)
local i = indiceDe(nom)
if not i then return end
local boite = StaticPopup_Show("UNLEARN_SKILL", nom)
if boite then boite.data = i end
end
local function creerOubli(carte, rang)
local O = N.oubli
local b = CreateFrame("Button", nil, carte)
b:SetWidth(O[1])
b:SetHeight(O[1])
b:SetPoint("LEFT", rang, "RIGHT", O[2], O[3])
local icone = b:CreateTexture(nil, "ARTWORK")
atlas(icone, "profession-button-red-crossmark", true)
icone:SetPoint("CENTER", b, "CENTER", 0, 0)
local enfonce = b:CreateTexture(nil, "OVERLAY")
atlas(enfonce, "profession-button-red-crossmark-pressed", true)
enfonce:SetPoint("CENTER", b, "CENTER", 0, 0)
enfonce:Hide()
b.icone, b.enfonce = icone, enfonce
b:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(UNLEARN_SKILL_TOOLTIP)
GameTooltip:Show()
end)
b:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
b:SetScript("OnMouseDown", function(self) self.enfonce:Show() end)
b:SetScript("OnMouseUp", function(self) self.enfonce:Hide() end)
b:SetScript("OnClick", function()
if carte.metier then LM.Oublier(carte.metier.nom) end
end)
return b
end
-- ------------------------------------------------------------ les cartes
-- LE FOND D'UNE CARTE. A la taille de son art : une piece. Plus etroite
-- (rangee a cote de la barre) ou plus basse (colonne sous trois rangees) :
-- l'art n'est pas ecrase, ses bords sont gardes -- le gauche d'une rangee,
-- le haut d'une colonne, sur N.rogne -- et le reste s'aligne sur le bord
-- oppose ; la bande entre les deux est rognee.
local function poserFond(c, nom)
local e = ForeverUI.AtlasEntry(nom)
if not e then return false end
c.atlasFond = nom
local W, H, B = e[6], e[7], N.rogne
local l, h = c:GetWidth(), c:GetHeight()
local du, dv = (e[3] - e[2]) / W, (e[5] - e[4]) / H
local fond, bord = c.fond, c.fondBord
fond:SetTexture(e[1])
bord:SetTexture(e[1])
fond:ClearAllPoints()
if l < W then
bord:ClearAllPoints()
bord:SetTexCoord(e[2], e[2] + du * B, e[4], e[5])
bord:SetWidth(B)
bord:SetPoint("TOPLEFT", c, "TOPLEFT", 0, 0)
bord:SetPoint("BOTTOMLEFT", c, "BOTTOMLEFT", 0, 0)
fond:SetTexCoord(e[3] - du * (l - B), e[3], e[4], e[5])
fond:SetPoint("TOPLEFT", c, "TOPLEFT", B, 0)
fond:SetPoint("BOTTOMRIGHT", c, "BOTTOMRIGHT", 0, 0)
bord:Show()
elseif h < H then
bord:ClearAllPoints()
bord:SetTexCoord(e[2], e[3], e[4], e[4] + dv * B)
bord:SetHeight(B)
bord:SetPoint("TOPLEFT", c, "TOPLEFT", 0, 0)
bord:SetPoint("TOPRIGHT", c, "TOPRIGHT", 0, 0)
fond:SetTexCoord(e[2], e[3], e[5] - dv * (h - B), e[5])
fond:SetPoint("TOPLEFT", c, "TOPLEFT", 0, -B)
fond:SetPoint("BOTTOMRIGHT", c, "BOTTOMRIGHT", 0, 0)
bord:Show()
else
-- la bande cachee garde sa derniere place
fond:SetTexCoord(e[2], e[3], e[4], e[5])
fond:SetAllPoints(c)
bord:Hide()
end
return true
end
local function creerFond(c)
c.fond = c:CreateTexture(nil, "BACKGROUND")
c.fondBord = c:CreateTexture(nil, "BACKGROUND")
c.fondBord:Hide()
end
local function creerPrincipal(parent, n)
local P = N.principal
local c = CreateFrame("Frame", nil, parent)
c:SetWidth(P[1])
c:SetHeight(P[2])
c.principal = true
creerFond(c)
c.nom = c:CreateFontString(nil, "OVERLAY", "GameFontNormal")
c.nom:SetJustifyH("LEFT")
c.nom:SetPoint("TOPLEFT", c, "TOPLEFT", N.nom[1], N.nom[2])
c.absentTitre = c:CreateFontString(nil, "OVERLAY", "GameFontNormal")
c.absentTitre:SetJustifyH("LEFT")
c.absentTitre:SetPoint("TOPLEFT", c, "TOPLEFT", N.nom[1], N.nom[2])
if n == 1 then
c.absentTitre:SetText(L.PROFESSIONSBOOK_FIRST_PROFESSION)
else
c.absentTitre:SetText(L.PROFESSIONSBOOK_SECOND_PROFESSION)
end
c.absentTexte = c:CreateFontString(nil, "OVERLAY")
c.absentTexte:SetFontObject(POLICES.small2)
c.absentTexte:SetWidth(N.absent)
c.absentTexte:SetJustifyH("LEFT")
c.absentTexte:SetPoint("CENTER", c, "CENTER", 0, 0)
c.absentTexte:SetText(L.PROFESSIONSBOOK_MISSING_PROFESSION)
local RP = N.rangPrincipal
c.rang = creerRang(c, RP[1], RP.decalage)
c.rang:SetPoint("RIGHT", c, "RIGHT", RP.x, 0)
c.oubli = creerOubli(c, c.rang)
c.sorts = { creerSort(c), creerSort(c) }
return c
end
-- les colonnes passent DEVANT la zone des rangees : leur niveau est pose
-- avant que leurs enfants ne naissent
local function creerSecondaire(contenu, cle)
local S = N.secondaire
local c = CreateFrame("Frame", nil, contenu)
c:SetFrameLevel(contenu:GetFrameLevel() + N.niveaux.colonnes)
c:SetWidth(S[1])
c:SetHeight(S[2])
c.cle = cle
creerFond(c)
poserFond(c, "profession-overview-card-generic-" .. cle)
c.nom = c:CreateFontString(nil, "OVERLAY", "GameFontNormal")
c.nom:SetJustifyH("LEFT")
c.nom:SetPoint("TOP", c, "TOP", 0, N.secondaireNom)
c.absentTitre = c:CreateFontString(nil, "OVERLAY", "GameFontNormal")
c.absentTitre:SetJustifyH("LEFT")
c.absentTitre:SetPoint("TOP", c, "TOP", 0, N.secondaireNom)
c.absentTitre:SetText(GetSpellInfo(METIERS[cle].nom) or "")
local T = N.secondaireTexte
c.absentTexte = c:CreateFontString(nil, "OVERLAY")
c.absentTexte:SetFontObject(POLICES.small2)
c.absentTexte:SetWidth(T[1])
c.absentTexte:SetJustifyH("LEFT")
c.absentTexte:SetJustifyV("TOP")
c.absentTexte:SetPoint("TOP", c.absentTitre, "BOTTOM", T[2], T[3])
c.absentTexte:SetText(METIERS[cle].absent)
local RS = N.rangSecondaire
c.rang = creerRang(c, RS[1], RS.decalage)
c.rang:SetPoint("TOP", c, "TOP", 0, RS.y)
c.sorts = {}
for k = 1, 4 do
c.sorts[k] = creerSort(c)
end
return c
end
-- les sorts d'une colonne : empiles depuis le bas (camelot) ; en ligne,
-- icones seules, quand la colonne a perdu la hauteur d'une rangee
local function poserSortsColonne(c)
local SS = N.sortsSecondaire
for k, b in ipairs(c.sorts) do
if k == 1 then
poser(b, "BOTTOMLEFT", c, "BOTTOMLEFT", SS.x, SS.y)
elseif c.reduite then
poser(b, "LEFT", c.sorts[k - 1], "RIGHT", N.enLigne, 0)
else
poser(b, "BOTTOM", c.sorts[k - 1], "TOP", 0, 0)
end
Gb.Montrer(b.texte, not c.reduite)
Gb.Montrer(b.sous, not c.reduite)
end
end
-- FormatProfession
local function formater(c, m, cases)
if c.principal then
poserFond(c, "profession-overview-card")
end
c.metier = m
if not m then
c.absentTitre:Show()
c.absentTexte:Show()
for _, b in ipairs(c.sorts) do
b.slot = nil
b:Hide()
end
c.rang:Hide()
c.nom:SetText("")
if c.oubli then c.oubli:Hide() end
return
end
c.absentTitre:Hide()
c.absentTexte:Hide()
c.nom:SetText(m.nom)
if c.principal then
poserFond(c, "profession-overview-card-" .. m.cle)
Gb.Montrer(c.oubli, m.abandon)
end
majRang(c.rang, m)
c.rang:Show()
local slots = sortsDu(m.cle, cases, #c.sorts)
for k, b in ipairs(c.sorts) do
if slots[k] then
remplirSort(b, slots[k])
b:Show()
else
b.slot = nil
b:Hide()
end
end
if c.principal then
local SP = N.sortsPrincipal
if #slots == 1 then
poser(c.sorts[1], "BOTTOMLEFT", c, "BOTTOMLEFT", SP.x, SP.seul)
else
poser(c.sorts[1], "BOTTOMLEFT", c, "BOTTOMLEFT", SP.x, SP.haut)
poser(c.sorts[2], "BOTTOMLEFT", c, "BOTTOMLEFT", SP.x, SP.bas)
end
end
end
-- les rangees hors de la zone sont CACHEES, pas seulement hors champ : leurs
-- sorts sont des boutons securises, qu'aucun clic ne doit trouver sous les
-- colonnes ou au-dessus de la fenetre. Le defilement va de rangee en
-- rangee : les rangees visibles sont entieres.
local function montrerRangees(f)
local visibles = math.min(f.nombre, N.rangees)
local d = LM.decalage or 0
for k, c in ipairs(f.principaux) do
Gb.Montrer(c, k <= f.nombre and k > d and k <= d + visibles)
end
end
-- LA DISPOSITION, selon le nombre de rangees : deux au moins (camelot) ;
-- trois visibles au plus, les colonnes perdant alors la hauteur d'une
-- rangee ; au-dela, la barre et sa gouttiere.
local function disposer(f, nombre)
local P, S = N.principal, N.secondaire
local pas = P[2] - P.ecart
local visibles = math.min(nombre, N.rangees)
local avec = nombre > N.rangees
local largeur = P[1] - (avec and N.gouttiere or 0)
for k = #f.principaux + 1, nombre do
local c = creerPrincipal(f.enfant, k)
c:SetPoint("TOPLEFT", f.principaux[k - 1], "BOTTOMLEFT", 0, P.ecart)
f.principaux[k] = c
end
for _, c in ipairs(f.principaux) do
c:SetWidth(largeur)
end
f.nombre = nombre
f.zone:SetWidth(largeur)
f.zone:SetHeight(visibles * pas + P.ecart)
f.enfant:SetWidth(largeur)
f.enfant:SetHeight(nombre * pas + P.ecart)
local reduite = visibles > 2
for _, c in ipairs(f.secondaires) do
c.reduite = reduite
c:SetHeight(S[2] - (reduite and pas or 0))
poserFond(c, c.atlasFond)
poserSortsColonne(c)
end
LM.decalage = math.max(0, math.min(LM.decalage or 0, nombre - visibles))
f.barre:Regler(nombre, visibles, LM.decalage)
f.zone:SetVerticalScroll(LM.decalage * pas)
montrerRangees(f)
end
-- ProfessionsBookFrameMixin:Update. Hors combat seulement : les boutons de
-- sort sont proteges (et le livre est ferme en combat).
function LM.Maj()
local f = LM.livre
if not f or not f:IsShown() or InCombatLockdown() then return end
local principaux, secondaires = LM.Lire()
local cases = lireGrimoire()
disposer(f, math.max(2, #principaux))
for i, c in ipairs(f.principaux) do
if i <= f.nombre then formater(c, principaux[i], cases) end
end
for _, c in ipairs(f.secondaires) do
formater(c, secondaires[c.cle], cases)
end
end
local function majRecharges()
local f = LM.livre
for _, liste in ipairs({ f.principaux, f.secondaires }) do
for _, c in ipairs(liste) do
for _, b in ipairs(c.sorts) do
if b.slot then majRecharge(b) end
end
end
end
end
-- ------------------------------------------------------------ le deplacement
-- LA FENETRE SE DEPLACE PAR SA BANDE DE TITRE (demande du 2026-09-28 : « la
-- fenetre de profession doit etre deplacable » ; camelot ne le permet pas).
-- Le livre et la page de fabrication sont une seule fenetre chez camelot :
-- une seule place retenue pour les deux (ForeverUIDB.positions.metiers, le
-- haut-centre depuis celui d'UIParent, comme l'inspection). Le systeme de
-- panneaux les repose a chaque ouverture : la place revient apres lui
-- (OnShow, UpdateUIPanelPositions). Les onglets suivent pendant le glisser.
-- Le livre, protege, ne bouge que hors combat -- il est ferme pendant.
local CLE_PLACE = "metiers"
local function positions()
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.positions = ForeverUIDB.positions or {}
return ForeverUIDB.positions
end
-- le haut-centre d'un cadre, depuis celui d'UIParent (nil tant que le
-- cadre n'est pas place)
local function hautCentre(cadre)
local cx, ux = cadre:GetCenter(), UIParent:GetCenter()
local haut, uHaut = cadre:GetTop(), UIParent:GetTop()
if not cx or not ux or not haut or not uHaut then return end
return cx - ux, haut - uHaut
end
local function bloque(f)
return f == LM.livre and InCombatLockdown()
end
function LM.Reposer(f)
local p = positions()[CLE_PLACE]
if not p or not f or not f:IsShown() or bloque(f) then return end
f:ClearAllPoints()
f:SetPoint("TOP", UIParent, "TOP", p.x, p.y)
end
local function rendreDeplacable(f, poignee)
f:SetMovable(true)
f:SetClampedToScreen(true)
poignee:EnableMouse(true)
poignee:RegisterForDrag("LeftButton")
poignee:SetScript("OnDragStart", function()
if bloque(f) then return end
f:StartMoving()
poignee:SetScript("OnUpdate", function() LM.PoserOnglets() end)
end)
poignee:SetScript("OnDragStop", function()
poignee:SetScript("OnUpdate", nil)
if bloque(f) then return end
f:StopMovingOrSizing()
local x, y = hautCentre(f)
if x then
f:ClearAllPoints()
f:SetPoint("TOP", UIParent, "TOP", x, y)
-- la place est a nous : le client ne la retient pas en plus
if f.SetUserPlaced then f:SetUserPlaced(false) end
positions()[CLE_PLACE] = { x = x, y = y }
end
LM.PoserOnglets()
end)
end
-- ------------------------------------------------------------ la fenetre
function LM.Ouvert()
return (LM.livre and LM.livre:IsShown()) or (TradeSkillFrame and TradeSkillFrame:IsShown()) or false
end
local function majMicro()
if ForeverUI.MajMicroMetiers then
ForeverUI.MajMicroMetiers(LM.Ouvert())
end
end
-- une page s'ouvre ou se ferme : le micro-bouton et les onglets suivent
local function actualiser()
majMicro()
if LM.MajOnglets then LM.MajOnglets() end
end
local EVENEMENTS_OUVERT = { "SKILL_LINES_CHANGED", "SPELLS_CHANGED", "SPELL_UPDATE_COOLDOWN" }
function LM.Construire()
if LM.livre then return LM.livre end
local f = CreateFrame("Frame", "ForeverUIProfessionsBook", UIParent)
f:SetWidth(N.fenetre[1])
f:SetHeight(N.fenetre[2])
f:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, -104)
f:SetToplevel(true)
f:EnableMouse(true)
f:Hide()
-- le panneau, declare par ses attributs (GetUIPanelWindowInfo les lit
-- avant la table UIPanelWindows) ; poses avant les boutons securises
for k, v in pairs(PANNEAU) do
f:SetAttribute("UIPanelLayout-" .. k, v)
end
f:SetAttribute("UIPanelLayout-defined", true)
f:SetAttribute("UIPanelLayout-enabled", true)
local habit = Gb.FenetrePortrait(f, {
portrait = ICONE_LIVRE, portraitCote = N.portrait.cote, portraitX = N.portrait.x, portraitY = N.portrait.y,
titre = string.format(TRADE_SKILL_TITLE, TRADE_SKILLS),
})
-- le portrait cuit rond : pose tel quel
habit.portrait:SetTexCoord(0, 1, 0, 1)
-- OverrideArt : Profession-Background-Overview a la place de la roche,
-- sans stries
if habit.roche.SetHorizTile then
habit.roche:SetHorizTile(false)
habit.roche:SetVertTile(false)
end
atlas(habit.roche, "profession-background-overview", true)
habit.stries:Hide()
f.habit = habit
rendreDeplacable(f, habit.bandeau)
local contenu = CreateFrame("Frame", "ForeverUIProfessionsBookContent", f)
contenu:SetAllPoints(f)
contenu:SetFrameLevel(f:GetFrameLevel() + N.niveaux.contenu)
f.contenu = contenu
-- la zone des rangees : ce qui depasse ses trois rangees ne se voit pas
local P, S, NV = N.principal, N.secondaire, N.niveaux
local zone = CreateFrame("ScrollFrame", "ForeverUIProfessionsBookScroll", contenu)
zone:SetFrameLevel(contenu:GetFrameLevel() + NV.zone)
zone:SetPoint("TOPLEFT", contenu, "TOPLEFT", P.x, P.y)
local enfant = CreateFrame("Frame", "ForeverUIProfessionsBookRows", zone)
enfant:SetFrameLevel(contenu:GetFrameLevel() + NV.rangees)
enfant:SetWidth(P[1])
enfant:SetHeight(1)
zone:SetScrollChild(enfant)
f.zone, f.enfant = zone, enfant
-- MinimalScrollBar, contre la zone ; un cran de molette : une rangee
local barre = ForeverUI.CreateScrollBar("ForeverUIProfessionsBookScrollBar", contenu, zone)
barre:SetFrameLevel(contenu:GetFrameLevel() + NV.zone)
barre.surDefilement = function(pas)
LM.decalage = pas
zone:SetVerticalScroll(pas * (P[2] - P.ecart))
montrerRangees(f)
end
barre:Hide()
f.barre = barre
zone:EnableMouseWheel(true)
zone:SetScript("OnMouseWheel", function(_, sens)
if barre:IsShown() then barre:Deplacer(barre.decalage - sens) end
end)
-- les cartes
f.principaux = { creerPrincipal(enfant, 1), creerPrincipal(enfant, 2) }
f.principaux[1]:SetPoint("TOPLEFT", enfant, "TOPLEFT", 0, 0)
f.principaux[2]:SetPoint("TOPLEFT", f.principaux[1], "BOTTOMLEFT", 0, P.ecart)
f.secondaires = {}
for k, cle in ipairs(SECONDAIRES) do
local c = creerSecondaire(contenu, cle)
if k == 1 then
c:SetPoint("TOPLEFT", zone, "BOTTOMLEFT", 0, S.ecart)
else
c:SetPoint("TOPLEFT", f.secondaires[k - 1], "TOPRIGHT", S.pas, 0)
end
f.secondaires[k] = c
end
disposer(f, 2)
-- la croix (UIPanelCloseButton : HideParentPanel)
local croix = CreateFrame("Button", "ForeverUIProfessionsBookCloseButton", f, "UIPanelCloseButton")
Gb.Croix(croix, f)
croix:SetFrameLevel(f:GetFrameLevel() + N.niveaux.croix)
f.croix = croix
f:SetScript("OnShow", function(self)
LM.Reposer(self)
for _, ev in ipairs(EVENEMENTS_OUVERT) do self:RegisterEvent(ev) end
LM.Maj()
PlaySound("igSpellBookOpen")
actualiser()
end)
f:SetScript("OnHide", function(self)
for _, ev in ipairs(EVENEMENTS_OUVERT) do self:UnregisterEvent(ev) end
StaticPopup_Hide("UNLEARN_SKILL")
PlaySound("igAbilityClose")
actualiser()
end)
f:SetScript("OnEvent", function(_, ev)
if ev == "SPELL_UPDATE_COOLDOWN" then
majRecharges()
elseif ev ~= "SKILL_LINES_CHANGED" or GetTime() >= silence then
LM.Maj()
end
end)
LM.livre = f
return f
end
-- ------------------------------------------------------------ les onglets
-- LES ONGLETS LATERAUX (camelot/blizzard_professionsframe.xml / .lua,
-- blizzard_professionstemplates : ProfessionsLargeRightTabMixin ; blizzard_-
-- sharedxml : LargeSideTabButtonTemplate). Ceux de la fenetre des metiers :
-- ils se tiennent a droite de la page montree, livre ou fabrication.
-- ensemble ProfessionsOverviewTab, TOPLEFT sur le TOPRIGHT de la
-- fenetre (0, -60) : icone INV_SideTab_Professions_c60,
-- infobulle TRADE_SKILLS ; clic : la page d'ensemble
-- metiers Professions1..7Tab, chacun sous le precedent (0, -2) :
-- RefreshRightTabs -- les principaux, puis secourisme, peche,
-- cuisine (l'ordre de GetProfessions), seulement ceux qui ont
-- une fabrication (CanTradeSkillShowCraftingUI) ; icone de la
-- competence, infobulle son nom ; clic : son sort
-- (CastProfessionSpell, sauf s'il est deja ouvert), la page de
-- fabrication
-- onglet 55 x 55 (common-sidetab sans ses 5 du bas) ; icone 50 x 50
-- (fillToInterior) rognee de 0,03125 ; enfonce : l'icone de
-- (1, -1), son IG_CHARACTER_INFO_TAB au relachement ; choisi :
-- common-sidetab-selected ; survol common-sidetab-hover ;
-- infobulle ANCHOR_RIGHT (-4, -4)
-- panneau largeur 750 (professionsFrameWidthOverride) : la place des
-- onglets est retenue a droite de la fenetre
--
-- CE QUI DIFFERE, ET POURQUOI.
-- * Un onglet de metier LANCE UN SORT : c'est un bouton securise, et le
-- cadre qui les porte devient protege. Il est donc a part, enfant de
-- UIParent, pose a droite de la fenetre montree -- ni enfant ni ancre
-- de TradeSkillFrame, que cela rendrait protege a son tour. Il suit la
-- fenetre a chaque placement des panneaux (UpdateUIPanelPositions), et
-- disparait en combat, comme le livre.
-- * L'icone est a (-3, 0), comme tous les onglets lateraux deja valides
-- de l'atelier, et cuite au masque (tools/cuire_masque.py). Camelot la
-- pose a (-4, 0) (camelot/shareduipaneltemplates.lua).
-- * L'onglet d'ensemble ferme la page de fabrication (la session de
-- metier de 3.3.5 prend fin avec sa fenetre) et ouvre le livre.
-- * Joaillerie et calligraphie, absentes de camelot, ont l'icone de leur
-- competence en 3.3.5.
-- * TROP D'ONGLETS POUR LA HAUTEUR DE LA FENETRE (serveur prive ; demande
-- du 2026-09-28, par anticipation) : ils retrecissent ensemble -- onglet,
-- icone, ecart, decalage de l'icone -- pour que le dernier finisse au
-- plus au bas de la fenetre. Le premier garde sa place (-60), et tous
-- restent colles au bord droit de la fenetre.
local ONGLET = { cote = 55, y = -60, ecart = -2, icone = 50, iconeX = -3, rognage = 0.03125 }
local ICONES_ONGLETS = "Interface" .. SEP .. "ForeverUI" .. SEP .. "tabicons" .. SEP
local ONGLETS_SECONDAIRES = { "firstaid", "fishing", "cooking" }
local function creerOnglet(parent, nom, securise)
local O = ONGLET
local b = CreateFrame("Button", nom, parent, securise and "SecureActionButtonTemplate" or nil)
b:SetWidth(O.cote)
b:SetHeight(O.cote)
b:RegisterForClicks("LeftButtonUp")
local fond = b:CreateTexture(nil, "BACKGROUND")
atlas(fond, "common-sidetab")
fond:SetAllPoints(b)
local icone = b:CreateTexture(nil, "ARTWORK")
icone:SetWidth(O.icone)
icone:SetHeight(O.icone)
icone:SetPoint("CENTER", b, "CENTER", O.iconeX, 0)
icone:SetTexCoord(O.rognage, 1 - O.rognage, O.rognage, 1 - O.rognage)
b.iconeX = O.iconeX
local choisi = b:CreateTexture(nil, "OVERLAY")
atlas(choisi, "common-sidetab-selected")
choisi:SetAllPoints(b)
choisi:Hide()
local survol = b:CreateTexture(nil, "HIGHLIGHT")
atlas(survol, "common-sidetab-hover")
survol:SetAllPoints(b)
b.icone, b.choisi = icone, choisi
b:SetScript("OnEnter", function(self)
if not self.infobulle then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT", -4, -4)
GameTooltip:SetText(self.infobulle)
GameTooltip:Show()
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
b:SetScript("OnMouseDown", function(self, bouton)
if bouton == "LeftButton" then poser(self.icone, "CENTER", self, "CENTER", self.iconeX + 1, -1) end
end)
b:SetScript("OnMouseUp", function(self, bouton)
if bouton == "LeftButton" then
poser(self.icone, "CENTER", self, "CENTER", self.iconeX, 0)
PlaySound("igCharacterInfoTab")
end
end)
return b
end
-- SelectBookPage : la page de fabrication se ferme, le livre s'ouvre
function LM.PageEnsemble()
if InCombatLockdown() or (LM.livre and LM.livre:IsShown()) then return end
if TradeSkillFrame and TradeSkillFrame:IsShown() then HideUIPanel(TradeSkillFrame) end
ShowUIPanel(LM.Construire())
end
local function creerOnglets()
local c = CreateFrame("Frame", "ForeverUIProfessionsTabs", UIParent)
c:SetWidth(ONGLET.cote)
c:SetHeight(1)
c:Hide()
local ensemble = creerOnglet(c, "ForeverUIProfessionsTab0")
ensemble.icone:SetTexture(ICONES_ONGLETS .. "inv_sidetab_professions_c60")
ensemble.infobulle = TRADE_SKILLS
ensemble:SetPoint("TOPLEFT", c, "TOPLEFT", 0, ONGLET.y)
ensemble:SetScript("OnClick", LM.PageEnsemble)
c.ensemble, c.metiers, c.nombre = ensemble, {}, 0
LM.onglets = c
return c
end
local function ongletMetier(c, k)
if not c.metiers[k] then
local b = creerOnglet(c, "ForeverUIProfessionsTab" .. k, true)
b:SetPoint("TOPLEFT", k == 1 and c.ensemble or c.metiers[k - 1], "BOTTOMLEFT", 0, ONGLET.ecart)
c.metiers[k] = b
end
return c.metiers[k]
end
-- le sort qui ouvre la fabrication d'un metier : le premier groupe
local function ouvreur(cle, cases)
local groupe = SORTS[cle][1]
for k = #groupe, 1, -1 do
if cases[groupe[k]] then return cases[groupe[k]] end
end
end
local function fenetreMontree()
if LM.livre and LM.livre:IsShown() then return LM.livre end
if TradeSkillFrame and TradeSkillFrame:IsShown() then return TradeSkillFrame end
end
-- la taille des onglets selon leur nombre et la hauteur de la fenetre ;
-- refaite seulement quand l'un ou l'autre change (PoserOnglets tourne a
-- chaque image pendant un glisser)
local function dimensionner(c, hauteur)
local O = ONGLET
local n = c.nombre + 1
local plein = n * O.cote - (n - 1) * O.ecart
local place = hauteur + O.y
local e = (place > 0 and plein > place) and place / plein or 1
if e == c.echelle and #c.metiers == c.dimensionnes then return e end
c.echelle, c.dimensionnes = e, #c.metiers
local liste = { c.ensemble }
for k = 1, #c.metiers do liste[k + 1] = c.metiers[k] end
for k, b in ipairs(liste) do
b:SetWidth(O.cote * e)
b:SetHeight(O.cote * e)
b.iconeX = O.iconeX * e
b.icone:SetWidth(O.icone * e)
b.icone:SetHeight(O.icone * e)
poser(b.icone, "CENTER", b, "CENTER", b.iconeX, 0)
if k > 1 then poser(b, "TOPLEFT", liste[k - 1], "BOTTOMLEFT", 0, O.ecart * e) end
end
return e
end
-- a droite de la fenetre montree, dans sa strate, sous son metal
function LM.PoserOnglets()
local c = LM.onglets
if not c or InCombatLockdown() then return end
local f = fenetreMontree()
local droite, haut = f and f:GetRight(), f and f:GetTop()
if not droite or not haut then
c:Hide()
return
end
local e = dimensionner(c, f:GetHeight() or 0)
c:ClearAllPoints()
c:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", droite, haut)
c:SetHeight(-ONGLET.y + (c.nombre + 1) * (ONGLET.cote - ONGLET.ecart) * e)
c:SetFrameStrata(f:GetFrameStrata())
c:SetFrameLevel(f:GetFrameLevel() + 1)
c:Show()
end
-- RefreshRightTabs et RightTabSelected. Hors combat seulement (boutons
-- securises).
function LM.MajOnglets()
if InCombatLockdown() then return end
local f = fenetreMontree()
if not f then
if LM.onglets then LM.onglets:Hide() end
return
end
local c = LM.onglets or creerOnglets()
local principaux, secondaires = LM.Lire()
local cases = lireGrimoire()
local liste = {}
local function ajouter(m)
local slot = m and METIERS[m.cle].onglet and ouvreur(m.cle, cases)
if slot then liste[#liste + 1] = { m = m, slot = slot } end
end
for _, m in ipairs(principaux) do ajouter(m) end
for _, cle in ipairs(ONGLETS_SECONDAIRES) do ajouter(secondaires[cle]) end
-- le metier ouvert : celui de la page de fabrication, s'il est le notre
local ouvert
if f == TradeSkillFrame and not vrai(IsTradeSkillLinked()) then
ouvert = nomsDesMetiers()[GetTradeSkillLine() or ""]
end
Gb.Montrer(c.ensemble.choisi, f == LM.livre)
for k, e in ipairs(liste) do
local b = ongletMetier(c, k)
local nom, rang = GetSpellName(e.slot, BOOKTYPE_SPELL)
local choisi = e.m.cle == ouvert
b.cle = e.m.cle
b.icone:SetTexture(ICONES_ONGLETS .. METIERS[e.m.cle].onglet)
b.infobulle = e.m.nom
Gb.Montrer(b.choisi, choisi)
if choisi then
b:SetAttribute("type", nil)
else
b:SetAttribute("type", "spell")
end
if rang and rang ~= "" then
b:SetAttribute("spell", nom .. "(" .. rang .. ")")
else
b:SetAttribute("spell", nom)
end
b:Show()
end
for k = #liste + 1, #c.metiers do c.metiers[k]:Hide() end
c.nombre = #liste
LM.PoserOnglets()
end
-- ToggleProfessionsBook : le livre, ou la page de fabrication ouverte, se
-- ferme ; sinon le livre s'ouvre -- hors combat (voir plus haut)
function LM.Basculer()
if LM.livre and LM.livre:IsShown() then
HideUIPanel(LM.livre)
elseif TradeSkillFrame and TradeSkillFrame:IsShown() then
HideUIPanel(TradeSkillFrame)
elseif InCombatLockdown() then
UIErrorsFrame:AddMessage(ERR_NOT_IN_COMBAT, 1.0, 0.1, 0.1, 1.0)
else
ShowUIPanel(LM.Construire())
end
end
-- la page de fabrication remplace le livre, et se deplace comme lui ;
-- l'entree en combat ferme le
-- livre et cache les onglets (PLAYER_REGEN_DISABLED passe AVANT le verrou du
-- combat), la sortie les rend ; le micro-bouton et les onglets suivent la
-- page de fabrication, les onglets le placement des panneaux ; la page de
-- fabrication garde a sa droite la place des onglets (largeur de camelot)
local veille = CreateFrame("Frame")
LM.veille = veille
local function brancherFabrication()
if TradeSkillFrame and not veille.branche then
veille.branche = true
TradeSkillFrame:SetAttribute("UIPanelLayout-width", LARGEUR_PANNEAU)
if TradeSkillFrame.foreverHabit then
rendreDeplacable(TradeSkillFrame, TradeSkillFrame.foreverHabit.bandeau)
end
TradeSkillFrame:HookScript("OnShow", function()
LM.Reposer(TradeSkillFrame)
actualiser()
end)
TradeSkillFrame:HookScript("OnHide", actualiser)
end
end
for _, ev in ipairs({ "ADDON_LOADED", "TRADE_SKILL_SHOW", "PLAYER_REGEN_DISABLED", "PLAYER_REGEN_ENABLED",
"SKILL_LINES_CHANGED", "SPELLS_CHANGED", "DISPLAY_SIZE_CHANGED", "UI_SCALE_CHANGED" }) do
veille:RegisterEvent(ev)
end
veille:SetScript("OnEvent", function(_, ev, nom)
if ev == "ADDON_LOADED" then
if nom == "Blizzard_TradeSkillUI" then brancherFabrication() end
elseif ev == "PLAYER_REGEN_DISABLED" then
if LM.livre and LM.livre:IsShown() then HideUIPanel(LM.livre) end
if LM.onglets then LM.onglets:Hide() end
elseif ev == "TRADE_SKILL_SHOW" then
if LM.livre and LM.livre:IsShown() then HideUIPanel(LM.livre) end
LM.MajOnglets()
elseif ev ~= "SKILL_LINES_CHANGED" or GetTime() >= silence then
LM.MajOnglets()
end
end)
brancherFabrication()
-- apres le systeme de panneaux : la place retenue, puis les onglets
if hooksecurefunc then
hooksecurefunc("UpdateUIPanelPositions", function()
LM.Reposer(LM.livre)
if TradeSkillFrame then LM.Reposer(TradeSkillFrame) end
LM.PoserOnglets()
end)
end