- Count gains worth more than a whole level as level-ups, and track the new level from PLAYER_LEVEL_UP so a lagging UnitLevel can't make the next kill look like another level-up. - Only label a gain "XP (quest)" when the quest reports an XP reward, and keep the label until a real gain arrives. - Clamp /xpp size to 10-60, matching the options slider. - Remove "hide Blizzard's floating XP text" and /xpp float. That text is drawn by the game engine on newer clients, so the CombatText hook never caught it. Old hideFloat settings are cleared on load. - Bump version to 1.0.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
419 lines
15 KiB
Lua
419 lines
15 KiB
Lua
--[[
|
|
================================================================================
|
|
XP Percent - Options.lua (options window)
|
|
================================================================================
|
|
Builds the "XP Percent" page in the game's options menu:
|
|
Esc > Options > AddOns > XP Percent (or type /xpp)
|
|
|
|
Sections on the page:
|
|
- Popup text: on/off, font size, color, test, drag-to-move, reset position
|
|
- Blizzard's default XP text: hide/show the chat line
|
|
- Other: also print % in chat, number of decimal places
|
|
|
|
How the controls stay in sync with the saved settings:
|
|
- Every control writes to the settings (ns.DB()) as soon as it's changed.
|
|
- Every control also registers a "refresher" function that loads the saved
|
|
value back into it. All refreshers run each time the page is shown, so
|
|
changes made with slash commands appear here too.
|
|
|
|
This file loads after XPPercent.lua (see the .toc), so everything that file
|
|
puts on `ns` (ns.DB, ns.Show, ns.ApplyStyle, ...) is already available.
|
|
================================================================================
|
|
--]]
|
|
|
|
-- See XPPercent.lua: ADDON is the folder name, ns is the shared addon table.
|
|
local ADDON, ns = ...
|
|
|
|
-- The page itself. It's just a blank frame; the options menu sizes and places it.
|
|
local panel = CreateFrame("Frame")
|
|
panel.name = "XP Percent" -- the name shown in the AddOns list
|
|
|
|
local category -- set by InitOptions() on newer clients; used to open the page
|
|
|
|
-- One function per control that loads the saved value into that control.
|
|
-- All of them run in Refresh(), each time the page is shown.
|
|
local refreshers = {}
|
|
|
|
-- True while Refresh() is running. Setting a slider's value from code fires the
|
|
-- same "value changed" script as a player dragging it. This flag lets that
|
|
-- script tell the two apart, so loading a value doesn't save it back and run
|
|
-- its side effects (like a preview popup) for nothing.
|
|
local refreshing = false
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- CONTROL BUILDERS
|
|
--------------------------------------------------------------------------------
|
|
-- Small helper functions that each create one kind of control. Positions are
|
|
-- given as `y`, pixels down from the top of the page (so always negative).
|
|
|
|
--[[
|
|
Header(text, y) - a gold section heading, e.g. "Popup text".
|
|
--]]
|
|
local function Header(text, y)
|
|
local h = panel:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
|
h:SetPoint("TOPLEFT", 16, y)
|
|
h:SetText(text)
|
|
return h
|
|
end
|
|
|
|
--[[
|
|
Checkbox(label, key, y, onChange) - a checkbox tied to one on/off setting.
|
|
label - text next to the box
|
|
key - which setting it controls, e.g. "hideChat"
|
|
y - vertical position
|
|
onChange - optional function to run after the setting changes
|
|
--]]
|
|
local function Checkbox(label, key, y, onChange)
|
|
-- UICheckButtonTemplate is Blizzard's standard checkbox.
|
|
local cb = CreateFrame("CheckButton", nil, panel, "UICheckButtonTemplate")
|
|
cb:SetPoint("TOPLEFT", 20, y)
|
|
|
|
-- The template's label is called .Text on newer clients and .text on some
|
|
-- older ones. If neither exists, make our own to the right of the box.
|
|
local text = cb.Text or cb.text
|
|
if not text then
|
|
text = cb:CreateFontString(nil, "ARTWORK")
|
|
text:SetPoint("LEFT", cb, "RIGHT", 2, 0)
|
|
end
|
|
text:SetFontObject("GameFontHighlight") -- white text, like Blizzard's options
|
|
text:SetText(label)
|
|
|
|
-- Clicked: save the new state. "and true or false" turns GetChecked()'s
|
|
-- result into a real boolean (some clients return 1/nil instead).
|
|
cb:SetScript("OnClick", function(self)
|
|
ns.DB()[key] = self:GetChecked() and true or false
|
|
if onChange then onChange() end
|
|
end)
|
|
|
|
-- When the page is shown, tick or untick to match the saved setting.
|
|
refreshers[#refreshers + 1] = function()
|
|
cb:SetChecked(ns.DB()[key])
|
|
end
|
|
|
|
return cb
|
|
end
|
|
|
|
--[[
|
|
Slider(label, key, minV, maxV, y, onChange) - a whole-number slider tied
|
|
to one numeric setting.
|
|
label - text above the slider; the current value is added after it
|
|
key - which setting it controls, e.g. "size"
|
|
minV, maxV - lowest and highest allowed values
|
|
y - vertical position
|
|
onChange - optional function(newValue) to run after the setting changes
|
|
|
|
Built from a plain Slider rather than a Blizzard template, because the
|
|
slider templates differ (or are missing) between game versions.
|
|
--]]
|
|
local function Slider(label, key, minV, maxV, y, onChange)
|
|
-- Title line above the slider, e.g. "Font size: 26".
|
|
local title = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
|
|
title:SetPoint("TOPLEFT", 24, y)
|
|
|
|
local s = CreateFrame("Slider", nil, panel)
|
|
s:SetOrientation("HORIZONTAL")
|
|
s:SetSize(220, 16)
|
|
s:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
|
|
s:SetMinMaxValues(minV, maxV)
|
|
s:SetValueStep(1) -- whole numbers only
|
|
-- Snap to whole numbers while dragging too (missing on very old clients).
|
|
if s.SetObeyStepOnDrag then s:SetObeyStepOnDrag(true) end
|
|
s:EnableMouseWheel(true)
|
|
|
|
-- What the slider looks like: a dark bar (the track) and Blizzard's
|
|
-- standard slider knob (the "thumb" you drag).
|
|
local track = s:CreateTexture(nil, "BACKGROUND")
|
|
track:SetColorTexture(0, 0, 0, 0.6) -- 60% opaque black
|
|
track:SetPoint("LEFT")
|
|
track:SetPoint("RIGHT")
|
|
track:SetHeight(6)
|
|
s:SetThumbTexture("Interface\\Buttons\\UI-SliderBar-Button-Horizontal")
|
|
|
|
-- Small grey min/max numbers under each end of the slider.
|
|
local low = s:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
|
|
low:SetPoint("TOPLEFT", s, "BOTTOMLEFT", 0, -2)
|
|
low:SetText(minV)
|
|
local high = s:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
|
|
high:SetPoint("TOPRIGHT", s, "BOTTOMRIGHT", 0, -2)
|
|
high:SetText(maxV)
|
|
|
|
-- Runs whenever the value changes, whether the player dragged it or
|
|
-- Refresh() set it from code.
|
|
s:SetScript("OnValueChanged", function(self, v)
|
|
v = math.floor(v + 0.5) -- round to a whole number
|
|
|
|
-- Always update the title. |cffffffff ... |r shows the number in white.
|
|
title:SetText(label .. ": |cffffffff" .. v .. "|r")
|
|
|
|
-- If this change came from Refresh(), stop here: nothing new to save.
|
|
if refreshing then return end
|
|
|
|
ns.DB()[key] = v
|
|
if onChange then onChange(v) end
|
|
end)
|
|
|
|
-- Mouse wheel over the slider: up = +1, down = -1. SetValue keeps it within
|
|
-- min/max and fires OnValueChanged, which saves the value.
|
|
s:SetScript("OnMouseWheel", function(self, delta)
|
|
self:SetValue(self:GetValue() + delta)
|
|
end)
|
|
|
|
-- When the page is shown, move the knob and title to the saved value.
|
|
-- The title is set here as well, because SetValue doesn't fire
|
|
-- OnValueChanged when the value is already the same.
|
|
refreshers[#refreshers + 1] = function()
|
|
s:SetValue(ns.DB()[key])
|
|
title:SetText(label .. ": |cffffffff" .. ns.DB()[key] .. "|r")
|
|
end
|
|
|
|
return s
|
|
end
|
|
|
|
--[[
|
|
Button(text, width, onClick) - a standard red Blizzard button.
|
|
--]]
|
|
local function Button(text, width, onClick)
|
|
local b = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
|
|
b:SetSize(width, 22)
|
|
b:SetText(text)
|
|
b:SetScript("OnClick", onClick)
|
|
return b
|
|
end
|
|
|
|
-- Shows a sample popup so the player can see a change straight away.
|
|
local function Preview()
|
|
ns.Show(1.25, "XP")
|
|
end
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- PAGE LAYOUT
|
|
--------------------------------------------------------------------------------
|
|
-- Controls are placed top to bottom using fixed y values (pixels down from the
|
|
-- top of the page). To add a control, give it a y and push later ones down.
|
|
|
|
-- Page title and short description.
|
|
local title = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
|
|
title:SetPoint("TOPLEFT", 16, -16)
|
|
title:SetText("XP Percent")
|
|
|
|
local sub = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
|
sub:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
|
|
sub:SetText("Shows XP gains as a percentage of your current level.")
|
|
|
|
|
|
---------------------------------------- Section: Popup text
|
|
Header("Popup text", -70)
|
|
|
|
Checkbox("Show the floating % popup", "popup", -90)
|
|
|
|
-- Font size. Every change re-applies the style and shows a preview.
|
|
Slider("Font size", "size", 10, 60, -128, function()
|
|
ns.ApplyStyle()
|
|
Preview()
|
|
end)
|
|
|
|
-- Color swatch: a small square showing the current color (a white border with
|
|
-- the color inside). Clicking it opens Blizzard's color picker.
|
|
local swatch = CreateFrame("Button", nil, panel)
|
|
swatch:SetSize(22, 22)
|
|
swatch:SetPoint("TOPLEFT", 24, -184)
|
|
|
|
local border = swatch:CreateTexture(nil, "BACKGROUND")
|
|
border:SetAllPoints() -- fills the whole square...
|
|
border:SetColorTexture(1, 1, 1) -- ...in white
|
|
|
|
local fill = swatch:CreateTexture(nil, "ARTWORK")
|
|
fill:SetPoint("TOPLEFT", 2, -2) -- 2 px smaller on every side, so a white
|
|
fill:SetPoint("BOTTOMRIGHT", -2, 2) -- edge shows around the color
|
|
|
|
local swatchLabel = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
|
|
swatchLabel:SetPoint("LEFT", swatch, "RIGHT", 8, 0)
|
|
swatchLabel:SetText("Text color (click to change)")
|
|
|
|
--[[
|
|
SetColor(r, g, b) - saves a new popup color (each channel 0-1) and updates
|
|
the swatch and the popup.
|
|
--]]
|
|
local function SetColor(r, g, b)
|
|
local c = ns.DB().color
|
|
c.r, c.g, c.b = r, g, b
|
|
fill:SetColorTexture(r, g, b)
|
|
ns.ApplyStyle()
|
|
end
|
|
|
|
-- When the page is shown, paint the swatch with the saved color.
|
|
refreshers[#refreshers + 1] = function()
|
|
local c = ns.DB().color
|
|
fill:SetColorTexture(c.r, c.g, c.b)
|
|
end
|
|
|
|
-- Swatch clicked: open the color picker at the current color.
|
|
swatch:SetScript("OnClick", function()
|
|
local c = ns.DB().color
|
|
|
|
-- Remember the starting color so Cancel can put it back.
|
|
local prev = { r = c.r, g = c.g, b = c.b }
|
|
|
|
-- The picker calls onChange every time the player moves the selector, so
|
|
-- the popup updates live while they choose.
|
|
local function onChange() SetColor(ColorPickerFrame:GetColorRGB()) end
|
|
local function onCancel() SetColor(prev.r, prev.g, prev.b) end
|
|
|
|
if ColorPickerFrame.SetupColorPickerAndShow then
|
|
-- Newer clients: one function call with a table of options.
|
|
ColorPickerFrame:SetupColorPickerAndShow({
|
|
r = c.r, g = c.g, b = c.b,
|
|
hasOpacity = false, -- no transparency slider
|
|
swatchFunc = onChange,
|
|
cancelFunc = onCancel,
|
|
})
|
|
else
|
|
-- Older clients: set fields on the picker frame, then show it.
|
|
ColorPickerFrame:Hide() -- close it if it's open for something else
|
|
ColorPickerFrame.hasOpacity = false
|
|
ColorPickerFrame.opacityFunc = nil
|
|
ColorPickerFrame.previousValues = prev
|
|
ColorPickerFrame.func = onChange
|
|
ColorPickerFrame.cancelFunc = onCancel
|
|
ColorPickerFrame:SetColorRGB(c.r, c.g, c.b)
|
|
ShowUIPanel(ColorPickerFrame)
|
|
end
|
|
end)
|
|
|
|
-- "Reset color": back to the default purple from XPPercent.lua.
|
|
local resetColor = Button("Reset color", 100, function()
|
|
local d = ns.defaults.color
|
|
SetColor(d.r, d.g, d.b)
|
|
Preview()
|
|
end)
|
|
resetColor:SetPoint("LEFT", swatchLabel, "RIGHT", 12, 0)
|
|
|
|
-- "Test popup": show a sample popup with the current settings.
|
|
local test = Button("Test popup", 110, Preview)
|
|
test:SetPoint("TOPLEFT", 24, -216)
|
|
|
|
-- "Unlock popup" / "Lock popup": one button that toggles drag-to-move mode.
|
|
-- Its text is updated in UpdateMoveControls() below.
|
|
local unlock = Button("Unlock popup", 120, function()
|
|
ns.SetUnlocked(not ns.IsUnlocked())
|
|
end)
|
|
unlock:SetPoint("LEFT", test, "RIGHT", 8, 0)
|
|
|
|
-- "Reset position": move the popup back to its default spot.
|
|
local resetPos = Button("Reset position", 120, function()
|
|
local db = ns.DB()
|
|
db.x, db.y = ns.defaults.x, ns.defaults.y
|
|
ns.ApplyStyle()
|
|
if ns.IsUnlocked() then
|
|
-- The blue box is already on screen and has just jumped to the new
|
|
-- spot; only the position readout needs updating.
|
|
ns.OnPositionChanged()
|
|
else
|
|
-- Otherwise show a sample popup so the player can see where it went.
|
|
Preview()
|
|
end
|
|
end)
|
|
resetPos:SetPoint("LEFT", unlock, "RIGHT", 8, 0)
|
|
|
|
-- Grey "Position: x, y" readout to the right of the buttons.
|
|
local posText = panel:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
|
|
posText:SetPoint("LEFT", resetPos, "RIGHT", 10, 0)
|
|
|
|
--[[
|
|
UpdateMoveControls() - updates the Unlock/Lock button text and the
|
|
position readout.
|
|
|
|
It is both a refresher (runs when the page opens) and ns.OnPositionChanged,
|
|
which XPPercent.lua calls after a drag, a lock/unlock, or /xpp pos. That
|
|
keeps the page correct even while it's open.
|
|
--]]
|
|
local function UpdateMoveControls()
|
|
local db = ns.DB()
|
|
unlock:SetText(ns.IsUnlocked() and "Lock popup" or "Unlock popup")
|
|
posText:SetText(string.format("Position: %d, %d", db.x, db.y))
|
|
end
|
|
refreshers[#refreshers + 1] = UpdateMoveControls
|
|
ns.OnPositionChanged = UpdateMoveControls
|
|
|
|
|
|
---------------------------------------- Section: Blizzard's default XP text
|
|
-- Untick this to keep Blizzard's own XP message alongside ours.
|
|
-- (Blizzard's floating "XP: 200" text is drawn by the game engine, which
|
|
-- addons can't hide, so there's no option for it.)
|
|
Header("Blizzard's default XP text", -256)
|
|
|
|
-- The chat filter in XPPercent.lua reads this setting every time, so
|
|
-- changing it takes effect immediately.
|
|
Checkbox("Hide Blizzard's \"You gain 200 experience\" chat message", "hideChat", -276)
|
|
|
|
|
|
---------------------------------------- Section: Other
|
|
Header("Other", -316)
|
|
|
|
Checkbox("Also print the % gain in chat", "chat", -336)
|
|
|
|
-- 0-4 decimal places, e.g. 2 shows "+1.25%" and 0 shows "+1%".
|
|
Slider("Decimal places", "decimals", 0, 4, -374)
|
|
|
|
-- Grey tip at the bottom of the page.
|
|
local hint = panel:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
|
|
hint:SetPoint("TOPLEFT", 24, -424)
|
|
hint:SetText("Tip: click \"Unlock popup\", drag the blue box anywhere, then click \"Lock popup\".")
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- REGISTRATION (adding the page to the game's options menu)
|
|
--------------------------------------------------------------------------------
|
|
|
|
--[[
|
|
Refresh() - loads every saved setting into its control.
|
|
Runs each time the page is shown.
|
|
--]]
|
|
local function Refresh()
|
|
refreshing = true -- tell slider scripts not to save during the sync
|
|
for _, fn in ipairs(refreshers) do fn() end
|
|
refreshing = false
|
|
end
|
|
panel:SetScript("OnShow", Refresh)
|
|
|
|
--[[
|
|
ns.InitOptions() - adds the page to the game's options menu.
|
|
Called once from PLAYER_LOGIN in XPPercent.lua.
|
|
|
|
The options menu was rebuilt in Dragonflight (10.0), so there are two ways:
|
|
- Newer clients: the Settings API. A "canvas layout" category means
|
|
"use this frame as the page as-is".
|
|
- Older clients: InterfaceOptions_AddCategory.
|
|
--]]
|
|
function ns.InitOptions()
|
|
-- Guard so the page can never be added twice.
|
|
if category or panel.registered then return end
|
|
|
|
if Settings and Settings.RegisterCanvasLayoutCategory then
|
|
category = Settings.RegisterCanvasLayoutCategory(panel, panel.name)
|
|
Settings.RegisterAddOnCategory(category)
|
|
elseif InterfaceOptions_AddCategory then
|
|
InterfaceOptions_AddCategory(panel)
|
|
end
|
|
|
|
panel.registered = true
|
|
end
|
|
|
|
--[[
|
|
ns.OpenOptions() - opens the options menu straight to this page.
|
|
Used by a bare /xpp.
|
|
--]]
|
|
function ns.OpenOptions()
|
|
if Settings and Settings.OpenToCategory and category then
|
|
-- Newer clients open a page by its category ID.
|
|
Settings.OpenToCategory(category:GetID())
|
|
elseif InterfaceOptionsFrame_OpenToCategory then
|
|
-- Older clients. Called twice on purpose: a long-standing Blizzard bug
|
|
-- makes the first call open the menu without selecting our page.
|
|
InterfaceOptionsFrame_OpenToCategory(panel)
|
|
InterfaceOptionsFrame_OpenToCategory(panel)
|
|
end
|
|
end
|