1846 lines
77 KiB
Lua
1846 lines
77 KiB
Lua
-- DopingControl ui/options.lua
|
|
-- Standalone options frame,
|
|
-- built with proven 1.12 options-window patterns
|
|
-- : lazy build on first Toggle, layout cursor, widget factories with
|
|
-- getglobal labels, GetChecked() == 1, save-and-chain tooltips (no
|
|
-- HookScript on 1.12), OnShow repopulation from DB, ESC via
|
|
-- UISpecialFrames, pfUI skin hooks. (No sliders needed here; the
|
|
-- SetMinMaxValues-before-SetValue rule therefore has no call site.)
|
|
--
|
|
-- Contents, top to bottom:
|
|
-- title "DopingControl"
|
|
-- [x] Test mode (simulated raid) -> DC.SetTestMode(v) (core/init.lua)
|
|
-- [x] Show minimap button -> DC_Minimap.Update() (features/minimap.lua)
|
|
-- [x] Auto-scan on ready check -> db.readyCheckScan
|
|
-- [x] Auto-open on ready check -> db.readyCheckOpen
|
|
-- divider
|
|
-- Editor: a tab selector (Consumables / Class buffs / Equipment /
|
|
-- Skill books, O.EditorTabs()) picks ONE of two mutually exclusive
|
|
-- content blocks that share the same position and width -- only one
|
|
-- is ever shown (ApplyEditorTabVisibility), so the window height is
|
|
-- fixed at build time to whichever block is taller and never jumps
|
|
-- when the tab changes:
|
|
-- - the three expectation tabs (O.ExpectationTabs(), expectations are
|
|
-- PER ROLE x CLASS) show a GROUPED grid inside a scroll frame (max
|
|
-- height O.GRID_MAX_H): per role one HEADER row (role name, role
|
|
-- color; clicking a header cell bulk-toggles that slot for ALL
|
|
-- performing classes of the role -- majority flip, O.BulkNewValue)
|
|
-- and beneath it one indented row per performing class (class name
|
|
-- in class color, own checkboxes on db.expectations[role][class]
|
|
-- [slotId]). Row order = O.GridRows: 25 rows (TANK 1+4, MELEE 1+6,
|
|
-- RANGED 1+1, CASTER 1+5, HEALER 1+4), identical for all three
|
|
-- tabs. Checkbox cells are POOLED (lazy creation, reuse per tab
|
|
-- switch, surplus hidden -- up to 25x16 on equipment). A click
|
|
-- applies to the matrix IMMEDIATELY. The editor always shows ALL
|
|
-- slots of the tab -- dynamic columns apply to the matrix, not here
|
|
-- (a fully disabled slot must stay re-enableable). Role header
|
|
-- cells have no tri-state on 1.12: all classes checked = checked,
|
|
-- none = unchecked, mixed = checked at 0.45 alpha. The
|
|
-- "Reset expectations" button (double-click-to-confirm --
|
|
-- StaticPopup is 1.12-fiddly: first click arms ("Click again to
|
|
-- confirm"), second click within 5 s reseeds every class line from
|
|
-- DC.DEFAULT_EXPECT via DC.ResetExpectations; the label reverts
|
|
-- automatically when the window lapses) belongs to this block.
|
|
-- - the Skill books tab (O.IsSkillBookTab) shows the SAME KIND OF
|
|
-- GRID as the expectation tabs, so both read as one table: sticky
|
|
-- 3-character column headers, one row per scanned player, a
|
|
-- checkbox at the crossing. The columns are the FIXED weapon-skill
|
|
-- set (O.BOOK_COLUMNS, 15), never the per-player skill list -- the
|
|
-- old layout wrapped each player's skills across up to 5 ragged
|
|
-- lines with the label next to every box, which made the tab
|
|
-- unreadable at raid size. Cell state comes from
|
|
-- O.BookCellState: worn = full strength (the common tick),
|
|
-- trainable = dimmed, not trainable = no checkbox at all. A player
|
|
-- whose class is unknown gets a hint line instead of cells. A tick
|
|
-- writes db.skillBooks[player][skill] = true (an untick writes
|
|
-- explicit false) and refreshes the matrix at once. Rows come from
|
|
-- the store and change with every scan, so the pool is rebuilt on
|
|
-- OnShow/Refresh; an empty store shows a hint line instead. Fixed
|
|
-- scroll height (O.BOOKS_MAX_H) -- the roster length never resizes
|
|
-- the window. The explanation line ("A book is worth about 0.6%
|
|
-- hit...") sits at the top of this block.
|
|
-- version footer from GetAddOnMetadata
|
|
--
|
|
-- Cross-module calls (DC.SetTestMode, DC_Minimap,
|
|
-- DC_Matrix, DC_Roles.ROLE_CLASSES, DC.ResetExpectations) are
|
|
-- existence-guarded so a partially loaded addon never hard-errors here;
|
|
-- O.RoleClasses falls back to inverting
|
|
-- DC_Roles.CLASS_ROLES when ROLE_CLASSES is absent.
|
|
-- Dofile-loadable under pure Lua 5.0 -- all WoW wiring behind CreateFrame;
|
|
-- the pure grid helpers at the top are offline-tested.
|
|
|
|
DopingControl = DopingControl or {}
|
|
local DC = DopingControl
|
|
|
|
DC_Options = DC_Options or {}
|
|
local O = DC_Options
|
|
|
|
-- seconds the "Reset expectations" confirmation stays armed
|
|
O.RESET_ARM_WINDOW = 5
|
|
|
|
-- The expectation grid and the weapon-skill-book list are separate TABS
|
|
-- of the same editor now and never show at once (ApplyEditorTabVisibility
|
|
-- picks one), so they no longer share a combined height budget -- each
|
|
-- gets its own scroll cap. The window height is fixed at BUILD time to
|
|
-- the taller of the two tab contents (see BuildOptionsFrame), so
|
|
-- switching to the Skill books tab never resizes the window; giving the
|
|
-- book list the SAME cap as the grid keeps that taller side unchanged and
|
|
-- avoids a large empty gap under the shorter tab.
|
|
O.GRID_MAX_H = 240
|
|
|
|
-- weapon-skill-book section: FIXED scroll height (the roster length must
|
|
-- never resize the window), same cap as the grid for the reason above.
|
|
O.BOOKS_MAX_H = O.GRID_MAX_H
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- O.BOOK_COLUMNS -- the FIXED column set of the Skill books grid, in
|
|
-- display order: one column per weapon skill that exists on this client,
|
|
-- grouped one-hand melee / two-hand melee / ranged. Same shape as the
|
|
-- equipment slot table (data/enchants.lua): `label` is the 3-character
|
|
-- column header, `full` the tooltip word, `skill` the write key into
|
|
-- db.skillBooks (and the exact string DC.CLASS_WEAPON_SKILLS uses).
|
|
--
|
|
-- The column set is FIXED, never derived from the current roster -- the
|
|
-- same reason the expectation editor always shows all slots of its tab:
|
|
-- columns that appear and disappear with the scan make the grid unreadable
|
|
-- and move every tick target. A cell is simply hidden on a row whose class
|
|
-- cannot train that skill (O.SkillBookRows' `has`).
|
|
--
|
|
-- This list is a literal on purpose (no dependency on data/hit.lua's load
|
|
-- order); test_options_grid pins it against DC.WEAPON_SKILL_OF_SUBTYPE's
|
|
-- value set and against every DC.CLASS_WEAPON_SKILLS entry, so a skill
|
|
-- added there can never silently lose its column.
|
|
-- ------------------------------------------------------------------
|
|
O.BOOK_COLUMNS = {
|
|
{ skill = "Axes", label = "AXE", sub = "1h", full = "Axes" },
|
|
{ skill = "Maces", label = "MAC", sub = "1h", full = "Maces" },
|
|
{ skill = "Swords", label = "SWD", sub = "1h", full = "Swords" },
|
|
{ skill = "Daggers", label = "DAG", sub = "1h", full = "Daggers" },
|
|
{ skill = "Unarmed", label = "UNA", sub = "fist", full = "Unarmed (fist weapons)" },
|
|
{ skill = "Two-Handed Axes", label = "2HA", sub = "2h", full = "Two-Handed Axes" },
|
|
{ skill = "Two-Handed Maces", label = "2HM", sub = "2h", full = "Two-Handed Maces" },
|
|
{ skill = "Two-Handed Swords", label = "2HS", sub = "2h", full = "Two-Handed Swords" },
|
|
{ skill = "Polearms", label = "POL", sub = "2h", full = "Polearms" },
|
|
{ skill = "Staves", label = "STV", sub = "2h", full = "Staves" },
|
|
{ skill = "Bows", label = "BOW", sub = "rng", full = "Bows" },
|
|
{ skill = "Crossbows", label = "XBW", sub = "rng", full = "Crossbows" },
|
|
{ skill = "Guns", label = "GUN", sub = "rng", full = "Guns" },
|
|
{ skill = "Thrown", label = "THR", sub = "rng", full = "Thrown" },
|
|
{ skill = "Wands", label = "WND", sub = "rng", full = "Wands" },
|
|
}
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Shared grid geometry for BOTH editor grids. The column pitch and the
|
|
-- header-tile size are deliberately the MAIN WINDOW's numbers
|
|
-- (MX.CELL_W 30 + MX.CELL_GAP 3, MX header tile 26 high with a 9 px label
|
|
-- over a 7 px sub) so the options tables read as the same furniture as
|
|
-- the matrix instead of as a second, denser style. They are repeated here
|
|
-- rather than read from DC_Matrix: ui/matrix.lua may not be loaded when
|
|
-- this file is dofile'd offline, and a silent nil would collapse the
|
|
-- layout to zero-width columns.
|
|
-- ------------------------------------------------------------------
|
|
O.CELL_W = 30 -- = MX.CELL_W
|
|
O.CELL_GAP = 3 -- = MX.CELL_GAP
|
|
O.COL_PITCH = 33 -- CELL_W + CELL_GAP, both grids
|
|
O.HEAD_H = 26 -- header tile height (= MX header tile: HEADER_H - 4)
|
|
O.HEAD_ROW_H = 30 -- header bar height (= MX.HEADER_H)
|
|
|
|
O.GRID_LABEL_W = 74 -- role/class label column (expectation grid)
|
|
O.BOOK_NAME_W = 96 -- player-name column (skill book grid)
|
|
O.ROW_H = 22 -- one grid row's height, both grids
|
|
|
|
-- equipment has the most configurable slots (11 armor enchants + neck +
|
|
-- 2 rings; trinkets are item tiles and never configurable, see
|
|
-- slotsForTab) -- the widest grid the pools have to serve.
|
|
O.MAX_SLOT_COLS = 14
|
|
|
|
-- x-offset of the 20 px check button inside its O.CELL_W-wide column
|
|
O.CELL_INSET = math.floor((O.CELL_W - 20) / 2)
|
|
|
|
-- ⚠ Lua 5.0 allows a function at most 32 UPVALUES, and BuildOptionsFrame
|
|
-- in the WoW section below sits right at that ceiling. Every geometry
|
|
-- number therefore lives HERE, on the O table (one upvalue), instead of
|
|
-- as its own file-local in that block -- twelve locals cost twelve
|
|
-- upvalues and overflowing is a COMPILE error that stops the whole file
|
|
-- from loading, i.e. the addon silently loses its options window.
|
|
|
|
-- content width of either grid: label column + one pitch per column.
|
|
function O.GridContentW(nCols)
|
|
return O.GRID_LABEL_W + (nCols or 0) * O.COL_PITCH
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Window width follows the CONTENT of the selected tab, the way the main
|
|
-- window follows its own column count -- a window sized once to its widest
|
|
-- tab leaves the narrow ones (class buffs: 6 columns) swimming in empty
|
|
-- space.
|
|
--
|
|
-- The floor is not a taste number: the editor's own tab BUTTON ROW has to
|
|
-- fit, so it is derived from the button geometry and the tab count. Add a
|
|
-- fifth tab and the floor grows by itself.
|
|
-- ------------------------------------------------------------------
|
|
O.FRAME_PAD_L = 24
|
|
O.FRAME_PAD_R = 24
|
|
O.SCROLLBAR_W = 22 -- UIPanelScrollFrameTemplate's bar, right of the scroll
|
|
O.TAB_BTN_W = 108
|
|
O.TAB_BTN_PITCH = 114
|
|
|
|
function O.MinFrameW()
|
|
local n = table.getn(O.EditorTabs())
|
|
if n < 1 then
|
|
n = 1
|
|
end
|
|
return O.FRAME_PAD_L + (n - 1) * O.TAB_BTN_PITCH + O.TAB_BTN_W
|
|
+ O.FRAME_PAD_R
|
|
end
|
|
|
|
function O.FrameWidthFor(contentW)
|
|
local w = O.FRAME_PAD_L + (contentW or 0) + O.SCROLLBAR_W + O.FRAME_PAD_R
|
|
local min = O.MinFrameW()
|
|
if w < min then
|
|
w = min
|
|
end
|
|
return w
|
|
end
|
|
|
|
-- total content width of the book section (name column + all skill
|
|
-- columns); the options frame is sized to fit this (see widestFrameW()).
|
|
function O.BooksContentW()
|
|
return O.BOOK_NAME_W + table.getn(O.BOOK_COLUMNS) * O.COL_PITCH
|
|
end
|
|
|
|
-- shown instead of checkboxes for a player whose class could not be
|
|
-- determined -- weapon skills are derived from the class
|
|
-- (DC.CLASS_WEAPON_SKILLS), so without a class there is nothing to offer.
|
|
O.SKILLBOOK_UNREADABLE_HINT = "class unknown - can't tell weapon skills"
|
|
|
|
-- ==================================================================
|
|
-- PURE SECTION -- grid row model (offline-testable)
|
|
-- ==================================================================
|
|
|
|
-- Class token order for role lines (WARRIOR,
|
|
-- PALADIN, HUNTER, ROGUE, PRIEST, SHAMAN, MAGE, WARLOCK, DRUID, filtered
|
|
-- per role). Used by the ROLE_CLASSES fallback inversion below.
|
|
O.CLASS_ORDER = { "WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST",
|
|
"SHAMAN", "MAGE", "WARLOCK", "DRUID" }
|
|
|
|
-- Performing classes of a role, ordered. Prefers DC_Roles.ROLE_CLASSES
|
|
-- (core/roles.lua); when it is absent,
|
|
-- derives the same lists by inverting DC_Roles.CLASS_ROLES in
|
|
-- O.CLASS_ORDER. Resolved at CALL time, never cached across files.
|
|
function O.RoleClasses(role)
|
|
if DC_Roles and DC_Roles.ROLE_CLASSES and DC_Roles.ROLE_CLASSES[role] then
|
|
return DC_Roles.ROLE_CLASSES[role]
|
|
end
|
|
local out = {}
|
|
local cr = DC_Roles and DC_Roles.CLASS_ROLES
|
|
if not cr then
|
|
return out
|
|
end
|
|
for i = 1, table.getn(O.CLASS_ORDER) do
|
|
local class = O.CLASS_ORDER[i]
|
|
local list = cr[class]
|
|
if list then
|
|
for j = 1, table.getn(list) do
|
|
if list[j] == role then
|
|
table.insert(out, class)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Ordered grid row descriptors for the expectation editor:
|
|
-- { kind = "role"|"class", role = <token>, class = <token|nil> }.
|
|
-- Identical for all three expectation tabs (the tab only changes the
|
|
-- COLUMNS); the parameter is kept for signature symmetry. 25 rows total:
|
|
-- TANK 1+4, MELEE 1+6, RANGED 1+1, CASTER 1+5, HEALER 1+4.
|
|
function O.GridRows(tab)
|
|
local rows = {}
|
|
local nR = table.getn(DC.ROLES)
|
|
for i = 1, nR do
|
|
local role = DC.ROLES[i]
|
|
table.insert(rows, { kind = "role", role = role })
|
|
local classes = O.RoleClasses(role)
|
|
for j = 1, table.getn(classes) do
|
|
table.insert(rows, { kind = "class", role = role,
|
|
class = classes[j] })
|
|
end
|
|
end
|
|
return rows
|
|
end
|
|
|
|
-- Bulk toggle target for a role header cell click: the new value is the
|
|
-- flip of the current majority. Strict majority checked -> uncheck all;
|
|
-- majority unchecked OR tie -> check all.
|
|
function O.BulkNewValue(checkedCount, total)
|
|
return not (checkedCount * 2 > total)
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Weapon skill books (+3). A quest book raises a weapon skill by 3 --
|
|
-- worth roughly 0.6% hit -- and there is NO way to read it off another
|
|
-- player (no skill channel exists for foreign units). So it is entered
|
|
-- by hand here, per player and per weapon skill, and the scan folds it
|
|
-- into that player's assumed weapon skill on the next read.
|
|
--
|
|
-- A book applies no matter what is currently in the player's hand (a
|
|
-- weapon swap or a spare off-hand does not un-learn a skill), so every
|
|
-- skill the player's CLASS can plausibly train is offered -- not just
|
|
-- whatever happens to be equipped at scan time. The currently WORN
|
|
-- skills are still called out (sorted first, rendered brighter): that is
|
|
-- the common case a raid leader wants to tick without hunting through the
|
|
-- full class list.
|
|
-- ------------------------------------------------------------------
|
|
|
|
-- Rows for the book section: { name, class, skills = { <skill>, ... },
|
|
-- wornCount = <n>, has = { [skill] = true }, worn = { [skill] = true },
|
|
-- unreadable = <bool> }.
|
|
--
|
|
-- `has`/`worn` are the render keys of the GRID (one fixed column per
|
|
-- weapon skill, O.BOOK_COLUMNS): `has` decides whether the row shows a
|
|
-- checkbox in that column at all, `worn` whether it renders at full
|
|
-- strength. They carry exactly the same information as the ordered
|
|
-- `skills` array below -- which the grid no longer needs for placement,
|
|
-- but which stays because it is the readable form of "what is this player
|
|
-- offered", and because worn-first order is what a per-row tooltip and any
|
|
-- future list rendering would use.
|
|
-- `skills` is the UNION of (a) every weapon skill DC.CLASS_WEAPON_SKILLS
|
|
-- lists for the player's class and (b) whatever the scan currently sees
|
|
-- equipped (subtype -> skill via DC.WEAPON_SKILL_OF_SUBTYPE,
|
|
-- data/hit.lua). Order: the CURRENTLY WORN skills first (`skills`
|
|
-- 1..wornCount, deduplicated -- both hands on the same skill line count
|
|
-- once), then the rest of the class list in ITS declared order. A worn
|
|
-- skill outside the class list (server-specific exception) still rides
|
|
-- along in the worn block -- see the header of DC.CLASS_WEAPON_SKILLS.
|
|
-- A player whose CLASS is unknown gets `unreadable = true` and an empty
|
|
-- `skills` -- still a row (the roster line is never silently dropped),
|
|
-- rendered as a hint instead of checkboxes (O.SKILLBOOK_UNREADABLE_HINT).
|
|
-- Players sorted by name (deterministic across scans, like every other
|
|
-- list in this addon).
|
|
function O.SkillBookRows(store)
|
|
local rows = {}
|
|
local players = (store and store.players) or {}
|
|
local subMap = DC.WEAPON_SKILL_OF_SUBTYPE or {}
|
|
local classSkills = DC.CLASS_WEAPON_SKILLS or {}
|
|
for i = 1, table.getn(players) do
|
|
local p = players[i]
|
|
local class = p.class
|
|
if type(class) == "string" and class ~= "" then
|
|
local seen, skills, worn = {}, {}, {}
|
|
local h = p.hit
|
|
if type(h) == "table" then
|
|
local subs = { h.mainSub, h.offSub }
|
|
for s = 1, 2 do
|
|
local sub = subs[s]
|
|
local skill = (type(sub) == "string") and subMap[sub]
|
|
or nil
|
|
if skill and not seen[skill] then
|
|
seen[skill] = true
|
|
worn[skill] = true
|
|
table.insert(skills, skill)
|
|
end
|
|
end
|
|
end
|
|
local wornCount = table.getn(skills)
|
|
local cls = classSkills[class] or {}
|
|
for c = 1, table.getn(cls) do
|
|
local skill = cls[c]
|
|
if not seen[skill] then
|
|
seen[skill] = true
|
|
table.insert(skills, skill)
|
|
end
|
|
end
|
|
table.insert(rows, { name = p.name, class = class,
|
|
skills = skills, wornCount = wornCount,
|
|
has = seen, worn = worn })
|
|
else
|
|
table.insert(rows, { name = p.name, class = class,
|
|
skills = {}, wornCount = 0,
|
|
has = {}, worn = {},
|
|
unreadable = true })
|
|
end
|
|
end
|
|
table.sort(rows, function(a, b) return a.name < b.name end)
|
|
return rows
|
|
end
|
|
|
|
-- Cell state for (row, column) in the Skill books grid:
|
|
-- "worn" -- the player is holding a weapon on this skill line right
|
|
-- now: the common tick, rendered at full strength
|
|
-- "train" -- the player's class can train it but nothing on that line
|
|
-- is equipped: still tickable, rendered dimmed
|
|
-- nil -- this class cannot train it (and it is not worn): no
|
|
-- checkbox at all, so the eye only ever meets cells that can
|
|
-- actually carry a book
|
|
-- A row with an unknown class has no cells at all (hint line instead).
|
|
function O.BookCellState(row, skill)
|
|
if not row or row.unreadable or type(skill) ~= "string" then
|
|
return nil
|
|
end
|
|
if row.worn and row.worn[skill] then
|
|
return "worn"
|
|
end
|
|
if row.has and row.has[skill] then
|
|
return "train"
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Empty-state line for the book section, nil once rows exist. Rows now
|
|
-- need only a known CLASS (see O.SkillBookRows), so this only fires for a
|
|
-- genuinely empty roster -- no scan has happened yet.
|
|
function O.SkillBookHint(rows)
|
|
if rows and table.getn(rows) > 0 then
|
|
return nil
|
|
end
|
|
return "No scanned players yet - scan the raid first."
|
|
end
|
|
|
|
-- Saved shape: db.skillBooks[playerName][skillName] = true|false.
|
|
function O.SkillBookGet(db, name, skill)
|
|
local books = db and db.skillBooks
|
|
local line = books and books[name]
|
|
return (line and line[skill]) and true or false
|
|
end
|
|
|
|
-- Unticking writes an explicit FALSE, never nil -- a bare nil would let a
|
|
-- later defaults top-up re-create the entry and silently re-tick a book
|
|
-- the user deliberately removed (same rule as the expectation grid).
|
|
function O.SkillBookSet(db, name, skill, value)
|
|
if not db or type(name) ~= "string" or type(skill) ~= "string" then
|
|
return
|
|
end
|
|
if db.skillBooks == nil then
|
|
db.skillBooks = {}
|
|
end
|
|
if type(db.skillBooks[name]) ~= "table" then
|
|
db.skillBooks[name] = {}
|
|
end
|
|
db.skillBooks[name][skill] = value and true or false
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Tabs the expectation editor offers.
|
|
-- A tab that never CONSULTS expectations must never appear here, or the
|
|
-- grid would let the user tick boxes that change nothing: DEBUFFS (every
|
|
-- column applies to every player), RESIST and HIT (display-only tabs --
|
|
-- their columns are always all active, see core/model.lua). The list is
|
|
-- derived from DC.TABS by exclusion rather than by index, so a new tab
|
|
-- shifting positions cannot silently drag a display tab into the editor.
|
|
-- ------------------------------------------------------------------
|
|
O.NO_EXPECTATION_TABS = {
|
|
DEBUFFS = true,
|
|
RESIST = true,
|
|
HIT = true,
|
|
}
|
|
|
|
function O.ExpectationTabs()
|
|
local out = {}
|
|
local src = (DC and DC.TABS)
|
|
or { "CONSUMABLES", "CLASSBUFFS", "EQUIPMENT" }
|
|
for i = 1, table.getn(src) do
|
|
if not O.NO_EXPECTATION_TABS[src[i]] then
|
|
table.insert(out, src[i])
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- The editor's FULL tab order: the three expectation tabs plus the
|
|
-- weapon-skill-book tab last. A SEPARATE list from O.ExpectationTabs() on
|
|
-- purpose -- SKILLBOOKS has no expectation grid (O.IsSkillBookTab), so any
|
|
-- code that walks "tabs with a grid" must keep using O.ExpectationTabs()
|
|
-- and never this one.
|
|
-- ------------------------------------------------------------------
|
|
O.SKILLBOOK_TAB = "SKILLBOOKS"
|
|
O.SKILLBOOK_TAB_LABEL = "Skill books"
|
|
|
|
function O.EditorTabs()
|
|
local out = O.ExpectationTabs()
|
|
table.insert(out, O.SKILLBOOK_TAB)
|
|
return out
|
|
end
|
|
|
|
-- True for the one editor tab that swaps the expectation grid for the
|
|
-- weapon-skill-book list (see the WoW section: RefreshEditor /
|
|
-- ApplyEditorTabVisibility).
|
|
function O.IsSkillBookTab(tab)
|
|
return tab == O.SKILLBOOK_TAB
|
|
end
|
|
|
|
-- Button label for an editor tab: DC.TAB_LABEL for the three real tabs,
|
|
-- O.SKILLBOOK_TAB_LABEL for the pseudo-tab that only exists in this editor.
|
|
function O.EditorTabLabel(tab)
|
|
if O.IsSkillBookTab(tab) then
|
|
return O.SKILLBOOK_TAB_LABEL
|
|
end
|
|
return (DC.TAB_LABEL and DC.TAB_LABEL[tab]) or tab
|
|
end
|
|
|
|
-- Class token -> display word ("WARRIOR" -> "Warrior") for class row labels.
|
|
function O.ClassTitle(class)
|
|
if type(class) ~= "string" or class == "" then
|
|
return "?"
|
|
end
|
|
return string.upper(string.sub(class, 1, 1))
|
|
.. string.lower(string.sub(class, 2))
|
|
end
|
|
|
|
-- ==================================================================
|
|
-- WOW SECTION -- frame, widgets, wiring
|
|
-- ==================================================================
|
|
if CreateFrame then
|
|
|
|
local frame = nil -- DopingControlOptionsFrame (lazy)
|
|
local checkboxes = {} -- top checkboxes, for OnShow repopulate
|
|
local expTab = "CONSUMABLES" -- editor tab (not persisted)
|
|
|
|
-- ⚠ Geometry deliberately has NO file-locals here (see the upvalue
|
|
-- note at O.MAX_SLOT_COLS): everything is read off O, which every
|
|
-- function in this block already captures as a single upvalue.
|
|
-- O.GRID_LABEL_W label column O.COL_PITCH column pitch
|
|
-- O.BOOK_NAME_W player-name column O.ROW_H row pitch
|
|
-- O.CELL_INSET check button inset O.MAX_SLOT_COLS widest grid
|
|
-- The two grids differ in column count (14 equipment slots vs. 15
|
|
-- weapon skills); the frame is BUILT at the wider of the two and
|
|
-- narrowed per tab by ApplyEditorWidth, so no widget is ever created
|
|
-- against a width it later has to grow past.
|
|
local function gridWidest()
|
|
return O.GridContentW(O.MAX_SLOT_COLS)
|
|
end
|
|
local function widestFrameW()
|
|
local c = gridWidest()
|
|
if O.BooksContentW() > c then
|
|
c = O.BooksContentW()
|
|
end
|
|
return O.FrameWidthFor(c)
|
|
end
|
|
|
|
-- grid pools (lazy creation, never destroyed, surplus hidden)
|
|
local gridChild = nil -- scroll child, parent of labels + cells
|
|
local gridRowMax = 0 -- highest row index ever created
|
|
local gridColMax = {} -- [r] = highest col index created for row r
|
|
local gridZebra = {} -- [r] = alternating-row wash texture
|
|
local eqHintFS = nil -- equipment-only dim note (RefreshGrid sets text/visibility)
|
|
local dividerTex = nil -- head divider, width follows the frame
|
|
local RefreshGrid -- forward declaration (click handlers call it)
|
|
|
|
-- weapon-skill-book section: same pooling pattern as the expectation
|
|
-- grid, and now the same geometry -- the pool addresses PLAYER ROWS x
|
|
-- the fixed O.BOOK_COLUMNS. Unlike the expectation grid the ROW
|
|
-- CONTENT comes from the STORE and changes with every scan, so the
|
|
-- pool is refilled on OnShow/Refresh (the row COUNT is what varies,
|
|
-- never the columns).
|
|
local booksChild = nil -- scroll child, parent of name labels + cells
|
|
local booksRowMax = 0
|
|
local booksColMax = {} -- [r] = highest col index created for row r
|
|
local booksZebra = {} -- [r] = alternating-row wash texture
|
|
local booksHintFS = nil -- empty-state line (O.SkillBookHint)
|
|
local RefreshBooks -- forward declaration
|
|
|
|
-- Full editor tab order -- resolved by the pure O.EditorTabs() above
|
|
-- (the three expectation tabs, DEBUFFS/RESIST/HIT excluded there,
|
|
-- plus SKILLBOOKS last).
|
|
local EDITOR_TABS = O.EditorTabs()
|
|
|
|
local function db()
|
|
return DopingControlDB
|
|
end
|
|
|
|
local function slotsForTab(tab)
|
|
if tab == O.SKILLBOOK_TAB then
|
|
-- no expectation grid on this tab (see O.IsSkillBookTab); the
|
|
-- caller hides the grid block entirely, this is defensive
|
|
return {}
|
|
elseif tab == "CLASSBUFFS" then
|
|
return DC.SLOTS_CLASSBUFFS or {}
|
|
elseif tab == "EQUIPMENT" then
|
|
-- only kind=="ench" slots are configurable here (14: the 11
|
|
-- armor slots + neck/rings, which are enchantable on this
|
|
-- server -- data/enchants.lua). T1/T2 (trinket item tiles)
|
|
-- have no enchant question to ask and never appear: the
|
|
-- matrix always shows them (item present/absent decides,
|
|
-- expectations are never consulted for tiles).
|
|
local all = DC.SLOTS_EQUIPMENT or {}
|
|
local out = {}
|
|
for i = 1, table.getn(all) do
|
|
if all[i].kind == "ench" then
|
|
table.insert(out, all[i])
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
return DC.SLOTS_CONSUMABLES or {}
|
|
end
|
|
|
|
-- expectations table (shape: [role][class][slotId] = true|nil),
|
|
-- created on demand (config.EnsureDefaults normally materialized +
|
|
-- migrated this at ADDON_LOADED; defensive for odd load orders)
|
|
local function expectations()
|
|
local d = db()
|
|
if not d.expectations then
|
|
local t = {}
|
|
for i = 1, table.getn(DC.ROLES) do
|
|
local role = DC.ROLES[i]
|
|
t[role] = {}
|
|
local classes = O.RoleClasses(role)
|
|
local def = (DC.DEFAULT_EXPECT and DC.DEFAULT_EXPECT[role])
|
|
or {}
|
|
for j = 1, table.getn(classes) do
|
|
local line = {}
|
|
for k, v in pairs(def) do
|
|
if v then
|
|
line[k] = true
|
|
end
|
|
end
|
|
t[role][classes[j]] = line
|
|
end
|
|
end
|
|
d.expectations = t
|
|
end
|
|
return d.expectations
|
|
end
|
|
|
|
-- Effective expectation line for (role, class): the explicit class
|
|
-- line when present, else the DC.DEFAULT_EXPECT role line (rule-5
|
|
-- runtime fallback -- same rule the model applies). Old-shape saved
|
|
-- tables (booleans directly under [role]) are migrated by
|
|
-- config.EnsureDefaults; here they simply fall back to the default.
|
|
local function effLine(exp, role, class)
|
|
local r = exp[role]
|
|
if r and type(r[class]) == "table" then
|
|
return r[class]
|
|
end
|
|
return (DC.DEFAULT_EXPECT and DC.DEFAULT_EXPECT[role]) or {}
|
|
end
|
|
|
|
-- Materialize the class line before the FIRST write: copy the
|
|
-- effective line, so toggling one slot never silently drops the
|
|
-- fallback values of the other slots (the model treats a present
|
|
-- class line as authoritative).
|
|
local function materializeLine(exp, role, class)
|
|
exp[role] = exp[role] or {}
|
|
if type(exp[role][class]) ~= "table" then
|
|
local copy = {}
|
|
local src = effLine(exp, role, class)
|
|
for k, v in pairs(src) do
|
|
if v then
|
|
copy[k] = true
|
|
end
|
|
end
|
|
exp[role][class] = copy
|
|
end
|
|
return exp[role][class]
|
|
end
|
|
|
|
local function refreshMatrix()
|
|
if DC_Matrix and DC_Matrix.Refresh then
|
|
DC_Matrix.Refresh()
|
|
end
|
|
end
|
|
|
|
local function panelBackdrop()
|
|
if DC_Widgets and DC_Widgets.PANEL_BACKDROP then
|
|
return DC_Widgets.PANEL_BACKDROP
|
|
end
|
|
if DC.PANEL_BACKDROP then
|
|
return DC.PANEL_BACKDROP
|
|
end
|
|
return {
|
|
bgFile = "Interface\\AddOns\\DopingControl\\textures\\panel_bg",
|
|
edgeFile = "Interface\\AddOns\\DopingControl\\textures\\panel_border",
|
|
tile = true, tileSize = 128, edgeSize = 16,
|
|
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
|
}
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- widget helpers
|
|
-- ----------------------------------------------------------------
|
|
local function ApplyFont(fs, size)
|
|
if pfUI and pfUI.font_default and pfUI_config and pfUI_config.global
|
|
and pfUI_config.global.font_size then
|
|
fs:SetFont(pfUI.font_default, pfUI_config.global.font_size)
|
|
elseif size then
|
|
fs:SetFont("Fonts\\FRIZQT__.TTF", size)
|
|
end
|
|
end
|
|
|
|
-- save-and-chain tooltip (no HookScript on 1.12)
|
|
local function AddTooltip(widget, text)
|
|
local oldEnter = widget:GetScript("OnEnter")
|
|
local oldLeave = widget:GetScript("OnLeave")
|
|
widget.dcTooltip = text
|
|
widget:SetScript("OnEnter", function()
|
|
if oldEnter then oldEnter() end
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText(this.dcTooltip, 1, 1, 1, 1, 1)
|
|
GameTooltip:Show()
|
|
end)
|
|
widget:SetScript("OnLeave", function()
|
|
if oldLeave then oldLeave() end
|
|
GameTooltip:Hide()
|
|
end)
|
|
end
|
|
|
|
local checkCounter = 0
|
|
local function CreateCheckbox(parent, label, getter, setter)
|
|
checkCounter = checkCounter + 1
|
|
local name = "DopingControlOptCheck" .. checkCounter
|
|
local cb = CreateFrame("CheckButton", name, parent,
|
|
"UICheckButtonTemplate")
|
|
cb:SetWidth(24)
|
|
cb:SetHeight(24)
|
|
local text = getglobal(name .. "Text")
|
|
text:SetText(label)
|
|
ApplyFont(text, 11)
|
|
cb.dcGet = getter
|
|
cb.dcSet = setter
|
|
cb:SetScript("OnClick", function()
|
|
this.dcSet(this:GetChecked() == 1) -- GetChecked returns 1|nil
|
|
end)
|
|
if pfUI and pfUI.api and pfUI.api.SkinCheckbox then
|
|
pfUI.api.SkinCheckbox(cb)
|
|
end
|
|
table.insert(checkboxes, cb)
|
|
return cb
|
|
end
|
|
|
|
local function CreateButton(parent, name, label, width, onClick)
|
|
local b = CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
|
|
b:SetWidth(width or 140)
|
|
b:SetHeight(22)
|
|
b:SetText(label)
|
|
b:SetScript("OnClick", onClick)
|
|
if pfUI and pfUI.api and pfUI.api.SkinButton then
|
|
pfUI.api.SkinButton(b)
|
|
end
|
|
return b
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- column-header furniture, deliberately identical to the MAIN
|
|
-- window's header row (ui/matrix.lua: header bar with theadBg/goldSoft,
|
|
-- one panel2/line tile per column, a 9 px label over a 7 px sub-line).
|
|
-- The two windows are meant to read as one tool -- bare grey text over
|
|
-- the grid did not.
|
|
--
|
|
-- These use their OWN font helper: the file-local ApplyFont takes
|
|
-- pfUI's GLOBAL font size when pfUI is loaded, which is right for
|
|
-- flowing option text but would blow a 7 px sub-line out of a 26 px
|
|
-- tile. ApplyFontFixed keeps our pixel size and only borrows pfUI's
|
|
-- font family -- exactly what DC_Widgets.ApplyFont does for the matrix.
|
|
-- ----------------------------------------------------------------
|
|
local function ApplyFontFixed(fs, size)
|
|
if DC_Widgets and DC_Widgets.ApplyFont then
|
|
DC_Widgets.ApplyFont(fs, size)
|
|
elseif pfUI and pfUI.font_default then
|
|
fs:SetFont(pfUI.font_default, size)
|
|
else
|
|
fs:SetFont("Fonts\\FRIZQT__.TTF", size)
|
|
end
|
|
end
|
|
|
|
local function colorOf(key, r, g, b)
|
|
local c = DC.COLORS and DC.COLORS[key]
|
|
if c then
|
|
return c
|
|
end
|
|
return { r = r, g = g, b = b }
|
|
end
|
|
|
|
local function solidBackdrop()
|
|
if DC_Widgets and DC_Widgets.SOLID_BACKDROP then
|
|
return DC_Widgets.SOLID_BACKDROP
|
|
end
|
|
return {
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
|
tile = false, edgeSize = 1,
|
|
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
|
}
|
|
end
|
|
|
|
-- the dark bar the tiles sit in; `leadLabel` is the main window's
|
|
-- "PLAYER" device -- it names the label column of the grid below.
|
|
local function CreateHeaderBar(parent, name, px, py, w, leadLabel)
|
|
local bar = CreateFrame("Frame", name, parent)
|
|
bar:SetWidth(w)
|
|
bar:SetHeight(O.HEAD_ROW_H)
|
|
bar:SetPoint("TOPLEFT", parent, "TOPLEFT", px, py)
|
|
bar:SetBackdrop(solidBackdrop())
|
|
local bg = colorOf("theadBg", 0.078, 0.094, 0.129)
|
|
local br = colorOf("goldSoft", 0.329, 0.275, 0.165)
|
|
bar:SetBackdropColor(bg.r, bg.g, bg.b, 1)
|
|
bar:SetBackdropBorderColor(br.r, br.g, br.b, 1)
|
|
local lead = bar:CreateFontString(name .. "Lead", "OVERLAY")
|
|
ApplyFontFixed(lead, 8)
|
|
local fc = colorOf("faint", 0.357, 0.384, 0.439)
|
|
lead:SetTextColor(fc.r, fc.g, fc.b)
|
|
lead:SetPoint("LEFT", bar, "LEFT", 6, 0)
|
|
lead:SetText(leadLabel or "")
|
|
return bar
|
|
end
|
|
|
|
-- one column tile. `left` is the x of the column INSIDE the bar (the
|
|
-- tile spans the full O.CELL_W, the check button below is centered in
|
|
-- it by O.CELL_INSET), so tile and cells cannot drift apart.
|
|
local function CreateHeaderTile(bar, name, left)
|
|
local t = CreateFrame("Frame", name, bar)
|
|
t:SetWidth(O.CELL_W)
|
|
t:SetHeight(O.HEAD_H)
|
|
t:SetPoint("LEFT", bar, "LEFT", left, 0)
|
|
t:SetBackdrop(solidBackdrop())
|
|
local bg = colorOf("panel2", 0.09, 0.106, 0.137)
|
|
local br = colorOf("line", 0.149, 0.173, 0.216)
|
|
t:SetBackdropColor(bg.r, bg.g, bg.b, 1)
|
|
t:SetBackdropBorderColor(br.r, br.g, br.b, 1)
|
|
t:EnableMouse(true)
|
|
local lbl = t:CreateFontString(name .. "Label", "OVERLAY")
|
|
ApplyFontFixed(lbl, 9)
|
|
local pc = colorOf("parch", 0.839, 0.804, 0.722)
|
|
lbl:SetTextColor(pc.r, pc.g, pc.b)
|
|
lbl:SetPoint("TOP", t, "TOP", 0, -3)
|
|
local sub = t:CreateFontString(name .. "Sub", "OVERLAY")
|
|
ApplyFontFixed(sub, 7)
|
|
local fc = colorOf("faint", 0.357, 0.384, 0.439)
|
|
sub:SetTextColor(fc.r, fc.g, fc.b)
|
|
sub:SetPoint("BOTTOM", t, "BOTTOM", 0, 3)
|
|
t.dcLabelFS = lbl
|
|
t.dcSubFS = sub
|
|
return t
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- grid cell handlers (defined once, shared by every pooled cell)
|
|
-- ----------------------------------------------------------------
|
|
local function GridCellOnClick()
|
|
local slotId = this.dcSlot
|
|
if not slotId or not this.dcRole then
|
|
return
|
|
end
|
|
local exp = expectations()
|
|
if this.dcKind == "role" then
|
|
-- header-row cell: bulk-toggle the slot for ALL performing
|
|
-- classes of the role -- majority flip
|
|
local role = this.dcRole
|
|
local classes = O.RoleClasses(role)
|
|
local n = table.getn(classes)
|
|
local checked = 0
|
|
for i = 1, n do
|
|
if effLine(exp, role, classes[i])[slotId] then
|
|
checked = checked + 1
|
|
end
|
|
end
|
|
local newVal = O.BulkNewValue(checked, n)
|
|
for i = 1, n do
|
|
local line = materializeLine(exp, role, classes[i])
|
|
if newVal then
|
|
line[slotId] = true
|
|
else
|
|
-- explicit false, NOT nil: a bare nil would let a
|
|
-- later EnsureExpectations top-up (core/config.lua)
|
|
-- silently re-check a slot the user deliberately
|
|
-- unchecked (A1 migration fix)
|
|
line[slotId] = false
|
|
end
|
|
end
|
|
else
|
|
local line = materializeLine(exp, this.dcRole, this.dcClass)
|
|
if this:GetChecked() == 1 then
|
|
line[slotId] = true
|
|
else
|
|
line[slotId] = false -- explicit deselect, see above
|
|
end
|
|
end
|
|
RefreshGrid() -- resync class rows + header aggregate cells
|
|
refreshMatrix() -- applies immediately
|
|
end
|
|
|
|
local function GridCellOnEnter()
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
if this.dcKind == "role" then
|
|
GameTooltip:SetText((DC.ROLE_LABEL[this.dcRole] or this.dcRole
|
|
or "?") .. " (all classes) - " .. (this.dcLabel or "?"),
|
|
1, 1, 1)
|
|
GameTooltip:AddLine("Click toggles this slot for every class of"
|
|
.. " the role (majority flip).", 0.8, 0.8, 0.8, 1)
|
|
else
|
|
GameTooltip:SetText((DC.ROLE_LABEL[this.dcRole] or this.dcRole
|
|
or "?") .. " " .. O.ClassTitle(this.dcClass) .. " - "
|
|
.. (this.dcLabel or "?"), 1, 1, 1)
|
|
GameTooltip:AddLine("Click to toggle whether this class is"
|
|
.. " expected to have it in this role.", 0.8, 0.8, 0.8, 1)
|
|
end
|
|
GameTooltip:Show()
|
|
end
|
|
|
|
local function GridCellOnLeave()
|
|
GameTooltip:Hide()
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- weapon-skill-book cell handlers (shared by every pooled cell)
|
|
-- ----------------------------------------------------------------
|
|
local function BookCellOnClick()
|
|
if not this.dcName or not this.dcSkill then
|
|
return
|
|
end
|
|
O.SkillBookSet(db(), this.dcName, this.dcSkill,
|
|
this:GetChecked() == 1)
|
|
-- No rescan needed: the assumed weapon skill is resolved when the
|
|
-- matrix builds its view-model (it reads db.skillBooks), so the
|
|
-- new cap is on screen with this refresh.
|
|
refreshMatrix()
|
|
end
|
|
|
|
local function BookCellOnEnter()
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText((this.dcName or "?") .. " - "
|
|
.. (this.dcSkill or "?"), 1, 1, 1)
|
|
-- the column header is 3 characters; without this line a dimmed
|
|
-- cell and a bright one would be indistinguishable on hover
|
|
if this.dcWorn then
|
|
GameTooltip:AddLine("Currently equipped on this skill line.",
|
|
0.6, 0.8, 0.6, 1)
|
|
else
|
|
GameTooltip:AddLine("Trainable by this class, nothing equipped"
|
|
.. " on it right now.", 0.7, 0.7, 0.7, 1)
|
|
end
|
|
GameTooltip:AddLine("Weapon skill book: +3 skill, about 0.6% hit."
|
|
.. " Nothing on this client can read it off another player, so"
|
|
.. " tick it by hand.", 0.8, 0.8, 0.8, 1)
|
|
GameTooltip:Show()
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- grid pools (lazy factories; positions are index-fixed at creation
|
|
-- because O.GridRows is identical across the three expectation tabs)
|
|
-- ----------------------------------------------------------------
|
|
-- alternating-row wash, one pooled texture per row of a grid. Same
|
|
-- device and same strength as the matrix rows (DC.ZEBRA_ALPHA) -- the
|
|
-- point is that a row can be followed across 14-15 columns by eye.
|
|
-- Textures live on the SCROLL CHILD at BACKGROUND layer; the check
|
|
-- buttons are child FRAMES and therefore always draw above them.
|
|
local function EnsureZebra(store, parent, r, w, rh)
|
|
local t = store[r]
|
|
if t then
|
|
return t
|
|
end
|
|
t = parent:CreateTexture(nil, "BACKGROUND")
|
|
local zc = (DC.COLORS and DC.COLORS.zebra) or { r = 1, g = 1, b = 1 }
|
|
t:SetTexture(zc.r, zc.g, zc.b)
|
|
t:SetAlpha(DC.ZEBRA_ALPHA or 0.035)
|
|
t:SetWidth(w)
|
|
t:SetHeight(rh)
|
|
t:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -((r - 1) * rh))
|
|
store[r] = t
|
|
return t
|
|
end
|
|
|
|
-- `on` is the CALLER's decision, not a parity of r: the expectation
|
|
-- grid stripes every second CLASS row while the role headers between
|
|
-- them stay unstriped (they carry their own emphasis), so the two
|
|
-- indices are not the same number.
|
|
local function PaintZebra(store, parent, r, w, rh, on)
|
|
local t = EnsureZebra(store, parent, r, w, rh)
|
|
-- width is re-applied every time, not just at creation: the grid
|
|
-- narrows with the tab (ApplyEditorWidth) and a stripe left at the
|
|
-- widest tab's width would run out under the scroll frame's edge
|
|
t:SetWidth(w)
|
|
if on then
|
|
t:Show()
|
|
else
|
|
t:Hide()
|
|
end
|
|
end
|
|
|
|
local function EnsureRowLabel(r)
|
|
local name = "DopingControlOptRowLabel" .. r
|
|
local fs = getglobal(name)
|
|
if fs then
|
|
return fs
|
|
end
|
|
fs = gridChild:CreateFontString(name, "ARTWORK",
|
|
"GameFontNormalSmall")
|
|
fs:SetJustifyH("LEFT")
|
|
if r > gridRowMax then
|
|
gridRowMax = r
|
|
end
|
|
if gridColMax[r] == nil then
|
|
gridColMax[r] = 0
|
|
end
|
|
return fs
|
|
end
|
|
|
|
local function EnsureGridCell(r, c)
|
|
local name = "DopingControlOptExpR" .. r .. "C" .. c
|
|
local cb = getglobal(name)
|
|
if cb then
|
|
return cb
|
|
end
|
|
cb = CreateFrame("CheckButton", name, gridChild,
|
|
"UICheckButtonTemplate")
|
|
cb:SetWidth(20)
|
|
cb:SetHeight(20)
|
|
cb:SetPoint("TOPLEFT", gridChild, "TOPLEFT",
|
|
O.GRID_LABEL_W + (c - 1) * O.COL_PITCH + O.CELL_INSET, -((r - 1) * O.ROW_H) - 1)
|
|
getglobal(name .. "Text"):SetText("")
|
|
cb:SetScript("OnClick", GridCellOnClick)
|
|
cb:SetScript("OnEnter", GridCellOnEnter)
|
|
cb:SetScript("OnLeave", GridCellOnLeave)
|
|
-- Deliberately NOT pfUI-skinned (unlike the top checkboxes):
|
|
-- SkinCheckbox puts its dark box on an own frame, and inside the
|
|
-- scroll child its frame level clamps ABOVE the check button --
|
|
-- the check mark rendered BEHIND the box (observed
|
|
-- in-game). The stock template draws its check on top by
|
|
-- construction; a slight style mix beats invisible state.
|
|
if r > gridRowMax then
|
|
gridRowMax = r
|
|
end
|
|
if gridColMax[r] == nil or c > gridColMax[r] then
|
|
gridColMax[r] = c
|
|
end
|
|
return cb
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- weapon-skill-book pools (lazy; ROW and COLUMN positions are
|
|
-- index-fixed like the expectation grid -- only which cells are SHOWN
|
|
-- changes with the roster, see RefreshBooks)
|
|
-- ----------------------------------------------------------------
|
|
local function EnsureBookLabel(r)
|
|
local name = "DopingControlOptBookName" .. r
|
|
local fs = getglobal(name)
|
|
if fs then
|
|
return fs
|
|
end
|
|
fs = booksChild:CreateFontString(name, "ARTWORK",
|
|
"GameFontNormalSmall")
|
|
ApplyFont(fs, 10)
|
|
fs:SetJustifyH("LEFT")
|
|
fs:SetWidth(O.BOOK_NAME_W - 4)
|
|
fs:SetPoint("TOPLEFT", booksChild, "TOPLEFT", 0,
|
|
-((r - 1) * O.ROW_H) - 5)
|
|
if r > booksRowMax then
|
|
booksRowMax = r
|
|
end
|
|
if booksColMax[r] == nil then
|
|
booksColMax[r] = 0
|
|
end
|
|
return fs
|
|
end
|
|
|
|
-- one pooled hint line per row, shown instead of the checkboxes for a
|
|
-- player whose class is unknown (O.SKILLBOOK_UNREADABLE_HINT)
|
|
local function EnsureBookRowHint(r)
|
|
local name = "DopingControlOptBookRowHint" .. r
|
|
local fs = getglobal(name)
|
|
if fs then
|
|
return fs
|
|
end
|
|
fs = booksChild:CreateFontString(name, "ARTWORK",
|
|
"GameFontNormalSmall")
|
|
ApplyFont(fs, 9)
|
|
fs:SetJustifyH("LEFT")
|
|
fs:SetTextColor(0.36, 0.38, 0.44)
|
|
fs:SetPoint("TOPLEFT", booksChild, "TOPLEFT", O.BOOK_NAME_W,
|
|
-((r - 1) * O.ROW_H) - 5)
|
|
return fs
|
|
end
|
|
|
|
local function EnsureBookCell(r, c)
|
|
local name = "DopingControlOptBookR" .. r .. "C" .. c
|
|
local cb = getglobal(name)
|
|
if cb then
|
|
return cb
|
|
end
|
|
cb = CreateFrame("CheckButton", name, booksChild,
|
|
"UICheckButtonTemplate")
|
|
cb:SetWidth(20)
|
|
cb:SetHeight(20)
|
|
cb:SetPoint("TOPLEFT", booksChild, "TOPLEFT",
|
|
O.BOOK_NAME_W + (c - 1) * O.COL_PITCH + O.CELL_INSET,
|
|
-((r - 1) * O.ROW_H) - 1)
|
|
-- no per-cell label any more: the column header carries the skill
|
|
-- name (that IS the readability fix -- see O.BOOK_COLUMNS)
|
|
getglobal(name .. "Text"):SetText("")
|
|
cb:SetScript("OnClick", BookCellOnClick)
|
|
cb:SetScript("OnEnter", BookCellOnEnter)
|
|
cb:SetScript("OnLeave", GridCellOnLeave)
|
|
-- deliberately NOT pfUI-skinned, for the same reason as the
|
|
-- expectation cells above (the skin's box clamps above the check
|
|
-- mark inside a scroll child)
|
|
if r > booksRowMax then
|
|
booksRowMax = r
|
|
end
|
|
if booksColMax[r] == nil or c > booksColMax[r] then
|
|
booksColMax[r] = c
|
|
end
|
|
return cb
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- weapon-skill-book refresh: one ROW per scanned player from the
|
|
-- CURRENT store (O.SkillBookRows), one cell per FIXED column
|
|
-- (O.BOOK_COLUMNS), check states from db.skillBooks. A cell exists
|
|
-- only where the player could own a book (O.BookCellState); worn
|
|
-- skills -- the common tick -- render at full strength, the rest of
|
|
-- the class list dimmed. A row whose class is unknown shows a hint
|
|
-- instead of cells.
|
|
-- ----------------------------------------------------------------
|
|
RefreshBooks = function()
|
|
if not booksChild then
|
|
return
|
|
end
|
|
local rows = O.SkillBookRows(DC.store)
|
|
local nRows = table.getn(rows)
|
|
local d = db()
|
|
for r = 1, nRows do
|
|
local rd = rows[r]
|
|
-- every row here IS a data row (one player), so plain parity
|
|
PaintZebra(booksZebra, booksChild, r, O.BooksContentW(), O.ROW_H,
|
|
math.mod(r, 2) == 0)
|
|
local lbl = EnsureBookLabel(r)
|
|
local cc = DC.COLORS and DC.COLORS["class" .. (rd.class or "")]
|
|
if cc then
|
|
lbl:SetTextColor(cc.r, cc.g, cc.b)
|
|
else
|
|
lbl:SetTextColor(0.79, 0.8, 0.83)
|
|
end
|
|
lbl:SetText(rd.name or "")
|
|
lbl:Show()
|
|
local rowHint = EnsureBookRowHint(r)
|
|
if rd.unreadable then
|
|
rowHint:SetText(O.SKILLBOOK_UNREADABLE_HINT)
|
|
rowHint:Show()
|
|
else
|
|
rowHint:Hide()
|
|
end
|
|
for c = 1, table.getn(O.BOOK_COLUMNS) do
|
|
local skill = O.BOOK_COLUMNS[c].skill
|
|
local state = O.BookCellState(rd, skill)
|
|
if state then
|
|
local cb = EnsureBookCell(r, c)
|
|
cb.dcName = rd.name
|
|
cb.dcSkill = skill
|
|
cb.dcWorn = (state == "worn")
|
|
-- Every offered cell renders at FULL strength: a
|
|
-- half-transparent CHECKED box reads as broken, not
|
|
-- as a hint. Which weapon is in hand is a scan
|
|
-- detail, not a property of the book, so it belongs
|
|
-- in the tooltip (BookCellOnEnter) and nowhere else. Which
|
|
-- weapon is in hand is a scan detail, not a property
|
|
-- of the book, so it belongs in the tooltip
|
|
-- (BookCellOnEnter) and nowhere else.
|
|
cb:SetAlpha(1)
|
|
cb:SetChecked(O.SkillBookGet(d, rd.name, skill)
|
|
and 1 or nil)
|
|
cb:Show()
|
|
else
|
|
local cb = getglobal("DopingControlOptBookR" .. r
|
|
.. "C" .. c)
|
|
if cb then
|
|
cb:Hide()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- surplus pooled rows (the roster shrinks between scans)
|
|
for r = nRows + 1, booksRowMax do
|
|
local lbl = getglobal("DopingControlOptBookName" .. r)
|
|
if lbl then
|
|
lbl:Hide()
|
|
end
|
|
if booksZebra[r] then
|
|
booksZebra[r]:Hide()
|
|
end
|
|
local rowHint = getglobal("DopingControlOptBookRowHint" .. r)
|
|
if rowHint then
|
|
rowHint:Hide()
|
|
end
|
|
for c = 1, (booksColMax[r] or 0) do
|
|
local cb = getglobal("DopingControlOptBookR" .. r
|
|
.. "C" .. c)
|
|
if cb then
|
|
cb:Hide()
|
|
end
|
|
end
|
|
end
|
|
local contentH = nRows * O.ROW_H + 4
|
|
if contentH < 1 then
|
|
contentH = 1
|
|
end
|
|
booksChild:SetHeight(contentH)
|
|
if booksHintFS then
|
|
booksHintFS:SetText(O.SkillBookHint(rows) or "")
|
|
end
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- expectation grid refresh (headers, row labels, check states, tabs)
|
|
-- ----------------------------------------------------------------
|
|
RefreshGrid = function()
|
|
local slots = slotsForTab(expTab)
|
|
local nSlots = table.getn(slots)
|
|
local exp = expectations()
|
|
local rows = O.GridRows(expTab)
|
|
local nRows = table.getn(rows)
|
|
-- column header tiles (outside the scroll frame, sticky). The tile
|
|
-- SET is per tab (consumables 11, class buffs 6, equipment 14), so
|
|
-- unlike the book grid's fixed columns these are refilled here.
|
|
for c = 1, O.MAX_SLOT_COLS do
|
|
local head = getglobal("DopingControlOptColHead" .. c)
|
|
if head then
|
|
if c <= nSlots then
|
|
local sd = slots[c]
|
|
head.dcLabelFS:SetText(sd.label or sd.id)
|
|
head.dcSubFS:SetText(sd.sub or "")
|
|
head.dcTooltip = sd.full or sd.id
|
|
head:Show()
|
|
else
|
|
head:Hide()
|
|
end
|
|
end
|
|
end
|
|
-- grouped rows: role header + indented class rows.
|
|
-- Stripes span THIS tab's content width, not the widest tab's.
|
|
local gridW = O.GridContentW(nSlots)
|
|
local dataIdx = 0
|
|
for r = 1, nRows do
|
|
local rd = rows[r]
|
|
local lbl = EnsureRowLabel(r)
|
|
local indent = 0
|
|
if rd.kind == "class" then
|
|
indent = 12
|
|
end
|
|
-- stripe every second CLASS row; role headers stay clean
|
|
if rd.kind == "class" then
|
|
dataIdx = dataIdx + 1
|
|
PaintZebra(gridZebra, gridChild, r, gridW, O.ROW_H,
|
|
math.mod(dataIdx, 2) == 0)
|
|
else
|
|
PaintZebra(gridZebra, gridChild, r, gridW, O.ROW_H, false)
|
|
end
|
|
if lbl.dcIndent ~= indent then
|
|
lbl.dcIndent = indent
|
|
lbl:ClearAllPoints()
|
|
lbl:SetPoint("TOPLEFT", gridChild, "TOPLEFT", indent,
|
|
-((r - 1) * O.ROW_H) - 5)
|
|
lbl:SetWidth(O.GRID_LABEL_W - indent)
|
|
end
|
|
if rd.kind == "role" then
|
|
-- 1.12 has no bold flag: size 11 + role color carry the
|
|
-- header hierarchy over the size-10 class rows
|
|
ApplyFont(lbl, 11)
|
|
local rc = DC.COLORS and DC.COLORS["role" .. rd.role]
|
|
if rc then
|
|
lbl:SetTextColor(rc.r, rc.g, rc.b)
|
|
end
|
|
lbl:SetText(DC.ROLE_LABEL[rd.role] or rd.role)
|
|
else
|
|
ApplyFont(lbl, 10)
|
|
local cc = DC.COLORS and DC.COLORS["class" .. rd.class]
|
|
if cc then
|
|
lbl:SetTextColor(cc.r, cc.g, cc.b)
|
|
end
|
|
lbl:SetText(O.ClassTitle(rd.class))
|
|
end
|
|
lbl:Show()
|
|
for c = 1, O.MAX_SLOT_COLS do
|
|
if c <= nSlots then
|
|
local sd = slots[c]
|
|
local cb = EnsureGridCell(r, c)
|
|
cb.dcKind = rd.kind
|
|
cb.dcRole = rd.role
|
|
cb.dcClass = rd.class
|
|
cb.dcSlot = sd.id
|
|
cb.dcLabel = sd.full or sd.id
|
|
if rd.kind == "role" then
|
|
-- aggregate cell: all classes checked = checked,
|
|
-- none = unchecked, mixed = checked at 0.45 alpha
|
|
-- (no tri-state CheckButton on 1.12)
|
|
local classes = O.RoleClasses(rd.role)
|
|
local nCl = table.getn(classes)
|
|
local nOn = 0
|
|
for k = 1, nCl do
|
|
if effLine(exp, rd.role, classes[k])[sd.id] then
|
|
nOn = nOn + 1
|
|
end
|
|
end
|
|
cb:SetChecked((nCl > 0 and nOn > 0) and 1 or nil)
|
|
if nOn > 0 and nOn < nCl then
|
|
cb:SetAlpha(0.45)
|
|
else
|
|
cb:SetAlpha(1)
|
|
end
|
|
else
|
|
cb:SetChecked(
|
|
effLine(exp, rd.role, rd.class)[sd.id] and 1
|
|
or nil)
|
|
cb:SetAlpha(1)
|
|
end
|
|
cb:Show()
|
|
else
|
|
local cb = getglobal("DopingControlOptExpR" .. r
|
|
.. "C" .. c)
|
|
if cb then
|
|
cb:Hide()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- hide surplus pooled rows (row count is constant today; the pool
|
|
-- pattern keeps this safe if GridRows ever shrinks)
|
|
for r = nRows + 1, gridRowMax do
|
|
local lbl = getglobal("DopingControlOptRowLabel" .. r)
|
|
if lbl then
|
|
lbl:Hide()
|
|
end
|
|
if gridZebra[r] then
|
|
gridZebra[r]:Hide()
|
|
end
|
|
for c = 1, (gridColMax[r] or 0) do
|
|
local cb = getglobal("DopingControlOptExpR" .. r .. "C" .. c)
|
|
if cb then
|
|
cb:Hide()
|
|
end
|
|
end
|
|
end
|
|
-- equipment-only dim note: the grid only configures ENCHANTS
|
|
-- (empty slots are always a gap, regardless of these checkboxes)
|
|
if eqHintFS then
|
|
if expTab == "EQUIPMENT" then
|
|
eqHintFS:SetText(
|
|
"checkboxes = enchant expectations; empty slots always count")
|
|
else
|
|
eqHintFS:SetText("")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- editor tab switching: exactly one of {grid block, book block} is
|
|
-- shown at a time, in the SAME position/width (see BuildOptionsFrame,
|
|
-- which sizes the window to the taller of the two so this never
|
|
-- resizes it). Named widgets are looked up by their global name --
|
|
-- every one of them is created once in BuildOptionsFrame and just
|
|
-- toggled here.
|
|
-- ----------------------------------------------------------------
|
|
local function ApplyEditorTabVisibility()
|
|
local isBooks = O.IsSkillBookTab(expTab)
|
|
local gridScroll = getglobal("DopingControlOptExpScroll")
|
|
local hintFS = getglobal("DopingControlOptExpHint")
|
|
local resetBtn = getglobal("DopingControlOptResetBtn")
|
|
local bookNote = getglobal("DopingControlOptBookNote")
|
|
local bookScroll = getglobal("DopingControlOptBookScroll")
|
|
local gridHeadBar = getglobal("DopingControlOptHeadBar")
|
|
local bookHeadBar = getglobal("DopingControlOptBookHeadBar")
|
|
if isBooks then
|
|
-- hiding the BAR hides its tiles with it (they are its
|
|
-- children), so a stale grid tab's headers cannot bleed
|
|
-- into this tab
|
|
if gridHeadBar then gridHeadBar:Hide() end
|
|
if bookHeadBar then bookHeadBar:Show() end
|
|
if gridScroll then gridScroll:Hide() end
|
|
if hintFS then hintFS:Hide() end
|
|
if eqHintFS then eqHintFS:Hide() end
|
|
if resetBtn then resetBtn:Hide() end
|
|
if bookNote then bookNote:Show() end
|
|
if bookScroll then bookScroll:Show() end
|
|
if booksHintFS then booksHintFS:Show() end
|
|
else
|
|
-- WHICH tiles of the grid bar are lit is governed by
|
|
-- RefreshGrid (per-tab slot count), not repeated here
|
|
if gridHeadBar then gridHeadBar:Show() end
|
|
if bookHeadBar then bookHeadBar:Hide() end
|
|
if gridScroll then gridScroll:Show() end
|
|
if hintFS then hintFS:Show() end
|
|
if eqHintFS then eqHintFS:Show() end
|
|
if resetBtn then resetBtn:Show() end
|
|
if bookNote then bookNote:Hide() end
|
|
if bookScroll then bookScroll:Hide() end
|
|
if booksHintFS then booksHintFS:Hide() end
|
|
end
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- Width follows the selected tab, like the main window follows its
|
|
-- column count. Only WIDTHS change here -- every widget is anchored
|
|
-- TOPLEFT (or to the frame's own right/bottom edge), so nothing needs
|
|
-- repositioning when the frame narrows.
|
|
-- ----------------------------------------------------------------
|
|
local function EditorContentW(tab)
|
|
if O.IsSkillBookTab(tab) then
|
|
return O.BooksContentW()
|
|
end
|
|
return O.GridContentW(table.getn(slotsForTab(tab)))
|
|
end
|
|
|
|
local function ApplyEditorWidth()
|
|
if not frame then
|
|
return
|
|
end
|
|
local contentW = EditorContentW(expTab)
|
|
frame:SetWidth(O.FrameWidthFor(contentW))
|
|
if dividerTex then
|
|
dividerTex:SetWidth(O.FrameWidthFor(contentW) - 48)
|
|
end
|
|
if O.IsSkillBookTab(expTab) then
|
|
return -- the book block is fixed-width (15 fixed columns)
|
|
end
|
|
local bar = getglobal("DopingControlOptHeadBar")
|
|
if bar then
|
|
bar:SetWidth(contentW)
|
|
end
|
|
local scroll = getglobal("DopingControlOptExpScroll")
|
|
if scroll then
|
|
scroll:SetWidth(contentW)
|
|
end
|
|
if gridChild then
|
|
gridChild:SetWidth(contentW)
|
|
end
|
|
end
|
|
|
|
-- the active editor tab's button is disabled (disabled = selected),
|
|
-- every other one enabled -- walks the FULL O.EditorTabs() order (4
|
|
-- buttons), unlike the old grid-only loop this replaces
|
|
local function UpdateTabButtons()
|
|
for i = 1, table.getn(EDITOR_TABS) do
|
|
local b = getglobal("DopingControlOptTabBtn" .. i)
|
|
if b then
|
|
if EDITOR_TABS[i] == expTab then
|
|
b:Disable()
|
|
else
|
|
b:Enable()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- single entry point for "the editor tab (may have) changed": always
|
|
-- refreshes both pools (pooled widgets are cheap to touch even while
|
|
-- hidden, and this keeps state consistent if the user flips tabs
|
|
-- quickly), then shows/hides the two content blocks and the tab row.
|
|
local function RefreshEditor()
|
|
RefreshGrid()
|
|
RefreshBooks()
|
|
ApplyEditorTabVisibility()
|
|
ApplyEditorWidth()
|
|
UpdateTabButtons()
|
|
end
|
|
|
|
-- reset button: double-click-to-confirm (see header)
|
|
local function ResetDisarmWatcher()
|
|
if not this.dcArmedUntil or GetTime() > this.dcArmedUntil then
|
|
this.dcArmedUntil = nil
|
|
this:SetText("Reset expectations")
|
|
this:SetScript("OnUpdate", nil)
|
|
end
|
|
end
|
|
|
|
local function ResetOnClick()
|
|
local now = GetTime()
|
|
if this.dcArmedUntil and now <= this.dcArmedUntil then
|
|
-- second click inside the window: reseed every (role, class)
|
|
-- line from DC.DEFAULT_EXPECT (config.ResetExpectations,
|
|
-- per-class shape; fallback: drop + re-materialize)
|
|
this.dcArmedUntil = nil
|
|
this:SetText("Reset expectations")
|
|
this:SetScript("OnUpdate", nil)
|
|
if DC.ResetExpectations then
|
|
DC.ResetExpectations(db())
|
|
else
|
|
db().expectations = nil
|
|
end
|
|
RefreshGrid() -- expectations() re-materializes when cleared
|
|
refreshMatrix()
|
|
else
|
|
-- first click (or lapsed): arm
|
|
this.dcArmedUntil = now + O.RESET_ARM_WINDOW
|
|
this:SetText("Click again to confirm")
|
|
this:SetScript("OnUpdate", ResetDisarmWatcher)
|
|
end
|
|
end
|
|
|
|
local function ResetDisarm(btn)
|
|
btn.dcArmedUntil = nil
|
|
btn:SetText("Reset expectations")
|
|
btn:SetScript("OnUpdate", nil)
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- frame build (lazy, once)
|
|
-- ----------------------------------------------------------------
|
|
local function BuildOptionsFrame()
|
|
if frame then
|
|
return frame
|
|
end
|
|
|
|
local f = CreateFrame("Frame", "DopingControlOptionsFrame", UIParent)
|
|
f:SetWidth(widestFrameW())
|
|
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
|
f:SetFrameStrata("DIALOG")
|
|
f:SetBackdrop(panelBackdrop())
|
|
f:SetBackdropColor(1, 1, 1, 0.97)
|
|
f:SetMovable(true)
|
|
f:EnableMouse(true)
|
|
f:RegisterForDrag("LeftButton")
|
|
f:SetScript("OnDragStart", function() this:StartMoving() end)
|
|
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
|
f:SetClampedToScreen(true)
|
|
|
|
-- title (gold)
|
|
local title = f:CreateFontString("DopingControlOptionsTitle",
|
|
"ARTWORK", "GameFontNormal")
|
|
title:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -16)
|
|
ApplyFont(title, 14)
|
|
local g = DC.COLORS and DC.COLORS.goldHi
|
|
if g then
|
|
title:SetTextColor(g.r, g.g, g.b)
|
|
else
|
|
title:SetTextColor(0.85, 0.66, 0.31)
|
|
end
|
|
title:SetText("DopingControl")
|
|
|
|
-- close X
|
|
local closeX = CreateFrame("Button", "DopingControlOptionsCloseX",
|
|
f, "UIPanelCloseButton")
|
|
closeX:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
|
|
if pfUI and pfUI.api and pfUI.api.SkinCloseButton then
|
|
pfUI.api.SkinCloseButton(closeX)
|
|
end
|
|
|
|
-- layout cursor
|
|
local x, y = 24, -44
|
|
local function place(w, dy)
|
|
w:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
|
|
y = y + dy
|
|
end
|
|
|
|
-- ------------------------------------------------------------
|
|
-- checkboxes
|
|
-- ------------------------------------------------------------
|
|
local cbTest = CreateCheckbox(f, "Test mode (simulated raid)",
|
|
function() return db().testMode end,
|
|
function(v)
|
|
if DC.SetTestMode then
|
|
DC.SetTestMode(v) -- core/init.lua: swaps store + rescan
|
|
else
|
|
db().testMode = v -- fallback if init not loaded yet
|
|
end
|
|
end)
|
|
place(cbTest, -28)
|
|
AddTooltip(cbTest, "Replace the scan with a fixed simulated raid.\nChat outputs are disabled while active.")
|
|
|
|
local cbMinimap = CreateCheckbox(f, "Show minimap button",
|
|
function() return db().showMinimapButton end,
|
|
function(v)
|
|
db().showMinimapButton = v
|
|
if DC_Minimap and DC_Minimap.Update then
|
|
DC_Minimap.Update()
|
|
end
|
|
end)
|
|
place(cbMinimap, -28)
|
|
|
|
local cbScan = CreateCheckbox(f, "Auto-scan on ready check",
|
|
function() return db().readyCheckScan end,
|
|
function(v) db().readyCheckScan = v end)
|
|
place(cbScan, -28)
|
|
|
|
local cbOpen = CreateCheckbox(f, "Auto-open on ready check",
|
|
function() return db().readyCheckOpen end,
|
|
function(v) db().readyCheckOpen = v end)
|
|
place(cbOpen, -28)
|
|
|
|
-- ------------------------------------------------------------
|
|
-- divider
|
|
-- ------------------------------------------------------------
|
|
local divider = f:CreateTexture("DopingControlOptDivider", "ARTWORK")
|
|
divider:SetHeight(1)
|
|
divider:SetWidth(widestFrameW() - 48)
|
|
local lc = DC.COLORS and DC.COLORS.line
|
|
if lc then
|
|
divider:SetTexture(lc.r, lc.g, lc.b)
|
|
else
|
|
divider:SetTexture(0.15, 0.17, 0.22)
|
|
end
|
|
divider:SetPoint("TOPLEFT", f, "TOPLEFT", x, y - 6)
|
|
dividerTex = divider
|
|
y = y - 14
|
|
|
|
-- ------------------------------------------------------------
|
|
-- expectation editor (role x class grouped rows)
|
|
-- ------------------------------------------------------------
|
|
local heading = f:CreateFontString("DopingControlOptExpHeading",
|
|
"ARTWORK", "GameFontNormalSmall")
|
|
ApplyFont(heading, 11)
|
|
heading:SetTextColor(0.79, 0.8, 0.83)
|
|
heading:SetText("Expectations (role x class x slot)")
|
|
place(heading, -22)
|
|
|
|
-- tab selector: one small button per EDITOR_TABS entry (the three
|
|
-- expectation tabs plus Skill books last); the selected one is
|
|
-- disabled. widestFrameW() already accounts for the widest of the two
|
|
-- content blocks below, so this fits comfortably.
|
|
local tabRowY = y
|
|
for i = 1, table.getn(EDITOR_TABS) do
|
|
local tabKey = EDITOR_TABS[i]
|
|
local b = CreateButton(f, "DopingControlOptTabBtn" .. i,
|
|
O.EditorTabLabel(tabKey), 108,
|
|
function()
|
|
expTab = this.dcTab
|
|
RefreshEditor()
|
|
end)
|
|
b:SetHeight(20)
|
|
b.dcTab = tabKey
|
|
b:SetPoint("TOPLEFT", f, "TOPLEFT", x + (i - 1) * 114, tabRowY)
|
|
end
|
|
y = y - 26
|
|
|
|
-- shared top for the two mutually exclusive content blocks below
|
|
-- (only one is ever shown, ApplyEditorTabVisibility); the window
|
|
-- height is fixed to whichever block is taller so switching tabs
|
|
-- never resizes it (see the height computation after both).
|
|
local contentTop = y
|
|
|
|
-- ------------------------------------------------------------
|
|
-- BLOCK 1: expectation grid (Consumables / Class buffs /
|
|
-- Equipment) -- column headers, scrolling grouped grid, hint,
|
|
-- equipment-only note, reset button
|
|
-- ------------------------------------------------------------
|
|
local gy = contentTop
|
|
|
|
-- column header bar + tiles (outside the scroll frame, so they stay
|
|
-- put while the grid scrolls) -- same furniture as the main window
|
|
local gridHeadBar = CreateHeaderBar(f, "DopingControlOptHeadBar",
|
|
x, gy, gridWidest(), "ROLE / CLASS")
|
|
for c = 1, O.MAX_SLOT_COLS do
|
|
local head = CreateHeaderTile(gridHeadBar,
|
|
"DopingControlOptColHead" .. c, O.GRID_LABEL_W + (c - 1) * O.COL_PITCH)
|
|
AddTooltip(head, "")
|
|
end
|
|
gy = gy - O.HEAD_ROW_H - 2
|
|
|
|
-- scroll frame around the grouped grid (the 25 rows would be
|
|
-- ~550px -- cap at O.GRID_MAX_H, scroll the rest)
|
|
local rows = O.GridRows(expTab)
|
|
local nRows = table.getn(rows)
|
|
local contentH = nRows * O.ROW_H + 4
|
|
local gridH = contentH
|
|
if gridH > O.GRID_MAX_H then
|
|
gridH = O.GRID_MAX_H
|
|
end
|
|
local scroll = CreateFrame("ScrollFrame", "DopingControlOptExpScroll",
|
|
f, "UIPanelScrollFrameTemplate")
|
|
scroll:SetPoint("TOPLEFT", f, "TOPLEFT", x, gy)
|
|
scroll:SetWidth(gridWidest())
|
|
scroll:SetHeight(gridH)
|
|
gridChild = CreateFrame("Frame", "DopingControlOptExpScrollChild",
|
|
scroll)
|
|
gridChild:SetWidth(gridWidest())
|
|
gridChild:SetHeight(contentH)
|
|
scroll:SetScrollChild(gridChild)
|
|
if pfUI and pfUI.api and pfUI.api.SkinScrollbar then
|
|
pfUI.api.SkinScrollbar(
|
|
getglobal("DopingControlOptExpScrollScrollBar"))
|
|
end
|
|
gy = gy - gridH - 8
|
|
|
|
-- hint
|
|
local hint = f:CreateFontString("DopingControlOptExpHint",
|
|
"ARTWORK", "GameFontNormalSmall")
|
|
ApplyFont(hint, 9)
|
|
hint:SetTextColor(0.36, 0.38, 0.44)
|
|
hint:SetText("Click a cell to toggle - applies immediately."
|
|
.. " Role-row cells set all classes of the role.")
|
|
hint:SetPoint("TOPLEFT", f, "TOPLEFT", x, gy)
|
|
gy = gy - 12
|
|
|
|
-- equipment-only note (RefreshGrid sets the text; empty string on
|
|
-- every other tab, so it costs one small blank line there)
|
|
local eqHint = f:CreateFontString("DopingControlOptEqHint",
|
|
"ARTWORK", "GameFontNormalSmall")
|
|
ApplyFont(eqHint, 9)
|
|
eqHint:SetTextColor(0.36, 0.38, 0.44)
|
|
eqHint:SetText("")
|
|
eqHint:SetPoint("TOPLEFT", f, "TOPLEFT", x, gy)
|
|
gy = gy - 14
|
|
eqHintFS = eqHint
|
|
|
|
-- reset button (double-click-to-confirm)
|
|
local resetBtn = CreateButton(f, "DopingControlOptResetBtn",
|
|
"Reset expectations", 160, ResetOnClick)
|
|
resetBtn:SetPoint("TOPLEFT", f, "TOPLEFT", x, gy)
|
|
gy = gy - 30
|
|
AddTooltip(resetBtn,
|
|
"Restore the default expectations for every class.\nFirst click arms, second click within "
|
|
.. O.RESET_ARM_WINDOW .. " s confirms.")
|
|
|
|
local gridBlockBottom = gy
|
|
|
|
-- ------------------------------------------------------------
|
|
-- BLOCK 2: weapon skill books -- explanation line at the TOP
|
|
-- (same spot the column headers occupy in block 1), then the
|
|
-- scrolling, wrapped skill list, then the empty-state hint
|
|
-- ------------------------------------------------------------
|
|
local by = contentTop
|
|
|
|
local bookNote = f:CreateFontString("DopingControlOptBookNote",
|
|
"ARTWORK", "GameFontNormalSmall")
|
|
ApplyFont(bookNote, 9)
|
|
bookNote:SetTextColor(0.36, 0.38, 0.44)
|
|
bookNote:SetText("A book is worth about 0.6% hit and cannot be read"
|
|
.. " off another player - tick what you know.")
|
|
bookNote:SetPoint("TOPLEFT", f, "TOPLEFT", x, by)
|
|
by = by - 16
|
|
|
|
-- sticky column header bar (outside the scroll frame, so it stays
|
|
-- put while the roster scrolls) -- same furniture as the
|
|
-- expectation grid above and as the main window, only with a FIXED
|
|
-- set of labels (O.BOOK_COLUMNS), filled once here.
|
|
local bookHeadBar = CreateHeaderBar(f, "DopingControlOptBookHeadBar",
|
|
x, by, O.BooksContentW(), "PLAYER")
|
|
for c = 1, table.getn(O.BOOK_COLUMNS) do
|
|
local cd = O.BOOK_COLUMNS[c]
|
|
local head = CreateHeaderTile(bookHeadBar,
|
|
"DopingControlOptBookColHead" .. c,
|
|
O.BOOK_NAME_W + (c - 1) * O.COL_PITCH)
|
|
head.dcLabelFS:SetText(cd.label)
|
|
head.dcSubFS:SetText(cd.sub or "")
|
|
AddTooltip(head, cd.full or cd.skill)
|
|
end
|
|
by = by - O.HEAD_ROW_H - 2
|
|
|
|
-- fixed height: the roster length must never resize the window;
|
|
-- width fits the fixed weapon-skill column set (O.BooksContentW)
|
|
-- Height: stretch this block so it ends exactly where the (taller)
|
|
-- grid block ends -- the window is sized to the taller of the two
|
|
-- and a fixed cap would leave dead space under this tab. 24 = the
|
|
-- 8 px gap plus the 16 px hint line that still follow below.
|
|
-- Floored at O.BOOKS_MAX_H so this block, not a shrinking grid
|
|
-- block, decides the minimum.
|
|
local booksScrollH = by - gridBlockBottom - 24
|
|
if booksScrollH < O.BOOKS_MAX_H then
|
|
booksScrollH = O.BOOKS_MAX_H
|
|
end
|
|
local bookScroll = CreateFrame("ScrollFrame",
|
|
"DopingControlOptBookScroll", f, "UIPanelScrollFrameTemplate")
|
|
bookScroll:SetPoint("TOPLEFT", f, "TOPLEFT", x, by)
|
|
bookScroll:SetWidth(O.BooksContentW())
|
|
bookScroll:SetHeight(booksScrollH)
|
|
booksChild = CreateFrame("Frame", "DopingControlOptBookScrollChild",
|
|
bookScroll)
|
|
booksChild:SetWidth(O.BooksContentW())
|
|
booksChild:SetHeight(booksScrollH)
|
|
bookScroll:SetScrollChild(booksChild)
|
|
if pfUI and pfUI.api and pfUI.api.SkinScrollbar then
|
|
pfUI.api.SkinScrollbar(
|
|
getglobal("DopingControlOptBookScrollScrollBar"))
|
|
end
|
|
by = by - booksScrollH - 8
|
|
|
|
-- empty state (RefreshBooks sets the text; empty string once rows
|
|
-- exist, so it costs one blank line then)
|
|
local bookHint = f:CreateFontString("DopingControlOptBookHint",
|
|
"ARTWORK", "GameFontNormalSmall")
|
|
ApplyFont(bookHint, 9)
|
|
bookHint:SetTextColor(0.36, 0.38, 0.44)
|
|
bookHint:SetText("")
|
|
bookHint:SetPoint("TOPLEFT", f, "TOPLEFT", x, by)
|
|
by = by - 16
|
|
booksHintFS = bookHint
|
|
|
|
local booksBlockBottom = by
|
|
|
|
-- the window is sized to the TALLER of the two blocks (more
|
|
-- negative y = descended further) so it never jumps when the
|
|
-- Skill books tab is selected
|
|
if gridBlockBottom < booksBlockBottom then
|
|
y = gridBlockBottom
|
|
else
|
|
y = booksBlockBottom
|
|
end
|
|
|
|
-- version footer
|
|
local footer = f:CreateFontString("DopingControlOptFooter",
|
|
"ARTWORK", "GameFontNormalSmall")
|
|
ApplyFont(footer, 9)
|
|
footer:SetTextColor(0.6, 0.6, 0.6)
|
|
local ver = GetAddOnMetadata
|
|
and GetAddOnMetadata("DopingControl", "Version")
|
|
footer:SetText("DopingControl " .. (ver or ""))
|
|
footer:SetPoint("BOTTOM", f, "BOTTOM", 0, 10)
|
|
|
|
-- window height: computed from the layout cursor (grows
|
|
-- with the grid but stays bounded by the scroll cap);
|
|
-- 34 = footer strip below the last placed widget
|
|
f:SetHeight(-y + 34)
|
|
|
|
-- OnShow repopulates every widget from DB
|
|
f:SetScript("OnShow", function()
|
|
for i = 1, table.getn(checkboxes) do
|
|
local cb = checkboxes[i]
|
|
cb:SetChecked(cb.dcGet() and 1 or nil)
|
|
end
|
|
RefreshEditor() -- grid/books pools + tab visibility + buttons
|
|
ResetDisarm(getglobal("DopingControlOptResetBtn"))
|
|
end)
|
|
|
|
-- ESC closes
|
|
table.insert(UISpecialFrames, "DopingControlOptionsFrame")
|
|
|
|
-- widgets are built shown by default; establish the correct
|
|
-- initial block visibility once even though OnShow will redo it
|
|
-- on every real Show() (defensive against odd load orders, same
|
|
-- spirit as the rest of this file)
|
|
frame = f
|
|
ApplyEditorTabVisibility()
|
|
-- the per-tab WIDTH is applied by RefreshEditor on the first
|
|
-- OnShow, not here: BuildOptionsFrame sits one upvalue under Lua
|
|
-- 5.0's hard limit of 32, and capturing ApplyEditorWidth as well
|
|
-- pushes it over ("too many upvalues" -- a COMPILE error, i.e. the
|
|
-- whole file stops loading). The frame is built at its widest, so
|
|
-- the only cost is that it stays wide until the first Show().
|
|
|
|
f:Hide()
|
|
return f
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- public API
|
|
-- ----------------------------------------------------------------
|
|
function O.Toggle()
|
|
local f = BuildOptionsFrame()
|
|
if f:IsShown() then
|
|
f:Hide()
|
|
else
|
|
f:Show()
|
|
end
|
|
end
|
|
|
|
function O.Open()
|
|
local f = BuildOptionsFrame()
|
|
f:Show()
|
|
end
|
|
|
|
-- external state changes (/dc test, minimap toggle) can resync an
|
|
-- open options window
|
|
function O.Refresh()
|
|
if frame and frame:IsShown() then
|
|
for i = 1, table.getn(checkboxes) do
|
|
local cb = checkboxes[i]
|
|
cb:SetChecked(cb.dcGet() and 1 or nil)
|
|
end
|
|
RefreshEditor() -- a fresh scan can add or drop book rows
|
|
end
|
|
end
|
|
|
|
-- convenience alias (minimap right-click / slash handler)
|
|
DC.ToggleOptions = O.Toggle
|
|
|
|
end
|