Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Frostforge/Core/CastBars.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,23 @@ function C:Artwork(config)
return config.castBarArt
end

-- Complete cast artwork is registered around one shared transparent opening.
-- Register the painted rails, not the differently padded transparent canvases.
-- Outer contours are never sampled out of a thin unit-shell strip. Corners use
-- the same horizontal/vertical scale; only connecting spans fit the native bar.
function C:Pieces(entry,w,h,weight,padding,mirror)
local factor=1.25*weight
local scale=math.min(h/32, .65)*factor
local limits=J.CastBorderFitLimits
-- One common cap keeps every design usable at extreme weight / tiny bars.
-- A design never changes corner scale just because its class changed.
scale=math.min(scale,(w+2*padding)*.95/limits[1],(h+2*padding)*.95/limits[2])
local side=48*scale
local x={-padding-side,-padding,w+padding,w+padding+side}
local y={-padding-side,-padding,h+padding,h+padding+side}
local inset=entry.castInsets
local left=(mirror and inset[3] or inset[1])*scale
local right=(mirror and inset[1] or inset[3])*scale
local top,bottom=inset[2]*scale,inset[4]*scale
local x={-padding-side+left,-padding+left,w+padding-right,w+padding+side-right}
local y={-padding-side+top,-padding+top,h+padding-bottom,h+padding+side-bottom}
local u=mirror and {512,464,48,0} or {0,48,464,512}
local v={0,48,80,128}
local pieces={}
Expand Down Expand Up @@ -149,7 +157,9 @@ function C:Paint(module,id,layout)
module.assetOK=true
for name,piece in pairs(pieces) do
local texture=module.textures[name]
if layout then
if layout or module.id~=id then
-- Only our unprotected texture regions move on an identity change;
-- the native bar and the decoration's frame anchor stay untouched.
texture:ClearAllPoints();texture:SetPoint("TOPLEFT",module.frame,"TOPLEFT",piece.x,-piece.y)
texture:SetSize(piece.w,piece.h)
end
Expand Down
2 changes: 1 addition & 1 deletion Frostforge/Core/Core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ local addonName, J = ...
-- Keep the addon folder and saved-variable keys stable for existing installs.
J.Brand = { name="Jiberish's Frostforge", shortName="Frostforge", website="https://theigloo.io" }
local Core = {
version = "0.9.10",
version = "0.9.11",
modules = {}, clients = {}, owned = {}, notices = {},
order = { "minimap", "playerFrame", "targetFrame", "focusFrame", "actionHub" },
settingsOrder = { "minimap", "playerFrame", "targetFrame", "focusFrame", "targetTargetFrame", "partyFrames", "actionHub" },
Expand Down
2 changes: 1 addition & 1 deletion Frostforge/Core/SmallFrames.lua
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ function S:Paint(module,kind,id,config,layout)
module.assetOK=true
if kind=="border" then
local entry=J.UnitSkinCatalog.entries[id]
if layout then
if layout or module.id~=id then
local pieces=J.CastBars:FitPieces(entry,g.w,g.h,config.smallFrameWeight,config.smallFramePadding,g.mirror,config.unitFrameWidth,config.unitFrameHeight)
for name,p in pairs(pieces) do
local t=module.textures[name]
Expand Down
3 changes: 2 additions & 1 deletion Frostforge/Frostforge.toc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
## X-Website: https://theigloo.io
## Notes: Optional sculpted unit-frame skins and fantasy portrait surrounds with Blinkii, mMediaTag, ElvUI and EllesmereUI support, matching cast-bar borders, action hubs and minimap artwork. In-game validation pending.
## Author: Jiberish
## Version: 0.9.10
## Version: 0.9.11
## SavedVariables: JiberishUIDB
## SavedVariablesPerCharacter: JiberishUICharacterDB
## OptionalDeps: LibSharedMedia-3.0, Blizzard_UnitFrame, Blizzard_ActionBar, Blizzard_Minimap, Blizzard_GameMenu, Blinkiis_Portraits, ElvUI, ElvUI_mMediaTag, EllesmereUI, EllesmereUIUnitFrames, EllesmereUIRaidFrames, EllesmereUIResourceBars, EllesmereUIActionBars, EllesmereUIMinimap
Expand All @@ -20,6 +20,7 @@ Core\Hubs.lua
Themes\Minimaps.lua
Core\Minimaps.lua
Themes\UnitSkins.lua
Themes\CastBorders.lua
Core\UnitSkins.lua
Core\CastBars.lua
Core\SmallFrames.lua
Expand Down
53 changes: 53 additions & 0 deletions Frostforge/Themes/CastBorders.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
local _, J = ...
-- Generated by tools/measure_cast_borders.py; original artwork stays unchanged.
-- Transparent inset from each shared slice boundary to the painted rail.
local insets = {
CLASS_DEATHKNIGHT = {3,7,3,2},
CLASS_DEMONHUNTER = {9.5,1,9.5,2},
CLASS_DRUID = {5,12,11.5,3},
CLASS_EVOKER = {8,3,8,2},
CLASS_HUNTER = {2,1,2,1},
CLASS_MAGE = {2,5,3,4},
CLASS_MONK = {2,2,2,2},
CLASS_PALADIN = {10,1,10,1},
CLASS_PRIEST = {23,4,23,2},
CLASS_ROGUE = {8,3,8,1},
CLASS_SHAMAN = {21,4,21.5,2},
CLASS_WARLOCK = {4,8,4.5,1},
CLASS_WARRIOR = {8,2,8,2},
FACTION_ALLIANCE = {13,1,13,1},
FACTION_HORDE = {7.5,2,9,2},
FACTION_NEUTRAL = {7,2,7,2},
RACE_BLOODELF = {11,0,11,0},
RACE_DARKIRONDWARF = {15,0,15,0},
RACE_DRACTHYR = {8,3,8,3},
RACE_DRAENEI = {4.5,0,5,1},
RACE_DWARF = {12,1,12,1},
RACE_EARTHENDWARF = {5,2,5,2},
RACE_GNOME = {8,1,8,1},
RACE_GOBLIN = {7,1,7,2},
RACE_HARANIR = {12,3,12,3},
RACE_HIGHMOUNTAINTAUREN = {10,3,10,2},
RACE_HUMAN = {23,2,23,2},
RACE_KULTIRAN = {13,1,13,1},
RACE_LIGHTFORGEDDRAENEI = {3,3,3,2},
RACE_MAGHARORC = {10.5,3,10.5,2},
RACE_MECHAGNOME = {2,1,2,1},
RACE_NIGHTBORNE = {15,0,15,0},
RACE_NIGHTELF = {3,1,3,1},
RACE_ORC = {11,4,11,2},
RACE_PANDAREN = {4,3,3.5,1},
RACE_SCOURGE = {1,12,2,6},
RACE_TAUREN = {3,2,3,1},
RACE_TROLL = {16,1,15.5,1},
RACE_VOIDELF = {4,2,5,2},
RACE_VULPERA = {6,3,5.5,2},
RACE_WORGEN = {14,1,14,1},
RACE_ZANDALARITROLL = {6,2,7,1},
}
J.CastBorderFitLimits = {0,0}
for id, inset in pairs(insets) do
J.UnitSkinCatalog.entries[id].castInsets = inset
J.CastBorderFitLimits[1] = math.max(J.CastBorderFitLimits[1],inset[1]+inset[3])
J.CastBorderFitLimits[2] = math.max(J.CastBorderFitLimits[2],inset[2]+inset[4])
end
2 changes: 1 addition & 1 deletion artwork/cast-bars/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<script src="runtime-borders.js"></script><script>
const $=id=>document.getElementById(id),images=new Map();let revision=0;
function load(id){if(!images.has(id)){const image=new Image;image.src='assets/'+castBorders.themes[id].file+'.png';images.set(id,new Promise((resolve,reject)=>{image.onload=()=>resolve(image);image.onerror=reject}))}return images.get(id)}
function draw(canvas,image,theme,style,compact=false){canvas.width=780;canvas.height=compact?270:330;const c=canvas.getContext('2d'),w=180,h=16,pieces=Object.values(castBorders.fittings[$('borderWidth').value+'x'+$('borderHeight').value][style]);
function draw(canvas,image,theme,style,compact=false){canvas.width=780;canvas.height=compact?270:330;const c=canvas.getContext('2d'),w=180,h=16,pieces=Object.values(theme.fittings[$('borderWidth').value+'x'+$('borderHeight').value][style]);
const left=Math.min(0,...pieces.map(p=>p.x)),right=Math.max(w,...pieces.map(p=>p.x+p.w)),top=Math.min(0,...pieces.map(p=>p.y)),bottom=Math.max(h,...pieces.map(p=>p.y+p.h));
const zoom=Math.min(3,(canvas.width-32)/(right-left),(canvas.height-32)/(bottom-top));c.translate((canvas.width-zoom*(right+left))/2,(canvas.height-zoom*(bottom+top))/2);c.scale(zoom,zoom);
c.fillStyle='#101718';c.fillRect(0,0,w,h);const fill=c.createLinearGradient(0,0,0,h);fill.addColorStop(0,'#c7b368');fill.addColorStop(.35,'#a69550');fill.addColorStop(1,'#67582e');c.fillStyle=fill;c.fillRect(0,0,w*Number($('progress').value)/100,h);
Expand Down
2 changes: 1 addition & 1 deletion artwork/cast-bars/runtime-borders.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artwork/settings/runtime-settings.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.9.11

- Align all 42 castbar borders to the same painted opening so class, race and faction changes retain a consistent fit, including during combat. Also applies to the shared Party and Target of Target borders. Original artwork and saved fitting settings are preserved.

## 0.9.10

- Start Action Hubs at 900 × 240, screen anchored, with offsets 0 / -14, scale 1 and opacity 1. Saved custom placement is preserved.
Expand Down
67 changes: 58 additions & 9 deletions tests/cast_bars.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ for _,interface in ipairs({120100,16001}) do
end
end

test("all 42 Bold cast identities have empty centers and fixed fitting",function(M)
test("all 42 Bold cast identities share a registered opening and intact corners",function(M)
local J=M.load();local bar=fixture(M,"BLIZZARD","targetFrame");bar.shown=true
enable(J,"targetFrame","BLIZZARD")
local c=J.CastBars.units.targetFrame
Expand All @@ -58,16 +58,20 @@ test("all 42 Bold cast identities have empty centers and fixed fitting",function
assert(c.id==id and c.frame.w==180 and c.frame.h==16)
for _,texture in pairs(c.textures) do
assert(texture.path==entry.cast and texture.w>0 and texture.h>0)
local _,_,_,x,ny=texture:GetPoint();local y=-ny
assert(x+texture.w<=0 or x>=180 or y+texture.h<=0 or y>=16,"Border overlaps native fill")
local uv=texture.texCoord
assert(uv[1]>uv[2],"Target crop not mirrored")
for _,value in ipairs(uv) do assert(value>=0 and value<=1) end
end
for _,weight in ipairs({.5,2}) do
for _,padding in ipairs({0,8}) do
local pieces=J.CastBars:Pieces(entry,8,4,weight,padding,false)
for _,p in pairs(pieces) do assert(p.w>0 and p.h>0 and (p.x+p.w<=0 or p.x>=8 or p.y+p.h<=0 or p.y>=4)) end
for _,p in pairs(pieces) do assert(p.w>0 and p.h>0) end
local scale=pieces['11'].w/48
near(pieces['11'].h/48,scale)
near(pieces['21'].x+(48-entry.castInsets[1])*scale,-padding)
near(pieces['23'].x+entry.castInsets[3]*scale,8+padding)
near(pieces['12'].y+(48-entry.castInsets[2])*scale,-padding)
near(pieces['32'].y+entry.castInsets[4]*scale,4+padding)
end
end
end
Expand All @@ -84,8 +88,8 @@ test("cast theme matching follows identity and selections independently of other
assert(c.id=="RACE_DWARF")
M.combat=true;M.tick(J.Core);assert(c.id=="RACE_DWARF","Matching selection reverted in combat")
M.combat=false;J.ProfileManager:Set("targetFrame","portraitMode","CLASS")
M.combat=true;M.unitData.target.class="SHAMAN";local writes=M.geometryWrites;M.tick(J.Core)
assert(c.id=="CLASS_SHAMAN" and M.geometryWrites==writes)
M.combat=true;M.unitData.target.class="SHAMAN";local writes=M.frameGeometryWrites;M.tick(J.Core)
assert(c.id=="CLASS_SHAMAN" and M.frameGeometryWrites==writes)
c.frame.protected=true;M.unitData.target.class="MAGE";M.tick(J.Core);assert(c.id=="CLASS_SHAMAN")
M.combat=false;M.event(J.Core,"PLAYER_REGEN_ENABLED");assert(c.id=="CLASS_MAGE")
J.ProfileManager:Set("targetFrame","castBarArt","RACE_SCOURGE")
Expand All @@ -94,6 +98,48 @@ test("cast theme matching follows identity and selections independently of other
J.ProfileManager:Set("targetFrame","shown",true);assert(not c.frame.shown and J.Core.modules.targetFrame.frame.shown)
end)

for _,interface in ipairs({120100,16001}) do
for _,source in ipairs({"BLIZZARD","ELLESMERE","ELVUI"}) do
test(source.." cast fit stays registered through combat class swaps on "..interface,function(M)
local J=M.load({interface=interface});local bar=fixture(M,source,"targetFrame");bar.shown=true
enable(J,"targetFrame",source)
J.ProfileManager:SetMany({{"targetFrame","castBarWidth",125},{"targetFrame","castBarHeight",75}})
local c=J.CastBars.units.targetFrame
local frames,textures,writes=#M.frames,M.textures,M.frameGeometryWrites
M.combat=true
local cornerScale
for id,entry in pairs(J.UnitSkinCatalog.entries) do
if id:match("^CLASS_") then
M.unitData.target.class=id:sub(7);M.event(J.Core,"PLAYER_TARGET_CHANGED");M.tick(J.Core)
assert(c.id==id and c.frame.shown and c.frame.w==180 and c.frame.h==16)
local scale=c.textures['11'].w/48
cornerScale=cornerScale or scale;near(scale,cornerScale)
local function xy(name)
local _,relative,_,x,y=c.textures[name]:GetPoint()
assert(relative==c.frame and c.textures[name].path==entry.cast)
return x,-y
end
-- Target artwork is mirrored: source right is displayed left.
local left=xy('21');local right=xy('23')
local _,top=xy('12');local _,bottom=xy('32')
near(left+(48-entry.castInsets[3])*scale,-23.5)
near(right+entry.castInsets[1]*scale,203.5)
near(top+(48-entry.castInsets[2])*scale,1)
near(bottom+entry.castInsets[4]*scale,15)
end
end
assert(M.frameGeometryWrites==writes and #M.frames==frames and M.textures==textures)
local geometry=M.geometryWrites;M.tick(J.Core);assert(M.geometryWrites==geometry)
c.frame.protected=true;local previous=c.id
M.unitData.target.class=previous=="CLASS_MAGE" and "DRUID" or "MAGE"
M.tick(J.Core);assert(c.id==previous and M.geometryWrites==geometry)
M.combat=false;M.event(J.Core,"PLAYER_REGEN_ENABLED")
assert(c.id=="CLASS_"..M.unitData.target.class)
assert((M.nativeLayoutWrites or 0)==0)
end)
end
end

test("cast border appearance restores without touching providers and reuses owned frames",function(M)
local J=M.load();local bar=fixture(M,"ELVUI","playerFrame");bar.shown=true
enable(J,"playerFrame","ELVUI");local c=J.CastBars.units.playerFrame
Expand Down Expand Up @@ -216,10 +262,13 @@ test("cast width and height resize only centered artwork and persist independent
enable(J,"playerFrame","ELLESMERE");local c=J.CastBars.units.playerFrame
J.ProfileManager:Set("playerFrame","castBarWidth",125)
J.ProfileManager:Set("playerFrame","castBarHeight",150)
local inset=J.UnitSkinCatalog.entries[c.id].castInsets
local scale=c.textures['11'].w/48
local _,_,_,x,ny=c.textures['12']:GetPoint()
near(x,(180-225)/2-1);near(c.textures['12'].w,227)
near(x-inset[1]*scale,(180-225)/2-1)
near(c.textures['12'].w+(inset[1]+inset[3])*scale,227)
local _,_,_,_,bottomY=c.textures['32']:GetPoint()
near(-bottomY,(16-24)/2+24+1)
near(-bottomY+inset[4]*scale,(16-24)/2+24+1)
assert(c.frame.w==180 and c.frame.h==16 and bar.w==180 and bar.h==16)
assert(J.ThemeManager:Resolve("targetFrame").castBarWidth==100)
assert(J.ThemeManager:Resolve("focusFrame").castBarHeight==100)
Expand Down Expand Up @@ -272,7 +321,7 @@ test("retired cast styles migrate to Bold without losing fitting or artwork",fun
player=J.ThemeManager:Resolve("playerFrame")
assert(player.castBarStyle=="CAPPED" and player.castBarWidth==115)
end
local corner=J.CastBars:Pieces(nil,180,16,1,1,false)['11']
local corner=J.CastBars:Pieces(J.UnitSkinCatalog.entries.CLASS_MAGE,180,16,1,1,false)['11']
near(corner.w,30);near(corner.h,30) -- The previous Bold geometry.
end)

Expand Down
5 changes: 4 additions & 1 deletion tests/mock_wow.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ local function writable(self,geometry)
if self.native then M.nativeWrites=M.nativeWrites+1; error("Attempt to mutate Blizzard frame") end
if M.combat and self.protected then error("Protected artwork changed during combat") end
M.writes=M.writes+1
if geometry then M.geometryWrites=M.geometryWrites+1 end
if geometry then
M.geometryWrites=M.geometryWrites+1
if self.kind~="Texture" then M.frameGeometryWrites=(M.frameGeometryWrites or 0)+1 end
end
end
function methods:IsForbidden() return self.forbidden or false end
function methods:IsProtected() return self.protected or false end
Expand Down
23 changes: 19 additions & 4 deletions tests/small_frames.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ local function enable(J,key,source,portrait)
{key,"unitFrameShown",true},{key,"shown",portrait or false}}))
end
local function record(J,key,root,kind) return J.SmallFrames.groups[key][kind or "border"].records[root] end
local function registered(J,key,m)
local g,c=m.geometry,J.ThemeManager:Read(key)
local inset=J.UnitSkinCatalog.entries[m.id].castInsets
local scale=m.textures['11'].w/48
local w,h=g.w*c.unitFrameWidth/100,g.h*c.unitFrameHeight/100
local function xy(name) local _,_,_,x,y=m.textures[name]:GetPoint();return x,-y end
local left=xy('21');local right=xy('23');local _,top=xy('12');local _,bottom=xy('32')
near(left+(48-(g.mirror and inset[3] or inset[1]))*scale,(g.w-w)/2-c.smallFramePadding)
near(right+(g.mirror and inset[1] or inset[3])*scale,(g.w+w)/2+c.smallFramePadding)
near(top+(48-inset[2])*scale,(g.h-h)/2-c.smallFramePadding)
near(bottom+inset[4]*scale,(g.h+h)/2+c.smallFramePadding)
end

for _,interface in ipairs({120100,16001}) do
for _,source in ipairs({"BLIZZARD","ELVUI","ELLESMERE"}) do
Expand All @@ -65,10 +77,11 @@ for _,interface in ipairs({120100,16001}) do
for _,m in ipairs({first,second,portrait}) do
assert(m.frame.parent==UIParent and not m.frame.mouse and not m.frame.keyboard and not m.frame.wheel)
end
local writes=M.geometryWrites;M.combat=true
local writes=M.frameGeometryWrites;M.combat=true
roots[1].unit="party1";roots[2].unit="party2";M.event(J.Core,"GROUP_ROSTER_UPDATE")
assert(first.id=="CLASS_DRUID" and second.id=="CLASS_HUNTER" and portrait.id=="CLASS_HUNTER")
assert(writes==M.geometryWrites,"Sorting rewrote protected layout")
assert(writes==M.frameGeometryWrites,"Sorting rewrote protected layout")
registered(J,"partyFrames",first);registered(J,"partyFrames",second)
M.unitData.party1=nil;M.tick(J.Core)
assert(not first.frame.shown and first.id=="CLASS_DRUID","Empty slot flashed Neutral")
roots[2].shown=false;M.tick(J.Core);assert(not second.frame.shown and not portrait.frame.shown)
Expand All @@ -81,11 +94,12 @@ for _,interface in ipairs({120100,16001}) do
local m=record(J,"targetTargetFrame",root)
assert(m.id=="CLASS_MAGE" and m.frame.shown)
for _,t in pairs(m.textures) do assert(t.texCoord[1]>t.texCoord[2]) end
M.combat=true;local writes=M.geometryWrites
M.combat=true;local writes=M.frameGeometryWrites
M.unitData.targettarget={player=false};M.event(J.Core,"UNIT_TARGET","target")
assert(m.id=="FACTION_NEUTRAL")
M.unitData.targettarget={player=true,class="DRUID"};M.event(J.Core,"UNIT_TARGET","target")
assert(m.id=="CLASS_DRUID" and m.frame.shown and writes==M.geometryWrites)
assert(m.id=="CLASS_DRUID" and m.frame.shown and writes==M.frameGeometryWrites)
registered(J,"targetTargetFrame",m)
M.unitData.targettarget=nil;M.event(J.Core,"PLAYER_TARGET_CHANGED")
assert(not m.frame.shown and m.id=="CLASS_DRUID")
assert(not next(J.Core.notices))
Expand All @@ -99,6 +113,7 @@ test("compact artwork retains every race class faction design and independent to
for id,entry in pairs(J.UnitSkinCatalog.entries) do
assert(J.ProfileManager:Set("partyFrames","smallFrameArt",id));assert(m.id==id)
for _,t in pairs(m.textures) do assert(t.path==entry.cast and t.w>0 and t.h>0) end
registered(J,"partyFrames",m)
end
assert(portrait.id=="CLASS_HUNTER")
J.ProfileManager:Set("partyFrames","smallFrameArt","MATCH")
Expand Down
Loading
Loading