Files
moon-well-ui/tools/cuire_creation.py
T
Adrestie 1de02d13a0 Ecrans d'accueil a la DA de camelot : connexion, menu, selection, dialogues et creation de personnage
- GlueXML ForeverUI (data/glue) : cadre 2:1, polices, atlas, gabarits ;
  connexion, menu, selection des personnages, dialogues, creation en deux
  etapes (races et classes ; personnalisation numerotee, listes ouvrantes,
  echantillons de couleur, apercu differe, nom en bas de l'ecran)
- choix valides calcules sur CharSections aux regles du moteur
  (tools/choix_personnalisation.py) ; couleurs des echantillons lues dans
  les textures du client (tools/couleurs_personnalisation.py), peau teintee
  par la lumiere du decor
- decors de la creation et de la selection reduits a 0,912 pour compenser
  le cadre 2:1 (tools/decors_accueil.py) ; deployer.py suit aussi
  Interface\Glues\Models
- patcheur optionnel de Wow.exe (tools/patcheur, source et executable) :
  CycleCharCustomization rend l'indice du reglage
- art des ecrans d'accueil (data/art), outils de decoupe, d'atlas et de
  donnees
- barre du bas : boutons JcJ et Aide retires, bandeau a la largeur de ses
  boutons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:44:01 +02:00

155 lines
6.0 KiB
Python

# -*- coding: utf-8 -*-
# This file is part of mod-forever-ui.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Cuire les masques de la creation de personnage dans l'alpha des icones.
POURQUOI. camelot decoupe les icones de la creation par des MaskTexture, que
3.3.5 n'a pas. Meme remede que tools/cuire_masque.py : l'alpha du masque est
multiplie dans celui de l'icone, une fois pour toutes.
LA GEOMETRIE, prise sur la source (RingedMaskedButtonMixin:OnLoad,
blizzard_sharedxml/shared/frametemplate/ringedframetemplate.lua:93-94 ;
blizzard_charactercreate/camelot/*.xml) :
bouton de race 79 x 79, icone sur tout le bouton, masque
character-create-icon-mask a 2 px du bord (75 x 75)
bouton de classe 66 x 66, meme masque a 2 px du bord (62 x 62)
portrait 62 x 62 (CharacterCreateDetaislListTemplate), masque
character-create-icon-circle-mask de (2, 0) a (-2, 4)
du portrait, soit le rectangle 2..60 x 0..58
Hors du masque, son wrap est CLAMPTOBLACKADDITIVE : rien.
CE QU'IL LIT. Les icones decoupees par tools/decouper_elements.py
(glues/raceicon128-*.png, glues/classicon-*.png), les fonds de faction et les
masques dans la feuille c60 unknown/8203433.png, aux regions de
tools/atlas_dump.json.
CE QU'IL ECRIT. charactercreate/bouton-<icone>.blp et
charactercreate/portrait-<icone>.blp (+ .png), 128 x 128, non compresses,
avec leurs mipmaps en alpha premultiplie.
"""
import glob
import io
import json
import os
import sys
from PIL import Image
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from foreverui import blp # noqa: E402
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ART = os.path.join(RACINE, "data", "art", "interface", "ForeverUI")
INDEX = os.path.join(RACINE, "tools", "atlas_dump.json")
FEUILLE_C60 = os.path.join(ART, "unknown", "8203433.png")
SORTIE = os.path.join(ART, "charactercreate")
TAILLE = 128
BOUTONS = {"raceicon128-": (79.0, 2.0), "classicon-": (66.0, 2.0)}
PORTRAIT = 62.0
PORTRAIT_MASQUE = (2.0, 0.0, 60.0, 58.0) # gauche, haut, droite, bas
def _region(nom):
for r in json.load(io.open(INDEX, encoding="utf-8"))["results"]:
if r["atlas"].lower() == nom and r.get("fileDataID") == 8203433:
return r["region"]
raise SystemExit("absent de l'index : %s" % nom)
def _masque(feuille, nom):
r = _region(nom)
return feuille.crop((r["left"], r["top"], r["left"] + r["width"], r["top"] + r["height"])).getchannel("A")
def _alpha(masque, u, v):
"""L'alpha du masque en (u, v) de [0, 1], lu en bilineaire ; 0 dehors."""
if u < 0 or v < 0 or u > 1 or v > 1:
return 0.0
w, h = masque.size
x = min(max(u * w - 0.5, 0), w - 1)
y = min(max(v * h - 0.5, 0), h - 1)
x0, y0 = int(x), int(y)
x1, y1 = min(x0 + 1, w - 1), min(y0 + 1, h - 1)
fx, fy = x - x0, y - y0
p = masque.load()
haut = p[x0, y0] * (1 - fx) + p[x1, y0] * fx
bas = p[x0, y1] * (1 - fx) + p[x1, y1] * fx
return (haut * (1 - fy) + bas * fy) / 255.0
def _cuire(icone, masque, cote, rect):
"""icone : image TAILLE x TAILLE posee sur un cadre de `cote` ; rect : le
masque dans ce cadre (gauche, haut, droite, bas)."""
g, h, d, b = rect
sortie = icone.copy()
px = sortie.load()
for y in range(TAILLE):
for x in range(TAILLE):
cx = (x + 0.5) * cote / TAILLE
cy = (y + 0.5) * cote / TAILLE
a = _alpha(masque, (cx - g) / (d - g), (cy - h) / (b - h))
r_, v_, b_, a_ = px[x, y]
px[x, y] = (r_, v_, b_, int(round(a_ * a)))
return sortie
def _ecrire(image, nom):
plein = image.convert("RGBa")
mips = []
mw, mh = image.size
while mw > 1 or mh > 1:
mw, mh = max(1, mw // 2), max(1, mh // 2)
mips.append((mw, mh, plein.resize((mw, mh), Image.LANCZOS).convert("RGBA").tobytes()))
base = os.path.join(SORTIE, nom)
with io.open(base + ".blp", "wb") as f:
f.write(blp.encoder(image.size[0], image.size[1], image.tobytes(), mips))
image.save(base + ".png")
def main():
os.makedirs(SORTIE, exist_ok=True)
feuille = Image.open(FEUILLE_C60).convert("RGBA")
carre = _masque(feuille, "character-create-icon-mask-c60")
rond = _masque(feuille, "character-create-icon-circle-mask-c60")
n = 0
for prefixe, (cote, marge) in sorted(BOUTONS.items()):
for chemin in sorted(glob.glob(os.path.join(ART, "glues", prefixe + "*.png"))):
nom = os.path.splitext(os.path.basename(chemin))[0]
icone = Image.open(chemin).convert("RGBA").resize((TAILLE, TAILLE), Image.LANCZOS)
_ecrire(_cuire(icone, carre, cote, (marge, marge, cote - marge, cote - marge)), "bouton-" + nom)
n += 1
if prefixe == "raceicon128-":
_ecrire(_cuire(icone, rond, PORTRAIT, PORTRAIT_MASQUE), "portrait-" + nom)
n += 1
# les portraits de faction : charactercreate-icon-alliancebg / -hordebg
for nom in ("charactercreate-icon-alliancebg-c60", "charactercreate-icon-hordebg-c60"):
r = _region(nom)
icone = feuille.crop((r["left"], r["top"], r["left"] + r["width"], r["top"] + r["height"]))
icone = icone.resize((TAILLE, TAILLE), Image.LANCZOS)
_ecrire(_cuire(icone, rond, PORTRAIT, PORTRAIT_MASQUE), "portrait-" + nom[:-len("-c60")])
n += 1
print("creation : %d images cuites -> %s" % (n, os.path.relpath(SORTIE, RACINE)))
if __name__ == "__main__":
main()