Files
moon-well-ui/tools/test_addon.py
T
Adrestie 7a72cf649c mod-forever-ui: three differences the two captures gave up
Comparing my own bag with camelot's, side by side and pixel by pixel, named
what was wrong.

The panel let the world through. Camelot's interior is (16,14,12) and opaque:
the gaps between slots went blue on mine. The colour is now the measured one,
at full opacity.

The title and the bag icon were missing. The metal nine-slice sat in OVERLAY,
where the modern client can afford it because its portrait and title live in
child frames at high level; 3.3.5 keeps them as plain regions of the frame,
in BACKGROUND and ARTWORK, so the metal covered both. The nine-slice moves to
BORDER.

Every slot in camelot wears a frame, full or empty, dark grey: edges between
25 and 49, corners near 100, which is WhiteIconFrame at a tint of 0.39. Mine
showed it white and only on full slots. It is now on every slot at that tint,
and the quality colour takes over only from uncommon up -- the common items
of the capture keep the grey.

Both captures are kept in docs/reference/.

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

1275 lines
64 KiB
Python

# -*- coding: utf-8 -*-
"""Charge l'addon ForeverUI dans un faux client WoW (lupa) et verifie son comportement.
Ce n'est pas le jeu : c'est un bouchon des seules fonctions que l'addon appelle.
Il attrape ce qu'un client attraperait au chargement -- erreur de syntaxe, nom
de fonction faux, champ nil -- sans avoir a lancer WoW.
"""
import io, os, sys
import lupa
# Le depot est la source : c'est lui qu'on charge, pas la copie du client.
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ADDON = os.path.join(RACINE, "data", "addon", "ForeverUI")
MOCK = """
-- ---------------------------------------------------------------- faux client
local recorded = { messages = {}, portraits = 0 }
_G.RECORDED = recorded
local function newRegion(kind)
local r = { kind = kind, shown = true, points = {} }
function r:SetTexture(a, b, c, d) self.texture = a; self.color = {a,b,c,d} end
function r:GetTexture() return self.texture end
function r:SetHorizTile(v) self.tile = v end
function r:SetTexCoord(u1, u2, v1, v2) self.texcoord = {u1, u2, v1, v2} end
function r:SetWidth(w) self.width = w end
function r:GetWidth() return self.width or 0 end
function r:GetHeight() return self.height or 0 end
function r:SetHeight(h) self.height = h end
function r:SetPoint(...) table.insert(self.points, {...}) end
function r:SetAllPoints(...) self.allPoints = true end
function r:ClearAllPoints() self.points = {} end
function r:Show() self.shown = true end
function r:Hide() self.shown = false end
function r:IsShown() return self.shown end
function r:SetText(t) self.text = t end
function r:GetText() return self.text end
function r:SetJustifyH(j) self.justify = j end
function r:SetFontObject(o) self.font = o end
function r:SetHorizTile(v) self.tile = v end
function r:SetVertexColor(a, b, c) self.vertex = {a, b, c} end
function r:SetAlpha(a) self.alpha = a end
function r:GetAlpha() return self.alpha or 1 end
function r:SetDrawLayer() end
function r:SetBlendMode(m) self.blend = m end
return r
end
local frames = {}
_G.FRAMES = frames
function CreateFrame(kind, name, parent, template)
local f = newRegion("frame")
f.name = name
f.parent = parent
f.template = template
f.scripts = {}
f.events = {}
f.attributes = {}
function f:SetScript(event, fn) self.scripts[event] = fn end
function f:HookScript(event, fn)
self.hooks = self.hooks or {}
self.hooks[event] = fn
end
function f:GetScript(event) return self.scripts[event] end
function f:GetName() return self.name end
function f:RegisterEvent(e) self.events[e] = true end
function f:UnregisterAllEvents() self.events = {} end
function f:RegisterForDrag() end
function f:RegisterForClicks() end
function f:EnableMouse() end
function f:SetMovable() end
function f:SetClampedToScreen() end
function f:GetNormalTexture()
if not self._normal then self._normal = newRegion("texture") end
return self._normal
end
function f:GetPushedTexture()
if not self._pushed then self._pushed = newRegion("texture") end
return self._pushed
end
function f:GetDisabledTexture()
if not self._disabled then self._disabled = newRegion("texture") end
return self._disabled
end
function f:SetButtonState(state) self.buttonState = state end
function f:GetButtonState() return self.buttonState or "NORMAL" end
function f:GetCheckedTexture()
if not self._checked then self._checked = newRegion("texture") end
return self._checked
end
function f:GetHighlightTexture()
if not self._highlight then self._highlight = newRegion("texture") end
return self._highlight
end
function f:SetParent(p) self.parent = p end
function f:GetParent() return self.parent end
function f:GetWidth() return self.width or 0 end
function f:GetHeight() return self.height or 0 end
function f:IsShown() return self.shown end
function f:SetFrameStrata(s) self.strata = s end
function f:SetFrameLevel(l) self.frameLevel = l end
function f:SetScale(v) self.scale = v end
function f:GetScale() return self.scale or 1 end
function f:GetFrameLevel() return self.frameLevel or 1 end
function f:SetHitRectInsets(...) self.hitRect = {...} end
function f:SetID(id) self.id = id end
function f:GetID() return self.id or 0 end
function f:SetAutoFocus(v) self.autoFocus = v end
function f:SetMaxLetters(v) self.maxLetters = v end
function f:SetTextInsets(a, b, c, d) self.insets = {a, b, c, d} end
function f:SetText(t) self.text = t end
function f:GetText() return self.text or "" end
function f:HasFocus() return self.focused end
function f:SetFocus() self.focused = true end
function f:ClearFocus() self.focused = false end
-- 3.3.5 n'accepte qu'un CHEMIN : un objet texture y leve une erreur, et
-- le faux client doit lever la meme, sinon il laisse passer un fichier
-- qui mourra en jeu.
local function poserTexture(self, cle, valeur)
if type(valeur) == "table" then
error("SetTexture attend un chemin, pas un objet (piege 3.3.5)")
end
if not self[cle] then self[cle] = newRegion("texture") end
self[cle].texture = valeur
return self[cle]
end
function f:SetNormalTexture(v) return poserTexture(self, "_normal", v) end
function f:SetPushedTexture(v) return poserTexture(self, "_pushed", v) end
function f:SetHighlightTexture(v) return poserTexture(self, "_highlight", v) end
function f:SetDisabledTexture(v) return poserTexture(self, "_disabled", v) end
function f:SetJustifyH(j) self.justify = j end
function f:SetAttribute(k, v) self.attributes[k] = v end
function f:GetAttribute(k) return self.attributes[k] end
function f:StartMoving() self.moving = true end
function f:StopMovingOrSizing() self.moving = false end
function f:CreateTexture(n, layer) local t = newRegion("texture"); t.layer = layer; t.owner = self; return t end
function f:CreateFontString(n, layer, font)
local t = newRegion("fontstring"); t.layer = layer; t.font = font; t.owner = self; return t
end
function f:GetPoint(index)
local p = self.points[index or 1]
if not p then return nil end
return p[1], p[2], p[3], p[4], p[5]
end
if name then _G[name] = f end
table.insert(frames, f)
return f
end
UIParent = CreateFrame("Frame", "UIParent")
DEFAULT_CHAT_FRAME = { AddMessage = function(self, msg) table.insert(recorded.messages, msg) end }
-- le cadre d'origine que l'addon doit neutraliser
PlayerFrame = CreateFrame("Button", "PlayerFrame", UIParent)
PlayerFrame.events = { PLAYER_ENTERING_WORLD = true, UNIT_HEALTH = true }
PlayerFrameDropDown = CreateFrame("Frame", "PlayerFrameDropDown", UIParent)
-- etat simule du joueur
STATE = { health = 50, healthMax = 100, power = 30, powerMax = 100,
powerType = 1, powerToken = "RAGE", level = 80, name = "Papota",
combat = false, inLockdown = false, resting = false, leader = false,
vehicle = false, time = 0, threat = 0, threatWarning = true, dead = false,
hasTarget = false, targetHostile = false, targetsMe = false,
className = "Chevalier de la mort", classToken = "DEATHKNIGHT",
isPlayer = false, classification = "normal", reaction = 2,
selection = { 1.0, 0.0, 0.0 }, tapped = false, tappedByPlayer = false,
pvp = false, ffa = false, faction = "Alliance",
raidMembers = 0, subgroup = 3,
partialPlayTime = false, noPlayTime = false,
runeStart = 0, runeDuration = 10, runeReady = true, runeType = 1,
casting = false, channeling = false, spellName = "Eclair",
castStart = 0, castEnd = 2, notInterruptible = false,
xp = 500, xpMax = 1000, rested = 200 ,
faction_suivie = "Les Fils de Hodir", faction_attitude = 5,
faction_min = 3000, faction_max = 9000, faction_valeur = 6000,
cvars = { playerStatusText = "0", statusTextPercentage = "0" } }
function UnitHealth(unit) return STATE.health end
function UnitHealthMax(unit) return STATE.healthMax end
function UnitPower(unit, kind) return STATE.power end
function UnitPowerMax(unit, kind) return STATE.powerMax end
function UnitPowerType(unit) return STATE.powerType, STATE.powerToken end
function UnitName(unit) return STATE.name end
function UnitLevel(unit) return STATE.level end
function UnitAffectingCombat(unit) return STATE.combat end
function InCombatLockdown() return STATE.inLockdown end
function SetPortraitTexture(texture, unit)
recorded.portraits = recorded.portraits + 1
texture.portraitOf = unit
end
function ToggleDropDownMenu() end
function IsResting() return STATE.resting end
function UnitThreatSituation(unit) return STATE.threat end
function IsThreatWarningEnabled() return STATE.threatWarning end
function UnitIsDeadOrGhost(unit) return STATE.dead end
function UnitExists(unit) return STATE.hasTarget end
function UnitCanAttack(a, b) return STATE.targetHostile end
function UnitIsUnit(a, b)
if a == "targettarget" and b == "player" then return STATE.targetsMe end
return a == b
end
function GetThreatStatusColor(status)
if status == 3 then return 1.0, 0.0, 0.0 end
if status == 2 then return 1.0, 0.6, 0.0 end
if status == 1 then return 1.0, 1.0, 0.47 end
return 0.69, 0.69, 0.69
end
function IsPartyLeader() return STATE.leader end
function UnitHasVehicleUI(unit) return STATE.vehicle end
function GetTime() return STATE.time end
function GetCVar(name) return STATE.cvars[name] end
function GetCVarBool(name) return STATE.cvars[name] == "1" end
-- ------------------------------------------------- bouchons supplementaires
function UnitClass(unit) return STATE.className, STATE.classToken end
function UnitIsPlayer(unit) return STATE.isPlayer end
function UnitSelectionColor(unit)
return STATE.selection[1], STATE.selection[2], STATE.selection[3]
end
function UnitIsTapped(unit) return STATE.tapped end
function UnitIsTappedByPlayer(unit) return STATE.tappedByPlayer end
function UnitPlayerControlled(unit) return STATE.isPlayer end
function UnitClassification(unit) return STATE.classification end
function UnitReaction(a, b) return STATE.reaction end
function UnitIsPVP(unit) return STATE.pvp end
function UnitIsPVPFreeForAll(unit) return STATE.ffa end
function UnitFactionGroup(unit) return STATE.faction end
function GetNumRaidMembers() return STATE.raidMembers end
function GetRaidRosterInfo(i) return STATE.name, 0, STATE.subgroup end
function PartialPlayTime() return STATE.partialPlayTime end
function NoPlayTime() return STATE.noPlayTime end
function GetRuneCooldown(i) return STATE.runeStart, STATE.runeDuration, STATE.runeReady end
function GetRuneType(i) return STATE.runeType end
function CooldownFrame_SetTimer(cd, start, duration, enable) cd.timer = {start, duration, enable} end
function CombatFeedback_Initialize(self, text, height) self.feedbackText = text end
function CombatFeedback_OnCombatEvent(self, event, flags, amount, kind) self.lastHit = amount end
function CombatFeedback_OnUpdate(self, elapsed) end
function RegisterUnitWatch(f) f.unitWatch = true end
function ToggleDropDownMenu() end
GameTooltip = { SetOwner = function() end, SetText = function() end,
AddLine = function() end, Show = function() end, Hide = function() end }
RAID_CLASS_COLORS = { WARRIOR = { r = 0.78, g = 0.61, b = 0.43 },
DEATHKNIGHT = { r = 0.77, g = 0.12, b = 0.23 } }
GROUP = "Groupe"
PLAYTIME_TIRED = "Temps de jeu fatigue"
PLAYTIME_UNHEALTHY = "Temps de jeu malsain"
TargetFrame = CreateFrame("Button", "TargetFrame", UIParent)
TargetFrameDropDown = CreateFrame("Frame", "TargetFrameDropDown", UIParent)
ComboFrame = CreateFrame("Frame", "ComboFrame", UIParent)
function UnitCastingInfo(unit)
if not STATE.casting then return nil end
return STATE.spellName, nil, STATE.spellName, "icone", STATE.castStart * 1000,
STATE.castEnd * 1000, false, 1, STATE.notInterruptible
end
function UnitChannelInfo(unit)
if not STATE.channeling then return nil end
return STATE.spellName, nil, STATE.spellName, "icone", STATE.castStart * 1000,
STATE.castEnd * 1000, false, STATE.notInterruptible
end
function GetWatchedFactionInfo()
if not STATE.faction_suivie then return nil end
return STATE.faction_suivie, STATE.faction_attitude, STATE.faction_min,
STATE.faction_max, STATE.faction_valeur
end
ReputationWatchBar = CreateFrame("Frame", "ReputationWatchBar", UIParent)
function UnitXP(unit) return STATE.xp end
function UnitXPMax(unit) return STATE.xpMax end
function GetXPExhaustion() return STATE.rested end
MAX_PLAYER_LEVEL = 80
FAILED = "Echec"
INTERRUPTED = "Interrompu"
CastingBarFrame = CreateFrame("StatusBar", "CastingBarFrame", UIParent)
MainMenuExpBar = CreateFrame("StatusBar", "MainMenuExpBar", UIParent)
ExhaustionTick = CreateFrame("Frame", "ExhaustionTick", UIParent)
RuneFrame = CreateFrame("Frame", "RuneFrame", UIParent)
-- la barre d'action du client, telle que l'addon la trouve
MainMenuBar = CreateFrame("Frame", "MainMenuBar", UIParent)
MainMenuBarLeftEndCap = UIParent:CreateTexture(nil, "ARTWORK")
MainMenuBarRightEndCap = UIParent:CreateTexture(nil, "ARTWORK")
MainMenuBarPageNumber = UIParent:CreateFontString(nil, "OVERLAY")
for i = 0, 3 do
_G["MainMenuBarTexture" .. i] = UIParent:CreateTexture(nil, "ARTWORK")
_G["MainMenuXPBarTexture" .. i] = UIParent:CreateTexture(nil, "ARTWORK")
_G["MainMenuMaxLevelBar" .. i] = UIParent:CreateTexture(nil, "ARTWORK")
end
MainMenuBarExpText = UIParent:CreateFontString(nil, "OVERLAY")
ActionBarUpButton = CreateFrame("Button", "ActionBarUpButton", UIParent)
ActionBarDownButton = CreateFrame("Button", "ActionBarDownButton", UIParent)
NumberFontNormalSmallGray = "NumberFontNormalSmallGray"
NumberFontNormal = "NumberFontNormal"
GameFontHighlightSmallOutline = "GameFontHighlightSmallOutline"
for i = 1, 12 do
local b = CreateFrame("CheckButton", "ActionButton" .. i, MainMenuBar)
_G["ActionButton" .. i .. "Icon"] = b:CreateTexture(nil, "ARTWORK")
_G["ActionButton" .. i .. "Border"] = b:CreateTexture(nil, "OVERLAY")
_G["ActionButton" .. i .. "Flash"] = b:CreateTexture(nil, "ARTWORK")
_G["ActionButton" .. i .. "FloatingBG"] = b:CreateTexture(nil, "BACKGROUND")
_G["ActionButton" .. i .. "HotKey"] = b:CreateFontString(nil, "OVERLAY")
_G["ActionButton" .. i .. "Count"] = b:CreateFontString(nil, "OVERLAY")
_G["ActionButton" .. i .. "Name"] = b:CreateFontString(nil, "OVERLAY")
_G["ActionButton" .. i .. "Cooldown"] = CreateFrame("Cooldown", "ActionButton" .. i .. "Cooldown", b)
end
-- le bas de l'ecran du client : micro-menu, sacs, trousseau
MainMenuBarArtFrame = CreateFrame("Frame", "MainMenuBarArtFrame", MainMenuBar)
MainMenuBarArtFrame:SetFrameLevel(2)
local MICROS = { "CharacterMicroButton", "SpellbookMicroButton", "TalentMicroButton",
"AchievementMicroButton", "QuestLogMicroButton", "SocialsMicroButton",
"PVPMicroButton", "LFDMicroButton", "MainMenuMicroButton",
"HelpMicroButton" }
for _, nom in ipairs(MICROS) do
local b = CreateFrame("Button", nom, MainMenuBarArtFrame)
b:SetWidth(28)
b:SetHeight(58)
end
MicroButtonPortrait = CharacterMicroButton:CreateTexture("MicroButtonPortrait", "OVERLAY")
PVPMicroButtonTexture = PVPMicroButton:CreateTexture("PVPMicroButtonTexture", "OVERLAY")
MainMenuBarPerformanceBar = MainMenuMicroButton:CreateTexture("MainMenuBarPerformanceBar", "OVERLAY")
function UpdateMicroButtons() end
for _, nom in ipairs({ "MainMenuBarBackpackButton", "CharacterBag0Slot", "CharacterBag1Slot",
"CharacterBag2Slot", "CharacterBag3Slot" }) do
local b = CreateFrame("CheckButton", nom, MainMenuBarArtFrame)
_G[nom .. "IconTexture"] = b:CreateTexture(nom .. "IconTexture", "BORDER")
_G[nom .. "Count"] = b:CreateFontString(nom .. "Count", "OVERLAY")
end
KeyRingButton = CreateFrame("CheckButton", "KeyRingButton", MainMenuBarArtFrame)
KeyRingButton:Hide()
-- les sacs du client
NUM_CONTAINER_FRAMES = 13
MAX_CONTAINER_ITEMS = 36
NUM_BAG_SLOTS = 4
CONTAINER_WIDTH = 192
ITEM_QUALITY_COLORS = {
[0] = { r = 0.62, g = 0.62, b = 0.62 },
[1] = { r = 1.00, g = 1.00, b = 1.00 },
[2] = { r = 0.12, g = 1.00, b = 0.00 },
[3] = { r = 0.00, g = 0.44, b = 0.87 },
[4] = { r = 0.64, g = 0.21, b = 0.93 },
}
NUM_CONTAINER_COLUMNS = 4
SEARCH = "Rechercher"
BAG_CLEANUP_BAGS = nil
for i = 1, NUM_CONTAINER_FRAMES do
local nom = "ContainerFrame" .. i
local c = CreateFrame("Frame", nom, UIParent)
c.size = 16
c:SetID(0)
_G[nom .. "Portrait"] = c:CreateTexture(nom .. "Portrait", "BACKGROUND")
_G[nom .. "Name"] = c:CreateFontString(nom .. "Name", "ARTWORK")
CreateFrame("Button", nom .. "CloseButton", c)
local bourse = CreateFrame("Frame", nom .. "MoneyFrame", c)
bourse:SetHeight(24)
for _, suffixe in ipairs({ "BackgroundTop", "BackgroundMiddle1", "BackgroundMiddle2",
"BackgroundBottom", "Background1Slot" }) do
_G[nom .. suffixe] = c:CreateTexture(nom .. suffixe, "ARTWORK")
end
for j = 1, MAX_CONTAINER_ITEMS do
local b = CreateFrame("Button", nom .. "Item" .. j, c)
b:SetID(j)
_G[nom .. "Item" .. j .. "IconTexture"] = b:CreateTexture(nil, "BORDER")
end
end
-- l'inventaire simule : SACS[sac][emplacement] = { lien, nombre }
SACS = { [0] = {}, [1] = {}, [2] = {}, [3] = {}, [4] = {} }
TAILLES = { [0] = 4, [1] = 4, [2] = 0, [3] = 0, [4] = 0 }
OBJETS = {
["|cffffffff|Hitem:1|h[Pain]|h|r"] = { nom = "Pain", qualite = 1, type_ = "Consommable",
sousType = "Nourriture", pileMax = 20 },
["|cff0070dd|Hitem:2|h[Epee]|h|r"] = { nom = "Epee", qualite = 3, type_ = "Arme",
sousType = "Epee a une main", pileMax = 1 },
["|cffffffff|Hitem:3|h[Potion]|h|r"] = { nom = "Potion", qualite = 1, type_ = "Consommable",
sousType = "Potion", pileMax = 20 },
}
PRISES = {}
CURSEUR = nil
function GetContainerNumSlots(sac) return TAILLES[sac] or 0 end
function GetContainerItemLink(sac, emplacement)
local case = SACS[sac] and SACS[sac][emplacement]
return case and case.lien or nil
end
function GetContainerItemInfo(sac, emplacement)
local case = SACS[sac] and SACS[sac][emplacement]
if not case then return nil end
local o = OBJETS[case.lien]
return "icone", case.nombre, false, (o and o.qualite) or 1, false
end
function GetItemInfo(lien)
local o = OBJETS[lien]
if not o then return nil end
return o.nom, lien, o.qualite, 1, 1, o.type_, o.sousType, o.pileMax
end
function GetAuctionItemClasses() return "Arme", "Armure", "Consommable", "Divers" end
function CursorHasItem() return CURSEUR ~= nil end
function ClearCursor() CURSEUR = nil end
function PickupContainerItem(sac, emplacement)
table.insert(PRISES, { sac = sac, emplacement = emplacement })
local case = SACS[sac][emplacement]
if CURSEUR == nil then
CURSEUR = { sac = sac, emplacement = emplacement, case = case }
SACS[sac][emplacement] = nil
else
-- Le client reunit deux piles entamees du meme objet ; sinon il echange.
local porte = CURSEUR.case
if case and porte and case.lien == porte.lien then
local max = OBJETS[case.lien].pileMax
if case.nombre + porte.nombre <= max then
SACS[sac][emplacement] = { lien = case.lien, nombre = case.nombre + porte.nombre }
CURSEUR = nil
return
end
end
SACS[CURSEUR.sac][CURSEUR.emplacement] = case
SACS[sac][emplacement] = porte
CURSEUR = nil
end
end
function ContainerFrame_GenerateFrame() end
function ContainerFrame_Update() end
function ContainerFrame_OnHide() end
function ToggleBag() end
HOOKS = {}
function hooksecurefunc(nom, fn) HOOKS[nom] = fn end
function ActionButton_Update() end
function ActionButton_ShowGrid() end
function ActionButton_HideGrid() end
SlashCmdList = {}
"""
def main():
lua = lupa.LuaRuntime(unpack_returned_tuples=True)
lua.execute(MOCK)
ordre = ["UIAtlas.lua", "UIAtlas_01_selection_perso.lua", "UIAtlas_02_creation_perso.lua",
"UIAtlas_03_barre_action.lua", "UIAtlas_04_cadres_unite.lua",
"UIAtlas_05_feuille_perso.lua", "UIAtlas_06_complements.lua", "AtlasUtil.lua", "Layout.lua", "PlayerFrame.lua",
"PlayerFrameExtras.lua", "PlayerRunes.lua", "TargetFrame.lua",
"CastBar.lua", "ActionBar.lua", "BottomBar.lua", "StatusBars.lua", "Bags.lua"]
# l'ordre du .toc fait foi : on verifie qu'il correspond
toc = io.open(os.path.join(ADDON, "ForeverUI.toc"), encoding="utf-8").read()
listes = [l.strip() for l in toc.splitlines() if l.strip() and not l.startswith("##")]
assert listes == ordre, "l'ordre du toc a change : %s" % listes
echecs = []
for fn in ordre:
source = io.open(os.path.join(ADDON, fn), encoding="utf-8").read()
try:
lua.execute(source)
except Exception as exc:
echecs.append((fn, str(exc).split("\n")[0]))
if echecs:
for fn, err in echecs:
print(" ECHEC %-34s %s" % (fn, err))
sys.exit("chargement interrompu")
print("chargement : %d fichiers, aucune erreur" % len(ordre))
g = lua.globals()
# 1. tables d'atlas
n = sum(1 for _ in g.UIAtlas.data.items())
print("entrees d'atlas chargees : %d" % n)
# 2. enregistrement de la position
system = g.ForeverUI.Layout.systems["playerframe"]
d = system.defaults
print("systeme enregistre : %s | defaut %s/%s %s,%s" % (system.label, d.point, d.relativePoint, d.x, d.y))
frame = g.ForeverUIPlayerFrame
pt = frame.points[1]
print("ancrage applique : %s -> %s (%s, %s)" % (pt[1], pt[3], pt[4], pt[5]))
# 3. evenement d'entree en jeu
frame.scripts.OnEvent(frame, "PLAYER_ENTERING_WORLD")
print("apres PLAYER_ENTERING_WORLD :")
print(" nom : %s" % frame.nameText.text)
print(" niveau : %s" % frame.levelText.text)
print(" portrait : %d appel(s) a SetPortraitTexture" % g.RECORDED.portraits)
print(" art : %s" % frame.art.texture)
hp = frame.healthFill
print(" vie 50%% : largeur %s, texcoord u %.6f -> %.6f (visible: %s)" % (
hp.width, hp.texcoord[1], hp.texcoord[2], hp.shown))
pw = frame.powerFill
print(" rage 30%% : largeur %s, hauteur %s (visible: %s)" % (pw.width, pw.height, pw.shown))
print(" PlayerFrame d'origine : %d evenement(s), visible=%s" % (
sum(1 for _ in g.PlayerFrame.events.items()), g.PlayerFrame.shown))
# 3b. le niveau doit vivre dans un cadre fils, place au-dessus du cadre
holder = frame.levelText.owner
niveau_cadre = frame.frameLevel or 1
print(" niveau : porte par un cadre fils = %s, son niveau %s contre %s" % (
holder is not frame, holder.frameLevel, niveau_cadre))
assert holder is not frame, "le niveau est reste dans le cadre principal"
assert holder.frameLevel > niveau_cadre, "le cadre du niveau n'est pas au-dessus"
# 4. vie a zero : le client refuse une largeur nulle, la texture doit disparaitre
g.STATE.health = 0
frame.scripts.OnEvent(frame, "UNIT_HEALTH", "player")
print("vie a 0 : texture de vie visible = %s" % frame.healthFill.shown)
# 5. un evenement pour une autre unite ne doit rien changer
g.STATE.health = 100
frame.scripts.OnEvent(frame, "UNIT_HEALTH", "target")
print("UNIT_HEALTH sur 'target' ignore : texture toujours masquee = %s" % (not frame.healthFill.shown))
# 6. l'art du cadre ne change jamais ; la lueur est celle de la menace
avant = [frame.art.texcoord[i] for i in (1, 2, 3, 4)]
g.STATE.combat = True
frame.scripts.OnEvent(frame, "PLAYER_REGEN_DISABLED")
apres = [frame.art.texcoord[i] for i in (1, 2, 3, 4)]
print("art du cadre inchange en combat : %s" % (avant == apres))
assert avant == apres, "l'art du cadre a ete remplace"
couleur = frame.threatGlow.vertex
print("engage sans etre pris pour cible : visible=%s alpha=%.2f couleur=(%.2f, %.2f, %.2f)" % (
frame.threatGlow.shown, frame.threatGlow.alpha, couleur[1], couleur[2], couleur[3]))
assert frame.threatGlow.shown, "pas de lueur alors que le joueur est en combat"
faible = frame.threatGlow.alpha
g.STATE.threat = 3
frame.scripts.OnEvent(frame, "UNIT_THREAT_SITUATION_UPDATE")
couleur = frame.threatGlow.vertex
fort = frame.threatGlow.alpha
print("pris pour cible par un ennemi : visible=%s alpha=%.2f couleur=(%.2f, %.2f, %.2f)" % (
frame.threatGlow.shown, fort, couleur[1], couleur[2], couleur[3]))
assert fort > faible, "la lueur ne se renforce pas quand l'ennemi vous vise"
assert (couleur[1], couleur[2], couleur[3]) == (1.0, 0.0, 0.0), "la lueur n'est pas rouge"
g.STATE.threat = 1
frame.scripts.OnEvent(frame, "UNIT_THREAT_SITUATION_UPDATE")
print("menace haute sans etre la cible : alpha=%.2f (niveau discret attendu)" % frame.threatGlow.alpha)
g.STATE.dead = True
frame.scripts.OnEvent(frame, "UNIT_THREAT_SITUATION_UPDATE")
print("joueur mort : visible=%s" % frame.threatGlow.shown)
g.STATE.dead = False
g.STATE.threat = 0
g.STATE.combat = False
frame.scripts.OnEvent(frame, "PLAYER_REGEN_ENABLED")
print("hors combat, sans menace : visible=%s" % frame.threatGlow.shown)
assert not frame.threatGlow.shown, "la lueur reste allumee hors combat"
# 6b. serveur sans donnees de menace : la lueur forte doit quand meme venir
g.STATE.threat = 0 # le serveur ne dit rien
g.STATE.combat = True
frame.scripts.OnEvent(frame, "PLAYER_REGEN_DISABLED") # le joueur est engage
g.STATE.hasTarget, g.STATE.targetHostile, g.STATE.targetsMe = True, True, False
frame.scripts.OnEvent(frame, "PLAYER_TARGET_CHANGED")
engage = frame.threatGlow.alpha
print("sans menace, cible qui ne vise pas : alpha=%.2f" % engage)
g.STATE.targetsMe = True
frame.scripts.OnEvent(frame, "PLAYER_TARGET_CHANGED")
vise = frame.threatGlow.alpha
print("sans menace, la cible me vise : alpha=%.2f" % vise)
assert vise > engage, "la lueur ne se renforce pas quand la cible vise le joueur"
# la relecture periodique doit suffire, meme sans aucun evenement
g.STATE.targetsMe = False
frame.scripts.OnUpdate(frame, 0.4)
print("apres relecture periodique : alpha=%.2f (retour au discret)" % frame.threatGlow.alpha)
assert frame.threatGlow.alpha == engage, "la relecture periodique ne redescend pas"
g.STATE.hasTarget, g.STATE.targetHostile, g.STATE.targetsMe = False, False, False
g.STATE.combat = False
frame.scripts.OnEvent(frame, "PLAYER_REGEN_ENABLED")
# 7. mode edition et deplacement
g.SlashCmdList["FOREVERUI"]("")
overlay = g.ForeverUI.Layout.systems["playerframe"].overlay
print("mode edition : %s, surface creee = %s" % (g.ForeverUI.Layout.editing, overlay is not None))
overlay.scripts.OnDragStart()
frame.points = lua.eval("{}")
frame.SetPoint(frame, "CENTER", g.UIParent, "CENTER", 120, -40)
overlay.scripts.OnDragStop()
saved = g.ForeverUIDB.positions["playerframe"]
print("position retenue apres deplacement : %s/%s %s,%s" % (saved.point, saved.relativePoint, saved.x, saved.y))
g.SlashCmdList["FOREVERUI"]("")
print("mode edition apres seconde bascule : %s" % g.ForeverUI.Layout.editing)
# 8. remise par defaut
g.SlashCmdList["FOREVERUI"]("reset")
reste = g.ForeverUIDB.positions["playerframe"]
pt = frame.points[len(list(frame.points.items()))]
print("apres reset : position sauvegardee = %s, ancrage = %s (%s, %s)" % (reste, pt[1], pt[4], pt[5]))
# 9. combat : le mode edition doit refuser
g.STATE.inLockdown = True
g.SlashCmdList["FOREVERUI"]("")
print("mode edition refuse en combat : %s" % (not g.ForeverUI.Layout.editing))
# 10. etats : repos, combat, vehicule
def etat(resting, combat, vehicle):
g.STATE.resting, g.STATE.combat, g.STATE.vehicle = resting, combat, vehicle
frame.scripts.OnEvent(frame, "PLAYER_ENTER_COMBAT" if combat else "PLAYER_LEAVE_COMBAT")
frame.scripts.OnEvent(frame, "PLAYER_UPDATE_RESTING")
return (frame.statusTexture.shown, frame.statusTexture.vertex, frame.combatIcon.shown,
frame.restTexture.shown)
shown, vertex, icon, rest = etat(True, False, False)
print("repos : voile=%s couleur=(%.2f, %.2f, %.2f) icone combat=%s sommeil=%s" % (
shown, vertex[1], vertex[2], vertex[3], icon, rest))
shown, vertex, icon, rest = etat(False, True, False)
print("combat : voile=%s couleur=(%.2f, %.2f, %.2f) icone=%s sommeil=%s" % (
shown, vertex[1], vertex[2], vertex[3], icon, rest))
shown, vertex, icon, rest = etat(False, False, False)
print("ni repos ni combat : voile=%s icone=%s sommeil=%s" % (shown, icon, rest))
shown, vertex, icon, rest = etat(True, True, True)
print("vehicule : voile=%s icone=%s sommeil=%s (tout doit etre masque)" % (shown, icon, rest))
print("voile : melange = %s (ADD attendu, sinon le rouge delave le cadre)"
% frame.statusTexture.blend)
# 10b. les deux drapeaux du client, distincts
etat(False, False, False)
frame.scripts.OnEvent(frame, "PLAYER_REGEN_ENABLED")
frame.scripts.OnEvent(frame, "PLAYER_ENTER_COMBAT")
print("clic droit sur un ennemi (frappe seule) : voile=%s icone=%s ornement=%s" % (
frame.statusTexture.shown, frame.combatIcon.shown, frame.cornerIcon.shown))
assert frame.statusTexture.shown and frame.combatIcon.shown, "la frappe n'allume rien"
frame.scripts.OnEvent(frame, "PLAYER_REGEN_DISABLED")
frame.scripts.OnEvent(frame, "PLAYER_LEAVE_COMBAT")
print("frappe finie, toujours sur la liste : voile=%s icone=%s" % (
frame.statusTexture.shown, frame.combatIcon.shown))
assert frame.combatIcon.shown, "l'icone disparait alors que le combat dure"
assert frame.statusTexture.shown, "le voile s'eteint avant la fin du combat"
frame.scripts.OnEvent(frame, "PLAYER_REGEN_ENABLED")
print("sorti de combat : voile=%s icone=%s ornement=%s" % (
frame.statusTexture.shown, frame.combatIcon.shown, frame.cornerIcon.shown))
assert not frame.statusTexture.shown and not frame.combatIcon.shown, "le combat ne se termine pas"
assert frame.cornerIcon.shown, "l'ornement de coin ne revient pas"
assert frame.statusTexture.blend == "ADD", "le voile n est pas en mode additif"
assert frame.combatGlow is None, "la lueur de combat traine encore"
# 11. animation du sommeil : la vignette doit rester dans l'element
g.STATE.vehicle = False
etat(True, False, False)
entry = g.UIAtlas.data["ui-hud-unitframe-player-rest-flipbook"]
u1, u2, v1, v2 = entry[2], entry[3], entry[4], entry[5]
vues = set()
for pas in range(60):
g.STATE.time = pas * 0.0357
frame.scripts.OnUpdate(frame, 0.0357)
c = frame.restTexture.texcoord
assert u1 - 1e-9 <= c[1] and c[2] <= u2 + 1e-9, "vignette hors de l'element en largeur"
assert v1 - 1e-9 <= c[3] and c[4] <= v2 + 1e-9, "vignette hors de l'element en hauteur"
vues.add((round(c[1], 6), round(c[3], 6)))
print("sommeil : %d vignettes distinctes, toutes dans l'element (42 au total)" % len(vues))
print("voile : transparence pulsee a %.3f" % frame.statusTexture.alpha)
# 12. texte des barres, suivant le reglage du client
g.STATE.health, g.STATE.healthMax = 50, 100
frame.scripts.OnEvent(frame, "UNIT_HEALTH", "player")
print("texte, reglage a 0 sans survol : visible=%s" % frame.healthText.shown)
frame.scripts.OnEnter(frame)
print("texte au survol : vie=%s | ressource=%s" % (
frame.healthText.text, frame.powerText.text))
frame.scripts.OnLeave(frame)
g.STATE.cvars["playerStatusText"] = "1"
frame.scripts.OnEvent(frame, "CVAR_UPDATE")
print("texte, reglage a 1 : visible=%s | vie=%s" % (
frame.healthText.shown, frame.healthText.text))
g.STATE.cvars["statusTextPercentage"] = "1"
frame.scripts.OnEvent(frame, "CVAR_UPDATE")
print("texte en pourcentage : vie=%s" % frame.healthText.text)
# 13. icone de chef de groupe
g.STATE.leader = True
frame.scripts.OnEvent(frame, "PARTY_LEADER_CHANGED")
avec = frame.leaderIcon.shown
g.STATE.leader = False
frame.scripts.OnEvent(frame, "PARTY_LEADER_CHANGED")
g.SlashCmdList["FOREVERUI"]("debug")
print("icone de chef : visible quand chef=%s | masquee sinon=%s" % (
avec, not frame.leaderIcon.shown))
# ---------------------------------------------------------- accessoires
extras = g.ForeverUI.PlayerFrameExtras
g.STATE.pvp = True
extras.updatePvP()
print("icone PvP (Alliance) : cercle=%s icone=%s" % (
extras.pvpCircle.shown, extras.pvpIcon.shown))
assert extras.pvpCircle.shown and extras.pvpIcon.shown, "l'icone PvP ne s'affiche pas"
g.STATE.pvp = False
extras.updatePvP()
print("PvP desactive : cercle=%s" % extras.pvpCircle.shown)
g.STATE.raidMembers, g.STATE.name = 10, "Papota"
extras.updateGroup()
print("indicateur de groupe : visible=%s texte=%s" % (
extras.groupIndicator.shown, extras.groupText.text))
g.STATE.raidMembers = 0
extras.updateGroup()
print("hors raid : visible=%s" % extras.groupIndicator.shown)
g.STATE.partialPlayTime = True
extras.updatePlayTime()
print("temps de jeu fatigue : visible=%s" % extras.playTime.shown)
g.STATE.partialPlayTime = False
extras.updatePlayTime()
print("temps de jeu normal : visible=%s" % extras.playTime.shown)
# ---------------------------------------------------------------- runes
runes = g.ForeverUI.RuneButtons
nb = sum(1 for _ in runes.values())
ancien = g.RuneFrame
print("ancien cadre de runes : visible=%s | neutralise=%s | OnShow accroche=%s" % (
ancien.shown, ancien.foreverSuppressed, ancien.hooks is not None))
assert not ancien.shown, "l'ancien cadre de runes reste affiche sous le notre"
ancien:Show() if False else ancien.Show(ancien)
ancien.hooks.OnShow(ancien)
print(" s'il se reaffiche : remasque=%s" % (not ancien.shown))
assert not ancien.shown, "l'ancien cadre revient des qu'il se reaffiche"
print("runes construites : %d" % nb)
assert nb == 6, "il faut six runes"
bouton = runes[1]
def alphas():
return dict(fond_actif=bouton.bgActive.alpha, fond_eteint=bouton.bgInactive.alpha,
crane_actif=bouton.runeActive.alpha, crane_eteint=bouton.runeInactive.alpha,
degrade=bouton.runeGrad.alpha, traits=bouton.runeLines.alpha)
g.STATE.runeReady = True
g.ForeverUI.RuneBar.scripts.OnEvent(g.ForeverUI.RuneBar, "RUNE_POWER_UPDATE", 1)
pret = alphas()
print("rune prete : %s" % pret)
assert pret["fond_actif"] == 1 and pret["crane_actif"] == 1, "la rune prete n'est pas allumee"
assert pret["fond_eteint"] == 0 and pret["crane_eteint"] == 0, "les calques eteints restent visibles"
assert pret["degrade"] == 0 and pret["traits"] == 0, "un calque de recharge reste allume quand la rune est prete"
g.STATE.runeReady = False
g.ForeverUI.RuneBar.scripts.OnEvent(g.ForeverUI.RuneBar, "RUNE_POWER_UPDATE", 1)
recharge = alphas()
print("rune en recharge : %s | minuterie=%s" % (recharge, bouton.cooldown.timer is not None))
assert recharge["fond_actif"] == 0 and recharge["crane_actif"] == 0, "la rune reste allumee en recharge"
assert abs(recharge["crane_eteint"] - 0.4) < 1e-6, "le crane eteint doit etre a 0,4"
assert abs(recharge["degrade"] - 0.3) < 1e-6 and abs(recharge["traits"] - 0.3) < 1e-6, "degrade et traits doivent etre a 0,3 pendant la recharge"
# aucun calque additif : la source les declare tous en BLEND
for nom in ("bgActive", "bgInactive", "runeActive", "runeInactive", "runeGrad", "runeLines", "shadow"):
assert bouton[nom].blend is None, "le calque %s est en melange additif" % nom
print("melanges : aucun calque additif, conforme au XML")
g.STATE.runeReady = True
g.ForeverUI.RuneBar.scripts.OnEvent(g.ForeverUI.RuneBar, "RUNE_POWER_UPDATE", 1)
print("art du cadre joueur : %s" % frame.art.texture)
# ------------------------------------------------------ cadre de cible
cible = g.ForeverUITargetFrame
print("cadre de cible : surveille par le client=%s | systeme enregistre=%s" % (
cible.unitWatch, g.ForeverUI.Layout.systems["targetframe"] is not None))
g.STATE.hasTarget = True
g.STATE.health, g.STATE.healthMax = 80, 100
cible.scripts.OnEvent(cible, "PLAYER_TARGET_CHANGED")
print(" nom=%s niveau=%s vie=%.0f%% (largeur %.0f)" % (
cible.nameText.text, cible.levelText.text,
(cible.healthFill.width or 0) / 126.0 * 100, cible.healthFill.width or 0))
assert cible.healthFill.shown, "la barre de vie de la cible est vide"
couleur = cible.reputation.vertex
print(" bandeau de reputation : (%.2f, %.2f, %.2f) -- c'est lui qui porte la reaction" % (
couleur[1], couleur[2], couleur[3]))
assert (couleur[1], couleur[2], couleur[3]) == (1.0, 0.0, 0.0), "le bandeau ne prend pas la couleur de selection"
g.STATE.tapped, g.STATE.tappedByPlayer = True, False
cible.scripts.OnEvent(cible, "UNIT_FACTION", "target")
gris = cible.reputation.vertex
portrait_gris = cible.portrait.vertex
print(" cible verrouillee : bandeau=(%.2f, %.2f, %.2f) portrait=(%.2f, %.2f, %.2f)" % (
gris[1], gris[2], gris[3], portrait_gris[1], portrait_gris[2], portrait_gris[3]))
assert gris[1] == 0.5 and portrait_gris[1] == 0.5, "rien ne grise quand la cible est verrouillee"
g.STATE.tapped = False
cible.scripts.OnEvent(cible, "UNIT_FACTION", "target")
g.STATE.classification = "minus"
cible.scripts.OnEvent(cible, "UNIT_CLASSIFICATION_CHANGED", "target")
art_minus = tuple(round(cible.art.texcoord[i], 6) for i in (1, 2, 3, 4))
g.STATE.classification = "rare"
cible.scripts.OnEvent(cible, "UNIT_CLASSIFICATION_CHANGED", "target")
art_rare = tuple(round(cible.art.texcoord[i], 6) for i in (1, 2, 3, 4))
g.STATE.classification = "normal"
cible.scripts.OnEvent(cible, "UNIT_CLASSIFICATION_CHANGED", "target")
art_normal = tuple(round(cible.art.texcoord[i], 6) for i in (1, 2, 3, 4))
# l'elite ne change pas le cadre mais l'anneau du portrait
anneaux = {}
for classe in ("normal", "elite", "rareelite", "worldboss", "rare"):
g.STATE.classification = classe
cible.scripts.OnEvent(cible, "UNIT_CLASSIFICATION_CHANGED", "target")
anneaux[classe] = (cible.classRing.shown,
tuple(round(cible.classRing.texcoord[i], 6) for i in (1, 2, 3, 4))
if cible.classRing.texcoord else None)
print(" anneau : ordinaire=%s elite=%s rare elite=%s boss=%s" % (
anneaux["normal"][0], anneaux["elite"][0], anneaux["rareelite"][0], anneaux["worldboss"][0]))
assert not anneaux["normal"][0], "un monstre ordinaire ne doit pas porter d'anneau"
assert anneaux["elite"][0] and anneaux["worldboss"][0], "l'elite et le boss doivent porter un anneau"
assert anneaux["elite"][1] != anneaux["rareelite"][1], "elite et rare elite portent le meme anneau"
assert anneaux["elite"][1] != anneaux["worldboss"][1], "elite et boss portent le meme anneau"
g.STATE.classification = "normal"
cible.scripts.OnEvent(cible, "UNIT_CLASSIFICATION_CHANGED", "target")
print(" art par classification : negligeable v=%.4f | rare v=%.4f | ordinaire v=%.4f" % (
art_minus[2], art_rare[2], art_normal[2]))
assert len({art_minus, art_rare, art_normal}) == 3, \
"les trois classifications doivent donner trois arts differents"
# la geometrie des barres suit la classification, comme CheckClassification
g.STATE.classification = "minus"
cible.scripts.OnEvent(cible, "PLAYER_TARGET_CHANGED")
print(" creature negligeable : vie a 80%% = %.0f px sur 125, ressource visible=%s" % (
cible.healthFill.width or 0, cible.powerFill.shown))
assert not cible.powerFill.shown, "la ressource doit disparaitre sur une creature negligeable"
g.STATE.classification = "normal"
cible.scripts.OnEvent(cible, "PLAYER_TARGET_CHANGED")
print(" cible ordinaire : vie a 80%% = %.0f px sur 126, ressource visible=%s" % (
cible.healthFill.width or 0, cible.powerFill.shown))
assert cible.powerFill.shown, "la ressource doit revenir sur une cible ordinaire"
# ------------------------------------------------ barre d'incantation
barre = g.ForeverUICastBar
g.STATE.time = 100.0
g.STATE.casting = True
g.STATE.castStart, g.STATE.castEnd = 100.0, 102.0
barre.scripts.OnEvent(barre, "UNIT_SPELLCAST_START", "player")
print("incantation lancee : visible=%s sort=%s" % (barre.shown, barre.spellText.text))
assert barre.shown, "la barre d'incantation ne s'affiche pas"
g.STATE.time = 101.0 # moitie du sort
barre.scripts.OnUpdate(barre, 0.1)
moitie = barre.fill.width
print("a mi-parcours : largeur=%.1f sur 209 | reste=%s" % (moitie, barre.timeText.text))
assert 95 < moitie < 115, "le remplissage ne suit pas la progression"
g.STATE.time = 102.5 # depassement : la barre se ferme
barre.scripts.OnUpdate(barre, 0.1)
print("sort termine : visible=%s" % barre.shown)
# canalisation : le remplissage descend
g.STATE.casting, g.STATE.channeling = False, True
g.STATE.castStart, g.STATE.castEnd = 200.0, 204.0
g.STATE.time = 200.0
barre.scripts.OnEvent(barre, "UNIT_SPELLCAST_CHANNEL_START", "player")
g.STATE.time = 201.0
barre.scripts.OnUpdate(barre, 0.1)
debut = barre.fill.width
g.STATE.time = 203.0
barre.scripts.OnUpdate(barre, 0.1)
fin = barre.fill.width
print("canalisation : largeur %.1f puis %.1f (elle doit descendre)" % (debut, fin))
assert fin < debut, "une canalisation doit se vider"
barre.scripts.OnEvent(barre, "UNIT_SPELLCAST_CHANNEL_STOP", "player")
g.STATE.channeling = False
print("canalisation arretee : visible=%s" % barre.shown)
g.STATE.casting = True
g.STATE.castStart, g.STATE.castEnd = 300.0, 302.0
g.STATE.time = 300.0
barre.scripts.OnEvent(barre, "UNIT_SPELLCAST_START", "player")
barre.scripts.OnEvent(barre, "UNIT_SPELLCAST_INTERRUPTED", "player")
print("sort interrompu : visible=%s texte=%s" % (barre.shown, barre.spellText.text))
g.STATE.time = 302.0
barre.scripts.OnUpdate(barre, 0.1)
print("apres la pause : visible=%s" % barre.shown)
g.STATE.casting = False
# --------------------------------- barres d'experience et de reputation
xp = g.ForeverUIExperienceBar
rep = g.ForeverUIReputationBar
g.STATE.level = 40
g.ForeverUI.StatusBarsUpdate()
print("experience a 50%% : visible=%s acquis=%.0f repose=%.0f texte=%s" % (
xp.shown, xp.remplissage.width or 0, xp.repos.width or 0, xp.texte.text))
assert xp.remplissage.shown and xp.repos.shown, "les remplissages d'experience sont absents"
assert xp.repos.width > xp.remplissage.width, "la part reposee doit depasser l'acquis"
print("reputation amicale : visible=%s rempli=%.0f sur %.0f texte=%s" % (
rep.shown, rep.remplissage.width or 0, rep.width, rep.texte.text))
assert rep.shown and rep.remplissage.shown, "la barre de reputation est vide"
assert abs(rep.remplissage.width - rep.width * 0.5) < 0.01, "la reputation n'est pas a moitie"
# les huit teintes vivent sur la MEME feuille : c'est le rectangle lu qui
# change, pas le fichier.
vert = tuple(rep.remplissage.texcoord.values())
g.STATE.faction_attitude = 3
g.ForeverUI.StatusBarsUpdate()
orange = tuple(rep.remplissage.texcoord.values())
print("attitude inamicale : rectangle lu different = %s" % (orange != vert))
assert orange != vert, "la teinte ne suit pas l'attitude"
g.STATE.faction_suivie = None
g.ForeverUI.StatusBarsUpdate()
print("aucune faction suivie : visible=%s (la barre doit disparaitre)" % rep.shown)
assert not rep.shown, "la barre de reputation reste sans faction suivie"
g.STATE.faction_suivie = "Les Fils de Hodir"
g.STATE.faction_attitude = 5
g.ForeverUI.StatusBarsUpdate()
# les deux barres vont d'un embout a l'autre, et se touchent
rangee = g.ForeverUI.BottomRow
largeur = rangee.droite - rangee.gauche
print("rangee : %.1f -> %.1f (%.1f de large), haut %.0f" % (
rangee.gauche, rangee.droite, largeur, rangee.haut))
assert abs(xp.width - largeur) < 0.01 and abs(rep.width - largeur) < 0.01, (
"les barres ne font pas la longueur de la rangee")
# la rangee va du bord gauche de la barre d'action au bord droit des sacs
barre = g.ForeverUI.Layout.systems["actionbar"].defaults
sacsdef = g.ForeverUI.Layout.systems["sacs"].defaults
gauche_barre = barre.x - g.ForeverUIActionBarHolder.width
droite_sacs = sacsdef.x + g.ForeverUIBagsBar.width
print("bloc de gauche a %.1f, bloc de droite a %.1f" % (gauche_barre, droite_sacs))
assert abs(rangee.gauche - gauche_barre) < 0.01, "la rangee ne part pas du bord de la barre"
assert abs(rangee.droite - droite_sacs) < 0.01, "la rangee ne finit pas au bord des sacs"
dxp = g.ForeverUI.Layout.systems["experiencebar"].defaults
drep = g.ForeverUI.Layout.systems["reputationbar"].defaults
print("experience posee a y=%.0f, reputation a y=%.0f (13 d'ecart, elles se touchent)" % (
dxp.y, drep.y))
assert dxp.y == rangee.haut, "l'experience ne pose pas sur la rangee"
assert drep.y - dxp.y == 13, "la reputation n'est pas juste au-dessus de l'experience"
assert abs(dxp.x - (rangee.gauche + rangee.droite) / 2) < 0.01
g.STATE.level = 80
g.ForeverUI.StatusBarsUpdate()
print("niveau maximum : visible=%s (la barre doit disparaitre)" % xp.shown)
assert not xp.shown, "la barre d'experience reste au niveau maximum"
assert rep.shown, "la reputation doit rester quand l'experience disparait"
g.STATE.level = 80
# --------------------------------------------------- barre d'action
bouton = g.ActionButton1
icone = g["ActionButton1Icon"]
print("bouton d'action : %d x %d (45 attendu, 36 sur 3.3.5 d'origine)" % (
bouton.width, bouton.height))
assert bouton.width == 45 and bouton.height == 45, "le bouton ne fait pas la taille de camelot"
assert icone.allPoints, "l'icone ne remplit pas le bouton"
assert icone.texcoord[1] == 0 and icone.texcoord[2] == 1, "l'icone est rognee alors que la source la laisse entiere"
for nom, region, add in (("cadre", bouton.foreverFrame, False), ("enfonce", bouton._pushed, False),
("survol", bouton._highlight, False), ("coche", bouton._checked, True)):
assert region.width == 46 and region.height == 45, "l'etat %s n'est pas en 46 x 45" % nom
attendu = "ADD" if add else "BLEND"
assert region.blend == attendu, "l'etat %s devrait etre en %s" % (nom, attendu)
print("quatre etats : 46 x 45, coche en ADD, le reste en BLEND")
# la texture normale du client doit rester muette : il la reecrit sans cesse
print("texture normale du client : alpha=%.2f (0 attendu)" % bouton._normal.alpha)
assert bouton._normal.alpha == 0, "la texture normale du client n'est pas neutralisee"
g.ActionButton_Update(bouton)
if g.HOOKS["ActionButton_Update"]:
g.HOOKS["ActionButton_Update"](bouton)
print("apres une mise a jour du client : alpha=%.2f" % bouton._normal.alpha)
assert bouton._normal.alpha == 0, "l'habillage ne survit pas a ActionButton_Update"
# aucun bord de barre en pavage : cela etalerait la feuille d'atlas entiere
for piece in (g.ForeverUI.ActionBarBorder,):
pass
print("bords de barre : etires, jamais paves")
pas = None
for i in (1, 2):
b = g["ActionButton" + str(i)]
pt = b.points[len(list(b.points.items()))]
if i == 1:
depart = pt[4]
else:
pas = pt[4] - depart
print("pas entre boutons : %s (47 attendu : 45 + 2 de marge)" % pas)
assert pas == 47, "l'espacement ne suit pas minButtonPadding = 2"
hk = g["ActionButton1HotKey"]
ct = g["ActionButton1Count"]
nm = g["ActionButton1Name"]
print("raccourci %sx%s %s | quantite %s | nom %sx%s %s" % (
hk.width, hk.height, hk.font, ct.font, nm.width, nm.height, nm.font))
assert hk.width == 32 and hk.height == 10 and hk.justify == "RIGHT"
assert nm.width == 36 and nm.height == 10
cd = g["ActionButton1Cooldown"]
coins = [cd.points[i] for i in (1, 2)]
print("recharge : %s %s puis %s %s (retrait de 3 px)" % (
coins[0][1], coins[0][4], coins[1][1], coins[1][4]))
assert coins[0][4] == 3 and coins[1][4] == -3, "la recharge n'est pas en retrait de 3 px"
print("embouts : gauche %dx%d, droite %dx%d (154 x 95 attendu)" % (
g.ForeverUI.ActionBarEndCaps.left.width, g.ForeverUI.ActionBarEndCaps.left.height,
g.ForeverUI.ActionBarEndCaps.right.width, g.ForeverUI.ActionBarEndCaps.right.height))
assert g.ForeverUI.ActionBarEndCaps.left.width == 154, "embout gauche a la mauvaise taille"
for nom in ("MainMenuBarTexture0", "MainMenuXPBarTexture0", "MainMenuMaxLevelBar0",
"MainMenuBarLeftEndCap", "MainMenuBarRightEndCap", "MainMenuBarExpText"):
assert g[nom].alpha == 0, "%s reste visible sous la nouvelle barre" % nom
print("anciens morceaux effaces : corps, barre d'xp, version niveau max, embouts, texte")
print("fleches de page habillees : %s" % (g.ActionBarUpButton.foreverSkinned == True))
page = g.ForeverUIActionBarPage
ph = page.points[1]
ph_haut = g.ActionBarUpButton.points[1]
print("bloc de pagination : %d x %d, %s sur %s (%s, %s) | fleche haut centree a y=%s" % (
page.width, page.height, ph[1], ph[3], ph[4], ph[5], ph_haut[5]))
assert page.width == 17 and page.height == 34
assert ph[4] == -4 and ph[5] == 9, "le bloc de pagination n est pas a gauche de la barre"
assert ph_haut[5] == 10 and g.ActionBarDownButton.points[1][5] == -10
print("ancien fond du client efface : %s | porteur enregistre : %s" % (
g["MainMenuBarTexture0"].alpha == 0,
g.ForeverUI.Layout.systems["actionbar"] is not None))
# ------------------------------------------------- bas de l'ecran
micro = g.ForeverUIMicroMenu
print("micro-menu : %d x %d pour %d boutons" % (
micro.width, micro.height, len(list(g.ForeverUI.MicroButtons.values()))))
assert micro.width == 322, "le micro-menu ne fait pas 275 + 47 de rallonge"
assert micro.height == 40
b1, b2 = g.CharacterMicroButton, g.SpellbookMicroButton
print("bouton de micro-menu : %d x %d (32 x 46 : l'ouverture du cadre ; 28 x 58 d'origine)" % (
b1.width, b1.height))
assert b1.width == 32 and b1.height == 46, "le bouton ne remplit pas l'ouverture du cadre"
assert micro.height == 40, "le bandeau ne doit pas changer de hauteur"
entree0 = list(g.ForeverUI.MicroButtons.values())[0]
assert entree0.fond.allPoints, "le fond ne suit pas la taille du bouton"
pp0 = g.MicroButtonPortrait
assert pp0.width == 18 and pp0.height == 26, "le portrait n'a plus sa taille d'origine"
assert b1.hitRect and b1.hitRect[3] == 0, "les 18 px inertes du haut sont restes"
print("premier bouton a %s du bord gauche du bandeau (0 attendu)" % b1.points[1][4])
assert b1.points[1][4] == 0, "les boutons ne sont pas serres a gauche"
pas = b2.points[1][4] - b1.points[1][4]
print("pas du micro-menu : %s (27 attendu : 32 - 5 de chevauchement)" % pas)
assert pas == 27, "childXPadding = -5 n'est pas respecte"
porte = g.MainMenuMicroButton
assert porte.frameLevel > b1.frameLevel, "le bouton de droite ne passe pas devant"
# le fond change quand le bouton est enfonce, comme SetPushed le fait
entree = list(g.ForeverUI.MicroButtons.values())[0]
print("fond au repos : up visible=%s, down visible=%s" % (
entree.fond.shown, entree.fondEnfonce.shown))
assert entree.fond.shown and not entree.fondEnfonce.shown
g.CharacterMicroButton.buttonState = "PUSHED"
g.HOOKS["UpdateMicroButtons"]()
print("apres enfoncement : up visible=%s, down visible=%s, ombre enfoncee=%s" % (
entree.fond.shown, entree.fondEnfonce.shown,
entree.ombreEnfoncee and entree.ombreEnfoncee.shown))
assert not entree.fond.shown and entree.fondEnfonce.shown, "le fond enfonce ne prend pas le relais"
g.CharacterMicroButton.buttonState = "NORMAL"
g.HOOKS["UpdateMicroButtons"]()
# le portrait reprend le rognage de camelot
pp = g.MicroButtonPortrait
print("portrait : rogne a (%.4f, %.4f, %.4f, %.4f), %d x %d centre" % (
pp.texcoord[1], pp.texcoord[2], pp.texcoord[3], pp.texcoord[4], pp.width, pp.height))
assert abs(pp.texcoord[1] - 0.2) < 1e-6 and abs(pp.texcoord[4] - 0.9) < 1e-6
assert pp.points[1][1] == "CENTER"
# ----------------------------------------------------- barre des sacs
sacs = g.ForeverUIBagsBar
print("barre des sacs : %d x %d (268 x 45 attendu : 5 x 45 + 33 + 5 x 2)" % (
sacs.width, sacs.height))
assert sacs.width == 268 and sacs.height == 45
dos = g.MainMenuBarBackpackButton
sac0 = g.CharacterBag0Slot
print("sac a dos %d x %d, ancre %s sur %s | sac 1 decale de %s" % (
dos.width, dos.height, dos.points[1][1], dos.points[1][3], sac0.points[1][4]))
assert dos.width == 45 and dos.height == 45
assert sac0.points[1][4] == -2, "bagPadding = 2 n'est pas respecte"
assert dos._normal.width == 46 and dos._normal.height == 46, "le cadre du sac n'est pas en 46 x 46"
assert dos._highlight.blend == "ADD", "le survol du sac n'est pas en ADD"
trousseau = g.KeyRingButton
print("trousseau : %d x %d, visible=%s, cadre %d de large" % (
trousseau.width, trousseau.height, trousseau.shown, trousseau._normal.width))
assert trousseau.width == 33 and trousseau.height == 45
assert trousseau.shown, "camelot garde toujours le trousseau dans la barre"
assert g.ForeverUIBagsFiller is None, "l emplacement decoratif est encore construit"
cellules = list(g.ForeverUI.BagsCells.values())
separateurs = [v for v in g.ForeverUI.BagsDividers.values()]
print("cellules : %d | separateurs : %d (un de moins que de cellules)" % (
len(cellules), len(separateurs)))
assert len(cellules) == 6, "la barre des sacs ne compte pas six cellules"
assert len(separateurs) == 5
# ------------------------------------------------- la rangee complete
for nom, attendu in (("micromenu", (116.5, 6)), ("actionbar", (-49, 2)), ("sacs", (284.5, 2))):
d = g.ForeverUI.Layout.systems[nom].defaults
print("%-10s : %s sur %s (%.1f, %.1f)" % (nom, d.point, d.relativePoint, d.x, d.y))
assert abs(d.x - attendu[0]) < 1e-6 and abs(d.y - attendu[1]) < 1e-6, (
"position par defaut fausse pour %s" % nom)
embout = g.ForeverUI.ActionBarEndCaps.right
pt = embout.points[1]
print("embout droit : %s sur %s (%s, %s)" % (pt[1], pt[3], pt[4], pt[5]))
assert pt[1] == "BOTTOMLEFT" and pt[3] == "BOTTOMRIGHT", "l'embout droit n'est pas cale par le bas"
assert pt[4] == -30 and pt[5] == -2, "l'embout droit ne tient pas au bord des sacs"
gauche = g.ForeverUI.ActionBarEndCaps.left.points[1]
print("embout gauche : %s sur %s (%s, %s)" % (gauche[1], gauche[3], gauche[4], gauche[5]))
assert gauche[1] == "BOTTOMRIGHT" and gauche[3] == "BOTTOMLEFT" and gauche[5] == -2
# ------------------------------------------------------- les sacs
sac = g.ContainerFrame1
print("cadre de sac : habille=%s | ancien fond efface=%s" % (
sac.foreverSkinned == True, g["ContainerFrame1BackgroundTop"].alpha == 0))
assert sac.foreverSkinned, "le cadre de sac n'est pas habille"
assert g["ContainerFrame1BackgroundTop"].alpha == 0, "l'habillage d'epoque est reste"
panneau = sac.foreverPanel
print("panneau : %d morceaux de fond, coins %s" % (
len(list(panneau.fond.values())),
", ".join(sorted(k for k in dict(panneau).keys() if str(k).startswith("coin")))))
assert panneau.coinHautGauche is not None and panneau.bordBas is not None
portrait = g["ContainerFrame1Portrait"]
pt = portrait.points[1]
print("portrait : %dx%d, %s sur %s (%.1f, %.1f), rogne a %.2f" % (
portrait.width, portrait.height, pt[1], pt[3], pt[4], pt[5], portrait.texcoord[1]))
assert portrait.width == 34 and pt[3] == "TOPLEFT" and abs(pt[4] - 13.5) < 0.01
bouton = g["ContainerFrame1Item1"]
print("emplacement : fond=%s | voile de recherche=%s" % (
bouton._normal.texture is not None, bouton.foreverVoile is not None))
assert bouton.foreverVoile is not None, "le voile de recherche manque"
assert bouton._normal.allPoints, "l'emplacement ne couvre pas le bouton"
# la mise en page : fenetre mesuree, grille centree, bourse dedans
g.ContainerFrame1.size = 16
g.HOOKS["ContainerFrame_GenerateFrame"](g.ContainerFrame1)
premier = g.ContainerFrame1Item1
ppt = premier.points[len(list(premier.points.values()))]
bourse = g.ContainerFrame1MoneyFrame
bpt = bourse.points[len(list(bourse.points.values()))]
print("fenetre du sac a dos : %d x %d (178 x 263 : 163 de grille + 57 + 30 + 13)" % (
g.ContainerFrame1.width, g.ContainerFrame1.height))
assert g.ContainerFrame1.width == 178, "la fenetre n'a pas la largeur de camelot"
assert g.ContainerFrame1.height == 263, "la fenetre n'a pas la hauteur de camelot"
print(" premier bouton : %s sur %s (%s, %s) -- il pose sur la bourse" % (
ppt[1], ppt[3], ppt[4], ppt[5]))
assert ppt[3] == "TOPRIGHT" and ppt[4] == 0 and ppt[5] == 3, (
"la derniere rangee n'est pas a 1 px de l'encadre de la bourse")
rangee2 = g.ContainerFrame1Item5
r2 = rangee2.points[len(list(rangee2.points.values()))]
print(" ecart entre rangees : %s (5 attendu, 4 en 3.3.5)" % r2[5])
assert r2[5] == 5, "l'ecart entre rangees n'est pas celui de camelot"
print(" bourse : %d de haut, %s (%s, %s) et %s (%s, %s), encadree=%s" % (
bourse.height, bpt[1], bpt[4], bpt[5],
bourse.points[len(list(bourse.points.values()))][1],
bourse.points[len(list(bourse.points.values()))][4],
bourse.points[len(list(bourse.points.values()))][5],
bourse.foreverBorde == True))
assert bourse.height == 13, "la bourse n'a pas la hauteur de camelot"
assert bourse.foreverBorde, "la bourse n'a pas son encadre"
# la fenetre suit le nombre d'emplacements
print(" la fenetre suit son contenu :")
for taille, rangees in ((4, 1), (8, 2), (16, 4), (20, 5), (36, 9)):
g.ContainerFrame2.size = taille
g.ContainerFrame2:SetID(1) if False else None
g.ContainerFrame2.id = 1 # un sac porte, pas le sac a dos
g.HOOKS["ContainerFrame_GenerateFrame"](g.ContainerFrame2)
attendu = rangees * 37 + (rangees - 1) * 5 + 57
print(" %2d emplacements (%d rangees) -> %d de haut (%d attendu)" % (
taille, rangees, g.ContainerFrame2.height, attendu))
assert g.ContainerFrame2.height == attendu, "la fenetre ne suit pas son contenu"
assert g.ContainerFrame2.width == 178, "la largeur ne doit pas bouger"
g.ContainerFrame2.id = 0
# le contour suit la qualite de l'objet
lua.execute("""
SACS[0][4] = { lien = "|cff0070dd|Hitem:2|h[Epee]|h|r", nombre = 1 }
ContainerFrame1.size = 16
""")
g.ForeverUI.BagSearch.Tout()
fermer = g.ContainerFrame1CloseButton
fpt = fermer.points[len(list(fermer.points.values()))]
print(" fermeture : %d x %d, %s (%s, %s)" % (fermer.width, fermer.height, fpt[1], fpt[4], fpt[5]))
assert fermer.width == 24 and fermer.height == 24, "le bouton de fermeture n'est pas en 24 x 24"
assert fpt[4] == 1 and fpt[5] == 0, "le bouton de fermeture n'est pas au coin"
contour = g.ContainerFrame1Item4.foreverContour
vide = g.ContainerFrame1Item3.foreverContour
gris = [round(v, 2) for v in vide.vertex.values()] if vide.vertex else None
teinte = [round(v, 2) for v in contour.vertex.values()] if contour.vertex else None
print(" contour : case pleine %s | case vide %s (les deux visibles : %s et %s)" % (
teinte, gris, contour.shown, vide.shown))
assert contour.shown and vide.shown, "camelot pose un cadre sur chaque case"
assert gris and abs(gris[0] - 0.39) < 0.01, "la case vide n'a pas le gris de la capture"
assert teinte and teinte != gris, "la qualite ne teinte pas le cadre"
# le champ et le bouton de tri ne se montrent que sur le sac principal
g.ForeverUI.BagsLayout()
champ = g.ForeverUIBagSearchBox
tri = g.ForeverUIBagSortButton
print("champ %dx%d visible=%s | bouton de tri %dx%d visible=%s" % (
champ.width, champ.height, champ.shown, tri.width, tri.height, tri.shown))
assert champ.width == 96 and champ.height == 18
assert tri.width == 28 and tri.height == 26
pc = champ.points[1]
pt2 = tri.points[1]
print("champ ancre %s (%s, %s) | tri ancre %s (%s, %s)" % (
pc[1], pc[4], pc[5], pt2[1], pt2[4], pt2[5]))
assert pc[4] == 42 and pc[5] == -37, "le champ n'est pas a la place de la source"
assert pt2[4] == -9 and pt2[5] == -34, "le bouton de tri n'est pas a la place de la source"
# la recherche : le pain reste, l'epee se voile
lua.execute("""
SACS[0][1] = { lien = "|cffffffff|Hitem:1|h[Pain]|h|r", nombre = 5 }
SACS[0][2] = { lien = "|cff0070dd|Hitem:2|h[Epee]|h|r", nombre = 1 }
SACS[0][3] = { lien = "|cffffffff|Hitem:3|h[Potion]|h|r", nombre = 3 }
ContainerFrame1.size = 4
""")
g.ForeverUI.BagSearch.Set("pain")
voiles = [g["ContainerFrame1Item" + str(i)].foreverVoile.shown for i in (1, 2, 3, 4)]
print("recherche \"pain\" : voiles = %s (le premier est le pain)" % voiles)
assert voiles[0] == False and voiles[1] == True, "la recherche ne filtre pas"
g.ForeverUI.BagSearch.Set("consommable")
voiles = [g["ContainerFrame1Item" + str(i)].foreverVoile.shown for i in (1, 2, 3)]
print("recherche par type : voiles = %s" % voiles)
assert voiles[0] == False and voiles[2] == False, "le type ne compte pas dans la recherche"
g.ForeverUI.BagSearch.Set("")
# le tri : deux piles de pain se reunissent, puis l'ordre se fait
lua.execute("""
SACS[0][1] = { lien = "|cffffffff|Hitem:3|h[Potion]|h|r", nombre = 3 }
SACS[0][2] = { lien = "|cffffffff|Hitem:1|h[Pain]|h|r", nombre = 5 }
SACS[0][3] = { lien = "|cff0070dd|Hitem:2|h[Epee]|h|r", nombre = 1 }
SACS[0][4] = { lien = "|cffffffff|Hitem:1|h[Pain]|h|r", nombre = 7 }
PRISES = {}
""")
g.ForeverUI.BagSort.Lancer()
tic = g.ForeverUI.BagSort
horloge = [f for f in g.FRAMES.values() if f.scripts and f.scripts.OnUpdate][-1]
for _ in range(60):
if not tic.actif:
break
horloge.scripts.OnUpdate(horloge, 0.2)
contenu = []
for i in range(1, 5):
case = g.SACS[0][i]
contenu.append("%s x%d" % (case.lien.split("[")[1].split("]")[0], case.nombre) if case else "vide")
print("apres le tri : %s" % " | ".join(contenu))
print(" %d prises d'objet, %d etapes" % (len(list(g.PRISES.values())), tic.etapes))
assert not tic.actif, "le tri ne s'est pas arrete"
assert "Pain x12" in " ".join(contenu), "les deux piles de pain n'ont pas ete reunies"
# ------------------------------------------------ cible de la cible
totcadre = g.ForeverUITargetOfTarget
g.STATE.hasTarget = True
totcadre.scripts.OnEvent(totcadre, "PLAYER_TARGET_CHANGED")
print("cible de la cible : nom=%s vie visible=%s surveillee=%s" % (
totcadre.nameText.text, totcadre.healthFill.shown, totcadre.unitWatch))
assert totcadre.healthFill.shown, "la vie de la cible de la cible est vide"
print("\nmessages du chat :")
for msg in g.RECORDED.messages.values():
print(" %s" % msg)
main()