-- 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