248 lines
9.4 KiB
Lua
248 lines
9.4 KiB
Lua
-- DopingControl core/roles.lua
|
|
-- Role suggestion heuristic + confirmed-role resolution: five roles, the
|
|
-- role hangs on the player NAME, and no heuristic ever decides silently --
|
|
-- a suggestion is always flagged as such and carries a reason string.
|
|
-- Badge UX: CLASS_ROLES = ordered plausible roles per class, cycle() =
|
|
-- badge right-click (step + immediate confirm).
|
|
-- Per-class expectations:
|
|
-- ROLE_CLASSES = generated inversion of CLASS_ROLES (ordered classes per
|
|
-- role) -- the domain of db.expectations[role][class].
|
|
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
|
|
|
|
DopingControl = DopingControl or {}
|
|
local DC = DopingControl
|
|
|
|
DC_Roles = DC_Roles or {}
|
|
|
|
-- Transient confirmations for the simulated raid (test mode). NEVER written
|
|
-- to SavedVariables: synthetic names must not pollute db.roles.
|
|
-- Reset by DC_Sim.buildStore() on every (re-)entry into test mode.
|
|
DC.simRoles = DC.simRoles or {}
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Plausible roles per class token, ORDERED (the order is
|
|
-- the badge right-click cycle order). suggest() below must only ever
|
|
-- return a role CONTAINED in the class's list -- property-tested over
|
|
-- every class x fact combination in the offline test suite.
|
|
-- ------------------------------------------------------------------
|
|
DC_Roles.CLASS_ROLES = {
|
|
WARRIOR = { "MELEE", "TANK" },
|
|
ROGUE = { "MELEE" },
|
|
HUNTER = { "RANGED", "MELEE" }, -- melee hunter is a real spec on TWoW
|
|
MAGE = { "CASTER" },
|
|
WARLOCK = { "CASTER" },
|
|
PRIEST = { "HEALER", "CASTER" },
|
|
DRUID = { "HEALER", "TANK", "MELEE", "CASTER" },
|
|
SHAMAN = { "HEALER", "TANK", "MELEE", "CASTER" },
|
|
PALADIN = { "HEALER", "TANK", "MELEE" },
|
|
}
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Per-class expectations:
|
|
-- ROLE_CLASSES = the INVERSION of CLASS_ROLES -- for each role the
|
|
-- ordered list of class tokens that can perform it, in the fixed
|
|
-- class order below, filtered per role. GENERATED from CLASS_ROLES so
|
|
-- the two tables can never drift apart.
|
|
-- Consumers: core/config.lua materializes db.expectations[role][class]
|
|
-- over exactly these lists, core/model.lua iterates them for the
|
|
-- dynamic-column check, and the options expectation grid renders one
|
|
-- indented class row per entry.
|
|
-- ------------------------------------------------------------------
|
|
DC_Roles.CLASS_ORDER = {
|
|
"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST",
|
|
"SHAMAN", "MAGE", "WARLOCK", "DRUID",
|
|
}
|
|
|
|
DC_Roles.ROLE_CLASSES = {}
|
|
do
|
|
-- fresh tables on every (re-)load -- regeneration, not accretion
|
|
local inv = DC_Roles.ROLE_CLASSES
|
|
for i = 1, table.getn(DC_Roles.CLASS_ORDER) do
|
|
local cls = DC_Roles.CLASS_ORDER[i]
|
|
local list = DC_Roles.CLASS_ROLES[cls] or {}
|
|
for j = 1, table.getn(list) do
|
|
local role = list[j]
|
|
if inv[role] == nil then
|
|
inv[role] = {}
|
|
end
|
|
table.insert(inv[role], cls)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Facts extraction: derive the suggestion facts from a store player entry.
|
|
-- Works on both aura paths: form NAMES from
|
|
-- the tooltip path, form SPELL IDS from the GUID path (which delivers ids
|
|
-- only).
|
|
-- [ASSUMPTION] Form aura spell IDs (vanilla 1.12): Cat Form 768, Bear Form
|
|
-- 5487, Dire Bear Form 9634, Moonkin Form 24858, Shadowform 15473.
|
|
-- ------------------------------------------------------------------
|
|
DC_Roles.FORM_NAMES = {
|
|
["Cat Form"] = "catForm",
|
|
["Bear Form"] = "bearForm",
|
|
["Dire Bear Form"] = "bearForm",
|
|
["Moonkin Form"] = "moonkin",
|
|
["Shadowform"] = "shadowform",
|
|
}
|
|
|
|
DC_Roles.FORM_IDS = {
|
|
[768] = "catForm",
|
|
[5487] = "bearForm",
|
|
[9634] = "bearForm",
|
|
[24858] = "moonkin",
|
|
[15473] = "shadowform",
|
|
}
|
|
|
|
function DC_Roles.factsFor(player)
|
|
local facts = {
|
|
hasShield = false, catForm = false, bearForm = false,
|
|
shadowform = false, moonkin = false,
|
|
}
|
|
if not player then
|
|
return facts
|
|
end
|
|
if player.aurasRead and player.auras then
|
|
if player.auras.names then
|
|
for name, key in pairs(DC_Roles.FORM_NAMES) do
|
|
if player.auras.names[name] then
|
|
facts[key] = true
|
|
end
|
|
end
|
|
end
|
|
if player.auras.ids then
|
|
for id, key in pairs(DC_Roles.FORM_IDS) do
|
|
if player.auras.ids[id] then
|
|
facts[key] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if player.gearRead and player.gear then
|
|
local off = player.gear[17] -- off hand inventory slot
|
|
if type(off) == "table" and off.subType == "Shields" then
|
|
facts.hasShield = true
|
|
end
|
|
end
|
|
return facts
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Suggestion heuristic. ALWAYS returns a
|
|
-- (role, reason) pair -- no suggestion without a stated reason (the badge
|
|
-- tooltip shows it).
|
|
-- ------------------------------------------------------------------
|
|
function DC_Roles.suggest(class, facts)
|
|
facts = facts or {}
|
|
if class == "WARRIOR" then
|
|
if facts.hasShield then
|
|
return "TANK", "shield equipped"
|
|
end
|
|
return "MELEE", "class default"
|
|
elseif class == "ROGUE" then
|
|
return "MELEE", "class default"
|
|
elseif class == "HUNTER" then
|
|
return "RANGED", "class default"
|
|
elseif class == "MAGE" then
|
|
return "CASTER", "class default"
|
|
elseif class == "WARLOCK" then
|
|
return "CASTER", "class default"
|
|
elseif class == "PRIEST" then
|
|
if facts.shadowform then
|
|
return "CASTER", "Shadowform at scan time"
|
|
end
|
|
return "HEALER", "class default"
|
|
elseif class == "DRUID" then
|
|
-- form precedence: bear > cat > moonkin > none (a druid can only be
|
|
-- in one form at a time; the order only matters for garbage input)
|
|
if facts.bearForm then
|
|
return "TANK", "Bear Form at scan time"
|
|
end
|
|
if facts.catForm then
|
|
return "MELEE", "Cat Form at scan time"
|
|
end
|
|
if facts.moonkin then
|
|
return "CASTER", "Moonkin Form at scan time"
|
|
end
|
|
return "HEALER", "class default"
|
|
elseif class == "SHAMAN" then
|
|
-- deliberately NOT shield->MELEE: resto shamans
|
|
-- carry shields too, so a shield is no enhancement signal here.
|
|
-- Plain class default; enhancers get confirmed by hand once.
|
|
return "HEALER", "class default"
|
|
elseif class == "PALADIN" then
|
|
if facts.hasShield then
|
|
return "TANK", "shield equipped"
|
|
end
|
|
return "HEALER", "class default"
|
|
end
|
|
-- unknown class token: still return a pair, never nil
|
|
return "MELEE", "unknown class"
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- Resolution: confirmed role wins over suggestion.
|
|
-- source "sim" -> confirmations live ONLY in DC.simRoles (transient);
|
|
-- db.roles is never consulted and never written for
|
|
-- simulated players (synthetic names must
|
|
-- not end up in SavedVariables).
|
|
-- source "scan" -> confirmations live in db.roles (persistent, keyed by
|
|
-- player name).
|
|
-- Returns role, suggested(bool), reason(string|nil):
|
|
-- confirmed -> (role, false, nil)
|
|
-- suggested -> (role, true, reason)
|
|
-- ------------------------------------------------------------------
|
|
function DC_Roles.resolve(db, storeSource, player)
|
|
local name = player and player.name
|
|
local confirmed
|
|
if storeSource == "sim" then
|
|
confirmed = name and DC.simRoles[name]
|
|
else
|
|
confirmed = name and db and db.roles and db.roles[name]
|
|
end
|
|
if confirmed then
|
|
return confirmed, false, nil
|
|
end
|
|
local role, reason = DC_Roles.suggest(player and player.class,
|
|
DC_Roles.factsFor(player))
|
|
return role, true, reason
|
|
end
|
|
|
|
-- Confirm a role (badge LEFT-click). Same routing rule as resolve().
|
|
function DC_Roles.confirm(db, storeSource, name, role)
|
|
if storeSource == "sim" then
|
|
DC.simRoles[name] = role
|
|
else
|
|
db.roles[name] = role
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------------
|
|
-- cycle(db, storeSource, name, class, currentRole) -> nextRole
|
|
-- Badge RIGHT-click: step to the entry AFTER currentRole
|
|
-- in CLASS_ROLES[class] (wrapping; currentRole not in the list -> the
|
|
-- FIRST entry) and CONFIRM it immediately -- same persistence routing as
|
|
-- confirm() (sim source -> DC.simRoles, else db.roles).
|
|
-- Single-entry classes wrap onto themselves (the UI treats right-click
|
|
-- as a no-op there and never calls this; calling it anyway is harmless:
|
|
-- the same role comes back and is re-confirmed).
|
|
-- Unknown class token: no list -> currentRole returned unchanged and
|
|
-- NOTHING confirmed (defensive; scan classes are always known tokens).
|
|
-- ------------------------------------------------------------------
|
|
function DC_Roles.cycle(db, storeSource, name, class, currentRole)
|
|
local list = DC_Roles.CLASS_ROLES[class]
|
|
if not list or table.getn(list) == 0 then
|
|
return currentRole
|
|
end
|
|
local n = table.getn(list)
|
|
local nextRole = list[1]
|
|
for i = 1, n do
|
|
if list[i] == currentRole then
|
|
nextRole = list[math.mod(i, n) + 1]
|
|
break
|
|
end
|
|
end
|
|
DC_Roles.confirm(db, storeSource, name, nextRole)
|
|
return nextRole
|
|
end
|