diff --git a/BulwarkFrame.toc b/BulwarkFrame.toc index 59a635e..3f88c8e 100644 --- a/BulwarkFrame.toc +++ b/BulwarkFrame.toc @@ -2,7 +2,7 @@ ## Title: BulwarkFrame ## Notes: Compact real-time readout for the shaman Earthen Bulwark. Requires SuperWoW. ## Author: ShempError -## Version: 0.2.0 +## Version: 0.3.0 ## SavedVariables: BulwarkFrameDB core\env.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index ef49367..b5ce4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ 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 ### Fixed diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e1bc95c --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index 2d8dcb2..df592f5 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,18 @@ readout. ## Options -**Frame** — lock, scale, width, per-bar heights, minimap button, hide-when-idle, combat-only. -**Elements** — each of the three readouts and every text label individually; swing marker on the -expiry bar or on its own line; max hit vs. max shield. -**Colours** — the three threshold colours and the swing marker, via the standard colour picker. -**Absorb model** — talent rank (5 / 10 / 15 %), the set-bonus reading, and whether the swing -marker is latency-adjusted. +**Frame** — lock, scale, width, per-bar heights, bar spacing, minimap button, hide-when-idle, +combat-only. +**Elements** — each of the three readouts and every text label individually (including the attack +-speed number); swing marker on the expiry bar or on its own line; max hit vs. max shield; expiry +bar opacity. +**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` 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_config.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 diff --git a/options.lua b/options.lua index ea5744e..4bd4fa4 100644 --- a/options.lua +++ b/options.lua @@ -14,7 +14,7 @@ local PANEL_W = 430 -- Content width of one column: panel minus both outer margins minus the gutter, halved. local COL_W = (PANEL_W - 48 - 12) / 2 local panel = nil -local cbIndex, slIndex, swIndex = 0, 0, 0 +local cbIndex, slIndex, swIndex, ebIndex = 0, 0, 0, 0 local refreshers = {} local function db() return BulwarkFrameDB end @@ -81,6 +81,8 @@ local function CreateCheckbox(parent, label, tip, getter, setter) local lbl = getglobal(name .. "Text") if lbl then lbl:SetText(label); ApplyFont(lbl) end 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() this.bfSet(this:GetChecked() == 1) Applied() @@ -188,6 +190,56 @@ local function CreateSwatch(parent, label, key, tip) return holder 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 b = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate") b:SetWidth(110) @@ -333,6 +385,12 @@ local function BuildPanel() function(v) db().swingHeight = math.floor(v) end) 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 ---------------------------------------------- local ry = -46 headR("Elements") @@ -383,7 +441,20 @@ local function BuildPanel() local e5 = CreateCheckbox(panel, "Expiry text", "Show the remaining seconds as a number.", function() return db().showExpiryText 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") @@ -394,7 +465,47 @@ local function BuildPanel() local sw3 = CreateSwatch(panel, "High", "colorGreen", "Colour above the high threshold.") placeR(sw3, 24) 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") @@ -420,6 +531,13 @@ local function BuildPanel() placeR(setBtn, 28) 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", "Shift the swing marker by your round-trip time, so it shows when to press rather than " .. "when the server swung.", @@ -427,6 +545,31 @@ local function BuildPanel() function(v) db().swingUseLatency = v; BF.RefreshSwingInputs() end) 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 ------------------------------------------------------------------------------- local demoBtn = CreateButton(panel, "Demo", "Run the display off a synthetic cycle so colours " .. "and sizes can be judged without a fight.", function()