diff --git a/AnySCT.toc b/AnySCT.toc
index 3fa2518..920ec86 100644
--- a/AnySCT.toc
+++ b/AnySCT.toc
@@ -2,7 +2,7 @@
## Title: AnySCT
## Notes: Blizzard-pet-style floating combat text for any units you pick: tanks, their pets and totems, mobs, enemy players.
## Author: ShempError
-## Version: 0.3.0
+## Version: 0.3.1
## X-Website: https://octowow.st/git/ShempError/AnySCT
## OptionalDeps: pfUI, CT_RaidAssist, oRA, TWThreat
## SavedVariables: AnySCTDB
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f8078f..35acb0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
All notable changes to AnySCT are documented here.
+## 0.3.1 — 2026-10-15
+
+### Fixed
+- Lines in a tower no longer overlap: each line reserves its real height (crits at the peak of their size pop) plus the suffix line hanging below the line above it, and crits now rise with the rest of the stack instead of standing still, so newer lines can no longer rise into them.
+- Crits of tiny procs (relative to the source's average hit) pop at up to half size.
+
## 0.3.0 — 2026-10-15
### Added
diff --git a/README.md b/README.md
index 62d4de2..e2be61f 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,14 @@ are on the list, your own SCT addon stays the only one that shows your damage).
*A listed tank hitting the training dummies in Orgrimmar, seen from another character: an orange melee
"Miss", a gold spell "Resist", and the experimental WAIT label under the dummy being attacked.*
+## What's new — 0.3.1 · 2026-10-15
+
+- **The line tower no longer overlaps.** Every line reserves its real height, including the crit size pop
+ and the spell / name line under it, and crits now rise with the rest of the stack, so newer lines can
+ not run into them.
+- **Crits of tiny procs pop smaller** (a crit "3" among Fire Strike ticks is half size), measured against
+ the source's average hit.
+
## What's new — 0.3.0 · 2026-10-15
- **Identical hits are merged.** Fire Strike style tick storms become one line with the running sum
@@ -45,6 +53,8 @@ are on the list, your own SCT addon stays the only one that shows your damage).
**Version history** — details in [CHANGELOG.md](CHANGELOG.md):
+- **0.3.1** (2026-10-15) — fix: the line tower no longer overlaps (real line heights, crits rise with the
+ stack), crits of tiny procs pop smaller.
- **0.3.0** (2026-10-15) — merged identical hits, "Nameplate + anchor" placement, a demo that plays your own
targetSCT's class burst and previews GO / WAIT, anchor drop fix.
- **0.2.0** (2026-10-15) — targetSCT rename, name option, per-targetSCT colours, regrouped options with
@@ -57,7 +67,7 @@ are on the list, your own SCT addon stays the only one that shows your damage).
Everything floats over the **victim** of the event, never over the source:
- **Melee in orange, special attacks in gold, heals in green.** Numbers use the client's own damage font,
- no outline, a black shadow. Ordinary hits rise and fade; **crits pop** at a larger size and stay in place.
+ no outline, a black shadow. Ordinary hits rise and fade; **crits pop** at a larger size and rise with the stack.
- **Words for what did not land**: Miss, Dodge, Parry, Block, Resist, Immune, ... in the same style.
- **Every hit is shown**: up to 16 lines per mob and 60 on screen by default, so a fast burst is not
recycled early (both are sliders on the Text tab).
@@ -71,11 +81,18 @@ Everything floats over the **victim** of the event, never over the source:
*A crit "366" popping over the Heroic Training Dummy, a "Block" in grey, and a GO label partly covered by
the crit.*
-
-
-*The Demo button at the fixed anchor (no target): a shaman burst with spell names on - a 783 Stormstrike
-crit, a white hit, merged Fire Strike ticks (7), Miss / Dodge / Parry / Block, and the GO / WAIT preview
-"WAIT 1200 / 1500" inside the anchor box.*
+
+
+
+ 
+ WAIT at the anchor — the Demo's shaman burst, no overlaps: a faded 783 Stormstrike on top, Dodge, 97, a merged 7 Fire Strike, Parry, Block, 100 Ancient Accord, a half-size crit 3 Fire Strike, and "WAIT 1350 / 1500" inside the anchor box
+ |
+
+ 
+ GO at the anchor — later in the burst: a fading Ancient Accord, 3 Fire Strike, Resist, 7 Fire Strike, a big crit 216, 194, a crit 760 Earth Shock, and "GO +1875" inside the box
+ |
+
+
## GO / WAIT threat label (experimental)
@@ -236,7 +253,7 @@ six sliders including the line limits) below.*
| Direction | text rises (Up) or sinks (Down) | Up |
| Number size | factor on the engine size, 0.5-3 | 1.00 |
| Word size | same for Miss / Dodge / Parry ... | 1.00 |
-| Rise (px) | how far a normal text rises, 0-300; crits stay in place | 120 |
+| Rise (px) | how far a normal text rises, 0-300; crits rise with the stack | 120 |
| Jitter (px) | random horizontal offset per line, 0-40 | 0 |
| Max lines | lines on screen over all mobs, 3-100; the oldest is recycled at the limit | 60 |
| Max lines per mob | lines over one mob, 1-30; lines live 1.5 s, so 16 shows every hit of a burst of about 10 per second | 16 |
diff --git a/options.lua b/options.lua
index 8eef1f9..aa51dd8 100644
--- a/options.lua
+++ b/options.lua
@@ -97,7 +97,7 @@ if CreateFrame then
"Size factor of damage numbers. Size = 0.018333 x screen diagonal x factor (crits 0.0275): 23 px at 1024x768, 54 px at 2560x1440." },
{ "wordFactor", "Word size", 0.5, 3, 0.05, "%.2f", "Size factor of Miss / Dodge / Parry / ..." },
{ "riseHeight", "Rise (px)", 0, 300, 5, nil,
- "How far a normal text rises (crits stay in place). The engine rises 2 yards; the screen distance depends on the camera." },
+ "How far a normal text rises (crits rise with the stack). The engine rises 2 yards; the screen distance depends on the camera." },
{ "mergeWindow", "Merge window (s)", 0.1, 1.5, 0.05, "%.2f",
"How long a line keeps absorbing identical hits, counted from its first hit. Short: more, smaller lines. Long: fewer lines that update longer. Only used with Merge identical hits." },
{ "jitter", "Jitter (px)", 0, 40, 1, nil, "Random horizontal offset per line. 0 like the engine; lines stack instead." },
diff --git a/screenshots/anchor-go.png b/screenshots/anchor-go.png
new file mode 100644
index 0000000..b60a4d6
Binary files /dev/null and b/screenshots/anchor-go.png differ
diff --git a/screenshots/anchor-wait.png b/screenshots/anchor-wait.png
new file mode 100644
index 0000000..f63ca82
Binary files /dev/null and b/screenshots/anchor-wait.png differ
diff --git a/screenshots/demo-anchor-tower.png b/screenshots/demo-anchor-tower.png
deleted file mode 100644
index 2f675b0..0000000
Binary files a/screenshots/demo-anchor-tower.png and /dev/null differ
diff --git a/sct.lua b/sct.lua
index aedea27..c3a3ac0 100644
--- a/sct.lua
+++ b/sct.lua
@@ -24,7 +24,6 @@ AnySCT_STYLE = {
fadeOutWord = 1.0, -- Miss / Dodge / Parry / Block ...
fadeOutAbsorb = 0.09, -- "Absorb" is a quick flicker
riseHeight = 120, -- px a normal text rises (engine: 2 world units; screen distance varies)
- critRiseFactor = 0, -- crits do not rise, they pop in place
sizeScale = 0.018333, -- text pixel size = scale x screen diagonal (1024x768 -> 23 px)
critSizeScale = 0.0275, -- crits (1024x768 -> 35 px)
shadow = 0.002, -- shadow offset, fraction of the viewport per axis (down-right)
@@ -230,6 +229,43 @@ S.ANCHOR_W, S.ANCHOR_H, S.ANCHOR_LABEL_SIZE, S.PLATE_LABEL_SIZE = 200, 30, 12, 1
function S.EstTextWidth(text, size) return string.len(text) * size * 0.62 end
-- Where anchor-mode damage lines start so they clear the box (and the list label above it).
function S.AnchorLineOffset(showLabel) return showLabel and 16 or 2 end
+-- ---- stack geometry -------------------------------------------------------------------------
+-- A line occupies [bottom - suffixH, bottom + numberH]: the number sits on its anchor point, the
+-- suffix (spell / name) hangs below it. Crits are reserved at the peak of their size pop. A new line
+-- is inserted at the bottom; every older line of the same mob moves up by the new line's height plus
+-- the suffix height of the line directly above it, which is what makes the new line fit under it.
+function S.CritPeak(kf)
+ local m = 1
+ for i = 1, table.getn(kf) do if kf[i][2] > m then m = kf[i][2] end end
+ return m
+end
+-- Distance of a line's anchor point from its start: the same rise rate for every line of a tower, so
+-- older lines (larger t) only ever move away from newer ones.
+function S.LineOffset(t, dur, rise, push) return t / dur * rise + push end
+function S.NumberHeight(base, isCrit, peak) return isCrit and base * peak or base end
+function S.SuffixHeight(base, hasSuffix)
+ if not hasSuffix then return 0 end
+ local sz = math.floor(base * 0.3)
+ if sz < 10 then sz = 10 end
+ return sz + 2
+end
+function S.StackAdvance(base, isCrit, peak, prevSuffixH)
+ return S.NumberHeight(base, isCrit, peak) + (prevSuffixH or 0)
+end
+-- A crit of a tiny proc must not pop at full crit size: scale by the amount against the source's
+-- running average hit (0.5 .. 1).
+function S.CritFactor(amount, ref)
+ if not ref or ref <= 0 then return 1 end
+ local f = (amount or 0) / ref
+ if f > 1 then return 1 end
+ if f < 0.5 then return 0.5 end
+ return f
+end
+function S.UpdateRef(ref, amount)
+ if not ref then return amount end
+ return ref + (amount - ref) * 0.1
+end
+
-- ---- merging identical attacks ---------------------------------------------------------------
-- Is the result a word (Miss / Dodge / ...)? Same rule as S.Format.
function S.IsWordResult(res, opt)
@@ -370,6 +406,8 @@ if CreateFrame then
local POOL_MAX = 100 -- slot tables only; FontStrings are created on first use (ensureFs)
local critAnimated = S.CurveAnimated(STYLE.critKeyframes)
local critActive = S.CurveActive(STYLE.critKeyframes)
+ local avgHit = {} -- per source: running average hit, for the crit pop scale
+ local critPeak = S.CritPeak(STYLE.critKeyframes)
local GAP = 3 -- px between the plate top and the text
local anchor, float, listLabel
@@ -548,13 +586,14 @@ if CreateFrame then
if t < STYLE.fadeIn then a = t / STYLE.fadeIn end
if t > s.fadeOut then a = a * (s.dur - t) / (s.dur - s.fadeOut) end
local rise = s.rise
+ -- crits rise with the tower like every other line: a line that stood still would
+ -- let the newer, rising lines below it run into it
if s.crit then
- rise = rise * STYLE.critRiseFactor
if critAnimated then
s.fs:SetTextHeight(s.base * S.KeyScale(STYLE.critKeyframes, t / s.dur))
end
end
- local off = (t / s.dur * rise + s.push) * s.dir
+ local off = S.LineOffset(t, s.dur, rise, s.push) * s.dir
s.fs:ClearAllPoints()
if s.plate then
needScan = true
@@ -632,9 +671,26 @@ if CreateFrame then
local base = S.TextSize(scale, factor, sw, sh)
s.base = base * mul
- -- older lines of the same mob move one text height away
+ -- tiny crits (procs) pop smaller: scale by the amount against the source's running average hit
+ local refKey = res.source or "?"
+ if not isWord and res.kind ~= "heal" then
+ if s.crit then s.base = s.base * S.CritFactor(res.amount, avgHit[refKey]) end
+ avgHit[refKey] = S.UpdateRef(avgHit[refKey], res.amount or 0)
+ end
+ s.sfxH = S.SuffixHeight(s.base, suffix ~= nil)
+
+ -- older lines of the same mob make room for this line's real height (crit peak included) plus
+ -- the suffix hanging below the line right above it
+ local prevSfx, prevT = 0, nil
for j = 1, POOL_MAX do
- if slots[j].active and slots[j].key == key then slots[j].push = slots[j].push + s.base end
+ local m = slots[j]
+ if m.active and m.key == key and j ~= i and (not prevT or m.t < prevT) then
+ prevT, prevSfx = m.t, m.sfxH or 0
+ end
+ end
+ local adv = S.StackAdvance(s.base, s.crit, critPeak, prevSfx)
+ for j = 1, POOL_MAX do
+ if slots[j].active and slots[j].key == key and j ~= i then slots[j].push = slots[j].push + adv end
end
s.active = true