BulwarkFrame v0.3.0

This commit is contained in:
2026-08-30 15:37:07 +02:00
parent 2b6de41d9a
commit dca4228c85
5 changed files with 201 additions and 10 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: BulwarkFrame ## Title: BulwarkFrame
## Notes: Compact real-time readout for the shaman Earthen Bulwark. Requires SuperWoW. ## Notes: Compact real-time readout for the shaman Earthen Bulwark. Requires SuperWoW.
## Author: ShempError ## Author: ShempError
## Version: 0.2.0 ## Version: 0.3.0
## SavedVariables: BulwarkFrameDB ## SavedVariables: BulwarkFrameDB
core\env.lua core\env.lua
+20
View File
@@ -2,6 +2,26 @@
All notable changes to BulwarkFrame are documented here. All notable changes to BulwarkFrame are documented here.
## v0.3.0 — 2026-08-30
### Added
- **Options panel controls for every setting that had none.** An audit of
`core/config.lua` found ten saved settings with no way to change them
short of hand-editing the SavedVariables file. The options panel now has:
a slider for the gap between the three bars (**Bar spacing**); a checkbox
for the attack-speed number on the expiry bar (**Attack speed text**); a
slider for the expiry bar's opacity (**Expiry bar opacity**); a colour
swatch plus opacity slider for the frame's background (**Background** /
**Background opacity**); sliders for the four colour-bucket thresholds
(**Pool red/yellow threshold**, **Expiry red/yellow threshold**); a
slider for the set-bonus percentage (**Set bonus value**); a slider for
how much of your latency shifts the swing marker (**Latency share**);
and, under a new **Aura IDs** heading, numeric edit boxes for the pool
and time buff spell ids, in case TurtleWoW renumbers the custom spell in
a future patch. Every control applies immediately, refreshes when the
panel reopens, and is covered by the Reset button, the same as every
pre-existing option.
## v0.2.0 — 2026-08-15 ## v0.2.0 — 2026-08-15
### Fixed ### Fixed
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ShempError
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+13 -6
View File
@@ -92,12 +92,18 @@ readout.
## Options ## Options
**Frame** — lock, scale, width, per-bar heights, minimap button, hide-when-idle, combat-only. **Frame** — lock, scale, width, per-bar heights, bar spacing, minimap button, hide-when-idle,
**Elements** — each of the three readouts and every text label individually; swing marker on the combat-only.
expiry bar or on its own line; max hit vs. max shield. **Elements** — each of the three readouts and every text label individually (including the attack
**Colours** — the three threshold colours and the swing marker, via the standard colour picker. -speed number); swing marker on the expiry bar or on its own line; max hit vs. max shield; expiry
**Absorb model** — talent rank (5 / 10 / 15 %), the set-bonus reading, and whether the swing bar opacity.
marker is latency-adjusted. **Colours** — the three threshold colours, the swing marker, and the background (with its own
opacity slider), via the standard colour picker; the pool and expiry bars' red/yellow/green
threshold sliders.
**Absorb model** — talent rank (5 / 10 / 15 %), the set-bonus reading and its percentage, and
whether the swing marker is latency-adjusted, and by how much.
**Aura IDs** — the pool and time buff spell ids, editable in case TurtleWoW renumbers the custom
spell in a future patch (`/bulwark probe` finds the current one).
Every change applies immediately — an options panel whose effect only shows after `/reload` Every change applies immediately — an options panel whose effect only shows after `/reload`
teaches you to distrust it. teaches you to distrust it.
@@ -140,6 +146,7 @@ lua50 tools/luatests/test_swing.lua
lua50 tools/luatests/test_env.lua lua50 tools/luatests/test_env.lua
lua50 tools/luatests/test_config.lua lua50 tools/luatests/test_config.lua
lua50 tools/luatests/test_slash.lua lua50 tools/luatests/test_slash.lua
lua50 tools/luatests/test_options.lua
``` ```
`test_slash.lua` is the exception to "no WoW API offline": it drives the real slash commands and `test_slash.lua` is the exception to "no WoW API offline": it drives the real slash commands and
+146 -3
View File
@@ -14,7 +14,7 @@ local PANEL_W = 430
-- Content width of one column: panel minus both outer margins minus the gutter, halved. -- Content width of one column: panel minus both outer margins minus the gutter, halved.
local COL_W = (PANEL_W - 48 - 12) / 2 local COL_W = (PANEL_W - 48 - 12) / 2
local panel = nil local panel = nil
local cbIndex, slIndex, swIndex = 0, 0, 0 local cbIndex, slIndex, swIndex, ebIndex = 0, 0, 0, 0
local refreshers = {} local refreshers = {}
local function db() return BulwarkFrameDB end local function db() return BulwarkFrameDB end
@@ -81,6 +81,8 @@ local function CreateCheckbox(parent, label, tip, getter, setter)
local lbl = getglobal(name .. "Text") local lbl = getglobal(name .. "Text")
if lbl then lbl:SetText(label); ApplyFont(lbl) end if lbl then lbl:SetText(label); ApplyFont(lbl) end
cb.bfSet = setter cb.bfSet = setter
cb.bfLabel = label -- scratch field for the offline stub; the real client's own label
-- text lives in the $parentText template region instead.
cb:SetScript("OnClick", function() cb:SetScript("OnClick", function()
this.bfSet(this:GetChecked() == 1) this.bfSet(this:GetChecked() == 1)
Applied() Applied()
@@ -188,6 +190,56 @@ local function CreateSwatch(parent, label, key, tip)
return holder return holder
end end
-- A numeric edit box: a label above a small InputBoxTemplate field. Used for the two spell ids,
-- which are not on any slider scale -- typing the exact id is the only sane input for one.
-- Applies on Enter or on losing focus (tabbing/clicking away), the same "commit, don't live-type"
-- behaviour InputBoxTemplate users get everywhere else in the default UI. An unparsable entry is
-- reverted to the current stored value rather than silently written as nil.
local function CreateEditBox(parent, label, tip, getter, setter)
ebIndex = ebIndex + 1
local name = "BulwarkFrameOptEdit" .. ebIndex
local holder = CreateFrame("Frame", name .. "Holder", parent)
holder:SetWidth(COL_W)
holder:SetHeight(34)
local lbl = holder:CreateFontString(name .. "Label", "OVERLAY", "GameFontNormalSmall")
lbl:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0)
lbl:SetJustifyH("LEFT")
lbl:SetText(label)
ApplyFont(lbl, 11)
local eb = CreateFrame("EditBox", name, holder, "InputBoxTemplate")
eb:SetWidth(90)
eb:SetHeight(18)
eb:SetAutoFocus(false)
eb:SetMaxLetters(6)
if eb.SetNumeric then eb:SetNumeric(true) end
eb:SetPoint("TOPLEFT", holder, "TOPLEFT", 4, -16)
local function commit()
local v = tonumber(eb:GetText())
if v then
eb.bfSet(math.floor(v))
else
eb:SetText(tostring(getter()))
end
eb:ClearFocus()
Applied()
end
eb.bfSet = setter
eb:SetScript("OnEnterPressed", commit)
eb:SetScript("OnEscapePressed", function() eb:SetText(tostring(getter())); eb:ClearFocus() end)
eb:SetScript("OnEditFocusLost", commit)
-- No AddRowHover here: an overlay button covering the row would sit above the edit box and
-- eat the click meant to focus it. The tooltip is on the field and the label only.
AddTooltip(eb, tip)
holder:EnableMouse(true)
AddTooltip(holder, tip)
table.insert(refreshers, function() eb:SetText(tostring(getter())) end)
return holder
end
local function CreateButton(parent, label, tip, onClick) local function CreateButton(parent, label, tip, onClick)
local b = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate") local b = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
b:SetWidth(110) b:SetWidth(110)
@@ -333,6 +385,12 @@ local function BuildPanel()
function(v) db().swingHeight = math.floor(v) end) function(v) db().swingHeight = math.floor(v) end)
sliderL(swingH) sliderL(swingH)
local spacing = CreateSlider(panel, "Bar spacing", 0, 10, 1, "Bar spacing: %d px",
"Vertical gap between the threshold bar, the expiry bar, and the swing line.",
function() return db().spacing end,
function(v) db().spacing = math.floor(v) end)
sliderL(spacing)
-- right column: elements, colours, model ---------------------------------------------- -- right column: elements, colours, model ----------------------------------------------
local ry = -46 local ry = -46
headR("Elements") headR("Elements")
@@ -383,7 +441,20 @@ local function BuildPanel()
local e5 = CreateCheckbox(panel, "Expiry text", "Show the remaining seconds as a number.", local e5 = CreateCheckbox(panel, "Expiry text", "Show the remaining seconds as a number.",
function() return db().showExpiryText end, function() return db().showExpiryText end,
function(v) db().showExpiryText = v end) function(v) db().showExpiryText = v end)
placeR(e5, 40) placeR(e5, 28)
local e6 = CreateCheckbox(panel, "Attack speed text",
"Show the current weapon attack speed on the left of the expiry bar.",
function() return db().showSpeedText end,
function(v) db().showSpeedText = v end)
placeR(e6, 28)
local expiryAlpha = CreateSlider(panel, "Expiry bar opacity", 0, 1, 0.05, "Expiry bar opacity: %.2f",
"Alpha of the expiry bar. Kept lower than the threshold bar by default so it reads as the "
.. "secondary readout.",
function() return db().expiryAlpha end,
function(v) db().expiryAlpha = v end)
sliderR(expiryAlpha)
headR("Colours") headR("Colours")
@@ -394,7 +465,47 @@ local function BuildPanel()
local sw3 = CreateSwatch(panel, "High", "colorGreen", "Colour above the high threshold.") local sw3 = CreateSwatch(panel, "High", "colorGreen", "Colour above the high threshold.")
placeR(sw3, 24) placeR(sw3, 24)
local sw4 = CreateSwatch(panel, "Swing marker", "colorSwing", "Colour of the swing marker.") local sw4 = CreateSwatch(panel, "Swing marker", "colorSwing", "Colour of the swing marker.")
placeR(sw4, 34) placeR(sw4, 24)
-- Background colour is a 4-element {r,g,b,a} table; CreateSwatch only ever touches indices
-- 1..3, so the alpha channel gets its own slider straight onto the same table rather than a
-- second colour-helper -- there is no opacity-capable picker in this file to reuse.
local sw5 = CreateSwatch(panel, "Background", "colorBg", "Background colour of the frame.")
placeR(sw5, 24)
local bgAlpha = CreateSlider(panel, "Background opacity", 0, 1, 0.05, "Background opacity: %.2f",
"Opacity of the frame's background panel.",
function() return (db().colorBg or {})[4] or 0.8 end,
function(v) local c = db().colorBg; if c then c[4] = v end end)
sliderR(bgAlpha)
headR("Thresholds")
local poolRed = CreateSlider(panel, "Pool red threshold", 0, 100, 5, "Pool turns red below: %.0f%%",
"The threshold bar turns red at or below this fraction of the pool.",
function() return (db().poolRed or 0.30) * 100 end,
function(v) db().poolRed = v / 100 end)
sliderR(poolRed)
local poolYellow = CreateSlider(panel, "Pool yellow threshold", 0, 100, 5, "Pool turns green above: %.0f%%",
"The threshold bar turns green above this fraction of the pool (yellow in between).",
function() return (db().poolYellow or 0.70) * 100 end,
function(v) db().poolYellow = v / 100 end)
sliderR(poolYellow)
local timeRed = CreateSlider(panel, "Expiry red threshold", 0, BF.BUFFER_DURATION, 0.5,
"Expiry turns red below: %.1fs",
"The expiry bar turns red once fewer than this many seconds remain.",
function() return db().timeRed or 2.0 end,
function(v) db().timeRed = v end)
sliderR(timeRed)
local timeYellow = CreateSlider(panel, "Expiry yellow threshold", 0, BF.BUFFER_DURATION, 0.5,
"Expiry turns green above: %.1fs",
"The expiry bar turns green once more than this many seconds remain (yellow in between).",
function() return db().timeYellow or 5.0 end,
function(v) db().timeYellow = v end)
sliderR(timeYellow)
headR("Absorb model") headR("Absorb model")
@@ -420,6 +531,13 @@ local function BuildPanel()
placeR(setBtn, 28) placeR(setBtn, 28)
table.insert(refreshers, function() setBtn:SetText("Set bonus: " .. (db().setBonusMode or "none")) end) table.insert(refreshers, function() setBtn:SetText("Set bonus: " .. (db().setBonusMode or "none")) end)
local setVal = CreateSlider(panel, "Set bonus value", 0, 20, 1, "Set bonus value: %d%%",
"The set bonus percentage the reading above applies. Only used while the set bonus reading "
.. "is 'add' or 'mult'.",
function() return db().setBonusValue end,
function(v) db().setBonusValue = math.floor(v) end)
sliderR(setVal)
local latency = CreateCheckbox(panel, "Latency-adjust swing", local latency = CreateCheckbox(panel, "Latency-adjust swing",
"Shift the swing marker by your round-trip time, so it shows when to press rather than " "Shift the swing marker by your round-trip time, so it shows when to press rather than "
.. "when the server swung.", .. "when the server swung.",
@@ -427,6 +545,31 @@ local function BuildPanel()
function(v) db().swingUseLatency = v; BF.RefreshSwingInputs() end) function(v) db().swingUseLatency = v; BF.RefreshSwingInputs() end)
placeR(latency, 28) placeR(latency, 28)
local latencyShare = CreateSlider(panel, "Latency share", 0, 1, 0.05, "Latency share: %.2f",
"How much of the round-trip time to shift the swing marker by: 1.00 = the full round trip, "
.. "0.50 = downstream only. Only used while latency-adjust is on.",
function() return db().swingLatencyShare end,
function(v) db().swingLatencyShare = v; BF.RefreshSwingInputs() end)
sliderR(latencyShare)
headR("Aura IDs")
local poolAuraBox = CreateEditBox(panel, "Pool aura spell id",
"Spell id of the Earthen Bulwark Durability debuff, whose stack count is the pool level. "
.. "TurtleWoW can renumber a custom spell between patches -- if the readout goes blank after "
.. "an update, /bulwark probe to find the new id and enter it here.",
function() return db().poolAura end,
function(v) db().poolAura = v end)
placeR(poolAuraBox, 36)
local timeAuraBox = CreateEditBox(panel, "Time aura spell id",
"Spell id of the Earthen Bulwark buff, whose remaining duration drives the expiry bar. "
.. "TurtleWoW can renumber a custom spell between patches -- if the readout goes blank after "
.. "an update, /bulwark probe to find the new id and enter it here.",
function() return db().timeAura end,
function(v) db().timeAura = v end)
placeR(timeAuraBox, 40)
-- footer ------------------------------------------------------------------------------- -- footer -------------------------------------------------------------------------------
local demoBtn = CreateButton(panel, "Demo", "Run the display off a synthetic cycle so colours " local demoBtn = CreateButton(panel, "Demo", "Run the display off a synthetic cycle so colours "
.. "and sizes can be judged without a fight.", function() .. "and sizes can be judged without a fight.", function()