promotional bannermobile promotional banner

Epic Damage Meter

A lightweight, non-bloated World of Warcraft damage meter designed for accurate real-time tracking and minimal to non existent performance impact.

File Details

EpicDamageMeter v.2.4.28

  • R
  • May 26, 2026
  • 447.06 KB
  • 933
  • 12.0.7+14
  • Classic TBC + 4

File Name

EpicDamageMeter.zip

Supported Versions

  • 12.0.7
  • 12.0.5
  • 12.0.1
  • 5.5.4
  • 5.5.3
  • 4.4.2
  • 4.4.1
  • 3.80.1
  • 3.80.0
  • 3.4.5
  • 3.4.4
  • 2.5.5
  • 2.5.4
  • 1.15.8
  • 1.15.7

EpicDamageMeter - Changelog
============================

Version 2.4.28
--------------
PvP Crash Fix: SecretValue Taint in Tooltip Cache Key

THE BUG
- 26x error spam in PvP: "attempt to compare local 'expectedKey'
  (a secret string value, while execution tainted by
  'EpicDamageMeter')" — fired every time a bar tooltip was shown
  on a Paladin (or any player) during an arena/BG match.
- Root cause: GetTooltipSpells and SetBarData both built a
  composite cache key via string.format("%s|%s|%s|%s", tostring(
  actor.guid), ...). On WoW 12.0+ in PvP, actor.guid can be a
  SecretValue. tostring() on a SecretValue returns a SECRET
  STRING, which then made the whole expectedKey secret. The
  subsequent `bar._cachedKey ~= expectedKey` comparison tainted
  Lua execution, throwing the error 26 times in a row.

THE FIX
- New _plain() helper at both cache-key build sites: returns the
  string form only if the value is NOT secret, otherwise returns
  a fallback. Falls back to bar.plainGUID (set for the local
  player) before giving up on "?".
- Comparison hardened: if bar._cachedKey itself is somehow
  secret, treat as cache miss and reset rather than running the
  `~=` compare. Plain == plain compares are safe; only mixed-
  taint compares throw.
- Both sites fixed in one pass: SetBarData (~line 1973) and
  GetTooltipSpells (~line 2934).

WHY THIS NEVER CAUGHT IT IN PvE
- In PvE, actor.guid is always plain. The bug only triggers
  when CLEU is restricted (arena, BG) so the C_DamageMeter path
  feeds the bar with secret GUIDs/names. SecretValues are a
  12.0+ feature; older clients never had this code path.

Version 2.4.27
--------------
Heal Tooltip: Merge Absorbs

THE BUG
- Side-by-side screenshot from the user — Details! showed nine
  healing entries for a Priest, EDM only five. The missing four
  were: "Schnelle Prognose", "Machtwort: Schild" (Power Word:
  Shield — an absorb), "Kosmische Welle" and "Gebet der
  Besserung". User flagged it as "HPS zeigt net alle Werte
  korrekt an".
- Root cause: when fetching the spell breakdown for the healing
  tooltip we only queried `Enum.DamageMeterType.HealingDone`,
  which returns only the actual-heal spells. Absorb-style
  spells (Power Word: Shield, Sacred Veil, etc.) live under
  `Enum.DamageMeterType.Absorbs` and never showed up.

THE FIX
- The Healing / HPS bar tooltip cache build now queries BOTH
  HealingDone AND Absorbs and merges the two spell lists into
  a single table (keyed by spellID).
- After merging, sort the combined list by `totalAmount`
  descending so a big shield doesn't end up below a small heal
  just because the API returned heals first.
- Damage / DPS mode path untouched (single enum, same as before).

NOTE
- The "Blitzheilung 238K vs 102K" amount discrepancy in the user's
  screenshots is most likely because Details! was on a longer /
  different combat segment when the screenshot was taken — the
  per-cell percentages already diverge (43.5% vs 40.2%), so the
  underlying segment totals were different snapshots, not a bug
  in our number. After this fix the COMPLETENESS of the spell
  list matches Details!; absolute amounts only line up when both
  meters are looking at the same segment at the same moment.

Version 2.4.26
--------------
Restore working math from reference v2.4.15

User uploaded a working reference build (v2.4.15) and pointed out
that my recent "improvements" broke the math everywhere. Compared
both trees and reverted the broken logic while keeping the visual
work intact.

REVERTED TO REFERENCE
- Core/Utils.lua restored byte-for-byte. My AbbreviateNumbers-based
  ResolveNumber (v2.4.21+) was over-engineered and returned 0 for
  some SecretValues — that's why the tracker showed "0 (0 DPS)"
  during combat. The reference's simple two-path is back:
    1. Plain numbers → tonumber, done.
    2. SecretValues → pcall'd string.format("%.0f", val) followed
       by pcall'd #length + string.byte byte-rebuild.
- UI/Graph.lua Update reverted to the reference's Strategy 1
  (Database) → Strategy 2 (tracker-fed values) order. No own
  sliding-window math.
- MainFrame UpdateBarsFromDamageMeter Abbr reverted to the
  reference's Details!-style two-path (SecretValue →
  AbbreviateNumbers, plain → Utils.FormatNumber).
- MainFrame graph-publishing reverted: writes `total` (cumulative)
  to _graphDPS like the reference, not a pre-divided rate.

KEPT FROM RECENT WORK
The following visual additions don't touch the math logic and
stay in place:
- Detail Window hero header + stat pills + ranked spell rows
- Settings hero logo + tagline banner + arena theme + version
  footer + ESC menu button
- Tracker title-bar mini hero logo
- Tracker bar gloss + shadow + edge highlight overlays
- Status bar combat-time icon + warm-gold colour + accent stripes
- Empty-combatSources bail (matches reference + Details! pattern;
  removed the synthesis hack)

Lesson learned: when the reference works, copy first, modify
second.

Version 2.4.25
--------------
Revert to Details! reference pattern

User feedback: stop inventing math, follow Details! exactly. I
sourced the relevant Details! code (parser_nocleu1.lua, boot.lua
in Tercioo/Details-Damage-Meter) and reverted the divergences:

  PER-SPELL DPS (was 2.4.24 amount / session.durationSeconds)
- Details reads spell.amountPerSecond DIRECTLY from
  C_DamageMeter (parser_nocleu1.lua:760-770). No own division.
- DetailWindow.ShowSpellList, MainFrame UpdateBars cache build,
  and MainFrame GetTooltipSpells lazy path all reverted to
  reading sp.amountPerSecond and feeding it through Abbr() like
  before.

  EMPTY combatSources (was 2.4.23 local-player synthesis)
- Details bails silently when session.combatSources is nil or
  empty (parser_nocleu1.lua:1859-1866). No synthesised local
  player row, no CLEU fallback.
- UpdateBarsFromDamageMeter reverted to the same behaviour. The
  "tracker shows no data during the SecretValue phase" is a
  Blizzard API artefact — every other meter on 12.0 has the
  same brief gap, and synthesising fake data created downstream
  consistency issues (math mismatches between bar and tooltip).

What this means:
- Numbers will match Details! exactly (since we use the same
  API fields).
- If Blizzard's stock meter shows different numbers, that's
  Blizzard's UI using a different formula — not a bug we should
  paper over with our own derivation.
- Brief no-data window at combat start (under arena/BG
  SecretValue restriction) is expected behaviour now.

Version 2.4.24
--------------
Per-Spell DPS Now Matches Blizzard's Damage Meter

THE PROBLEM
- Side-by-side screenshot from the user: Blizzard's stock meter
  showed "Heilige Pein 40.779 (3.719)", EDM Detail Window showed
  "40.8K (4.3K DPS)" — same spell, same moment, same player.
  Amounts matched (40K), DPS rates differed by ~16%.
- Root cause: we read the per-spell DPS from C_DamageMeter's
  spell.amountPerSecond, which is a different metric than what
  Blizzard's own UI displays. Blizzard's stock meter divides the
  spell's total damage by the session.durationSeconds (a stable
  "since combat start" duration). The API's amountPerSecond
  appears to be either an instantaneous or shorter-window rate,
  which tends to read higher.

THE FIX
- Per-spell DPS in the Detail Window's spell list and in the
  main-window bar tooltip is now computed as
    spell.totalAmount / session.durationSeconds
  exactly like Blizzard's UI does. We fetch session.durationSeconds
  via CallSessionAPI / GetCombatSessionFromType right before the
  spell loop and divide once per spell.
- Applies to:
    DetailWindow.ShowSpellList    (Damage + Healing tabs)
    MainFrame UpdateBarsFromDamageMeter spell-cache build
    MainFrame GetTooltipSpells    lazy-fetch path
- DB segment duration is still used as a fallback for Classic /
  when C_DamageMeter doesn't expose durationSeconds.

Version 2.4.23
--------------
Synthetic-Source Fallback — Bars during SecretValue Combat

THE REGRESSION
- Reported: tracker showed no data during combat and only filled
  in after PLAYER_REGEN_ENABLED. Debug log captured the smoking
  gun: "Synced C_DamageMeter -> segment: dmg=92872 heal=0
  players=0". `dmg` had a real value, but `players` (i.e.
  combatSources) was empty.
- Why: in arenas / BGs / wherever the SecretValue restriction is
  active, Blizzard's session.combatSources is empty during the
  fight. The API still gives you a session.totalAmount (a
  SecretValue you can pass to widgets) but no per-source array.
  UpdateBarsFromDamageMeter hit `if sourceCount == 0 then return
  false end` and fell back to the Database path — which has no
  CLEU-fed data on Retail since 2.4.8 — so the tracker rendered
  empty bars.

THE FIX
- When session.combatSources is empty AND session.totalAmount
  resolves to a non-zero number, synthesise a one-row source
  for the local player:
    name           = UnitName("player")
    classFilename  = the player's class
    sourceGUID     = UnitGUID("player")
    isLocalPlayer  = true
    totalAmount    = session.totalAmount  (still secret, OK —
                                          widgets handle it)
    amountPerSecond = session.amountPerSecond
- The rest of the render pipeline (which already handles
  SecretValues via SetText / SetMinMaxValues) shows the player
  their own bar with the live session total during the fight.
- Marked on the instance via `_syntheticSource` flag so we can
  later add a small "(local view)" hint or skip features that
  need a real multi-source array (left as a TODO).

Version 2.4.22
--------------
Taint-Hardening — Utils.ResolveNumber

CRITICAL FIX
- Fixed: UpdateBarsFromDamageMeter threw repeatedly with
  "attempt to get length of local 'str' (a secret string value,
  while execution tainted by 'EpicDamageMeter')" at Utils.lua:110.
- Root cause: 2.4.21 added an AbbreviateNumbers fast-path for
  SecretValues that called `#str > 0` without wrapping in pcall.
  AbbreviateNumbers returns a tainted string when fed a
  SecretValue, and `#` on a tainted string throws. The error
  fired ~4× per refresh tick, polluting the debug log.
- Rewrote ResolveNumber so EVERY taint-sensitive operation is
  pcall-guarded. The new flow:
    1. tonumber for plain values (fast path)
    2. AbbreviateNumbers (preferred) OR string.format (fallback)
       to get a representation of the SecretValue
    3. string.byte loop (taint-safe — returns plain integers)
       to rebuild a plain Lua string
    4. tonumber on the plain string (handles "12345" form)
       OR Utils.ParseAbbrev (handles "10.5K" / "1.2M" form)
  Nothing in this function can throw past pcall now. Worst case
  is returning 0 if the byte loop can't read any chars.

Version 2.4.21
--------------
Tracker / Graph / Footer Fixes

TRACKER SHOWED "0 (0 DPS)" DURING COMBAT
- Fixed: after the 2.4.11 ResolveNumber tightening, the tracker
  bar displayed "0 (0 DPS)" mid-combat and only filled in once
  PLAYER_REGEN_ENABLED fired. Root cause: forcing every
  SecretValue through the byte-rebuild slow path returned 0
  whenever string.format("%.0f", secretValue) didn't produce a
  parseable string — which on Midnight 12.0 happens for any rate
  that includes a fractional component.
- New strategy: ask Blizzard's AbbreviateNumbers (which handles
  SecretValues correctly) for a clean string like "10.5K", parse
  that back via Utils.ParseAbbrev into a plain number. The
  byte-rebuild path stays as a final fallback only.

GRAPH MIRRORS THE TRACKER NOW
- Per user request: removed the 3-second rolling-window
  calculation added in 2.4.20. The graph just plots whatever
  per-second rate the tracker is currently displaying.
- The tracker now publishes a proper RATE (not the cumulative
  total) on the Retail path too — `total / duration` when the
  display mode is DAMAGE_DONE / HEALING_DONE, or `total` when
  the mode is already DPS / HPS.
- A sample is pushed every Update tick while in combat so the
  line never freezes — even a momentary 0 between bursts is a
  real value worth plotting.

VERSION FOOTER POSITION
- Fixed: the "EpicDM v2.4.20" label sat in mid-air below the
  Reset Data / Reload UI buttons with a big gap. Now anchored
  to the bottom edge of the sidebar with a thin separator above
  it — visually grouped with the action buttons.

Version 2.4.20
--------------
Graph Sliding-Window + Decimal Precision + Stale Tooltip Cache

GRAPH — FLAT LINE BUG
- Fixed: the DPS/HPS graph drew a flat line that never moved during
  combat — every sample showed the same value as the last. Root
  cause: Graph:Update computed `dps = totalDamage / segmentDuration`,
  i.e. the *cumulative average* DPS since combat start. That value
  trends toward a constant the longer the fight runs, so the line
  flattens almost immediately.
- Replaced with a 3-second rolling-window: each tick we record
  (timestamp, cumulative total) into a buffer, drop entries older
  than the window, then derive the rate from the delta over the
  window span. The line now visibly moves with burst/lull cycles
  the way a real DPS meter should.
- Window state is cleared when combat ends so a new pull starts
  with a clean rate.

TRACKER — DECIMAL PRECISION
- Fixed: bar values rendered as "10K DPS" with no decimal place.
  The user asked for "10.4K DPS". The Abbr helper in MainFrame
  routed SecretValues straight through Blizzard's AbbreviateNumbers,
  which doesn't add a decimal for plain values < 1000 multiples.
  Now it normalises through Utils.ResolveNumber first (unwraps
  SecretValues to plain numbers) and formats with Utils.FormatNumber
  (`%.1f%s`) so we always get one decimal place.

BAR TOOLTIP — MISMATCHED MATH
- Fixed: hovering a bar showed spell totals that summed to more
  than the tracker bar value (e.g. tooltip showed 3.6M of spells
  under a 1.3M bar). Root cause: cache key was
  `guid|mode|segmentIndex`. A new combat keeps segmentIndex at 0
  (still "current"), so the cached spells from the *previous*
  combat survived into the new one — the tracker re-rendered with
  fresh data but the tooltip kept the stale list.
- Cache key now includes the Blizzard session ID
  (`instance._blzSessionId` or `EDM.Core.latestBlzSessionId`), so
  every new combat invalidates the cache automatically.

Version 2.4.19
--------------
DPS/HPS Graph Polish

GLYPH FIX
- The bottom-right duration label was showing the literal byte
  sequence "xE2x8FxB1 :43" instead of a stopwatch glyph. Root
  cause: U+23F1 STOPWATCH isn't in WoW's default font glyph set,
  so the renderer fell back to printing the escape bytes raw.
  Same problem on the crosshair tooltip's time line.
- Replaced both with a font-safe bullet glyph (•) in muted gold:
  "|cff998866●|r 0:43". Renders in every font we ship with.

GRAPH CHROME
- Added split accent stripes on the Y-axis edge: top half tinted
  the DPS red, bottom half tinted the HPS green, both fading
  toward the midpoint. Gives the chart a "billboard" frame
  without obscuring data.
- Bottom of the canvas now has a faint white-to-transparent
  horizontal accent stripe that visually separates the chart
  body from the X-axis labels.

Version 2.4.18
--------------
Tracker Polish

TRACKER TITLE BAR
- New mini hero emblem in the title bar (18x18) — a compressed
  version of the Settings hero logo: fire gradient (yellow→navy)
  with 3 rising damage-bars (6/9/13px). Renders the addon's brand
  on the tracker too instead of just plain text. Title text now
  anchors to the emblem's right side.

TRACKER BARS
- Bars get the same depth treatment the Detail Window ability
  bars use:
    * bar.gloss   — top 50% height, white→transparent vertical
                    gradient at 18% alpha. Lit-from-above feel.
    * bar.shadow  — bottom 45% height, transparent→black gradient
                    at 40% alpha. Adds depth at the bar's foot.
  Both live on bar.statusBar at OVERLAY layers so they paint above
  the colored fill (not below it, where they were invisible).

STATUS BAR (BOTTOM STRIP)
- Combat duration now has a ⏱ glyph and warm-gold text colour
  instead of the previous dim grey. Two short 1px vertical accent
  stripes flank the time — gives the strip a "billboard" feel.
- Total label ("Total: ...") split-coloured: dim grey label,
  bright white value. Both dynamic-update paths in UpdateBars
  preserve the colour tags now (label colour kept in sync after
  data refreshes).

Version 2.4.17
--------------
Hero Logo

The tiny 28px "E" emblem in the Settings title was replaced with a
proper hero block, inspired by the reference image the user shared:
fire-coloured shield with rising damage bars, plus a stacked brand
mark next to it.

Because we can't ship a .png that matches Blizzard's interface rules
in this state, the whole emblem is rendered procedurally in Lua —
all done with WHITE8X8 textures and gradients. Layers, top to bottom:

  outer glow      additive orange/red soft halo behind the emblem
  outer border    LOGO_BORDER (theme-aware)
  base fill       vertical gradient: yellow-top → deep-navy-bottom
  flame layer     additive orange wash on the upper 60%
  damage bars     5 vertical bars stepped 8 / 14 / 20 / 26 / 32 px
                  high in left-to-right order, coloured from gold
                  through red — mirrors the rising-damage motif in
                  the reference logo
  top sheen       short white gradient at the top for a "lit" feel
  inner border    1px white at 15% alpha for a carved edge

Brand wordmark to the right is now stacked:
  EPIC        small ember-orange tag, 9pt outlined
  DAMAGE METER  17pt outlined wordmark, two-tone

The title bar height didn't change — the hero fits in the existing
52px strip. Wizard / theme / close buttons on the right unchanged.

Version 2.4.16
--------------
ESC Menu Position Fix + ArenaCore-Style Tagline + Version Footer

ESC MENU
- Fixed: the "EpicDM" entry added in 2.4.15 was barely visible —
  it landed near the top of the menu, overlapping with the
  "Optionen" button. Root cause: the anchor relied on
  GameMenuButtonAddons which is nil on Midnight 12.0, then fell
  back to a hard-coded TOP offset that put us next to the title.
- The button now repositions itself on every ESC-menu open. It
  walks the menu's children, finds the lowest-positioned visible
  Button, and anchors below it — including below buttons added
  by OTHER addons (SUI / Arena Core / etc.). The defer runs twice
  (immediately and again 50ms later) to catch addons that inject
  their buttons after the first OnShow tick.
- The GameMenuFrame grows automatically by the overhang so our
  button is never clipped.

SETTINGS — ArenaCore-INSPIRED FOOTER + TAGLINE
- New 22px tagline banner directly below the title, rotating
  through five EDM-themed slogans on every panel open:
    * "Track every hit. Own every fight."
    * "Numbers don't lie — read the meter."
    * "From pull to parse, every cooldown counted."
    * "Know your damage. Earn the kill."
    * "Big numbers. Cleaner reads. Fewer surprises."
  Theme-aware gradient: arena = orange-to-purple wash (matches
  the ArenaCore tagline look), dark = accent gradient, light =
  subtle accent tint.
- Version footer at the bottom of the sidebar showing the
  "EpicDM v2.4.16" mark in muted text. Pulls the version from
  the TOC at runtime so it stays in sync.

Version 2.4.15
--------------
Light-Mode Fix + ESC-Menu Button + Arena Theme

LIGHT MODE
- Fixed: section headers were unreadable in light mode — the dark
  accent-tinted gradient overlay I added in 2.4.13 dominated the
  card colour and inverted the page (light bg + dark headers with
  dark text). Now adapts per theme:
    light → very subtle accent-tint that fades to zero
    dark  → keeps the moody accent depth gradient
- Text outline + shadow disabled in light mode (was creating a
  black halo around dark text on light bg).

ESC MENU BUTTON
- The in-game ESC menu now has an "Epic DM" entry, the same way
  SUI and Arena Core surface their settings. Tries the modern
  Menu.ModifyMenu API first (Midnight 12.0 dropdown menu) and
  falls back to a GameMenuButtonTemplate button on older clients.
  Clicking it opens the EDM settings panel and dismisses the
  game menu.

ARENA THEME (NEW)
- New "arena" theme variant, inspired by Arena Core's deep purple
  aesthetic. Cycle with the sun/moon button: dark → light → arena
  → dark. While in arena mode the icon tints purple to signal the
  third state.
- Palette: near-black panels, saturated purple accent, yellow
  hover on nav items (matches Arena Core's yellow glow on active
  category).
- Section headers in arena mode get a thicker 2px purple border,
  heavier wash gradient, and BOLD UPPERCASE titles in accent
  purple instead of the standard text colour — the "section card"
  feel from the reference.

Version 2.4.14
--------------
Tracker / Details Polish + Display Bug Fixes

TRACKER
- Title bar no longer duplicates the mode name. Previously the
  title showed "Epic DM  Verursachter Schaden" while the toolbar
  button directly below also said "Verursachter Schaden" — visual
  redundancy that pushed the brand off-screen on narrow windows.
  Title is now just the brand (+ enemy indicator when active);
  mode lives exclusively in the toolbar button.

DETAIL WINDOW
- Fixed: header pills still showed "0 DPS / 0 HPS" while spell rows
  rendered correct per-second values. Root cause: the previous
  fallback divided by the DB segment duration which is clamped to
  1s after a segment is reset — producing absurd values OR being
  short-circuited by a SecretValue rate that resolved to a tiny
  non-zero number. Now reads `session.durationSeconds` from
  C_DamageMeter (the duration the meter actually measured) as the
  primary divisor, with a session-level fallback when the per-source
  data doesn't carry it, and treats any rate <= 0 as "missing" so
  the fallback always kicks in for the bad cases.
- Fixed: spell rows below 1000 DPS displayed raw IEEE-754 floats
  ("777.76580810547 DPS"). The Abbr helper short-circuited around
  Utils.ResolveNumber for SecretValues and routed them through
  Blizzard's AbbreviateNumbers, which doesn't round low values.
  Both Abbr helpers in DetailWindow now go through ResolveNumber
  first, then Utils.FormatNumber — guaranteed clean integers.
- Spell-row depth: the gloss / shadow overlays added in 2.4.10
  weren't actually visible because they sat on the bar parent
  while the StatusBar child rendered above them. Moved both to
  bar.statusBar at OVERLAY layer so they paint on top of the
  colored fill. Added a 2px right-edge highlight texture (hidden
  by default, ready for show* code to position).

Version 2.4.13
--------------
Pill Look Goes System-Wide

TOOLBAR SPACING
- Fixed: in the meter toolbar, the mode label ("Verursachter Schaden"),
  the Team/Enemy toggle ("Team"), and the segment label ("Aktuell")
  visually fused into each other on long localised strings. Root
  cause: mode button was a fixed 90px wide and the German label
  overflows beyond that width, bleeding into the next button.
- Mode button now auto-fits its label width (clamped 60-220px) via
  a new `Instance:ResizeModeBtn()` method that's called on init
  and on every SetMode. Gaps between mode/enemy/divider/segment
  bumped from 3-4px to 7-8px so labels stay visually separate.

PILL DESIGN — REUSABLE WIDGETS
- Extracted the Detail Window header pill into
  EDM.Widgets:CreateStatPill(parent, opts) so the same dark-card +
  top thin / bottom thick / left vertical accent stripes + accent-
  tinted depth gradient style can be reused across the addon.
  Returns a frame with :SetValue(value, sub, isZero) for live updates.
- Added EDM.Widgets:CreateSectionBar(parent, opts) — a wider
  horizontal sibling for section headers (single-line title + meta).
  Same accent palette, same lit-from-top feel.

SETTINGS PANEL
- Settings section headers (Config:CreateSection) adopt the pill
  look: dark backdrop, horizontal accent-tinted depth gradient,
  top + bottom + left-vertical accent stripes in the theme's
  ACCENT colour. Section title outlined for better legibility on
  the new gradient bg, chevron is now accent-coloured.
- Hover state intensifies all three accent stripes plus the border
  rather than just dimming the bg — clear visual feedback that the
  section is interactive/collapsible.

Version 2.4.12
--------------
Detail Window Pass 2 — Pills, Targets, DPS

PILLS
- Split the confusing "INT / DSP" combo pill into two separate pills:
  "INTERRUPTS" and "DISPELS". User feedback was that "INT / DSP"
  read like a DPS abbreviation. Each pill now has its own label,
  accent colour, and dim-when-zero state.
- Added a second (thicker) bottom accent stripe and a subtle
  accent-tinted depth gradient on each pill. Active pills feel
  more "lit" while zero-valued pills clearly recede.
- Default window grew from 460x560 to 520x580 so the new 6-pill
  row doesn't truncate the "INTERRUPTS" label.

HEADER DPS / HPS FALLBACK
- Fixed: header pills showed "0 DPS" and "0 HPS" while the spell
  rows showed correct DPS values (e.g. "Göttlicher Hammer 7.1K
  DPS"). C_DamageMeter's amountPerSecond is sometimes nil / 0 /
  SecretValue even when totalAmount is populated — happens on
  Overall sessions, post-combat snapshots and expired sessions.
- Header now computes rate = total / duration as a fallback when
  the session-provided rate is missing, so DPS/HPS stays in sync
  with the spell rows.

TARGETS TAB
- Fixed: "Ziele" tab was empty on Retail because we removed CLEU
  registration in 2.4.8 (12.0 moved combat events to EventRegistry
  and forbade CLEU for addons). actor.targets had no data.
- Now inverts C_DamageMeter's EnemyDamageTaken view: each enemy
  session has combatSources listing the players who damaged it.
  Walking those and matching the actor's GUID gives "what targets
  did THIS player damage, and for how much". SecretValue-safe
  matching via pcall'd equality.
- Target rows now show "Damage  DPS · X.X%" in the same compact
  layout the spell rows use, plus the gold/silver/bronze rank
  badge on the top 3 targets.

Version 2.4.11
--------------
Bug Fixes + Debug Console + Detail Window Polish

BAR TOOLTIP — STALE SPELL CACHE
- Fixed: bar tooltip on hover showed spells from the wrong player.
  Repro: hovering Toilettegodx (Monk) in the Healing window showed
  Warlock spells like Drain Soul / Dark Pact / Healthstone — those
  were Méuw's (a Warlock who occupied that bar slot earlier).
- Root cause: bars are pooled and recycled across actors / modes /
  segments. `bar._cachedSpells` (populated by UpdateBars for the
  tooltip) had no invalidation key, so a bar that previously showed
  Player A and was later reassigned to Player B kept serving A's
  spells. The bug also surfaced when switching segments or modes on
  the same window.
- Fix: every cache write now tags the bar with a composite key
  (`actor.guid | mode | segmentIndex`). Both the cache-write paths
  (UpdateBars and GetTooltipSpells lazy fetch) set the key, and
  every cache read checks it. Mismatch → drop and re-fetch.
- Also fixed a second SecretValue arithmetic site at MainFrame.lua:
  1938 (cache build during UpdateBars) — same `tonumber(secret)`
  taint leak we fixed at line 2918 last release.


CRASH / TAINT
- Fixed: 14x "attempt to perform arithmetic on local 'amt' (a secret
  number value)" at MainFrame.lua:2916 (GetTooltipSpells). The bar
  tooltip's spell loop called `tonumber(sp.totalAmount)` which on a
  SecretValue returns the still-secret value, then `spellTotal + amt`
  blew up. Now uses Utils.ResolveNumber which handles SecretValues
  via the byte-rebuild slow path.
- Fixed: Utils.ResolveNumber fast-path silently passed SecretValues
  through. `tonumber(secret) > 0` doesn't throw and the comparison
  reports ok, so the wrapper assumed the value was clean and returned
  it. Subsequent arithmetic / format calls then leaked taint or
  produced raw float strings. Added an explicit issecretvalue() check
  before the fast path so SecretValues always take the byte-rebuild
  route.
- Fixed: 6x "attempt to call a nil value" at DebugConsole.lua:671
  when running `/mem`. Midnight 12.0 moved GetNumAddOns, GetAddOnInfo,
  IsAddOnLoaded, GetAddOnMemoryUsage, UpdateAddOnMemoryUsage under
  C_AddOns. The command now resolves through C_AddOns first and
  falls back to the legacy globals where they still exist.

DETAIL WINDOW
- Fixed: spell row DPS showed raw floats like "687.46350097656 DPS"
  for values below 1000. Root cause was the same ResolveNumber leak
  above — perSecond is a SecretValue on Retail and slipped past
  the rounding. The Utils.ResolveNumber fix resolves this generically,
  and the row layout was rewritten:
    * Value (top-right, big) : amount only, e.g. "41K"
    * Detail (bottom-right)  : "687 DPS · 26.1%" (rate + share)
    * Subtext (bottom-left)  : "5 hits · 2 crits (40%)"
  Eye lands on the big number, then drops to the rate+share line.
- Header pills: zero-valued pills now dim themselves (value muted,
  border faded, accent strip dimmed) so the eye skips empty stats.
- Removed redundant "I / D" sub-label under Int/Dsp pill — the
  pill's main label already says "INT / DSP".

DEBUG CONSOLE
- Fixed: errors captured by the in-addon console showed up as
  "Runtime: table: 0x..." with no actual message. BugGrabber-format
  error objects use the field name `.msg` (not `.message` which the
  old extractor expected). The extractor now probes multiple field
  names — msg, message, error, errorMessage, text, what, desc,
  reason — and falls back to serialising primitive table fields so
  you always see something concrete instead of an opaque table id.
- Stack trace shown bumped from 3 to 6 lines (3 was too short to
  see where the error actually came from).
- NEW console command: `/edm console` -> `last` (also `error last`)
  dumps the full last captured error: message, full stack trace,
  and the first 12 lines of locals. No more BugSack required for
  basic debugging — though BugSack still works on top if installed.
--------------
Detail Window Redesign
- Reworked the Player Detail window UI top-to-bottom while keeping all
  existing functionality, signatures and data flow intact.

  HEADER (was: cramped text rows separated by pipes)
    * Default window grew from 420×520 to 460×560 for breathing room.
    * Larger class icon (54px, was 48), framed with class-tinted border.
    * Big soft class-color halo glow behind the icon.
    * Class-color top accent band runs across the entire header.
    * Player name now in 16px outlined class color (was 13).
    * Class subtitle line under the name (small, muted).
    * "Dmg / DPS | Heal / HPS | Deaths | Interrupts | Dispels | Absorbs"
      one-line text block REPLACED by a row of five stat PILLS:
        DAMAGE   - big total + small "X DPS" sub
        HEALING  - big total + small "X HPS" sub
        ABSORBS  - big total
        DEATHS   - big number (greys out when zero)
        INT/DSP  - combined interrupts/dispels as "12 / 3"
      Each pill has its own colored top accent (red/green/gold/purple/blue).
    * Pills flex-resize on window resize.

  TABS (was: text-only with thin underline)
    * Now a segmented control: 30px tall, gradient bg, top accent strip,
      bottom indicator. Each tab carries its own type color (Damage red,
      Healing green, Targets orange, Damage Taken crimson).
    * Hover state subtly tints the bg toward the tab's accent.
    * Active tab gets a stronger tinted gradient + full-color top accent
      bar + bottom indicator line.

  ABILITY BARS (was: flat red statusbar, tiny icon)
    * Layered rendering: dark base + vertical depth gradient + status bar
      (skin-aware texture) + top gloss highlight + bottom shadow.
    * Slightly larger icon with 1px dark border (separates from bg).
    * Spell name jumped from 10px to 11px outlined; value text bolder.
    * Hits/crits sub-line now in its own field (was crammed in tiny text).
    * Top 3 entries get a gold/silver/bronze rank badge on the icon.
    * Subtle row divider for cleaner list rhythm.

  All show* functions (ShowDamageAbilities, ShowHealingAbilities,
  ShowTargets, ShowDamageTaken, ShowSpellList, ShowSpellTooltip,
  ShowSpellDetail) and the live C_DamageMeter integration are unchanged.
  Backward-compat shims keep self.header.stats1/stats2 callable as
  no-ops in case any code path still touches them.

Version 2.4.9
-------------
Bug Fix
- Fixed: hovering a player bar during combat on Retail showed 0/0/0
  in the tooltip; the right values only appeared after combat ended.
  Root cause: UpdateBarsFromDamageMeter writes a zero-initialised
  actor stub into bar.actorData (the bars get their width directly
  from C_DamageMeter session values, the stub is for spell-cache
  lazy loading). The tooltip read those zeros instead of asking
  C_DamageMeter itself. After combat, OnCombatEnd imports session
  totals back into the database actor, which is why post-combat
  worked.
- Tooltip:ShowActorTooltip now queries C_DamageMeter live for the
  hovered source's damage / healing / absorbs / damage-taken /
  interrupts / dispels across multiple session enums, picking the
  freshest available value and falling back to the database actor
  only when live data is missing. Duration is taken from the
  session itself so it matches what the bars are rendering.
- SecretValue-safe throughout: GUID/name comparisons go through a
  pcalled equality wrapper, formatting routes secret numbers through
  AbbreviateNumbers, division skipped entirely for secret damage.

Theme Pack + Animation Framework
- NEW: 8 new skins, expanding the catalogue from 23 -> 31. Each
  one targets a stylistic gap the existing themes don't cover:
    * Terminal — retro CRT phosphor green on near-black, monospace
      font, scanline overlay, sharp edges. For DOS nostalgia.
    * Sakura — soft rose pastel on dark wine background, gentle
      gradients, cherry-blossom palette. Daytime / lighter mood.
    * Bloodforge — Death Knight runic aesthetic, crimson on iron,
      gothic Morpheus serif, runic accent borders.
    * Vaporwave — Y2K sunset: magenta/cyan/violet on twilight grid.
      Dreamy gradients, neon-with-soft-edges (NOT Cyberpunk's harsh
      industrial; this is Lisa Frank / Macintosh Plus).
    * Newsprint — anti-color skin. Cream paper, black ink, serif.
      Class colors intentionally muted so typography does the
      talking. For anyone tired of the class-color rainbow.
    * Tactical — military HUD: amber on dark olive, monospace,
      crosshair-style accents. Reads like a cockpit display.
    * Brutalist — architecture-inspired heavy minimalism. Raw
      concrete grays, oversized typography, 3px black borders,
      yellow alert accents. Nothing soft, nothing decorated.
    * Glitch — broken-LCD aesthetic with chromatic aberration.
      Red shadow on cyan text, RGB-tinted edges. Pixel statusbar
      texture. For when you want the UI to feel slightly wrong.
- NEW: Procedural statusbar palettes (Skins:CreateProceduralBar) —
  Lua-generated gradients registered as LSM statusbars, no .tga
  artwork needed. Ships with Sunset, Ocean, Toxic, Ember, Aurora
  ready-made palettes for quick custom-theming.
- NEW: UI/Animations.lua module with reusable effects:
    * A:Pulse(frame, opts)         — breathing opacity loop
    * A:Glow(frame, opts)          — one-shot glow burst with scale
    * A:CountUp(fs, from, to, opts) — number-tween FontString with
                                      ease-out / linear / ease-in
    * A:Shimmer(frame, opts)       — light-band slides across (good
                                      for #1 rank highlight)
    * A:RecordFlash(frame, opts)   — celebratory flash for new bests
    * A:Shake(frame, opts)         — damped oscillation for emphasis
    * A:CancelAll(frame)           — cleanup all anims on a frame
    * A.FormatBig(v)               — formats 1234567 -> "1.23M"
  All self-cleaning: starting a new anim on the same frame cancels
  the previous one to avoid stacking.
- NEW: Animations wired into Bars.lua. Each bar now reacts to its
  data live:
    * CountUp on the value FontString when DPS changes by ≥100
      (deltas below that snap to avoid micro-tween churn)
    * Glow burst in class color when the bar climbs a rank
    * Continuous Shimmer band sliding across the #1 bar
    * Gold RecordFlash the first time a bar takes #1 in the
      current (actor, mode) — celebrates breaking through
  Per-bar state tracking (peak value, last rank, shimmer flag,
  ever-was-first flag) makes the triggers idempotent — they
  don't refire on every refresh during a single rank streak.
  Anim state is reset on bar recycling (ReleaseBar) and on theme
  changes (ApplySettings) so nothing leaks between segments.
- NEW: Settings hooks bars.useAnimations (master toggle) and
  bars.animOptOut = { CountUp = true, Glow = true, ... } for
  per-anim opt-out. Default: all on. Defaults can be wired into
  Config.lua's Bars tab in a follow-up.

Version 2.4.8
-------------
Retail (Midnight 12.0) — ADDON_ACTION_FORBIDDEN fix
- Root cause: Midnight 12.0 migrated combat / unit / roster
  events from the legacy Frame:RegisterEvent API to the new
  global EventRegistry (CallbackRegistryMixin). The legacy path
  now fires ADDON_ACTION_FORBIDDEN for these events; only
  lifecycle events (ADDON_LOADED, PLAYER_LOGIN) still work,
  which is why AceAddon kept loading fine while combat tracking
  stayed silently broken.
- Solution: register every restricted event through
  EventRegistry:RegisterFrameEventAndCallback(event, fn, owner).
  Callback signature is (ownerID, ...eventArgs) — no event-name
  arg, which differs from the OnEvent script style. DAMAGE_METER_*
  events are custom (not real frame events) so they use
  EventRegistry:RegisterCallback instead.
- COMBAT_LOG_EVENT_UNFILTERED is NO LONGER registered on Retail —
  Blizzard removed addon CLEU access entirely in 12.0. Damage
  data flows through C_DamageMeter exclusively (the server-combat
  ticker in OnEnable polls C_DamageMeter.GetCombatSessionFromType
  each tick and UpdateFromDamageMeter renders from it).
- ENCOUNTER_STATE_CHANGED dropped — it is a custom callback on
  EncounterJournal, not a real frame event. ENCOUNTER_START /
  ENCOUNTER_END (real events, registered via EventRegistry) cover
  the encounter-tracking need.
- TOC: bumped Interface 120001 -> 120005 and Version 2.4.3 -> 2.4.8.
- Applied to Core_Retail.lua (10 frame events + 3 C_DamageMeter
  callbacks) and Parser_Retail.lua (6 frame events).
- OnDisable now unregisters via EventRegistry:UnregisterFrameEventAndCallback
  and UnregisterCallback so a /reload doesn't double-register.
- Core:RegisterEvents() and Parser:RegisterEvents() are now
  no-op stubs (kept for API compatibility).

Known follow-up: BroadcasterTools.lua and ArenaSummary.lua still
use legacy RegisterEvent for UNIT_SPELLCAST_*, COMBAT_LOG_EVENT_*,
PVP_MATCH_COMPLETE. Those features (Action Tracker, Event Tracker,
Arena Summary) will hit the same restriction when activated.
Migration to EventRegistry planned for next release.

Note: earlier 2.4.8 attempts (pcall wrap, dedicated frame,
C_Timer.After defer, file-load registration, Interface bump,
Frame:RegisterEventCallback) all failed because they fixed the
wrong layer. RegisterEventCallback is a CallbackRegistryMixin
method whose event set is closed and mixin-local — it rejects
raw frame events like PLAYER_REGEN_DISABLED. EventRegistry is
the right entry point.

Version 2.4.7
-------------
Broadcaster Tools (Retail only)
- NEW: Action Tracker overlay — shows your spell casts in a scrolling bar
  list. Cast bars fill as you cast; instant casts show a 1.2s decay. Tracks
  UNIT_SPELLCAST_* events for full cast state (start, success, interrupt,
  channel). Enable/Disable from Broadcaster Tools settings tab.
- NEW: Event Tracker overlay — shows nearby combat events: defensive
  cooldowns, offensive cooldowns (90s+), crowd control, and spell interrupts.
  Events appear as color-coded rows with a 1-second fill animation.
  Built-in curated spell database covering all classes.
- NEW: Arena DPS Bar — split bar comparing your team's damage vs the enemy
  team in arena. 5-second rolling window with 100ms sampling. Activates
  automatically when entering an arena zone.
- Broadcaster Tools tab is now Retail-only (hidden on Classic/TBC/Wrath/MoP).
- Config UI: Enable/Disable + Options buttons for each tracker (like Details).
- "Disable Mythic+ Stuff" toggle now finds the Mythic Dungeon button
  dynamically instead of hardcoding index [4].

Easter Egg
- NEW: Snake minigame in Settings sidebar! Click the Snake tab,
  click the grid, and use Arrow keys or WASD to play. Space to
  pause, R to restart. Speed increases as you eat. High score
  saved per profile. A little something for raid downtime.
- Fixed Snake game freezing / becoming unresponsive after a while.
  Root cause: EditBox keyboard capture lost focus (ESC, clicking
  elsewhere, combat lockdown) with no recovery path. Now auto-pauses
  on focus loss with a clear "Click to resume" overlay. Click or
  press R to restart after Game Over without needing keyboard focus.
- Fixed Snake grid clipped on left side. Grid was offset by
  -SIDEBAR_WIDTH/2 from the scroll child center; now centred at 0.
- Fixed Click to Play — clicking the grid now immediately starts
  the game (snake moves right). Previously only keyboard worked.
- Fixed Snake 180-degree turn death: direction checks now use the
  queued direction instead of the current one, preventing instant
  self-collision from fast opposite-key presses.
- Snake visual overhaul:
  - Animated effects: death flash (red overlay fade-out), screen
    shake on death, floating "+10" score popups that rise and fade,
    golden level-up flash when ranking up.
  - Rank system: Worm → Snake → Serpent → Viper → Python → Wyrm
    (WoW item-quality colours: green/blue/purple/orange/red/gold).
  - Speed bar in header showing current game speed percentage.
  - Animated score counter that smoothly counts up on eat.
  - Persistent stats: games played, total food eaten, longest snake.
  - Golden shimmering food with dual-frequency pulse.
  - Brighter snake head, distinct neck, smoother body gradient.
  - Dimmed overlay backgrounds for all states with 3 text lines.
  - Red-tinted grid on death, darker grid during play.
  - Outer glow border on grid, icon in title.
  - Footer: high score (gold), stats, controls hint.
  - Proper cleanup (OnUpdate + EditBox) when switching tabs.

Debug Console & Debug Parity
- NEW: In-game Debug Console — CMD-like window showing debug output,
  errors, and system messages in real-time. Color-coded by category
  (CORE=green, COMBAT=orange, ERROR=red, DATA=cyan, PVP=magenta,
  PERF=teal, EVENT=blue, WARN=orange). 500-line ring buffer,
  copy-pasteable text. Toggle with /edm console. All clients.
- Console toolbar: one-click buttons for Clear, Diag, Errors, Mem, GC.
- Console filter tabs: click category tabs (CORE, COMBAT, DATA, ERROR,
  PVP, UI, PARSER, EVENT, SYSTEM) to show/hide specific log categories.
  "All" button resets filters.
- Console Tab-completion: press Tab to autocomplete commands.
- Console commands: help, clear, diag, errors, log [n] [cat], debug
  on|off, info, events, mem, gc, segments, search <text>, fps,
  profile, api, lua <code>, reset, reload.
- Console "lua" command: execute arbitrary Lua in a sandboxed env with
  EDM namespace access. Useful for live addon inspection.
- Console "api" command: checks availability of all relevant WoW APIs.
- Console "segments" command: shows current + stored segment details.
- Console "search" command: find matching lines in console history.
- Console "fps" command: shows framerate and network latency.
- Console "profile" command: shows active profile and available list.
- NEW: Dedicated "Debug" tab in Settings sidebar — full diagnostic
  panel with inline pass/fail results, error log viewer (last 15
  errors with timestamps), system info panel (addon version, TOC,
  client, memory, FPS, latency, segments, profile), WoW API
  availability checker (13 APIs), and quick-action buttons (Open
  Console, Run Diagnostic, Show Errors).
- Debug/Record Timeline toggles moved from Advanced section into the
  new Debug tab's General section.
- Debug Console now embedded directly in the Settings Debug tab:
  scrollable output area, toolbar buttons (Clear, Diag, Errors, Mem,
  Info), command input with > prompt, Tab-completion, and command
  history (Up/Down arrows). Matches the addon's own design language.
  Live-refresh: output updates automatically as new log entries arrive.
  /edm console now opens Settings on the Debug tab instead of a popup.
  Standalone BugSack-style window still available via DC:Toggle().
- Fixed Unicode square characters in diagnostic output and settings.
  Replaced checkmark (U+2713) and cross (U+2717) with ASCII "OK"/"X"
  — WoW's game fonts lack these glyphs, causing visible squares.
- Hardcoded SelectTab(6) for Profiles tab replaced with dynamic
  FindTabIndex("BuildProfilesTab") lookup — tab indices no longer
  break when new tabs are added.
- Classic debug parity: /edm diag, /edm errors, /edm log [n] [cat],
  and /edm console now available on Classic Era, TBC Anniversary,
  MoP Classic, and Wrath Titan Forged (previously Retail-only).

Arena Summary (Retail only)
- Fixed Arena Summary showing wrong/missing data compared to WoW's
  default scoreboard. Enemy team was empty, friendly team showed tiny
  numbers (e.g. 45K instead of 915K). Root cause: CLEU combat log data
  is restricted in WoW 12.x arenas. Switched primary data source to
  C_PvP.GetScoreInfo() — the same API WoW's own scoreboard uses.
  Falls back to CLEU segment data on older clients.
- NEW: Arena match history — matches are saved persistently (up to 50)
  in your profile. Browse past matches with Prev/Next navigation
  buttons after the arena has ended. Each saved match stores team
  rosters, damage, healing, deaths, kills, rating, and rating change.
- NEW: Rating change display — shows +/- rating in green/red next to
  player names when available from the scoreboard API.
- NEW: Data source indicator — label shows whether data comes from
  "Scoreboard" (C_PvP API), "Combat Log" (CLEU fallback), or
  "No data" so you know the source at a glance.
- NEW: Date/time shown in subtitle for saved matches when browsing
  history.

Spell Casts Tracking
- NEW: "Casts" column in spell tooltip — hover any player bar (Damage,
  Healing, DPS, etc.) to see how many times each spell was cast. Shown
  alongside the existing Total / DPS / % columns in a light blue color.
  Cast data comes from SPELL_CAST_SUCCESS events via CLEU.
- NEW: "Casts" display mode — dedicated bar view ranking players by total
  spell casts. Cycles alongside existing modes in the menu and rotation.
  Tooltip shows per-spell cast breakdown with count and percentage.
- Pet casts are attributed to owners via GetEffectiveSource mapping.
- Works on all clients (Retail, Classic, TBC, Wrath, MoP).

Bug Fixes
- Fixed Action Tracker entries disappearing immediately after cast
  completes. Entries now stay visible in their final state (green for
  success, red for interrupted) with a gentle fade, and are only
  removed when pushed off by newer casts. Channels and instants also
  persist instead of vanishing after 1.2-2s.
- Fixed "cannot be indexed with secret keys" errors in arena/PvP.
  UnitGUID() returns tainted secret values for arena/nameplate units
  in WoW 12.x. Added SafeGUID/SafeName wrappers with issecretvalue()
  guards in Parser_Retail (NAME_PLATE_UNIT_ADDED, AddPetForUnit,
  UpdatePetMapping) and ArenaSummary (BuildRosterMaps).
- Debug Console now captures real Lua runtime errors (not just
  manual LogError calls). Hooks into BugGrabber if installed,
  otherwise wraps the global error handler via seterrorhandler().
  Only shows errors from EpicDamageMeter files. Errors include
  stack traces (first 3 frames) and occurrence count.
- Fixed DPS/HPS colored bars invisible during combat. Bar width used
  amountPerSecond (rate, e.g. 8.9K) against topValue=session.maxAmount
  (total, e.g. 890K), producing ~1% fill. Now always uses totalAmount
  for bar width — proportions are identical since all actors share the
  same combat duration.
- Fixed 5 diagnostic FAIL results (Constants, MainFrame, Config UI,
  Graph, DetailWindow). RunDiagnostic was checking wrong namespace
  paths (EDM.C, EDM.UI.MainFrame, EDM.UI.Config, EDM.UI.Graph,
  EDM.UI.DetailWindow). Corrected to actual paths (EDM.Constants,
  EDM.UI, EDM.Config, EDM.Graph, EDM.DetailWindow).

Version 2.4.6
-------------
Full Compatibility Audit — Retail + Classic/TBC
- Fixed ADDON_ACTION_FORBIDDEN error spam (6x) on TBC Anniversary Edition.
  AceEvent frame was named, causing Blizzard taint tracking to block
  RegisterEvent during combat. Changed to unnamed frame. Also defers event
  registration if the addon loads during combat (InCombatLockdown guard).
- Fixed ADDON_ACTION_BLOCKED on mode menu open: SetPropagateKeyboardInput
  is a protected function on Retail/TBC Anniversary. Replaced with
  UISpecialFrames for ESCAPE-to-close (standard non-protected pattern).
- Fixed Overhealing and Threat modes showing no data on TBC Anniversary.
  Root cause: ADDON_ACTION_FORBIDDEN blocked CLEU event registration →
  parser never received combat log events → database stayed empty.
- Fixed DPS/HPS bar display to match Damage Done/Healing Done format.
  Now shows "52.3K (476 DPS) 43.4%" instead of "476/s 43.4%".
- Fixed "table index is nil" crash at Parser_Classic.lua:506 when taking
  fall damage (ENVIRONMENTAL_DAMAGE). sourceGUID is nil for environmental
  events — can't be used as a table key. Now uses "Environment" fallback.
- Fixed SetGradient API crash on Classic and TBC Anniversary. The wrapper
  now auto-probes at first call: tries new 2-arg CreateColor form, if it
  errors falls back to old 7-arg form. Previous detection via GetBuildInfo
  TOC version was unreliable on TBC Anniversary (Retail client, but
  reported Classic-like version). All 60+ SetGradient calls across
  MainFrame, Config, DetailWindow, Graph, Skins, Widgets, ArenaSummary,
  DeathRecap now go through the wrapper.
- Fixed ColorPickerFrame:SetupColorPickerAndShow() crash on Classic/TBC.
  This API is Retail 10.2.5+ only. Added compat path using legacy
  ColorPickerFrame direct-property method for Classic clients.
- Fixed SecretValue arithmetic crash in Threat mode (Retail PvP). The
  threat value can be wrapped by WoW 12.0+ SecretValue. Now washed
  through tonumber(tostring()) before arithmetic.
- Fixed mode menu ESCAPE-to-close broken by OnShow overwrite. Two
  SetScript("OnShow") handlers were registered on the same frame —
  the second killed the first (which enabled keyboard propagation).
  Merged into a single OnShow handler.
- Fixed Database.lua nil crash when profile.combat sub-table is nil.
  All 5 occurrences of EDM.db.profile.combat.X now have full nil-guard
  chain.
- Fixed C_DamageMeter session fetch crash on Retail. GetCombatSessionFrom
  Type/ID calls can throw if the session is tainted. Now wrapped in pcall.
- Fixed duplicate "Max Spells" slider in tooltip settings (one with wrong
  range 3-15 and default 6 vs correct 3-30 default 30). Removed duplicate.
- Fixed Skins.list nil crash in Setup Wizard Step 2 when Skins module
  hasn't loaded yet.
- Improved client detection: granular per-expansion flags (isVanilla, isTBC,
  isWrath, isCata, isMoP) instead of binary isRetail/isClassic. isRetail is
  now strictly Midnight (12.x+). Added hasMythicPlus flag (Retail only).
- Mythic Dungeon settings tab now hidden on non-Retail clients (Classic Era,
  TBC Anniversary, Wrath, MoP). Config sidebar dynamically filters categories
  based on client capabilities.