diff --git a/CHANGELOG.md b/CHANGELOG.md index 702e6c2..6e1e5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,38 @@ All notable changes to ShaguDPS for WoW 1.18.1 clients. Release notes with install steps and validation details for 4.5 and 4.6 are in [docs/](docs/). -## Unreleased +## 4.6.2 + +### Fixed + +- ClassicAPI pet ownership cache now checks each event's current flags. Demons + that turn hostile stop contributing to the former owner immediately, and + regained control is recognized without waiting for cache expiry. Track All + records hostile creatures separately. Includes captured Infernal control-loss + flag regression cases; Nampower/chat behavior is unchanged. + +### Added + +- Options window follows ShaguTweaks Darkened/Golden UI through its optional + recolor function, applied after controls are built whenever the window opens. +- Separate **Fights** (latest 15 boss kills) and **Wipes** (latest 5 boss wipes) + submenus, with independent retention limits and automatic separation of existing + history. Reset Data clears both lists; saved selections retain their IDs. +- Separate opt-in **Smooth Damage / Heal Bars** and **Smooth Threat Bars** settings. + Uses ClassicAPI native interpolation when available, with immediate bars otherwise. + Labels and meter data stay immediate; dispel bars remain immediate. +- Lua 5.0 regression coverage for smoothing fallback, independent toggles, recycled + rows, segment/target changes, and warlock DoT, demon and guardian attribution. +- ClassicAPI 1.20.4 validation checklist in `docs/validation-4.6.2.md`. + +### Changed + +- Merged pet damage/healing now preserves each ability in the owner's breakdown + (for example `Pet: Gratal - Firebolt`). Labels are cached per pet and ability, + avoiding repeated string construction on combat events. Previously recorded + combined pet entries remain combined; reset data for a clean comparison. +- Reassigned rows and changed views, segments or threat targets snap to their new + values instead of animating from unrelated data. Both healing layers ease together. ### Documentation diff --git a/README.md b/README.md index f9628f9..01d8a51 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,16 @@ overhealing to heal lines and name the boss, kill or wipe and the fight length f ### Segments and boss fight history -The segment menu reads *Current*, *Fights* and *Overall*. +The segment menu reads *Current*, *Fights*, *Wipes* and *Overall*. -- *Fights* opens the last 10 boss fights, newest first, with their length. Wipes are greyed out. +- *Fights* opens the last 15 boss kills, newest first, with their length. +- *Wipes*, below Fights, opens the last 5 boss wipes. Each list has its own limit, + so wipes cannot evict kills. Existing history is separated automatically. - A fight counts as a boss fight when someone in the group targeted a raid boss (boss classification or skull level) while it ran. Trash fights are not kept. - The history is saved per character and survives reloads and logouts. A raid boss fight - takes roughly 50 KB, so ten of them stay under 1 MB. *Reset Data* clears it. + takes roughly 50 KB; up to 20 saved attempts use roughly 1 MB at that size. + Actual size varies with fight data. *Reset Data* clears both lists. - With ClassicAPI, boss deaths are detected by GUID; otherwise the chat death message is used. ### Threat @@ -116,9 +119,16 @@ of it the unit was active. and *Blizzard Retail (Modern)* (`/sdps texture 8`) with modern palette colors for all nine classes. - **Class icons** beside player names in Damage, Heal and Threat, scaled with the bar height. - **Lock button** next to the + button locks or unlocks moving and resizing of all windows. +- **Native bar smoothing:** separate *Smooth Damage / Heal Bars* and *Smooth Threat Bars* + options, both off by default. Requires ClassicAPI's status-bar interpolation feature; + other clients keep immediate bars. Options take effect without a reload. Only the + fills animate: numbers, ranking and recorded totals update immediately. Dispel bars + stay immediate, and rows snap when reassigned or switched to another segment/target. - New windows start with the size and view of the first window. - Long names no longer run into the numbers, and unmerged pets show as "Owner - Pet". - The settings window closes with Escape. +- The settings window follows ShaguTweaks Darkened/Golden UI when its recolor + function is available, including controls built on first opening. ### Performance @@ -139,6 +149,12 @@ settings show which source is in use and why a forced one isn't available. Pets and totems of group members are matched to their owner through the GUID (`UnitOwnerGUID` with ClassicAPI, the owner token with SuperWoW). +With pet merging enabled, the owner's breakdown keeps separate pet abilities, +such as `Pet: Gratal - Firebolt`. Existing combined pet entries remain as recorded; +reset data to start a clean breakdown after updating. Pet/ability labels are cached. +With ClassicAPI, current combat-event flags invalidate cached ownership when a +demon turns hostile or returns to your control. Hostile damage is not credited to +the former owner; Track All can still show the creature separately. ### Healing and overhealing @@ -204,6 +220,9 @@ reset it back to `40`. ## Diagnostics +The [4.6.2 validation checklist](docs/validation-4.6.2.md) covers warlock damage/pets, +healing accuracy, native smoothing, and a repeatable in-game performance comparison. + - `/sdps threatdebug` counts threat requests, replies and changed replies for ten seconds. - `/sdps perf` measures ShaguDPS for ten seconds. Memory is reported in KB with its change; garbage collection can change that value. CPU results need a ClassicAPI build with addon diff --git a/ShaguDPS.toc b/ShaguDPS.toc index d7c752c..e675da2 100644 --- a/ShaguDPS.toc +++ b/ShaguDPS.toc @@ -2,7 +2,7 @@ ## Title: |cffffcc00Shagu|cffffffffDPS ## Author: Shagu; Duciel (Nampower parser); OEVG2 (4.0) ## Notes: A simple and lightweight DPS and damage meter. Reads ClassicAPI or Nampower combat events when available, the chat combat log otherwise. -## Version: 4.6.1 +## Version: 4.6.2 ## OptionalDeps: pfUI ## SavedVariablesPerCharacter: ShaguDPS_Config, ShaguDPS_Fights diff --git a/core.lua b/core.lua index 55d3eba..228e602 100644 --- a/core.lua +++ b/core.lua @@ -91,6 +91,8 @@ local config = { texture = 2, pastel = 0, class_icons = 1, + smooth_bars = 0, + smooth_threat = 0, lock = 0, theme = 1, -- 1: classic Blizzard look, 0: original flat look } diff --git a/docs/validation-4.6.2.md b/docs/validation-4.6.2.md new file mode 100644 index 0000000..5b2dfd8 --- /dev/null +++ b/docs/validation-4.6.2.md @@ -0,0 +1,84 @@ +# ShaguDPS 4.6.2 validation (ClassicAPI 1.20.4) + +## Automated checks + +Run each `tests/*.lua` file from the repository root using Lua 5.0. The +`threat-benchmark.lua` script requires `0.5` as its first argument. + +The regression scenarios cover direct and periodic damage, demon and guardian +ownership, merged and separate pets, exclusion of outsiders, effective healing +with positive/zero/invalid overhealing, unavailable target visibility, dispels, +fight history, threat, display caching and the profiling command. Smoothing tests +cover both toggles, both healing layers, unsupported/partial APIs, row reuse, +view/segment/target changes and forced refreshes. + +These use mocked WoW APIs and synthetic events. They cannot verify what the +server sends, real rendering, native animation cost, or actual raid accuracy. + +## In-game accuracy check (OEVG) + +1. Select ClassicAPI as the data source and reset meter data. Enable `/combatlog` + for a short controlled fight; disable it afterwards. ClassicAPI also writes + structured events to `Logs/WoWCombatLog-MMDDYY_HHMMSS.txt`. +2. Cast Corruption alone, then Shadow Bolt. Compare your spell breakdown totals + with damage event amounts in the structured log for your source GUID. +3. Repeat with an imp attacking. With Merge Pets enabled, Firebolt should appear + under your pet contribution. Reset and repeat with merging off: the imp gets + a separate row. Test an Infernal or other guardian if available. + For a summoned demon that turns hostile, verify the pet contribution stops + at control loss, including its first hostile hit and Immolation ticks. Your + own damage against the hostile demon still counts. If control is regained, + pet attribution should resume immediately. Start logging before summoning and + reset data before each attempt; old incorrect totals are not rewritten. +4. Compare successful direct and periodic heals against a group member with known + missing health, then a full-health member. Compare raw totals and effective + healing (amount minus overhealing where valid). Out-of-view or percentage-only + health can require an estimate; the client log is not server-authoritative. +5. End combat and verify Current, Overall and a saved boss fight separately. + Use fresh resets between merge-mode comparisons so earlier totals don't mix. +6. Check the segment menu: Fights lists boss kills and Wipes beneath it lists failed + attempts, newest first. Select entries in Damage, Heal and Dispels; reload and + verify both histories persist. Reset Data should clear both. Limits are 15 kills + and 5 wipes independently; older mixed history is separated automatically. + +## Smoothing and compatibility (OEVG) + +1. Open `/sdps config`. Both smoothing toggles start off. Turn on only damage/heal + smoothing; confirm Damage and Heal fills ease while Threat remains immediate. +2. Turn on only threat smoothing; verify the independent behavior. Toggle both + off while fills are moving: the next forced repaint snaps to current values. +3. Confirm healing's effective and raw-healing layers ease together and labels + still show current values. Dispels remain immediate. +4. Change ranking, scroll, switch views/segments, hide/show a window and switch + between threat targets (including same-name mobs). New identities should snap. +5. Check multiple windows and both Classic/Flat themes, including option text fit. +6. Without ClassicAPI interpolation, the addon must retain ordinary immediate bars + with either toggle enabled. No Lua animation loop is installed. + +## In-game performance comparison (OEVG) + +1. Run `/console scriptProfile 1`, then `/reload`. +2. With a fixed number of visible windows/rows and the same source/time settings, + run `/sdps perf` during ten seconds of a busy fight. Record CPU ms/s and memory + with both smoothing toggles off; repeat in comparable combat with each toggle + on separately and then both on. Repeat samples if combat load differs greatly. +3. Inspect GentleGC freeze reports and visible frame pacing too. Native rendering + cost may not appear in addon Lua CPU; memory deltas also reflect collection. +4. Run `/console scriptProfile 0`, then `/reload` when finished. + +No in-game performance gain or accuracy result is claimed by these offline tests. +ClassicAPI remains optional. No changes are needed in OEVG's ShaguTweaks options: +these settings affect only ShaguDPS's own status bars. + +## In-game results and remaining checks (November 6, 2026) + +- Imp tests matched logged player and pet damage exactly, both merged and separate. +- After the ownership-cache fix, the Infernal test excluded all 2,494 logged + damage against the player after control loss. Player spells and friendly melee + matched. One friendly 40-damage Immolation hit was absent: the meter showed + 19,737 versus 19,777 expected from player damage plus friendly pet events. + This discrepancy remains unresolved; do not claim complete damage accuracy. +- The summon-time Inferno Effect was logged with hostile NPC flags before pet + control and is excluded by the current group-source rules. +- Native smoothing performance, 15-kill/5-wipe menus and the options-window + recolor still need in-game confirmation beyond automated regression checks. diff --git a/fights.lua b/fights.lua index f30df28..bbb2385 100644 --- a/fights.lua +++ b/fights.lua @@ -8,7 +8,7 @@ local parser = ShaguDPS.parser local data = ShaguDPS.data -local MAX_FIGHTS = 10 +local MAX_FIGHTS, MAX_WIPES = 15, 5 local fights = CreateFrame("Frame") ShaguDPS.fights = fights @@ -17,6 +17,20 @@ ShaguDPS.fights = fights fights.list = {} fights.next_id = 1 +-- Keep the existing saved list/IDs, with independent retention per result. +fights.Trim = function(self) + local kills, wipes, i = 0, 0, 1 + while i <= table.getn(self.list) do + local fight = self.list[i] + if fight.kill then kills = kills + 1 else wipes = wipes + 1 end + if (fight.kill and kills > MAX_FIGHTS) or (not fight.kill and wipes > MAX_WIPES) then + table.remove(self.list, i) + else + i = i + 1 + end + end +end + local boss, boss_dead, boss_guid local dispels_recorded local structured_deaths = CombatLogGetCurrentEventInfo and UnitGUID @@ -78,7 +92,7 @@ fights.Get = function(self, id) end fights.Reset = function(self) - for k in pairs(self.list) do self.list[k] = nil end + while table.getn(self.list) > 0 do table.remove(self.list) end end -- the fight shown by a window: segment 2 with config.fight = id @@ -119,9 +133,7 @@ parser.OnFightEnd = function(duration) fights.next_id = fights.next_id + 1 table.insert(fights.list, 1, fight) - while table.getn(fights.list) > MAX_FIGHTS do - table.remove(fights.list) - end + fights:Trim() boss, boss_dead, boss_guid = nil, nil, nil end @@ -152,6 +164,7 @@ fights:SetScript("OnEvent", function() for _, fight in pairs(fights.list) do if fight.id >= fights.next_id then fights.next_id = fight.id + 1 end end + fights:Trim() elseif event == "COMBAT_LOG_EVENT_UNFILTERED" then -- Death events often have no source; match the destination GUID directly. if boss_guid and arg2 == "UNIT_DIED" and arg8 == boss_guid then boss_dead = true end diff --git a/parser-native.lua b/parser-native.lua index ff363b7..beaab17 100644 --- a/parser-native.lua +++ b/parser-native.lua @@ -77,20 +77,21 @@ end -- every hit. Entries expire so group changes are picked up. local cache, cache_size, cache_trackall = {}, 0, nil -local function CacheGet(guid) +local function CacheGet(guid, flags) -- toggling "track all units" changes what is tracked if cache_trackall ~= config.track_all_units then cache, cache_size, cache_trackall = {}, 0, config.track_all_units end local entry = cache[guid] - if entry and entry.expire > GetTime() then return true, entry.name end + if entry and entry.expire > GetTime() and (not flags or entry.flags == flags) then return true, entry.name end end -local function CacheSet(guid, name, lifetime) +local function CacheSet(guid, name, lifetime, flags) if cache_size > 2000 then cache, cache_size = {}, 0 end if not cache[guid] then cache_size = cache_size + 1 end cache[guid] = cache[guid] or {} + cache[guid].flags = flags cache[guid].name = name cache[guid].expire = GetTime() + lifetime return name @@ -122,31 +123,33 @@ local TYPE_PLAYER, TYPE_PET, TYPE_GUARDIAN = 1024, 4096, 8192 local function CLEUSource(guid, name, flags) if not guid or not name or not flags or flags < 0 then return nil end - local found, label = CacheGet(guid) + -- Event flags are current even when the GUID's cached owner is not. A + -- control/reaction/type change must invalidate both positive and negative hits. + local found, label = CacheGet(guid, flags) if found then return label end local grouped = hasbit(flags, 1) or hasbit(flags, 2) or hasbit(flags, 4) if hasbit(flags, TYPE_PLAYER) then if grouped and Tracked(name) then - return CacheSet(guid, name, 60) + return CacheSet(guid, name, 60, flags) elseif config.track_all_units == 1 then - return CacheSet(guid, name, 60) + return CacheSet(guid, name, 60, flags) end - return CacheSet(guid, false, 10) + return CacheSet(guid, false, 10, flags) end - if hasbit(flags, TYPE_PET) or hasbit(flags, TYPE_GUARDIAN) then + if (hasbit(flags, TYPE_PET) or hasbit(flags, TYPE_GUARDIAN)) and not hasbit(flags, 64) then local owner = hasbit(flags, 1) and UnitName("player") or nil local label = PetLabel(guid, name, owner) - if label then return CacheSet(guid, label, 30) end + if label then return CacheSet(guid, label, 30, flags) end end if config.track_all_units == 1 and name ~= UNKNOWNOBJECT then - return CacheSet(guid, name, 30) + return CacheSet(guid, name, 30, flags) end - return CacheSet(guid, false, 10) + return CacheSet(guid, false, 10, flags) end -- arg12 onward depend on the subevent (see the ClassicAPI documentation) diff --git a/parser.lua b/parser.lua index 0774e90..70053b0 100644 --- a/parser.lua +++ b/parser.lua @@ -54,15 +54,21 @@ local function trim(str) return clean end --- "Wolf (Hunter)" from the GUID-based sources is shown as "Pet: Wolf" +-- Cache labels per pet and spell; repeated hits only perform table lookups. local pet_actions = {} -local function PetAction(source) - local action = pet_actions[source] - if not action then - action = "Pet: " .. gsub(source, "%s%(.-%)$", "") - pet_actions[source] = action +local function PetAction(source, action) + local pet = pet_actions[source] + if not pet then + pet = { prefix = "Pet: " .. gsub(source, "%s%(.-%)$", "") .. " - ", spells = {} } + pet_actions[source] = pet end - return action + action = action or UNKNOWN or "Unknown" + local label = pet.spells[action] + if not label then + label = pet.prefix .. action + pet.spells[action] = label + end + return label end local function combat() @@ -335,7 +341,7 @@ parser.AddData = function(self, source, action, target, value, school, datatype, -- remove pet data entry[source] = nil - action = PetAction(source) + action = PetAction(source, action) source = data["classes"][source] -- write data into owner diff --git a/settings.lua b/settings.lua index ae8a50f..b66a48b 100644 --- a/settings.lua +++ b/settings.lua @@ -324,6 +324,8 @@ local function Build() settings:CreateConfig("Bar Height", "height", "number") settings:CreateConfig("Bar Spacing", "spacing", "number") settings:CreateConfig("Show Class Icons", "class_icons", "boolean") + settings:CreateConfig("Smooth Damage / Heal Bars", "smooth_bars", "boolean") + settings:CreateConfig("Smooth Threat Bars", "smooth_threat", "boolean") settings:CreateConfig("Pastel Colors", "pastel", "boolean") settings:CreateConfig("Show Backdrops", "backdrop", "boolean") settings:CreateConfig("Lock Windows", "lock", "boolean") @@ -372,6 +374,11 @@ settings:SetScript("OnShow", function() this.built = true Build() end + -- ShaguTweaks owns its palette and texture exclusions. Recolor only when + -- opening, after lazy-built controls exist; no polling or hard dependency. + if ShaguTweaks and type(ShaguTweaks.DarkenFrame) == "function" then + ShaguTweaks.DarkenFrame(this) + end end) -- close with escape diff --git a/tests/fights.lua b/tests/fights.lua index fa8a75c..4d63e2d 100644 --- a/tests/fights.lua +++ b/tests/fights.lua @@ -105,3 +105,29 @@ fights.scripts.OnEvent() parser.OnFightEnd(10) assert(fights.list[1].kill, 'chat fallback when boss GUID cannot be resolved') print('Fight history regression checks passed (Lua 5.0)') + +-- Existing mixed saved history migrates without changing IDs or snapshots. +local saved = {} +for id = 40, 1, -1 do + table.insert(saved, {id=id,kill=math.mod(id,2)==0,name='Boss '..id,classes={},damage={},heal={}}) +end +ShaguDPS_Fights=saved +event='VARIABLES_LOADED' +fights.scripts.OnEvent() +local kills,wipes=0,0 +for _, fight in ipairs(fights.list) do + if fight.kill then kills=kills+1 else wipes=wipes+1 end +end +assert(kills==15 and wipes==5 and table.getn(fights.list)==20,'independent history limits') +assert(fights.list==saved and fights.next_id==41,'preserve saved table and monotonic IDs') +assert(fights:Get(40) and fights:Get(12) and not fights:Get(10),'newest fifteen kills retained') +assert(fights:Get(39) and fights:Get(31) and not fights:Get(29),'newest five wipes retained') +table.insert(fights.list,1,{id=41,kill=false}) +fights:Trim() +assert(fights:Get(12) and not fights:Get(31),'new wipe cannot evict a kill') +table.insert(fights.list,1,{id=42,kill=true}) +fights:Trim() +assert(fights:Get(41) and not fights:Get(12),'new kill cannot evict a wipe') +fights:Reset() +assert(table.getn(saved)==0,'reset clears kills and wipes') +print('15 kill / 5 wipe retention, migration and reset checks passed (Lua 5.0)') diff --git a/tests/history-menu.lua b/tests/history-menu.lua new file mode 100644 index 0000000..35f12fc --- /dev/null +++ b/tests/history-menu.lua @@ -0,0 +1,35 @@ +-- Exercise the actual submenu filtering and reuse without the WoW renderer. +local file=assert(io.open('window.lua')) +local source=file:read('*a');file:close() +local first=assert(string.find(source,' frame.ToggleFightList = function',1,true)) +local last=assert(string.find(source,' frame.btnResize =',first,true)) +local function text() + return {SetText=function(self,value) self.value=value end} +end +local list={entries={},fights={},shown=false, + IsShown=function(self) return self.shown end, + Show=function(self) self.shown=true end,Hide=function(self) self.shown=false end, + ClearAllPoints=function() end,SetPoint=function(self,point,anchor) self.anchor=anchor end, + SetHeight=function(self,value) self.height=value end, + SetBackdrop=function() end,SetBackdropColor=function() end,SetBackdropBorderColor=function() end} +for i=1,20 do list.entries[i]={caption=text(),time=text(),Show=list.Show,Hide=list.Hide} end +local frame={fightList=list,btnFights={},btnWipes={},GetID=function() return 1 end} +local config={[1]={segment=1}} +ShaguDPS={fights={list={ + {id=3,kill=true,name='Kill',duration=60}, + {id=2,kill=false,name='Wipe',duration=30}, + {id=1,kill=true,name='Older kill',duration=90}, +}}} +assert(loadstring('return function(frame,config)\n'..string.sub(source,first,last-1)..'\nend'))()(frame,config) +frame:ToggleFightList(false) +assert(list.shown and list.anchor==frame.btnFights and list.entries[1].id==3 and list.entries[2].id==1) +assert(not list.entries[3].shown,'kill submenu excludes wipes') +local scratch=list.fights +frame:ToggleFightList(true) +assert(list.shown and list.anchor==frame.btnWipes and list.entries[1].id==2) +assert(not list.entries[2].shown and list.fights==scratch,'wipe submenu filters and reuses storage') +frame:ToggleFightList(true);assert(not list.shown,'same submenu toggles closed') +ShaguDPS.fights.list={} +frame:ToggleFightList(true) +assert(list.entries[1].id==nil and string.find(list.entries[1].caption.value,'No boss wipes',1,true)) +print('Separate Fights/Wipes submenu filtering and empty-state checks passed (Lua 5.0)') diff --git a/tests/smoothing.lua b/tests/smoothing.lua new file mode 100644 index 0000000..20575d5 --- /dev/null +++ b/tests/smoothing.lua @@ -0,0 +1,44 @@ +ShaguDPS = { expansion=function() return 'vanilla' end, window={}, config={} } +local file=assert(io.open('window.lua')) +local source=file:read('*a');file:close() +local boundary=assert(string.find(source,'-- time used for',1,true)) +assert(loadstring(string.sub(source,1,boundary-1)))() +local config=ShaguDPS.config +local function bar() + return { + SetMinMaxValues=function(self,min,max,mode) self.maximum,self.rangeMode=max,mode end, + SetValue=function(self,value,mode) self.value,self.mode=value,mode end, + GetInterpolatedValue=function() end,SetToTargetValue=function() end, + } +end +local row=bar();row.lowerBar=bar() +local segment={} +local function update(name,view,target,force) + ShaguDPS.window.SetBarValues(row,name or 'Warlock',segment,view or 1,target,1000,400,1000,600,force) +end +config.smooth_bars,config.smooth_threat=1,0 +update() +assert(row.mode==nil,'plain client gets vanilla argument lists') +Enum={StatusBarInterpolation={ExponentialEaseOut=1}} +update() +assert(row.mode==0,'new row snaps') +row.wasShown=true +update() +assert(row.mode==1 and row.rangeMode==1 and row.lowerBar.mode==1,'both healing layers ease together') +update('Mage') +assert(row.mode==0,'reordered rows snap') +update('Mage');assert(row.mode==1) +segment={};update('Mage');assert(row.mode==0,'new segments snap') +update('Mage',3);assert(row.mode==0,'view changes snap') +update('Mage',3);assert(row.mode==1,'healing smoothing enabled') +update('Mage',5,'boss1');update('Mage',5,'boss1') +assert(row.mode==0,'threat defaults to immediate independently') +config.smooth_threat=1;config.smooth_bars=0 +update('Mage',5,'boss1');assert(row.mode==1) +update('Mage',5,'boss2');assert(row.mode==0,'target switches snap') +update('Mage',5,'boss2',true);assert(row.mode==0,'forced refresh snaps') +update('Mage',1);update('Mage',1);assert(row.mode==0,'damage toggle independent') +update('Mage',6);update('Mage',6);assert(row.mode==0,'dispels remain immediate') +row.wasShown=false;update('Mage',5,'boss2');assert(row.mode==0,'hidden rows snap on reuse') +row.GetInterpolatedValue=nil;update();assert(row.mode==nil,'partial API falls back') +print('Native smoothing toggles, row reuse and fallback checks passed (Lua 5.0)') diff --git a/tests/warlock-accuracy.lua b/tests/warlock-accuracy.lua new file mode 100644 index 0000000..14a6df2 --- /dev/null +++ b/tests/warlock-accuracy.lua @@ -0,0 +1,69 @@ +-- Extend the real parser/storage compatibility checks with warlock events. +dofile('tests/classicapi-updates.lua') +local oldName=UnitName +UnitName=function(unit) + if unit=='raid3' or unit=='0xWARLOCK' then return 'Warlock' end + if unit=='0xIMP' then return 'Imp' end + if unit=='0xGUARDIAN' then return 'Infernal' end + return oldName(unit) +end +UnitOwnerGUID=function(unit) + if unit=='0xIMP' or unit=='0xGUARDIAN' then return '0xWARLOCK' end +end +local p,d=ShaguDPS.parser,ShaguDPS.data +dofile('parser-native.lua') +p.sources.classicapi.enable() +local function damage(kind,guid,name,flags,spell,amount) + arg2,arg3,arg4,arg5,arg6,arg8,arg9=kind,false,guid,name,flags,'0xENEMY','Enemy' + arg12,arg13,arg14,arg15=172,spell,32,amount + p.OnEvent() +end +damage('SPELL_PERIODIC_DAMAGE','0xWARLOCK','Warlock',1028,'Corruption',120) +damage('SPELL_PERIODIC_DAMAGE','0xWARLOCK','Warlock',1028,'Corruption',120) +damage('SPELL_DAMAGE','0xWARLOCK','Warlock',1028,'Shadow Bolt',500) +assert(d.damage[0].Warlock.Corruption==240 and d.damage[0].Warlock['Shadow Bolt']==500) +damage('SPELL_DAMAGE','0xIMP','Imp',4100,'Firebolt',80) +damage('SPELL_DAMAGE','0xGUARDIAN','Infernal',8196,'Immolation',60) +assert(d.damage[0].Warlock['Pet: Imp - Firebolt']==80 and d.damage[0].Warlock['Pet: Infernal - Immolation']==60) +assert(d.damage[0].Warlock._sum==880,'DoTs, direct damage, pet and guardian count once') +assert(d.damage[1].Warlock._sum==880,'current and overall agree') +damage('SPELL_DAMAGE','0xIMP','Imp',4100,'Fire Shield',25) +assert(d.damage[0].Warlock['Pet: Imp - Fire Shield']==25,'pet spells stay separate') +damage('SPELL_DAMAGE','0xIMP','Imp',4100,'Firebolt',20) +assert(d.damage[0].Warlock['Pet: Imp - Firebolt']==100,'repeated spells reuse their entry') +ShaguDPS.config.merge_pets=0 +damage('SPELL_DAMAGE','0xIMP','Imp',4100,'Firebolt',90) +assert(d.damage[0]['Imp (Warlock)'].Firebolt==90 and d.damage[0].Warlock._sum==925) +damage('SPELL_DAMAGE','0xOUTSIDER','Stranger',1032,'Shadow Bolt',999) +assert(not d.damage[0].Stranger,'outsiders excluded') +local before=d.heal[0].Priest._esum +arg2,arg3,arg4,arg5,arg6,arg8,arg9='SPELL_PERIODIC_HEAL',false,'0xPRIEST','Priest',1025,'0xTARGET','Mage' +arg12,arg13,arg14,arg15,arg16=139,'Renew',2,100,60 +p.OnEvent() +assert(d.heal[0].Priest._esum==before+40,'periodic healing uses valid overhealing') +UnitHealthMax=function() return 100 end +UnitHealth=function() return 95 end +arg16=0 +before=d.heal[0].Priest._esum +p.OnEvent() +assert(d.heal[0].Priest._esum==before+5,'percentage-like health keeps estimation fallback') +-- Recorded control-loss flags: same GUID changes from mine/friendly/pet to +-- outsider/hostile/NPC. Its first hostile hit must not reuse the owner cache. +ShaguDPS.config.merge_pets=1 +local groupName=UnitName +UnitName=function(unit) if unit=='player' then return 'Warlock' end return groupName(unit) end +damage('SPELL_DAMAGE','0xGUARDIAN','Infernal',4369,'Immolation',20) +local ownerTotal=d.damage[0].Warlock._sum +damage('SPELL_DAMAGE','0xGUARDIAN','Infernal',68168,'Immolation',30) +damage('SPELL_DAMAGE','0xGUARDIAN','Infernal',2632,'Immolation',30) +assert(d.damage[0].Warlock._sum==ownerTotal,'hostile NPC events never credit former owner') +-- A hostile unit retaining a PET type must also ignore stale UnitOwnerGUID. +damage('SPELL_DAMAGE','0xGUARDIAN','Infernal',4424,'Immolation',30) +assert(d.damage[0].Warlock._sum==ownerTotal,'hostile pet flags override stale owner APIs') +damage('SPELL_DAMAGE','0xGUARDIAN','Infernal',4369,'Immolation',20) +assert(d.damage[0].Warlock._sum==ownerTotal+20,'control regained bypasses cached exclusion immediately') +ShaguDPS.config.track_all_units=1 +damage('SPELL_DAMAGE','0xGUARDIAN','Infernal',68168,'Immolation',30) +assert(d.damage[0].Warlock._sum==ownerTotal+20 and d.damage[0].Infernal._sum==30, + 'track-all records hostile creature independently without crediting owner') +print('Warlock DoT, demon, guardian and merge accuracy checks passed (Lua 5.0)') diff --git a/window.lua b/window.lua index be820d1..a5fa2ce 100644 --- a/window.lua +++ b/window.lua @@ -14,6 +14,38 @@ local spairs = ShaguDPS.spairs local round = ShaguDPS.round local skin = ShaguDPS.skin +-- Native interpolation only; no Lua animation driver. Recycled rows and +-- changed segments/targets snap immediately instead of borrowing old fills. +window.SetBarValues = function(bar, name, segment, view, target, maximum, value, lowerMaximum, lowerValue, force) + local modes = Enum and Enum.StatusBarInterpolation + local native = modes and type(modes.ExponentialEaseOut) == "number" + and bar.GetInterpolatedValue and bar.SetToTargetValue + and bar.lowerBar.GetInterpolatedValue and bar.lowerBar.SetToTargetValue + local enabled = (view == 5 and config.smooth_threat == 1) + or ((view == 1 or view == 3) and config.smooth_bars == 1) + local same = bar.wasShown and bar.smoothName == name and bar.smoothSegment == segment + and bar.smoothView == view and bar.smoothTarget == target + local interpolation = native and enabled and same and not force and modes.ExponentialEaseOut or 0 + + if native then + bar:SetMinMaxValues(0, maximum, interpolation) + bar:SetValue(value, interpolation) + if lowerMaximum and lowerValue then + bar.lowerBar:SetMinMaxValues(0, lowerMaximum, interpolation) + bar.lowerBar:SetValue(lowerValue, interpolation) + end + else + bar:SetMinMaxValues(0, maximum) + bar:SetValue(value) + if lowerMaximum and lowerValue then + bar.lowerBar:SetMinMaxValues(0, lowerMaximum) + bar.lowerBar:SetValue(lowerValue) + end + end + bar.smoothName, bar.smoothSegment = name, segment + bar.smoothView, bar.smoothTarget = view, target +end + -- time used for "per second" values: each unit's own active time, or with the -- "Fight Duration" time mode the time the whole segment lasted (set by Refresh) local fight_time = nil @@ -231,7 +263,8 @@ local menubuttons = { -- segments ["Current"] = { 0, 1, -25.5, "Current Segment", "|cffffffffShow current fight", "segment" }, ["Fights"] = { 1, 2, -25.5, "Boss Fights", "|cffffffffPick a past boss fight", "segment" }, - ["Overall"] = { 2, 0, -25.5, "Overall Segment", "|cffffffffShow all fights", "segment" }, + ["Wipes"] = { 2, 2, -25.5, "Boss Wipes", "|cffffffffPick a past boss wipe", "segment" }, + ["Overall"] = { 3, 0, -25.5, "Overall Segment", "|cffffffffShow all fights", "segment" }, -- modes ["Damage"] = { 0, 1, 25.5, "Damage View", "|cffffffffDamage done and damage per second", "view" }, @@ -888,7 +921,8 @@ local function Refresh(self, force, report) elseif config[wid].segment == 1 then self.btnCurrent.caption:SetTextColor(1,.9,0,1) elseif config[wid].segment == 2 then - self.btnFights.caption:SetTextColor(1,.9,0,1) + local saved = ShaguDPS.fights:Get(config[wid].fight) + self[saved.kill and "btnFights" or "btnWipes"].caption:SetTextColor(1,.9,0,1) end self.btnSegment.caption:SetText(SegmentName(config[wid])) @@ -1003,6 +1037,7 @@ local function Refresh(self, force, report) end -- clear previous results for id, bar in pairs(self.bars) do + bar.wasShown = bar:IsShown() bar.lowerBar:Hide() bar:Hide() end @@ -1019,6 +1054,10 @@ local function Refresh(self, force, report) if report then first, last = 1, math.min(count, math.max(last, 10)) end local columnWidths = {1, 1, 1} + local smoothTarget + if config[wid].view == 5 then + smoothTarget = UnitGUID and UnitGUID("target") or UnitName("target") + end -- only the units that get a bar (or a chat line) need their values read for i = first, last do @@ -1040,13 +1079,13 @@ local function Refresh(self, force, report) self.bars[bar].title = self.values.name self.bars[bar].unit = name - self.bars[bar]:SetMinMaxValues(0, self.values[template.bar_max]) - self.bars[bar]:SetValue(self.values[template.bar_val]) + window.SetBarValues(self.bars[bar], name, self.segment, config[wid].view, smoothTarget, + self.values[template.bar_max], self.values[template.bar_val], + template.bar_lower_max and self.values[template.bar_lower_max], + template.bar_lower_val and self.values[template.bar_lower_val], force) -- enable lower bar if template requires it if template.bar_lower_max and template.bar_lower_val then - self.bars[bar].lowerBar:SetMinMaxValues(0, self.values[template.bar_lower_max]) - self.bars[bar].lowerBar:SetValue(self.values[template.bar_lower_val]) self.bars[bar].lowerBar:Show() else self.bars[bar].lowerBar:Hide() @@ -1191,7 +1230,7 @@ local function CreateWindow(wid) frame.btnSegment.caption:SetFont(STANDARD_TEXT_FONT, 9) frame.btnSegment.caption:SetText("Overall") frame.btnSegment.caption:SetAllPoints() - frame.btnSegment.tooltip = { "Select Segment", "|cffffffffOverall, Current" } + frame.btnSegment.tooltip = { "Select Segment", "|cffffffffCurrent, Fights, Wipes, Overall" } frame.btnSegment:SetScript("OnEnter", btnEnter) frame.btnSegment:SetScript("OnLeave", btnLeave) frame.btnSegment:SetScript("OnClick", function() @@ -1206,10 +1245,12 @@ local function CreateWindow(wid) if frame.btnCurrent:IsShown() or config[frame:GetID()].view == 5 then frame.btnOverall:Hide() frame.btnFights:Hide() + frame.btnWipes:Hide() frame.btnCurrent:Hide() else frame.btnOverall:Show() frame.btnFights:Show() + frame.btnWipes:Show() frame.btnCurrent:Show() end end) @@ -1240,6 +1281,7 @@ local function CreateWindow(wid) frame.btnReceived:Hide() frame.btnOverall:Hide() frame.btnFights:Hide() + frame.btnWipes:Hide() frame.btnCurrent:Hide() else frame.btnDamage:Show() @@ -1255,6 +1297,7 @@ local function CreateWindow(wid) end frame.btnOverall:Hide() frame.btnFights:Hide() + frame.btnWipes:Hide() frame.btnCurrent:Hide() end end) @@ -1287,7 +1330,7 @@ local function CreateWindow(wid) button:SetScript("OnEnter", btnEnter) button:SetScript("OnLeave", btnLeave) button:SetScript("OnClick", function() - if name == "Fights" then return frame:ToggleFightList() end + if name == "Fights" or name == "Wipes" then return frame:ToggleFightList(name == "Wipes") end config[frame:GetID()][template[6]] = template[2] if template[6] == "dispels_received" then config[frame:GetID()].view = 6 end @@ -1494,13 +1537,24 @@ local function CreateWindow(wid) frame.fightList:SetWidth(150) frame.fightList:Hide() frame.fightList.entries = {} + frame.fightList.fights = {} frame.btnFights:SetScript("OnHide", function() frame.fightList:Hide() end) + frame.btnWipes:SetScript("OnHide", function() frame.fightList:Hide() end) - frame.ToggleFightList = function(self) + frame.ToggleFightList = function(self, wipes) local list = self.fightList - if list:IsShown() then return list:Hide() end + if list:IsShown() and list.wipes == wipes then return list:Hide() end + list.wipes = wipes + list:ClearAllPoints() + list:SetPoint("TOPLEFT", wipes and self.btnWipes or self.btnFights, "TOPRIGHT", 4, 2) - local fights = ShaguDPS.fights and ShaguDPS.fights.list or {} + local fights = list.fights + while table.getn(fights) > 0 do table.remove(fights) end + if ShaguDPS.fights then + for _, fight in ipairs(ShaguDPS.fights.list) do + if (not fight.kill) == wipes then table.insert(fights, fight) end + end + end local count = math.max(1, table.getn(fights)) for i = 1, math.max(count, table.getn(list.entries)) do @@ -1544,7 +1598,7 @@ local function CreateWindow(wid) elseif not fight then entry.id = nil entry.tooltip = nil - entry.caption:SetText("|cff888888No boss fights yet") + entry.caption:SetText(wipes and "|cff888888No boss wipes yet" or "|cff888888No boss kills yet") entry.time:SetText("") entry:Show() else @@ -1609,6 +1663,7 @@ local function CreateWindow(wid) frame.btnReceived, frame.btnOverall, frame.btnFights, + frame.btnWipes, frame.btnCurrent }