local root=(arg[1] or '.')..'/' local function Check(native) local env=setmetatable({},{__index=getfenv(0)});env._G=env local modules,frames,timers,slots,effects={},{},{},{},{} local now,keys,totemReads,effectReads,schoolReads=10,false,0,0,0 local methods={} local function object(kind,name,parent) local f=setmetatable({kind=kind,parent=parent,scripts={},events={},shown=true,writes=0}, {__index=function(_,key)return methods[key] or (string.find(key,'^[A-Z]') and function()end or nil)end}) table.insert(frames,f);if name then env[name]=f end;return f end function methods:RegisterEvent(name)self.events[name]=true end function methods:UnregisterAllEvents()self.events={}end function methods:SetScript(k,v)self.scripts[k]=v end function methods:Show()self.shown=true end function methods:Hide()self.shown=false end function methods:IsShown()return self.shown end function methods:CreateTexture()return object('Texture',nil,self)end function methods:CreateFontString()return object('FontString',nil,self)end function methods:SetTexture(path)self.path=path;self.writes=self.writes+1 end function methods:SetText(text)self.text=text;self.writes=self.writes+1 end function methods:SetTextColor(...)self.textColor=arg end function methods:SetStatusBarColor(...)self.color=arg end function methods:SetVertexColor(...)self.color=arg end function methods:SetWidth(n)self.width=n end function methods:SetStatusBarTexture(p)self.path=p end function methods:SetMinMaxValues()self.timer=nil end function methods:SetValue(v)self.value=v end function methods:SetTimerDuration(d,_,direction)self.timer,self.direction=d,direction end env.CreateFrame=object;env.UIParent=object('Frame') env.SlashCmdList={};env.ShaguTweaks_config={} env.GetTime=function()return now end env.UnitClass=function()return 'Shaman','SHAMAN' end env.GameTooltip={Hide=function()end,SetOwner=function()end,SetTotem=function()end} env.Enum={StatusBarInterpolation={Immediate=0},StatusBarTimerDirection={RemainingTime=1},NumericRuleFormatRounding={Down=2}} env.C_Timer={NewTimer=function(delay,fn) local t={at=now+delay,callback=fn};function t:Cancel()self.cancelled=true end table.insert(timers,t);return t end} env.C_DurationUtil={CreateDuration=function() local d={finish=0,total=0} function d:SetTimeFromEnd(finish,total)self.finish,self.total=finish,total end function d:GetRemainingDuration()return math.max(0,self.finish-now)end return d end,CreateDurationTextBinding=function() local b={} function b:SetFontString(text)self.text=text end function b:SetDuration(d)self.duration=d end function b:SetFormatter(f)self.formatter=f end function b:SetExpiredText(t)self.expired=t end function b:SetUpdateInterval(t)self.interval=t end function b:Enable()self.enabled=true end function b:Disable()self.enabled=false end return b end} env.C_StringUtil={CreateNumericRuleFormatter=function()return {SetBreakpoints=function(self,rules)self.rules=rules end}end} env.GetTotemInfo=function(slot) local data=slots[slot] if data then return true,'Totem',data.start,data.total,'icon' end return false,'',0,0,'' end env.GetTotemTimeLeft=function(slot)totemReads=totemReads+1;local s=slots[slot];return s and math.max(0,s.start+s.total-now)or 0 end env.GetTotemDuration=function(slot) local d=env.C_DurationUtil.CreateDuration();local s=slots[slot] if s then d:SetTimeFromEnd(s.start+s.total,s.total)end return d end env.C_LossOfControl={GetActiveLossOfControlDataCount=function()effectReads=effectReads+1;return table.getn(effects)end, GetActiveLossOfControlData=function(i) local e=effects[i] return {locType=e.kind,timeRemaining=e.finish and math.max(0,e.finish-now),duration=e.total, displayText=e.label or e.kind,lockoutSchool=e.mask or 0,iconTexture='effect'} end,GetSchoolLockout=function() schoolReads=schoolReads+1;local mask,left=0,0 for _,e in ipairs(effects)do if e.kind=='SCHOOL_INTERRUPT' and e.finish>now then mask=e.mask;left=math.max(left,e.finish-now)end end return mask,mask~=0 and left or nil end} env.C_Spell={GetSchoolString=function()return 'Nature' end} env.ShaguTweaks={T=setmetatable({},{__index=function(_,k)return k end}), API={durationtimers=native,AreMoveModifiersDown=function()return keys end},GetGlobalEnv=function()return env end} function env.ShaguTweaks:register(m)modules[m.title]=m;return m end local function load(path)local f=assert(loadfile(root..path));setfenv(f,env);f()end local function emit(name) env.event=name for _,f in ipairs(frames)do if f.events[name]then env.this=f;f.scripts.OnEvent()end end env.this=nil end local function advance(delta) local finish=now+delta for iteration=1,1000 do local nextTimer for _,t in ipairs(timers)do if not t.cancelled and not t.fired and t.at<=finish and (not nextTimer or t.at=candidate.threshold then rule=candidate end end local function rounded(v,step,mode) if not step then return v end return (mode==2 and math.floor(v/step) or math.floor(v/step+.5))*step end n=rounded(n,rule.step,rule.rounding) if rule.components then local values={} for _,part in ipairs(rule.components)do local v=n;if part.div then v=v/part.div end;if part.mod then v=math.mod(v,part.mod)end table.insert(values,rounded(v,part.step,part.rounding)) end return string.format(rule.format,unpack(values)) end return string.format(rule.format,n) end if native then assert(not holder.scripts.OnUpdate and totem.binding.enabled and totem.bar.timer.total==30 and totem.bar.direction==1) assert(totem.binding.formatter==env.ShaguTweaksTotem3.binding.formatter) assert(totem.binding.formatter.rules[4].format=='|cffffffff%d:%02d') assert(formatted(totem.binding.formatter,5)=='|cffff33335') assert(formatted(totem.binding.formatter,5.001)=='|cffffe6335') assert(formatted(totem.binding.formatter,10)=='|cffffe63310') assert(formatted(totem.binding.formatter,10.001)=='|cffffffff10') assert(formatted(totem.binding.formatter,61.9)=='|cffffffff1:01') local original=totem.expiry;slots[1]={start=10,total=60};emit('PLAYER_TOTEM_UPDATE') assert(original.cancelled and totem.bar.timer.finish==70) local oldTimer=totem.expiry;keys=true;emit('MODIFIER_STATE_CHANGED') assert(oldTimer.cancelled and not totem.binding.enabled and totem.bar.value==1) keys=false;emit('MODIFIER_STATE_CHANGED');assert(totem.binding.enabled) local before=totemReads;advance(61);assert(totemReads==before and not holder.shown) assert(not totem.binding.enabled and not totem.shown) slots[2]={start=now,total=30};emit('PLAYER_TOTEM_UPDATE') local earth=env.ShaguTweaksTotem2;local pending=earth.expiry modules['Totem Timers']:disable();assert(pending.cancelled and not earth.binding.enabled) modules['Totem Timers']:enable();assert(earth.binding.enabled) else assert(holder.scripts.OnUpdate and not totem.binding) assert(totem.time.text=='29') now=36;env.arg1=.1;holder.scripts.OnUpdate();assert(totem.time.text=='3' and totem.time.textColor[2]==.2) end modules['Totem Timers']:disable() effects={{kind='ROOT',finish=now+20,total=20},{kind='STUN',finish=now+4,total=4}} load('mods/loss-of-control.lua');modules['Loss of Control Alert']:enable() local alert=env.ShaguTweaksLossOfControl assert(alert.title.text=='Stunned') if native then assert(not alert.scripts.OnUpdate and alert.bar.timer.total==4) local before=effectReads;local writes=alert.icon.writes advance(2);assert(effectReads==before and alert.icon.writes==writes,'native animation must not rescan/repaint') advance(2.1);assert(alert.title.text=='Rooted' and alert.shown) effects={};emit('LOSS_OF_CONTROL_UPDATE');assert(not alert.shown) effects={{kind='STUN'}};emit('LOSS_OF_CONTROL_UPDATE') assert(alert.shown and alert.time.text=='' and not alert.bar.shown) keys=true;emit('MODIFIER_STATE_CHANGED');assert(alert.time.text=='8.0' and alert.bar.value==1) keys=false;emit('MODIFIER_STATE_CHANGED');assert(alert.time.text=='' and alert.shown) effects={{kind='SCHOOL_INTERRUPT',mask=8,finish=now+6,total=6}} emit('LOSS_OF_CONTROL_UPDATE');local listReads=effectReads;advance(.4) assert(effectReads==listReads and schoolReads>0,'steady lockouts use scalar queries only') local oldEnd=alert.bar.timer.finish effects[1].finish=now+10;effects[1].total=10 advance(.3);assert(alert.bar.timer.finish>oldEnd and effectReads>listReads) local scalarQuery=env.C_LossOfControl.GetSchoolLockout env.C_LossOfControl.GetSchoolLockout=nil listReads=effectReads;advance(.3);assert(effectReads>listReads,'older scalar-less API must still detect extensions') env.C_LossOfControl.GetSchoolLockout=scalarQuery advance(6);assert(alert.shown,'old expiry must not hide the extended lockout') local beforeDisable=effectReads modules['Loss of Control Alert']:disable();advance(20) assert(not alert.shown and effectReads==beforeDisable) modules['Loss of Control Alert']:enable();assert(not alert.shown) -- A higher-priority newly applied effect replaces the lockout immediately. effects={{kind='ROOT',finish=now+10,total=10}};emit('LOSS_OF_CONTROL_UPDATE') table.insert(effects,{kind='FEAR',finish=now+3,total=3});emit('LOSS_OF_CONTROL_ADDED') assert(alert.title.text=='Feared');advance(3.1);assert(alert.title.text=='Rooted') else assert(alert.scripts.OnUpdate and alert.time.text=='4.0') env.arg1=.05;alert.scripts.OnUpdate();assert(alert.title.text=='Stunned') end modules['Loss of Control Alert']:disable() end Check(true);Check(false) print('Control timers: native totem fill/text/expiry, replacements, preview/disable cleanup, loss-of-control priority/untimed effects, school extension scalar checks and older API fallback passed')