BulwarkFrame v0.2.0
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
## Title: BulwarkFrame
|
||||
## Notes: Compact real-time readout for the shaman Earthen Bulwark. Requires SuperWoW.
|
||||
## Author: ShempError
|
||||
## Version: 0.2.0-dev
|
||||
## Version: 0.2.0
|
||||
## SavedVariables: BulwarkFrameDB
|
||||
|
||||
core\env.lua
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to BulwarkFrame are documented here.
|
||||
|
||||
## v0.2.0 — 2026-08-15
|
||||
|
||||
### Fixed
|
||||
- **Max health readout stays accurate through stamina buffs and level-ups.**
|
||||
It was previously only re-read on zone-in and gear changes, so a Fortitude,
|
||||
Gift of the Wild, food buff, or world buff picked up mid-raid left every
|
||||
number on the readout roughly 10% low until the next zone change. It now
|
||||
refreshes on any player max-health change and on level-up.
|
||||
- **"Combat only" no longer gets stuck off after a mid-fight reload.** The
|
||||
combat flag was only ever set by entering or leaving combat, so a `/reload`
|
||||
taken during a fight left it false for the rest of the encounter and hid the
|
||||
frame outright for anyone using the "Combat only" option. It is now seeded
|
||||
from the live combat state on login and on entering the world.
|
||||
- **The frame can no longer get stranded off-screen.** `/bulwark reset` and
|
||||
the options panel's Reset button wrote the default position into the saved
|
||||
settings but never moved the on-screen frame, so a frame dragged to the
|
||||
edge stayed there until the next login. Reset now actually re-anchors the
|
||||
frame, which is also clamped so it cannot be dragged off-screen in the
|
||||
first place.
|
||||
- **Right-clicking the minimap button now reliably hides the "SuperWoW
|
||||
required" hint.** The manual-hide flag was checked after that hint, so
|
||||
without SuperWoW installed the hint frame could never be dismissed, even
|
||||
though the minimap toggle reported it as hidden.
|
||||
- **`/bulwark show` and `/bulwark demo` now actually bring the frame back.**
|
||||
Both commands cleared "hide when idle" and "combat only" but left a manual
|
||||
minimap-toggle hide in place, so the frame stayed off-screen and demo mode
|
||||
ran invisibly. Both commands now clear the manual hide as well, and say so
|
||||
in their chat confirmation.
|
||||
- **The options panel no longer shows stale values after a slash command.**
|
||||
`/bulwark reset`, `/bulwark lock`, `/bulwark show`, and `/bulwark demo`
|
||||
changed settings without refreshing an already-open options panel, so its
|
||||
checkboxes, sliders, and buttons kept showing the pre-command state —
|
||||
nudging a stale slider could even silently undo what the command had just
|
||||
done. The panel now re-syncs itself after every one of these commands.
|
||||
- **The absorb-rate readout no longer shows a number for an unlearned
|
||||
talent.** A talent rank of zero was treated as "no value set" rather than
|
||||
"rank 0", so an additive set-bonus reading could produce a plausible-looking
|
||||
absorb rate for a talent the character never trained. It now correctly
|
||||
shows `--` in that case.
|
||||
|
||||
### Changed
|
||||
- **Lower CPU usage while the buffer is inactive.** The update loop now
|
||||
detaches itself once there is nothing left to animate — no active buffer,
|
||||
no swing in progress — instead of redrawing five times a second regardless,
|
||||
and re-attaches automatically the moment a swing lands or the buffer
|
||||
refills.
|
||||
- **Removed the addon's remaining per-tick and per-swing memory
|
||||
allocation.** The colour-threshold lookup and the aura scan that runs on
|
||||
every incoming swing no longer build and discard temporary tables and
|
||||
closures, avoiding needless garbage-collector churn during combat.
|
||||
@@ -81,10 +81,10 @@ a `git pull`.
|
||||
| Command | Does |
|
||||
|---|---|
|
||||
| `/bulwark` or `/bulwark options` | opens the options panel (also: left-click the minimap button) |
|
||||
| `/bulwark demo` | runs the display off a synthetic 12 s cycle — judge colours and sizes without a fight |
|
||||
| `/bulwark demo` | runs the display off a synthetic 12 s cycle — judge colours and sizes without a fight (clears the manual hide, so the frame is actually on screen) |
|
||||
| `/bulwark probe` | dumps the live aura / weapon / talent data to chat **and**, with SuperWoW, to `imports\bulwark_probe.txt` |
|
||||
| `/bulwark lock` | locks or unlocks dragging |
|
||||
| `/bulwark show` | pins the frame visible (turns off hide-when-idle and combat-only) |
|
||||
| `/bulwark show` | pins the frame visible (clears the manual hide and turns off hide-when-idle and combat-only) |
|
||||
| `/bulwark reset` | restores every setting to its default |
|
||||
|
||||
`/bf` works as a short form of all of them. Right-clicking the minimap button shows or hides the
|
||||
@@ -139,8 +139,15 @@ lua50 tools/luatests/test_calc.lua
|
||||
lua50 tools/luatests/test_swing.lua
|
||||
lua50 tools/luatests/test_env.lua
|
||||
lua50 tools/luatests/test_config.lua
|
||||
lua50 tools/luatests/test_slash.lua
|
||||
```
|
||||
|
||||
`test_slash.lua` is the exception to "no WoW API offline": it drives the real slash commands and
|
||||
the real options panel against a small widget stub. The questions it asks — which of the three
|
||||
suppressors wins, and whether a command re-syncs an open panel — are pure control flow, and that
|
||||
is reproducible without a client. Rendering, templates and event ordering are not, and stay
|
||||
in-game work.
|
||||
|
||||
**Looking at the UI without the game:** `node tools/uipreview/render.js` draws the frame as a PNG
|
||||
across six states (full → empty, including both colour steps) at 3× into `tools/uipreview/out/`.
|
||||
Same geometry and colour buckets as the addon, so the look can be iterated without a client
|
||||
|
||||
+4
-1
@@ -168,7 +168,10 @@ C.RANK_RATE = { [0] = 0, [1] = 0.05, [2] = 0.10, [3] = 0.15 }
|
||||
-- neither is assumed; the caller decides once measurement settles it.
|
||||
function C.rateForRank(rank, addBonus, multBonus)
|
||||
local base = C.RANK_RATE[rank or -1]
|
||||
if not base then return 0 end
|
||||
-- `<= 0` and not just `not base`: rank 0 maps to a rate of 0, and 0 is truthy in Lua, so a
|
||||
-- bare nil check would let an additive set bonus resurrect a rate for a talent the character
|
||||
-- has not learned -- exactly the confidently wrong number this guard exists to prevent.
|
||||
if not base or base <= 0 then return 0 end
|
||||
if type(addBonus) == "number" then base = base + addBonus end
|
||||
if type(multBonus) == "number" then base = base * multBonus end
|
||||
return base
|
||||
|
||||
@@ -32,6 +32,43 @@ BF.state = {
|
||||
|
||||
-- ---- buff scan --------------------------------------------------------------------------
|
||||
|
||||
-- Walks one of the 64-slot unit aura enumerations looking for the buffer, and returns the
|
||||
-- (possibly raised) stack count plus the found flag.
|
||||
--
|
||||
-- Deliberately a file-scope function taking its inputs as arguments rather than a closure built
|
||||
-- inside ScanAuras: as a closure it allocated one object per CALL plus one more per aura SLOT (the
|
||||
-- per-slot pcall wrapper), and ScanAuras is the PLAYER_AURAS_CHANGED handler -- an event that
|
||||
-- fires on every stack change of the buffer, i.e. on every absorbed hit. Roughly two dozen
|
||||
-- throwaway closures per incoming melee swing is exactly the GC churn the rest of this addon
|
||||
-- takes care to avoid.
|
||||
--
|
||||
-- The per-slot pcall went with it. It was never per-slot error recovery either: the old code
|
||||
-- aborted the WHOLE walk as soon as one slot failed, so a single pcall around the walk (see the
|
||||
-- call sites) covers the same ground -- and UnitBuff/UnitDebuff on "player" with an integer index
|
||||
-- does not throw to begin with. The one behavioural difference is that a walk which errors part
|
||||
-- way through now contributes nothing instead of its partial count, which is the safer of the two.
|
||||
--
|
||||
-- Note the id position differs between the two calls -- an easy trap: reading return 4 from
|
||||
-- UnitBuff (or 3 from UnitDebuff) silently yields nil and the aura is never matched.
|
||||
local function scanStacks(st, fn, idPos, poolId, timeId, stacks, found)
|
||||
local i = 1
|
||||
while i <= 64 do
|
||||
local r1, r2, r3, r4 = fn("player", i)
|
||||
if r1 == nil then break end
|
||||
local id = (idPos == 4) and tonumber(r4) or tonumber(r3)
|
||||
if id == poolId or id == timeId then
|
||||
local n = tonumber(r2)
|
||||
if n and n > stacks then
|
||||
stacks = n
|
||||
found = true
|
||||
st.hasPool = true
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return stacks, found
|
||||
end
|
||||
|
||||
-- Scans the player's buffs once and writes pool/time into BF.state.
|
||||
-- GetPlayerBuff walks HELPFUL slots; GetPlayerBuffID (SuperWoW) gives the spell id, which is the
|
||||
-- whole reason SuperWoW is a hard requirement -- matching the icon texture instead would collide
|
||||
@@ -83,28 +120,17 @@ function BF.ScanAuras()
|
||||
-- The durability aura runs as a DEBUFF, and the HELPFUL player-buff enumeration above never
|
||||
-- lists it. Reading only that API is why a full buffer reported a single stack -- and one stack
|
||||
-- renders as 61 against a maximum of 6117: a plausible number, wrong by a factor of a hundred.
|
||||
-- Note the id position differs between the two calls -- an easy trap: reading return 4 from
|
||||
-- UnitBuff (or 3 from UnitDebuff) silently yields nil and the aura is never matched.
|
||||
local scanStacks = function(fn, idPos)
|
||||
local i = 1
|
||||
while i <= 64 do
|
||||
local r1, r2, r3, r4
|
||||
local ok = pcall(function() r1, r2, r3, r4 = fn("player", i) end)
|
||||
if not ok or r1 == nil then break end
|
||||
local id = (idPos == 4) and tonumber(r4) or tonumber(r3)
|
||||
if id == poolId or id == timeId then
|
||||
local n = tonumber(r2)
|
||||
if n and n > stacks then
|
||||
stacks = n
|
||||
found = true
|
||||
st.hasPool = true
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
--
|
||||
-- The walk itself lives at file scope (see scanStacks above). pcall is handed the arguments
|
||||
-- directly instead of a wrapper closure, so this path allocates nothing at all.
|
||||
if UnitDebuff then
|
||||
local ok, s, f = pcall(scanStacks, st, UnitDebuff, 4, poolId, timeId, stacks, found)
|
||||
if ok then stacks, found = s, f end
|
||||
end
|
||||
if UnitBuff then
|
||||
local ok, s, f = pcall(scanStacks, st, UnitBuff, 3, poolId, timeId, stacks, found)
|
||||
if ok then stacks, found = s, f end
|
||||
end
|
||||
if UnitDebuff then pcall(function() scanStacks(UnitDebuff, 4) end) end
|
||||
if UnitBuff then pcall(function() scanStacks(UnitBuff, 3) end) end
|
||||
|
||||
-- A buffer aura with no applications at all still means "buffer up": 1.12 reports a
|
||||
-- single-stack aura without a count, and treating that as 0 would blank the bar while it is
|
||||
@@ -179,6 +205,15 @@ driver:RegisterEvent("PLAYER_REGEN_DISABLED")
|
||||
driver:RegisterEvent("PLAYER_REGEN_ENABLED")
|
||||
driver:RegisterEvent("UNIT_INVENTORY_CHANGED")
|
||||
|
||||
-- Max health is the base of EVERY number on screen (maxPool = maxHP/5), so it must not be read
|
||||
-- only on zone-in and gear swaps. A stamina buff landing at the raid entrance -- Fortitude, Gift
|
||||
-- of the Wild, food, a world buff -- moves max health without either of those events, and the
|
||||
-- readout would then be ~10% low for the whole encounter. UNIT_MAXHEALTH carries a unit token in
|
||||
-- arg1 and fires for every group member, so it has to be filtered; PLAYER_LEVEL_UP carries the
|
||||
-- new level in arg1 and must NOT go through that filter.
|
||||
driver:RegisterEvent("UNIT_MAXHEALTH")
|
||||
driver:RegisterEvent("PLAYER_LEVEL_UP")
|
||||
|
||||
-- SuperWoW's UNIT_CASTEVENT is the swing source: arg3 is "MAINHAND"/"OFFHAND" for auto-attacks.
|
||||
-- It is language independent and fires on misses too, unlike parsing combat-log text -- which is
|
||||
-- how the third-party swing timers get it wrong on a dodge.
|
||||
@@ -192,15 +227,32 @@ driver:SetScript("OnEvent", function()
|
||||
if arg3 == "MAINHAND" and arg1 == BF.playerGuid then
|
||||
BF.state.lastSwingAt = GetTime()
|
||||
BF.RefreshSwingInputs()
|
||||
-- Redraw explicitly, because this branch returns before the shared RequestUpdate at
|
||||
-- the end of the handler. Without it a swing landing while idle and out of combat
|
||||
-- never re-attaches the update loop and the marker simply does not move -- the one
|
||||
-- wakeup path that is neither an aura nor a combat event. The call stays INSIDE the
|
||||
-- main-hand test on purpose: UNIT_CASTEVENT fires for every cast of every unit in
|
||||
-- range, and redrawing on all of them is exactly the churn this addon avoids.
|
||||
if BF.RequestUpdate then BF.RequestUpdate() end
|
||||
end
|
||||
return
|
||||
end
|
||||
if event == "PLAYER_REGEN_DISABLED" then
|
||||
if event == "UNIT_MAXHEALTH" then
|
||||
-- Health only: no GUID re-capture, no swing re-read. This fires for party and raid
|
||||
-- members too, hence the filter.
|
||||
if arg1 == "player" then BF.state.maxHP = UnitHealthMax("player") or 0 end
|
||||
elseif event == "PLAYER_LEVEL_UP" then
|
||||
BF.state.maxHP = UnitHealthMax("player") or 0
|
||||
elseif event == "PLAYER_REGEN_DISABLED" then
|
||||
BF.state.inCombat = true
|
||||
elseif event == "PLAYER_REGEN_ENABLED" then
|
||||
BF.state.inCombat = false
|
||||
elseif event == "PLAYER_ENTERING_WORLD" or event == "UNIT_INVENTORY_CHANGED" then
|
||||
BF.state.maxHP = UnitHealthMax("player") or 0
|
||||
-- Seeded from the live API, not only from the REGEN transitions: those fire on ENTERING
|
||||
-- combat, never on login, so a /reload taken mid-fight would leave the flag false for the
|
||||
-- rest of the encounter and strand anyone running "Combat only".
|
||||
BF.state.inCombat = UnitAffectingCombat("player") and true or false
|
||||
BF.CapturePlayerGuid()
|
||||
BF.RefreshSwingInputs()
|
||||
end
|
||||
|
||||
@@ -23,29 +23,58 @@ SLASH_BULWARKFRAME2 = "/bf"
|
||||
SlashCmdList["BULWARKFRAME"] = function(input)
|
||||
local cmd = string.lower(input or "")
|
||||
-- No string.match / no ':' string methods -- Lua 5.0.
|
||||
--
|
||||
-- Set by every branch that writes BulwarkFrameDB. The options panel is built once and its
|
||||
-- widgets then hold their own state, so a slash command that changes a setting behind an open
|
||||
-- panel left every checkbox, slider and cycle button showing the pre-command values -- and the
|
||||
-- first nudge of a stale slider writes the discarded value straight back over the change. The
|
||||
-- panel's own Reset button already calls BF.RefreshOptions for exactly this reason; collecting
|
||||
-- it here gives the slash side the same treatment, and keeps a future command from silently
|
||||
-- drifting out of sync again.
|
||||
local dirty = false
|
||||
if cmd == "" or cmd == "options" or cmd == "config" then
|
||||
BF.ToggleOptions()
|
||||
elseif cmd == "demo" then
|
||||
-- Read before the toggle: BF.ToggleDemo clears the manual hide so the frame it promises
|
||||
-- actually appears, and that is worth saying rather than leaving the user to wonder why
|
||||
-- an unrelated setting moved.
|
||||
local wasHidden = BulwarkFrameDB.hidden and true or false
|
||||
local on = BF.ToggleDemo()
|
||||
msg("demo " .. (on and "on" or "off"))
|
||||
if on and wasHidden then
|
||||
msg("demo on (manual hide cleared)")
|
||||
else
|
||||
msg("demo " .. (on and "on" or "off"))
|
||||
end
|
||||
dirty = true
|
||||
elseif cmd == "probe" then
|
||||
BF.RunProbe()
|
||||
elseif cmd == "lock" then
|
||||
BulwarkFrameDB.locked = not BulwarkFrameDB.locked
|
||||
BF.ApplyLayout()
|
||||
msg(BulwarkFrameDB.locked and "frame locked" or "frame unlocked")
|
||||
dirty = true
|
||||
elseif cmd == "reset" then
|
||||
BF.resetConfig()
|
||||
BF.ApplyLayout()
|
||||
BF.RequestUpdate()
|
||||
BF.UpdateMinimapButton()
|
||||
msg("settings reset to defaults")
|
||||
dirty = true
|
||||
elseif cmd == "show" then
|
||||
-- All THREE suppressors, not just the two automatic ones. The manual hide (minimap
|
||||
-- right-click) is a SavedVariable and overrides both of them in RequestUpdate, so leaving
|
||||
-- it set meant '/bulwark show' reported success while the frame stayed off screen -- and
|
||||
-- no other command except a full reset could clear it.
|
||||
BulwarkFrameDB.hidden = false
|
||||
BulwarkFrameDB.hideWhenInactive = false
|
||||
BulwarkFrameDB.hideOutOfCombat = false
|
||||
BF.RequestUpdate()
|
||||
msg("frame pinned visible (hide-when-idle and combat-only turned off)")
|
||||
msg("frame pinned visible (manual hide, hide-when-idle and combat-only turned off)")
|
||||
dirty = true
|
||||
else
|
||||
msg("commands: options | demo | probe | lock | show | reset")
|
||||
end
|
||||
-- Safe unconditionally: the refresher list is empty until the panel has been built, so this is
|
||||
-- a no-op for anyone who never opened it.
|
||||
if dirty and BF.RefreshOptions then BF.RefreshOptions() end
|
||||
end
|
||||
|
||||
@@ -434,6 +434,12 @@ local function BuildPanel()
|
||||
this:SetText(on and "Demo: on" or "Demo")
|
||||
end)
|
||||
demoBtn:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 24, 20)
|
||||
-- The one stateful widget that had no refresher, unlike modeBtn and setBtn above: demo mode can
|
||||
-- also be toggled from '/bulwark demo', and without this the button kept whatever label it last
|
||||
-- set itself -- reading "Demo" over a running demo, or "Demo: on" over a live readout.
|
||||
table.insert(refreshers, function()
|
||||
demoBtn:SetText(BF.IsDemo() and "Demo: on" or "Demo")
|
||||
end)
|
||||
|
||||
local probeBtn = CreateButton(panel, "Probe", "Dump the live aura/weapon data we still need "
|
||||
.. "(also /bulwark probe).", function() BF.RunProbe() end)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-- * nothing is allocated per OnUpdate frame -- no string building, no tables. Text is only
|
||||
-- pushed when it actually changed, and the loop returns early when there is nothing to draw;
|
||||
-- * when the buffer is down and we are out of combat the OnUpdate script is detached entirely
|
||||
-- rather than left spinning on a hidden frame.
|
||||
-- rather than left spinning -- whether the frame is hidden or merely idle on screen.
|
||||
|
||||
BulwarkFrame = BulwarkFrame or {}
|
||||
local BF = BulwarkFrame
|
||||
@@ -23,6 +23,13 @@ local demo = nil -- demo mode state, nil when off
|
||||
local UPDATE_INTERVAL = 0.05
|
||||
local sinceUpdate = 0
|
||||
|
||||
-- Colour-threshold configs, allocated once and refilled in place. Built as table literals inside
|
||||
-- the update loop they were the only per-tick allocation left in this file: two tables every
|
||||
-- 50 ms, created purely to be read twice and thrown away. calc.lua reads .red/.yellow and never
|
||||
-- retains the table, so reusing one instance is safe.
|
||||
local poolCfg = { red = 0, yellow = 0 }
|
||||
local timeCfg = { red = 0, yellow = 0 }
|
||||
|
||||
-- ---- helpers ----------------------------------------------------------------------------
|
||||
|
||||
local function db() return BulwarkFrameDB end
|
||||
@@ -70,9 +77,14 @@ local function BuildFrame()
|
||||
frame = CreateFrame("Frame", "BulwarkFrameMain", UIParent)
|
||||
frame:SetWidth(d.width)
|
||||
frame:SetHeight(60)
|
||||
frame:SetPoint(d.point, UIParent, d.relPoint, d.x, d.y)
|
||||
-- The anchor is applied by ApplyLayout (called at the end of this function), so that reset and
|
||||
-- the options panel go through the SAME code path a fresh login does. Anchoring here as well
|
||||
-- would leave two placement sites and let them disagree.
|
||||
frame:SetScale(d.scale)
|
||||
frame:SetMovable(true)
|
||||
-- Without this the frame can be dragged until only a sliver is left on screen, and the
|
||||
-- documented rescue ('/bulwark reset') then has to be able to fetch it back.
|
||||
frame:SetClampedToScreen(true)
|
||||
frame:EnableMouse(not d.locked)
|
||||
frame:RegisterForDrag("LeftButton")
|
||||
frame:SetScript("OnDragStart", function() if not db().locked then this:StartMoving() end end)
|
||||
@@ -153,6 +165,12 @@ function BF.ApplyLayout()
|
||||
frame:SetWidth(w)
|
||||
frame:SetScale(d.scale)
|
||||
frame:EnableMouse(not d.locked)
|
||||
-- Re-anchor from the saved placement. Without this, '/bulwark reset' (and the panel's Reset
|
||||
-- button) put CENTER/0/-120 back into the config while the frame itself stayed wherever it was
|
||||
-- dragged -- so the documented way to rescue a frame from the screen edge did nothing until
|
||||
-- the next login.
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint(d.point or "CENTER", UIParent, d.relPoint or "CENTER", d.x or 0, d.y or 0)
|
||||
|
||||
if d.showThreshold then
|
||||
poolBar:ClearAllPoints()
|
||||
@@ -273,7 +291,8 @@ local function updateDisplay()
|
||||
if d.showThreshold then
|
||||
local frac = C.barFraction(stacks)
|
||||
poolBar:SetValue(frac)
|
||||
local r, g, b = bucketRGB(C.poolColorName(frac, { red = d.poolRed, yellow = d.poolYellow }))
|
||||
poolCfg.red, poolCfg.yellow = d.poolRed, d.poolYellow
|
||||
local r, g, b = bucketRGB(C.poolColorName(frac, poolCfg))
|
||||
poolBar:SetStatusBarColor(r, g, b)
|
||||
if d.showThresholdText then
|
||||
local points = C.poolPoints(stacks, maxHP)
|
||||
@@ -291,7 +310,8 @@ local function updateDisplay()
|
||||
if d.showExpiry then
|
||||
local tf = C.timeFraction(timeLeft, BF.BUFFER_DURATION)
|
||||
timeBar:SetValue(tf)
|
||||
local r, g, b = bucketRGB(C.timeColorName(timeLeft, { red = d.timeRed, yellow = d.timeYellow }))
|
||||
timeCfg.red, timeCfg.yellow = d.timeRed, d.timeYellow
|
||||
local r, g, b = bucketRGB(C.timeColorName(timeLeft, timeCfg))
|
||||
-- Dimmed on purpose: same buckets, lower alpha, so the threshold bar stays the primary
|
||||
-- readout instead of two equally loud bars sitting on top of each other.
|
||||
timeBar:SetStatusBarColor(r, g, b, d.expiryAlpha or 0.65)
|
||||
@@ -339,6 +359,16 @@ function BF.RequestUpdate()
|
||||
local d = db()
|
||||
local st = BF.state
|
||||
|
||||
-- The deliberate hide is honoured FIRST, ahead of demo mode and ahead of the environment
|
||||
-- hint. Checked further down it was unreachable whenever SuperWoW was missing, so the
|
||||
-- "SuperWoW required" frame could not be dismissed at all: right-clicking the minimap button
|
||||
-- flipped the flag and announced "hidden" while the frame stayed on screen.
|
||||
if d.hidden then
|
||||
frame:Hide()
|
||||
frame:SetScript("OnUpdate", nil)
|
||||
return
|
||||
end
|
||||
|
||||
if demo then
|
||||
frame:Show()
|
||||
if not frame:GetScript("OnUpdate") then frame:SetScript("OnUpdate", BF.OnUpdate) end
|
||||
@@ -355,12 +385,34 @@ function BF.RequestUpdate()
|
||||
|
||||
local shouldShow = st.active or st.inCombat or not d.hideWhenInactive
|
||||
if d.hideOutOfCombat and not st.inCombat then shouldShow = false end
|
||||
if d.hidden then shouldShow = false end -- explicitly hidden via the minimap button
|
||||
|
||||
if shouldShow then
|
||||
hintText:Hide()
|
||||
frame:Show()
|
||||
if not frame:GetScript("OnUpdate") then frame:SetScript("OnUpdate", BF.OnUpdate) end
|
||||
-- "On screen" and "has work to do" are different questions. With hideWhenInactive off
|
||||
-- (the default) the frame stays visible while idle -- but once the buffer is down and
|
||||
-- combat has ended there is nothing left to animate, so the OnUpdate script is still
|
||||
-- detached: it was otherwise left spinning from login to logout, redrawing a saturated
|
||||
-- swing marker every rendered frame and walking the whole buff list five times a second
|
||||
-- for a readout that cannot change. The aura, combat and swing handlers in data.lua each
|
||||
-- end in a RequestUpdate call and re-attach it, so nothing has to poll for the wakeup.
|
||||
--
|
||||
-- A swing still in flight counts as work even with the buffer down and combat over:
|
||||
-- auto-attacking out of combat is ordinary play, and parking on it would freeze the
|
||||
-- marker where it stood. Once progress saturates this stops being true and the loop's
|
||||
-- own RequestUpdate call parks it.
|
||||
local swinging = d.showSwing and st.swingSpeed and st.swingSpeed > 0
|
||||
and S.hasSwung(st.lastSwingAt)
|
||||
and S.progress(st.lastSwingAt, GetTime(), st.swingSpeed, st.swingOffset) < 1
|
||||
if st.active or st.inCombat or swinging then
|
||||
if not frame:GetScript("OnUpdate") then frame:SetScript("OnUpdate", BF.OnUpdate) end
|
||||
else
|
||||
frame:SetScript("OnUpdate", nil)
|
||||
-- One last pass, unconditionally: the frame is visible and nothing will tick it, so
|
||||
-- it has to be left parked on the correct final state rather than on empty text.
|
||||
updateDisplay()
|
||||
updateSwingMarker()
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
frame:SetScript("OnUpdate", nil) -- idle costs nothing at all
|
||||
@@ -373,7 +425,10 @@ function BF.OnUpdate()
|
||||
if sinceUpdate < UPDATE_INTERVAL then return end
|
||||
sinceUpdate = 0
|
||||
local active = updateDisplay()
|
||||
if not demo and not active and not BF.state.inCombat and db().hideWhenInactive then
|
||||
-- Re-evaluated whatever hideWhenInactive says: with it off the frame stays on screen, but the
|
||||
-- loop still has nothing left to do, and RequestUpdate is what parks it. Gating this on the
|
||||
-- setting is why the handler used to run for the whole session in the default configuration.
|
||||
if not demo and not active and not BF.state.inCombat then
|
||||
BF.RequestUpdate()
|
||||
end
|
||||
end
|
||||
@@ -388,6 +443,13 @@ function BF.ToggleDemo(on)
|
||||
if on then
|
||||
demo = { start = GetTime(), maxHP = (UnitHealthMax("player") or 4588) }
|
||||
if demo.maxHP <= 0 then demo.maxHP = 4588 end
|
||||
-- Turning demo on is an explicit "show me the frame", so it clears the manual hide.
|
||||
-- RequestUpdate honours that hide FIRST (it has to -- otherwise the "SuperWoW required"
|
||||
-- frame cannot be dismissed), which meant '/bulwark demo' over a minimap-hidden frame
|
||||
-- reported "demo on", drew nothing, ticked nothing, and left the user silently parked in
|
||||
-- demo mode until they un-hid the frame. Clearing the flag here keeps the precedence
|
||||
-- where it belongs and still makes the command deliver what it announces.
|
||||
db().hidden = false
|
||||
-- Synthetic numbers MUST be labelled as such. Unlabelled demo data is indistinguishable
|
||||
-- from a live readout that happens to be wrong, and that is the one failure this addon
|
||||
-- exists to avoid.
|
||||
@@ -421,6 +483,11 @@ function BF.InitUI()
|
||||
local ok = BulwarkFrameEnv.check(SUPERWOW_VERSION)
|
||||
BF.envOk = ok
|
||||
BF.state.maxHP = UnitHealthMax("player") or 0
|
||||
-- Seeded here as well as in the PLAYER_ENTERING_WORLD handler: this runs off PLAYER_LOGIN and
|
||||
-- the order of the two is not guaranteed. PLAYER_REGEN_DISABLED only fires on ENTERING combat,
|
||||
-- so without a live read a /reload taken mid-fight leaves "Combat only" users with no frame
|
||||
-- for the rest of the encounter.
|
||||
BF.state.inCombat = UnitAffectingCombat("player") and true or false
|
||||
BF.CapturePlayerGuid()
|
||||
if ok then
|
||||
BF.RefreshSwingInputs()
|
||||
|
||||
Reference in New Issue
Block a user