DopingControl v0.6.1

This commit is contained in:
2026-08-03 09:47:19 +02:00
commit 6859d73285
41 changed files with 15056 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
-- DopingControl data/classbuffs.lua
-- Class-buffs tab:
-- slot definitions + buff name -> slot table with rank spell IDs.
--
-- [ASSUMPTION] Rank IDs below are from vanilla 1.12 knowledge and are
-- not individually verified in-game. Verify via
-- the probe dump before trusting ID matching; TWoW may add extra ranks.
-- Name-based detection alone is sufficient for tab purposes EXCEPT the
-- "Shadow Protection" name collision: the priest buff and the old
-- protection-potion aura (7242) share the name -> ID-before-name matching
-- is mandatory once both tabs are live.
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
DopingControl = DopingControl or {}
local DC = DopingControl
-- ------------------------------------------------------------------
-- Slot definitions (array, order = column order). Same field shape as
-- DC.SLOTS_CONSUMABLES. Slot ids: AI/MOTW/PWF/SPROT/EMB/SOUL;
-- tile labels/sub-labels: INT/MTW/FRT/SP/EMB/SST. EMB (Emerald Blessing)
-- carries NO seed entry in data/expectations.lua -- see that file's
-- header for why -- so it starts as an inactive dynamic column, exactly
-- like the equipment tab's T1/T2.
-- ------------------------------------------------------------------
DC.SLOTS_CLASSBUFFS = {
{ id = "AI", label = "INT", sub = "Arcane", full = "Arcane Intellect", kind = "aura",
items = "Arcane Intellect / Brilliance (mage)" },
{ id = "MOTW", label = "MTW", sub = "Wild", full = "Mark of the Wild", kind = "aura",
items = "Mark / Gift of the Wild (druid)" },
{ id = "PWF", label = "FRT", sub = "Priest", full = "Power Word: Fortitude", kind = "aura",
items = "Power Word / Prayer of Fortitude (priest)" },
{ id = "SPROT", label = "SP", sub = "situat.", full = "Shadow Protection", kind = "aura",
items = "Shadow Protection (priest) - situational per boss" },
{ id = "EMB", label = "EMB", sub = "hit", full = "Emerald Blessing", kind = "aura",
items = "Emerald Blessing (druid, Emerald Sanctum questline) - also grants +1% spell hit, see Resistances" },
{ id = "SOUL", label = "SST", sub = "healers", full = "Soulstone", kind = "aura",
items = "Soulstone (warlock) - expected only for healers" },
}
-- ------------------------------------------------------------------
-- Buff name -> slot + rank IDs ([ASSUMPTION], see header).
-- ------------------------------------------------------------------
DC.CLASSBUFFS = {
["Arcane Intellect"] = { slot = "AI", ids = { 1459, 1460, 1461, 10156, 10157 } },
["Arcane Brilliance"] = { slot = "AI", ids = { 23028 } },
["Mark of the Wild"] = { slot = "MOTW", ids = { 1126, 5232, 6756, 5234, 8907, 9884, 9885 } },
["Gift of the Wild"] = { slot = "MOTW", ids = { 21849, 21850 } },
["Power Word: Fortitude"] = { slot = "PWF", ids = { 1243, 1244, 1245, 2791, 10937, 10938 } },
["Prayer of Fortitude"] = { slot = "PWF", ids = { 21562, 21564 } },
["Shadow Protection"] = { slot = "SPROT", ids = { 976, 10957, 10958 } }, -- name collides with potion aura 7242
["Prayer of Shadow Protection"] = { slot = "SPROT", ids = { 27683 } },
-- raid-wide druid buff on this server; both spell ids seen for it
["Emerald Blessing"] = { slot = "EMB", ids = { 57108, 57109 } },
["Soulstone Resurrection"] = { slot = "SOUL", ids = { 20707, 20762, 20763, 20764, 20765, 20766 } },
}
-- ------------------------------------------------------------------
-- Merge rank IDs into the global spellID -> slotId map started by
-- data/consumables.lua (slot ids are unique across tabs -> no ambiguity;
-- the ID sets are disjoint, test_data.lua asserts this).
-- ------------------------------------------------------------------
DC.SPELL_TO_SLOT = DC.SPELL_TO_SLOT or {}
for name, def in pairs(DC.CLASSBUFFS) do
local n = table.getn(def.ids)
for i = 1, n do
DC.SPELL_TO_SLOT[def.ids[i]] = def.slot
end
end
+132
View File
@@ -0,0 +1,132 @@
-- DopingControl data/consumables.lua
-- Consumables tab: slot definitions + buff->slot table + spellID->slot map.
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
DopingControl = DopingControl or {}
local DC = DopingControl
-- ------------------------------------------------------------------
-- Slot definitions (array, order = column order).
-- Fields: id (stable key, used by DEFAULT_EXPECT / model / UI),
-- label (column tile abbrev), sub (tile sub-label), full (long name,
-- used in whisper/report), kind ("aura" | "weapon"),
-- items (tooltip example text, optional).
-- Separator decision (once for all data files): items strings are
-- tooltip/FontString-only, so they may use the middle-dot list separator
-- "\194\183" (UTF-8 middle dot renders fine in own
-- FontStrings; the group headers already use the same glyph). Em dashes
-- stay ASCII "-" (that glyph is unproven in the 1.12 font). SENT
-- chat lines remain pure ASCII -- they never use these
-- strings.
-- ------------------------------------------------------------------
DC.SLOTS_CONSUMABLES = {
{ id = "FLASK", label = "FLK", sub = "Flask", full = "Flask", kind = "aura",
items = "Flask of the Titans \194\183 Distilled Wisdom \194\183 Supreme Power" },
{ id = "FOOD", label = "FOD", sub = "Food", full = "Food buff", kind = "aura",
items = "Well Fed - e.g. Grilled Squid \194\183 Blessed Sunfruit" },
{ id = "AP", label = "AP", sub = "Attack", full = "Attack power", kind = "aura",
items = "Winterfall Firewater \194\183 Juju Might" },
{ id = "STR", label = "STR", sub = "Str.", full = "Strength", kind = "aura",
items = "Juju Power \194\183 Elixir of Giants" },
{ id = "AGI", label = "AGI", sub = "Agi.", full = "Agility", kind = "aura",
items = "Elixir of the Mongoose" },
{ id = "SP", label = "SP", sub = "Spell", full = "Spell power", kind = "aura",
items = "Greater Arcane Elixir \194\183 Shadow Power \194\183 Firepower" },
{ id = "MP5", label = "MP5", sub = "Mana", full = "Mana regen", kind = "aura",
items = "Mageblood Potion \194\183 Nightfin Soup" },
{ id = "ARM", label = "ARM", sub = "Armor", full = "Armor", kind = "aura",
items = "Elixir of Superior Defense" },
{ id = "HP", label = "HP", sub = "Stam.", full = "Stamina/HP", kind = "aura",
items = "Elixir of Fortitude \194\183 Rumsey Rum Black Label" },
{ id = "FR", label = "FR", sub = "fire", full = "Fire protection", kind = "aura",
items = "Greater Fire Protection Potion - situational per boss; enable/disable via the expectation matrix" },
{ id = "WPN", label = "WPN", sub = "temp.", full = "Weapon (temporary)", kind = "weapon", -- NOT an aura slot
items = "Dense Sharpening Stone \194\183 Brilliant Wizard/Mana Oil - permanent enchant: Equipment tab" },
}
-- ------------------------------------------------------------------
-- Buff name -> slot table.
-- spellID = 0 means "match by name only" (no known aura spell ID).
-- ------------------------------------------------------------------
DC.CONSUMABLES = {
-- FLASK
["Flask of the Titans"] = { slot = "FLASK", spellID = 17626, item = "Flask of the Titans" },
["Flask of Supreme Power"] = { slot = "FLASK", spellID = 17628, item = "Flask of Supreme Power" },
["Flask of Distilled Wisdom"] = { slot = "FLASK", spellID = 17627, item = "Flask of Distilled Wisdom" },
["Flask of Chromatic Resistance"] = { slot = "FLASK", spellID = 17629, item = "Flask of Chromatic Resistance" },
["Flask of Petrification"] = { slot = "FLASK", spellID = 17624, item = "Flask of Petrification" },
-- FOOD (buff names, not item names; TWoW custom foods likely all show "Well Fed" - unverified)
["Well Fed"] = { slot = "FOOD", spellID = 0, item = "Grilled Squid etc." },
-- ["Food"] (the eating tick) deliberately REMOVED: it is
-- the sit-and-eat channel aura, not a food buff -- it made the FOOD
-- cell green for anyone currently chewing (field-tested). Only
-- lasting food buffs count.
["Increased Stamina"] = { slot = "FOOD", spellID = 0, item = "stamina food" },
["Dragonbreath Chili"] = { slot = "FOOD", spellID = 15852, item = "Dragonbreath Chili" },
-- AP
["Winterfall Firewater"] = { slot = "AP", spellID = 17038, item = "Winterfall Firewater" },
["Juju Might"] = { slot = "AP", spellID = 16329, item = "Juju Might" },
-- STR
["Juju Power"] = { slot = "STR", spellID = 16323, item = "Juju Power" },
["Elixir of Giants"] = { slot = "STR", spellID = 11405, item = "Elixir of Giants" },
["Elixir of the Giants"] = { slot = "STR", spellID = 11405, item = "Elixir of Giants" }, -- AURA name on this client (measured live: SpellInfo(11405) = "Elixir of the Giants"); the item is named without "the", and matching runs on the aura name first
["Elixir of Brute Force"] = { slot = "STR", spellID = 17537, item = "Elixir of Brute Force" },
["Strike of the Scorpok"] = { slot = "STR", spellID = 10669, item = "Strike of the Scorpok" }, -- measured live in a raid (26 players); was tallied as unknown
["Rage of Ages"] = { slot = "STR", spellID = 0, item = "R.O.I.D.S." }, -- aura name differs from the item; own spell ID not measured yet
-- AGI
["Elixir of the Mongoose"] = { slot = "AGI", spellID = 17538, item = "Elixir of the Mongoose" },
["Elixir of Greater Agility"] = { slot = "AGI", spellID = 11334, item = "Elixir of Greater Agility" },
["Greater Agility"] = { slot = "AGI", spellID = 11334, item = "Elixir of Greater Agility" }, -- AURA name (the item name never appears as a buff); was tallied as unknown 14x
["Elixir of Agility"] = { slot = "AGI", spellID = 11328, item = "Elixir of Agility" },
-- SP (merged: arcane_elixir + school_elixir + dreamtonic + dreamshard)
["Greater Arcane Elixir"] = { slot = "SP", spellID = 17539, item = "Greater Arcane Elixir" },
["Arcane Elixir"] = { slot = "SP", spellID = 11390, item = "Arcane Elixir" },
["Elixir of Shadow Power"] = { slot = "SP", spellID = 11474, item = "Elixir of Shadow Power" },
["Elixir of Frost Power"] = { slot = "SP", spellID = 21920, item = "Elixir of Frost Power" },
["Greater Frost Power"] = { slot = "SP", spellID = 56544, item = "Elixir of Greater Frost Power" }, -- AURA name (TurtleWoW-added). NOT measured in-game: taken from a TurtleWoW spell database (56544 = "Greater Frost Power", "Increases frost spell damage by up to 40 for 3600 sec.") plus the item name in pfQuest-turtle's item DB; the aura drops "Elixir of" the same way Giants/Agility above do
["Elixir of Greater Firepower"] = { slot = "SP", spellID = 26276, item = "Elixir of Greater Firepower" },
["Elixir of Firepower"] = { slot = "SP", spellID = 7844, item = "Elixir of Firepower" },
["Dreamtonic"] = { slot = "SP", spellID = 45489, item = "Dreamtonic" }, -- use-spell ID; the aura's own ID is unverified
["Dreamshard Elixir"] = { slot = "SP", spellID = 45427, item = "Dreamshard Elixir" }, -- use-spell ID; the aura's own ID is unverified
-- MP5
["Mageblood Potion"] = { slot = "MP5", spellID = 24363, item = "Mageblood Potion" },
["Mageblood"] = { slot = "MP5", spellID = 24363, item = "Mageblood Potion" }, -- TWoW buff-name variant (old Data.lua kept both)
-- ARM
["Elixir of Superior Defense"] = { slot = "ARM", spellID = 11348, item = "Elixir of Superior Defense" },
["Elixir of Greater Defense"] = { slot = "ARM", spellID = 11349, item = "Elixir of Greater Defense" },
-- HP (merged: def_fortitude + alcohol STA variants)
["Elixir of Fortitude"] = { slot = "HP", spellID = 3593, item = "Elixir of Fortitude" },
["Health II"] = { slot = "HP", spellID = 3593, item = "Elixir of Fortitude" }, -- AURA name on this client (measured live: SpellInfo(3593) = "Health II")
["Rumsey Rum Black Label"] = { slot = "HP", spellID = 25804, item = "Rumsey Rum Black Label" },
["Rumsey Rum"] = { slot = "HP", spellID = 0, item = "Rumsey Rum" },
["Rumsey Rum Light"] = { slot = "HP", spellID = 0, item = "Rumsey Rum Light" },
["Medivh's Merlot"] = { slot = "HP", spellID = 57106, item = "Medivh's Merlot" }, -- use-spell ID; the aura's own ID is unverified
["Gordok Green Grog"] = { slot = "HP", spellID = 0, item = "Gordok Green Grog" },
-- FR
["Greater Fire Protection"] = { slot = "FR", spellID = 17543, item = "Greater Fire Protection Potion" },
["Fire Protection"] = { slot = "FR", spellID = 7233, item = "Fire Protection Potion" },
-- WPN: intentionally NO aura entries - temp enchant is not a buff
}
-- ------------------------------------------------------------------
-- spellID -> slotId map, generated from DC.CONSUMABLES (spellID > 0).
-- Collision rule: at classify time an ID match beats a
-- name match. data/classbuffs.lua later MERGES its rank IDs into this same
-- map (slot ids are unique across tabs, so one flat map is safe).
-- ------------------------------------------------------------------
DC.SPELL_TO_SLOT = {}
for name, def in pairs(DC.CONSUMABLES) do
if def.spellID and def.spellID > 0 then
DC.SPELL_TO_SLOT[def.spellID] = def.slot
end
end
+95
View File
@@ -0,0 +1,95 @@
-- DopingControl data/enchants.lua
-- Equipment tab: 14 enchant slots + 2 item-tile slots (both trinkets), in
-- paperdoll order, with 1.12 inventory slot IDs.
-- Enchant columns are checked present/missing only (enchant ID in the item
-- link ~= 0); an EMPTY enchant slot is always a gap (core/model.lua), so it
-- is never gated by the expectation checkbox -- only the "has an enchant"
-- question is. Item-tile slots (trinkets) show the equipped item and
-- ignore expectations entirely; only an EMPTY slot counts as a gap there.
--
-- Server note: neck and both rings ARE enchantable on this server (a
-- server-custom feature beyond vanilla WoW, where they take no enchant) --
-- NECK/R1/R2 are therefore kind="ench" like any armor slot, not item
-- tiles. Their expectation defaults to OFF (data/expectations.lua): unlike
-- the ARMOR enchant slots, a neck/ring enchant is not assumed baseline
-- raid gear, so the columns only track it once a role/class opts in via
-- the options grid.
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
DopingControl = DopingControl or {}
local DC = DopingControl
-- ------------------------------------------------------------------
-- Slot definitions (array, order = column order). Same field shape as the
-- other tabs plus invSlot (GetInventoryItemLink slot number). Ids
-- HEAD/SHLD/... with tile labels HEA/SHO/...
-- ------------------------------------------------------------------
DC.SLOTS_EQUIPMENT = {
{ id = "HEAD", label = "HEA", sub = "slot 1", full = "Head", kind = "ench", invSlot = 1 },
{ id = "NECK", label = "NCK", sub = "slot 2", full = "Neck", kind = "ench", invSlot = 2 }, -- enchantable on this server (custom, unlike vanilla)
{ id = "SHLD", label = "SHO", sub = "slot 3", full = "Shoulders", kind = "ench", invSlot = 3 },
{ id = "BACK", label = "BCK", sub = "slot 15", full = "Back", kind = "ench", invSlot = 15 },
{ id = "CHST", label = "CHE", sub = "slot 5", full = "Chest", kind = "ench", invSlot = 5 },
{ id = "WRST", label = "WRI", sub = "slot 9", full = "Wrist", kind = "ench", invSlot = 9 },
{ id = "HAND", label = "HND", sub = "slot 10", full = "Hands", kind = "ench", invSlot = 10 },
{ id = "LEGS", label = "LEG", sub = "slot 7", full = "Legs", kind = "ench", invSlot = 7 },
{ id = "FEET", label = "FEE", sub = "slot 8", full = "Feet", kind = "ench", invSlot = 8 },
{ id = "MAIN", label = "MH", sub = "slot 16", full = "Main hand", kind = "ench", invSlot = 16 },
{ id = "OFFH", label = "OH", sub = "slot 17", full = "Off hand", kind = "ench", invSlot = 17 },
{ id = "RNGD", label = "RNG", sub = "slot 18", full = "Ranged (scope)", kind = "ench", invSlot = 18 }, -- scope counts as the enchant
{ id = "R1", label = "R1", sub = "slot 11", full = "Ring 1", kind = "ench", invSlot = 11 }, -- enchantable on this server (custom, unlike vanilla)
{ id = "R2", label = "R2", sub = "slot 12", full = "Ring 2", kind = "ench", invSlot = 12 }, -- enchantable on this server (custom, unlike vanilla)
{ id = "T1", label = "T1", sub = "slot 13", full = "Trinket 1", kind = "trinket", invSlot = 13 },
{ id = "T2", label = "T2", sub = "slot 14", full = "Trinket 2", kind = "trinket", invSlot = 14 },
}
-- ------------------------------------------------------------------
-- Item-subtype exemptions: whether an enchant applies at all is a
-- property of the equipped ITEM, not of the role. If the equipped item's
-- itemSubType (GetItemInfo return 6 on TWoW 1.18.1)
-- is listed for the slot, the cell classifies as NOTEXP instead
-- of MISSING.
--
-- Shape: DC.ENCH_EXEMPT[slotId][itemSubType] = true
--
-- [ASSUMPTION] English-client subtype strings: wands = "Wands", off-hand
-- frills/holdables = "Miscellaneous", thrown = "Thrown" (thrown weapons
-- cannot take a scope -- same item-property principle as wands; "Thrown"
-- is a deliberate extension --
-- kept because dropping it would show thrown users a
-- false MISSING scope). All three strings are on the probe checklist:
-- the /dc probe battery dumps GetItemInfo subType (return 6) for every
-- equipped slot ("Q5b" lines, core/probe.lua) -- a mismatch means the
-- exemption silently never fires and must be corrected here.
-- ------------------------------------------------------------------
DC.ENCH_EXEMPT = {
RNGD = { ["Wands"] = true, ["Thrown"] = true }, -- Wands: no scope; Thrown: no scope [ASSUMPTION]
OFFH = { ["Miscellaneous"] = true }, -- held-in-off-hand frills: not enchantable
}
-- ------------------------------------------------------------------
-- Class-based enchant exemptions:
-- whole CLASSES for which an ench slot never applies,
-- regardless of what (if anything) is equipped. DC_Model.classify checks
-- this FIRST, before gearRead/EMPTY/expectation logic, and returns
-- NOTEXP (detail "no ranged enchant for this class").
--
-- Shape: DC.ENCH_CLASS_EXEMPT[slotId][classToken] = true
-- (classToken = english GetRaidRosterInfo return-6 token)
--
-- RNGD: shamans/druids/priests/mages/warlocks carry wands/totems/idols/
-- relics or nothing there -- no scope ever applies.
-- PALADIN added: vanilla paladins have a permanently EMPTY ranged
-- slot (no ranged/relic item exists for them on 1.12), so a scope can
-- never apply either.
-- ------------------------------------------------------------------
DC.ENCH_CLASS_EXEMPT = {
RNGD = {
SHAMAN = true,
DRUID = true,
PRIEST = true,
MAGE = true,
WARLOCK = true,
PALADIN = true, -- permanently empty ranged slot in vanilla
},
}
+122
View File
@@ -0,0 +1,122 @@
-- DopingControl data/expectations.lua
-- Default expectation matrix DC.DEFAULT_EXPECT[role][slotId] = true.
-- One flat slot-id namespace across all three tabs (ids are unique).
--
-- This table STAYS the role x slot
-- SEED. The saved/live shape is per class --
-- db.expectations[role][class][slotId] -- materialized from these lines
-- by core/config.lua (DC.EnsureExpectations) for every class that can
-- perform the role (DC_Roles.ROLE_CLASSES). Class-differentiated
-- DEFAULTS are deliberately NOT shipped; the one
-- motivating example lives here as a comment only:
-- a TANK-confirmed SHAMAN wants AI = true (Arcane Intellect for the
-- mana-fed tank kit) while a TANK WARRIOR does not -- the user flips
-- expectations.TANK.SHAMAN.AI in the options grid. AI stays
-- a CASTER/HEALER role default for every performing class.
--
-- Role-line choices:
-- Consumables: standard raid role lines.
-- Class buffs: SOUL only HEALER, AI only CASTER/HEALER, SPROT situational
-- for all five -- like FR. EMB (Emerald Blessing) has NO
-- seed entry: the buff comes from a raid questline and most
-- druids do not have it. The column therefore stays hidden
-- until someone enables it per role/class in the options
-- grid; the dynamic-column rule makes an unexpected slot
-- disappear entirely (same treatment as T1/T2 below).
-- Equipment: ALL armor enchant slots for ALL roles;
-- per-item exemptions (wand/holdable) are handled at
-- classify time via DC.ENCH_EXEMPT, not here.
-- NECK/R1/R2 (enchantable on this server, a custom feature
-- beyond vanilla) default to OFF for every role: unlike
-- the armor slots, a neck/ring enchant is not assumed
-- baseline raid gear -- enable it per role/class in the
-- options grid when it matters. T1/T2 (trinket item tiles)
-- carry NO seed entry at all -- the model ignores
-- expectations for tile-kind slots entirely (item
-- present/absent decides, always shown, never gated).
--
-- Known debatable rows -- documented deliberately, the shipped defaults
-- win:
-- * TANK+AP: some tank consumable lists include Winterfall
-- Firewater; the default has no AP for TANK.
-- * MELEE+FLASK: some melee lists include Flask of the Titans; the
-- default has no FLASK for MELEE.
-- * MELEE/RANGED+HP: some melee lists have neither Fortitude nor
-- alcohol; HP=true is the weakest row of the matrix.
-- * RANGED+AP/WPN: Firewater/Juju Might are MELEE attack power; ranged
-- AP comes from food. Arguably AP=false, WPN=false
-- for RANGED; the default says true.
-- * HEALER+SP: healer sets may carry Dreamshard (+healing), which the
-- 11-slot merge puts into SP; the default has no SP for
-- HEALER -> Dreamshard is invisible for healers by
-- default.
-- * Equipment: an alternative default would differentiate per role
-- (OFFH only TANK/MELEE, RNGD only RANGED,
-- casters/healers neither); deliberately flattened to
-- all-roles + ENCH_EXEMPT item checks.
--
-- This table is the pristine seed: core/config.lua materializes it into
-- db.expectations (per class, deep copies) on first load; the options
-- editor mutates only the materialized copies, never this table.
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
DopingControl = DopingControl or {}
local DC = DopingControl
DC.DEFAULT_EXPECT = {
TANK = {
-- consumables
FLASK = true, FOOD = true, STR = true, AGI = true, ARM = true,
HP = true, FR = true, WPN = true,
-- class buffs
MOTW = true, PWF = true, SPROT = true,
-- equipment (RNGD default only for RANGED:
-- scopes only matter for hunters; flip in the options
-- expectation matrix anytime)
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
},
MELEE = {
-- consumables
FOOD = true, AP = true, STR = true, AGI = true,
HP = true, FR = true, WPN = true,
-- class buffs
MOTW = true, PWF = true, SPROT = true,
-- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
},
RANGED = {
-- consumables
FOOD = true, AP = true, AGI = true,
HP = true, FR = true, WPN = true,
-- class buffs
MOTW = true, PWF = true, SPROT = true,
-- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
RNGD = true,
},
-- CASTER/HEALER: RNGD also dropped by role default (their classes are
-- additionally exempt via ENCH_CLASS_EXEMPT - belt and suspenders)
CASTER = {
-- consumables
FLASK = true, FOOD = true, SP = true, MP5 = true,
FR = true, WPN = true,
-- class buffs
AI = true, MOTW = true, PWF = true, SPROT = true,
-- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
},
HEALER = {
-- consumables
FLASK = true, FOOD = true, MP5 = true,
HP = true, FR = true, WPN = true,
-- class buffs (SOUL: expected ONLY here)
AI = true, MOTW = true, PWF = true, SPROT = true, SOUL = true,
-- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
},
}
+319
View File
@@ -0,0 +1,319 @@
-- DopingControl data/hit.lua
-- Hit-chance tables for the HIT tab: what GEAR and AURAS contribute, plus
-- the caps every column is judged against. The third contributor,
-- TALENTS, lives in its own file (data/talenthit.lua) because it is a
-- different kind of source -- looked up by talent name and rank rather
-- than parsed out of a tooltip.
--
-- WHY TOOLTIP PARSING (measured, not assumed): this client family exposes
-- NO hit API at all -- GetHitModifier, GetSpellHitModifier and
-- GetCombatRating are all absent (verified on the live client). The only
-- readable source of a player's +hit is the text of the equipped items'
-- tooltips, which IS readable for foreign units in range via
-- GameTooltip:SetInventoryItem(unit, slot). Everything in this file exists
-- to turn those tooltip lines into a number and to bound that number
-- against the caps of the current patch.
--
-- WHAT CANNOT BE READ (in principle, not a missing feature): a foreign
-- player's WEAPON SKILL. The server never sends PLAYER_SKILL_INFO for
-- anyone but the own character and no inspect-skill channel exists, so
-- weapon skill is MEASURED for the own character (GetSkillLineInfo, see
-- scan/hit.lua) and ESTIMATED for everyone else -- base skill + racial
-- specialization + an optional per-player "weapon skill book" flag the
-- user sets in the options. Every consumer must present the foreign value
-- as an assumption, never as a measurement.
--
-- PATCH BASIS (current server patch): weapon-skill scaling was rewritten
-- to LINEAR, the old 305-skill breakpoint was removed and the hidden 1%
-- suppression was dropped. Consequences encoded below: the yellow melee
-- hit cap against a boss three levels above the player is 8% (it was 9%
-- before the rewrite) and the dual-wield WHITE cap is 27%. The spell hit
-- cap is 16%, derived from the server's own clamp (see DC.HIT_CAPS below)
-- rather than measured -- it is a single constant here so a later
-- correction changes one line.
--
-- English client assumed: both the tooltip patterns and the weapon-skill
-- names below are English strings. On a localized client nothing matches
-- and the hit columns simply read 0 rather than showing wrong numbers.
--
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
DopingControl = DopingControl or {}
local DC = DopingControl
-- ------------------------------------------------------------------
-- Magic schools that can carry a school-specific spell-hit line.
-- Order is display order for tooltip detail lines.
--
-- The CAPITALIZED spelling is load-bearing twice over: it is the word that
-- appears in the item tooltip ("Improves your chance to hit with Fire
-- spells by 1%"), so it is both the generated pattern text below and the
-- key of the parsed `schools` table.
-- ------------------------------------------------------------------
DC.HIT_SCHOOLS = { "Arcane", "Fire", "Frost", "Holy", "Nature", "Shadow" }
-- ------------------------------------------------------------------
-- DC.HIT_SCHOOL_KEY -- lowercase school id -> the CAPITALIZED key used by
-- everything that came out of a tooltip.
--
-- Two spellings exist because two sources exist and neither may be bent to
-- the other: tooltip text dictates "Fire", while the column definitions
-- (DC.SLOTS_HIT, core/const.lua) and the talent tables
-- (DC.TALENT_HIT, data/talenthit.lua) are hand-written data that follows
-- the file-wide lowercase-id convention. This table is GENERATED from
-- DC.HIT_SCHOOLS so the two can never drift apart, and DC_Hit.SchoolHit
-- (scan/hit.lua) is the one function callers should use instead of
-- indexing either table by hand.
-- ------------------------------------------------------------------
DC.HIT_SCHOOL_KEY = {}
for i = 1, table.getn(DC.HIT_SCHOOLS) do
local school = DC.HIT_SCHOOLS[i]
DC.HIT_SCHOOL_KEY[string.lower(school)] = school
end
-- ------------------------------------------------------------------
-- DC.HIT_PATTERNS -- ARRAY (order matters, FIRST MATCH WINS), each entry
-- { pattern = <Lua 5.0 string.find pattern with ONE numeric capture>,
-- kind = "melee" | "spell" | "both" | <school name> }
--
-- kind semantics (folded by DC_Hit.SumGear):
-- "melee" generic physical hit. 1.12 has no separate ranged-hit affix:
-- the same "chance to hit" line applies to melee AND ranged
-- attacks, so the MELEE bucket feeds both hit columns.
-- "spell" spell hit against every school.
-- "both" "spells and attacks": adds to melee AND spell.
-- <school> school-specific spell hit (e.g. "Fire"): it does NOT raise
-- the general spell-hit number, it is carried separately and
-- shown as an extra, because it only helps that one school.
--
-- Ordering rule: the more specific phrasings come first. The four
-- phrasings do not actually overlap as literals ("hit with spells and
-- attacks by", "hit with Fire spells by", "hit with spells by", "hit by"),
-- but the order keeps that from becoming load-bearing if a pattern is ever
-- widened.
--
-- Extension point: a phrasing that is not listed here is SILENTLY IGNORED
-- (never guessed at). Server-custom item text that grants hit therefore
-- needs its pattern added here -- this table is the single place.
-- ------------------------------------------------------------------
DC.HIT_PATTERNS = {
-- combined affix ("spells and attacks")
{ pattern = "Improves your chance to hit with spells and attacks by (%d+)%%",
kind = "both" },
}
-- school-specific spell hit, generated so the school list stays the single
-- source of truth ("Improves your chance to hit with Fire spells by 1%")
for i = 1, table.getn(DC.HIT_SCHOOLS) do
local school = DC.HIT_SCHOOLS[i]
table.insert(DC.HIT_PATTERNS, {
pattern = "Improves your chance to hit with " .. school
.. " spells by (%d+)%%",
kind = school,
})
end
-- general spell hit, then the generic (melee + ranged) hit line
table.insert(DC.HIT_PATTERNS,
{ pattern = "Improves your chance to hit with spells by (%d+)%%",
kind = "spell" })
table.insert(DC.HIT_PATTERNS,
{ pattern = "Improves your chance to hit by (%d+)%%",
kind = "melee" })
-- ------------------------------------------------------------------
-- DC.WEAPON_SKILL_OF_SUBTYPE -- GetItemInfo itemSubType -> weapon SKILL
-- name (the string GetSkillLineInfo reports for that weapon).
--
-- Absent key = "not a weapon" and that is load-bearing: off-hand items
-- whose subtype is missing here (shields, held-in-off-hand items, totems,
-- idols, librams, fishing poles) are what tells the caller that the player
-- is NOT dual-wielding.
--
-- Verified live on this client: "One-Handed Maces" (plus the non-weapon
-- subtypes "Shields" and "Totems", deliberately absent). The remaining
-- strings follow the standard 1.12 weapon subclass naming and are
-- UNVERIFIED -- each one is only a lookup key, so a wrong string degrades
-- to "unknown weapon" (no skill assumption, no dual-wield claim), never to
-- a wrong number.
--
-- Two mappings are worth calling out:
-- "Fist Weapons" -> "Unarmed": fist weapons run on the Unarmed skill
-- line, they have no skill of their own.
-- "Wands" -> "Wands": the skill line exists, but whether wand attacks
-- obey weapon skill or spell hit is DISPUTED on this patch. The
-- mapping only names the skill line; the ranged hit column
-- deliberately ignores wands (see DC.RANGED_ATTACK_SUBTYPE).
-- ------------------------------------------------------------------
DC.WEAPON_SKILL_OF_SUBTYPE = {
["One-Handed Axes"] = "Axes",
["Two-Handed Axes"] = "Two-Handed Axes",
["One-Handed Maces"] = "Maces", -- verified live
["Two-Handed Maces"] = "Two-Handed Maces",
["One-Handed Swords"] = "Swords",
["Two-Handed Swords"] = "Two-Handed Swords",
["Daggers"] = "Daggers",
["Fist Weapons"] = "Unarmed", -- see header
["Polearms"] = "Polearms",
["Staves"] = "Staves",
["Bows"] = "Bows",
["Crossbows"] = "Crossbows",
["Guns"] = "Guns",
["Thrown"] = "Thrown",
["Wands"] = "Wands", -- see header
}
-- Subtypes that make the RANGED hit question meaningful, i.e. things that
-- actually swing as a ranged ATTACK. Wands are excluded on purpose (their
-- hit mechanic is disputed, see above) and so is every non-weapon relic;
-- a player without one of these gets no ranged hit column value at all.
DC.RANGED_ATTACK_SUBTYPE = {
["Bows"] = true,
["Crossbows"] = true,
["Guns"] = true,
["Thrown"] = true,
}
-- ------------------------------------------------------------------
-- DC.CLASS_WEAPON_SKILLS -- class token -> ordered array of the weapon
-- skills that class can train (Vanilla 1.12 base list). Every name here is
-- a VALUE of DC.WEAPON_SKILL_OF_SUBTYPE above, same spelling -- the
-- options editor's Skill books tab (ui/options.lua, O.SkillBookRows)
-- writes db.skillBooks[player][skill] keyed by these exact strings, so a
-- typo here would silently create a book entry the hit calc never reads.
--
-- This server can diverge from the Vanilla baseline (extra or missing
-- trainable weapons for a class). If a player is seen wielding a skill
-- that is not in their class's list here, the Skill books tab still
-- offers it -- a WORN skill always rides along regardless of the class
-- list (see O.SkillBookRows) -- but if that turns out to be the NORMAL
-- case for a class rather than a one-off exception, extend the list below
-- rather than leaning on the worn-skill fallback forever.
-- ------------------------------------------------------------------
DC.CLASS_WEAPON_SKILLS = {
WARRIOR = { "Axes", "Two-Handed Axes", "Maces", "Two-Handed Maces",
"Swords", "Two-Handed Swords", "Daggers", "Unarmed",
"Polearms", "Staves", "Bows", "Crossbows", "Guns",
"Thrown" },
PALADIN = { "Axes", "Two-Handed Axes", "Maces", "Two-Handed Maces",
"Swords", "Two-Handed Swords", "Polearms" },
HUNTER = { "Axes", "Two-Handed Axes", "Swords", "Two-Handed Swords",
"Daggers", "Unarmed", "Polearms", "Staves", "Bows",
"Crossbows", "Guns", "Thrown" },
ROGUE = { "Daggers", "Unarmed", "Maces", "Swords", "Bows",
"Crossbows", "Guns", "Thrown" },
PRIEST = { "Daggers", "Maces", "Staves", "Wands" },
SHAMAN = { "Axes", "Two-Handed Axes", "Maces", "Two-Handed Maces",
"Daggers", "Unarmed", "Staves" },
MAGE = { "Daggers", "Swords", "Staves", "Wands" },
WARLOCK = { "Daggers", "Swords", "Staves", "Wands" },
DRUID = { "Maces", "Two-Handed Maces", "Daggers", "Unarmed",
"Staves", "Polearms" },
}
-- ------------------------------------------------------------------
-- Weapon skill baseline and modifiers.
--
-- DC.BASE_WEAPON_SKILL: a level-60 character caps every weapon skill at
-- 300 (5 x level). This tool is a raid tool, so 60 is assumed for the
-- estimate of FOREIGN players; the own character is measured and never
-- uses this number.
--
-- DC.RACIAL_SKILL_BONUS: the racial weapon specialization bonus. The value
-- is DISPUTED -- the classic value is +5, and a server-side change to +3
-- has been reported but is UNVERIFIED. It is ONE constant on purpose:
-- changing this single line changes every racial bonus and every derived
-- cap.
--
-- DC.SKILL_BOOK_BONUS: a quest weapon-skill book grants +3 skill. It is
-- invisible on other players (see the header), which is why it is an
-- opt-in per-player checkbox in the options rather than a scan result.
-- ------------------------------------------------------------------
DC.BASE_WEAPON_SKILL = 300
DC.RACIAL_SKILL_BONUS = 5
DC.SKILL_BOOK_BONUS = 3
-- ------------------------------------------------------------------
-- DC.RACE_WEAPON_SKILL -- race -> { [weapon skill name] = bonus }.
--
-- Keys: BOTH spellings are listed for every race that has a bonus -- the
-- display name ("Night Elf") and the file token ("NightElf", "Scourge"),
-- because the race string reaches us from UnitRace, whose two returns are
-- exactly those two spellings. Aliases point at the SAME table, so a race
-- is never counted twice.
--
-- Races without a weapon specialization (night elves, tauren, undead) are
-- deliberately absent: an absent race resolves to "no bonus", which is
-- also what an unknown/newer race token resolves to (nil-safe by design).
-- ------------------------------------------------------------------
local B = DC.RACIAL_SKILL_BONUS
local HUMAN_SKILLS = {
["Swords"] = B, ["Two-Handed Swords"] = B,
["Maces"] = B, ["Two-Handed Maces"] = B,
}
local ORC_SKILLS = {
["Axes"] = B, ["Two-Handed Axes"] = B,
}
local DWARF_SKILLS = { ["Guns"] = B }
local TROLL_SKILLS = { ["Bows"] = B, ["Thrown"] = B }
local GNOME_SKILLS = { ["Daggers"] = B, ["Swords"] = B }
-- races without a weapon specialization share ONE empty table
local NO_SKILLS = {}
DC.RACE_WEAPON_SKILL = {
["Human"] = HUMAN_SKILLS,
["Orc"] = ORC_SKILLS,
["Dwarf"] = DWARF_SKILLS,
["Troll"] = TROLL_SKILLS,
["Gnome"] = GNOME_SKILLS,
-- no weapon specialization; listed for readability, empty on purpose
["Night Elf"] = NO_SKILLS,
["NightElf"] = NO_SKILLS,
["Tauren"] = NO_SKILLS,
["Undead"] = NO_SKILLS,
["Scourge"] = NO_SKILLS,
}
-- ------------------------------------------------------------------
-- DC.HIT_CAPS -- hit percentages needed against a boss THREE LEVELS above
-- the player on the current patch, at base weapon skill (300):
-- meleeYellow 8 -- special attacks / abilities ("yellow" swings)
-- meleeWhiteDW 27 -- auto attacks while DUAL WIELDING (the 19% dual-
-- wield penalty rides on top); a single weapon's white
-- swings share the yellow cap
-- ranged 8 -- ranged attacks behave like yellow melee here
-- spell 16 -- the server clamps spell hit chance to 99%, so 1%
-- miss can never be removed: base miss vs a +3-level
-- boss is 17%, which makes 16% the effective cap
--
-- DC.CAP_PER_SKILL: every weapon skill point ABOVE 300 lowers the melee/
-- ranged cap by 0.2 percentage points (linear scaling, see header). A
-- racial specialization (+5 skill) therefore lowers the yellow cap from
-- 8.0 to 7.0, and a weapon skill book (+3) is worth about 0.6% hit.
-- ------------------------------------------------------------------
DC.HIT_CAPS = {
meleeYellow = 8,
meleeWhiteDW = 27,
ranged = 8,
spell = 16,
}
DC.CAP_PER_SKILL = 0.2
-- ------------------------------------------------------------------
-- DC.HIT_AURAS -- hit granted by AURAS rather than gear, folded on top of
-- the gear sum by scan/engine.lua's BuildPlayer (no extra scan: the buff
-- names are already in the store from the same aura read the class-buff
-- tab uses). Keys are buff NAMES (matched against player.auras.names);
-- values are { melee=, ranged=, spell= } percentages -- every field is
-- OPTIONAL, since a hit-granting aura need not touch all three columns.
-- ------------------------------------------------------------------
DC.HIT_AURAS = {
-- raid-wide druid buff: +1% SPELL hit only (also grants movement
-- speed and casting mana regen, which do not concern this addon) --
-- verified against the in-game tooltip text, so this is deliberately
-- NOT a melee/ranged entry; add further hit-granting auras here (a
-- food buff with hit would be the likeliest addition)
["Emerald Blessing"] = { spell = 1 },
}
+139
View File
@@ -0,0 +1,139 @@
-- DopingControl data/resist.lua
-- Resistance-reconstruction tables for the RESIST tab: what GEAR tooltips
-- and RACE grant, mirroring data/hit.lua's split for the HIT tab (patterns
-- + constants here, the pure parser in scan/resist.lua).
--
-- WHY RECONSTRUCTION (measured, not assumed -- see scan/hit.lua's header
-- for the full story): the server does not transmit
-- UNIT_FIELD_RESISTANCES for foreign players at all (Player::
-- InitVisibleBits() never sets the bit for anyone but the owning client),
-- so UnitResistance/GetUnitField can NEVER answer for a foreign unit --
-- this is not a corner case scan/engine.lua's three read paths occasionally
-- miss, it is the only path that can ever ANSWER for one. The equipped
-- items' tooltips are readable for foreign units in range
-- (GameTooltip:SetInventoryItem), exactly like the HIT tab's +hit lines,
-- so the total is rebuilt from the same source: item stat text, plus the
-- racial bonus below (which no tooltip ever states).
--
-- DC.RESIST_SCHOOLS -- the five schools that carry a resistance stat, in
-- TOOLTIP capitalization ("Fire Resistance"), paired with the
-- UnitResistance() school index used throughout the addon (core/const.lua
-- DC.SLOTS_RESIST: 2 fire / 3 nature / 4 frost / 5 shadow / 6 arcane; 0
-- physical and 1 holy are never queried -- there is no "Holy Resistance"
-- item stat in 1.12, which is also why this list has no sixth entry).
-- Kept in sync with DC.SLOTS_RESIST BY HAND (self-contained, like
-- DC.HIT_SCHOOLS/DC.SLOTS_HIT in data/hit.lua and core/const.lua) --
-- test_resist.lua pins the two against each other so they cannot drift
-- apart silently.
DopingControl = DopingControl or {}
local DC = DopingControl
DC.RESIST_SCHOOLS = {
{ name = "Fire", school = 2 },
{ name = "Nature", school = 3 },
{ name = "Frost", school = 4 },
{ name = "Shadow", school = 5 },
{ name = "Arcane", school = 6 },
}
-- DC.RESIST_SCHOOL_NUM -- tooltip school name -> UnitResistance() index,
-- GENERATED from DC.RESIST_SCHOOLS so the two tables cannot drift apart
-- (same idea as DC.HIT_SCHOOL_KEY in data/hit.lua).
DC.RESIST_SCHOOL_NUM = {}
for i = 1, table.getn(DC.RESIST_SCHOOLS) do
local e = DC.RESIST_SCHOOLS[i]
DC.RESIST_SCHOOL_NUM[e.name] = e.school
end
-- ------------------------------------------------------------------
-- DC.RESIST_PATTERNS -- ARRAY (order matters, FIRST MATCH WINS), each
-- entry { pattern = <Lua 5.0 string.find pattern with ONE numeric
-- capture>, kind = <school name> | "all" }.
--
-- Measured tooltip forms (2026-08-02, own-character tooltip, English
-- client):
-- "+10 Arcane Resistance" -- PLAIN stat line, no "Equip:"
-- "+10 Fire Resistance" -- (item base stats print this way,
-- "+10 Nature Resistance" -- unlike +hit which is always an
-- "+1 Fire Resistance" -- "Equip:" trigger line)
-- "Equip: +8 All Resistances." -- multi-school EQUIP trigger --
-- applies to ALL FIVE schools at
-- once; a parser keyed on the five
-- single-school patterns alone would
-- silently miss this line entirely
-- (the trap this table exists to
-- close, see scan/resist.lua
-- R.SumLines).
--
-- Both anchored at the LINE START (^): a stat line is the line's entire
-- content on this client, so anchoring guards against a flavor-text
-- sentence that happens to contain "Fire Resistance" mid-phrase. No
-- trailing anchor: the plain form has nothing after the number+word (WoW
-- classic tooltip text is fixed, so this cannot mismatch), and the Equip
-- form is allowed to keep trailing punctuation.
--
-- Extension point (mirrors DC.HIT_PATTERNS): a phrasing not listed here is
-- SILENTLY IGNORED, never guessed at. An "Equip: +N <School> Resistance."
-- single-school equip form is conceivable on this server but UNMEASURED --
-- it is deliberately absent until someone reads it off a real tooltip.
-- ------------------------------------------------------------------
DC.RESIST_PATTERNS = {}
for i = 1, table.getn(DC.RESIST_SCHOOLS) do
local e = DC.RESIST_SCHOOLS[i]
table.insert(DC.RESIST_PATTERNS, {
pattern = "^%+(%d+) " .. e.name .. " Resistance",
kind = e.name,
})
end
table.insert(DC.RESIST_PATTERNS, {
pattern = "^Equip: %+(%d+) All Resistances",
kind = "all",
})
-- ------------------------------------------------------------------
-- DC.RACE_RESIST -- race -> { [UnitResistance school index] = bonus }.
-- INNATE racial resistance, granted by the race itself rather than any
-- item -- invisible to a tooltip scan, so it has to be added on top of the
-- gear sum by hand (scan/engine.lua's E.AssembleResists gear-fallback
-- path) or the reconstructed total would silently understate every member
-- of these five races by exactly this amount.
--
-- Keys: BOTH spellings for every race that has a bonus (display name and
-- file token), same reasoning as DC.RACE_WEAPON_SKILL in data/hit.lua --
-- scan/hit.lua's H.ReadUnit stores `token or display` as the race passed
-- through the store, so either spelling must resolve. Aliases point at the
-- SAME table (never a separate copy), so a race can never end up counted
-- with two different bonus tables that could drift apart.
--
-- Verified racial resistances (task brief, 2026-08-02):
-- Dwarf +10 Frost
-- Gnome +10 Arcane
-- Night Elf +10 Nature
-- Tauren +10 Nature
-- Undead/Forsaken +10 Shadow
-- Human, Orc, Troll: none
--
-- WARNING -- NOT COVERED: TurtleWoW's two extra playable races, Goblin and
-- High Elf/Blood Elf. Their racial resistance (if they even have one) is
-- UNKNOWN -- it is not guessed here. Both are simply ABSENT from this
-- table, which is safe by construction: DC_Resist.RaceBonus (scan/
-- resist.lua) returns an empty bonus table for any race it does not
-- recognize, exactly the same degrade an unlisted/future race token gets.
-- Do not "fix" this by inventing a number -- add the real one once it is
-- measured, the same way the five below were.
-- ------------------------------------------------------------------
local DWARF_RESIST = { [4] = 10 } -- frost
local GNOME_RESIST = { [6] = 10 } -- arcane
local NIGHTELF_RESIST = { [3] = 10 } -- nature
local TAUREN_RESIST = { [3] = 10 } -- nature
local UNDEAD_RESIST = { [5] = 10 } -- shadow
DC.RACE_RESIST = {
["Dwarf"] = DWARF_RESIST,
["Gnome"] = GNOME_RESIST,
["Night Elf"] = NIGHTELF_RESIST,
["NightElf"] = NIGHTELF_RESIST,
["Tauren"] = TAUREN_RESIST,
["Undead"] = UNDEAD_RESIST,
["Scourge"] = UNDEAD_RESIST,
}
+171
View File
@@ -0,0 +1,171 @@
-- DopingControl data/talenthit.lua
-- The THIRD source of hit, next to gear and auras: TALENTS.
--
-- WHY ITS OWN FILE: gear hit is parsed from item tooltips, talent hit is
-- looked up by talent NAME and rank. The two arrive over completely
-- different paths -- GetTalentInfo for the own character, the server's
-- inspect protocol (scan/talents.lua) for everyone else -- and only meet
-- again in DC_Hit.TalentHit. Keeping the talent tables apart from
-- data/hit.lua keeps that split visible in the file layout instead of
-- hiding it inside one big table.
--
-- WHAT COUNTS AS SPELL HIT HERE: several talents are worded as "reduces
-- the chance that the opponent can resist your <school> spells by X%".
-- That is not a resistance mechanic -- it runs through the server's
-- resist-miss spell modifier, i.e. it IS spell hit for that school, and it
-- stacks with +hit from items. They are therefore modelled as
-- school-specific spell hit and not as anything resistance-shaped.
--
-- BUCKETS -- an entry is
-- { name = <talent name, EXACTLY as GetTalentInfo returns it>,
-- tree = <talent tab name; documentation only, never matched on>,
-- ranks = <max rank; the per-rank arrays must have this length>,
-- melee = { <per rank> }, -- generic physical hit (melee column)
-- ranged = { <per rank> }, -- generic ranged hit (ranged column)
-- spell = { <per rank> }, -- GENERIC spell hit: every school
-- offhand = { <per rank> }, -- OFF-HAND swings only, see below
-- dwExtra = { <per rank> }, -- extra melee hit while DUAL WIELDING
-- schools = { [<lowercase school id>] = { <per rank> } } }
-- Every bucket is optional; an absent bucket contributes nothing. School
-- ids are lowercase to match DC.SLOTS_HIT (core/const.lua); the
-- CAPITALIZED tooltip spelling that gear parsing produces is bridged by
-- DC.HIT_SCHOOL_KEY (data/hit.lua).
--
-- `offhand` is deliberately NOT part of the melee number. Off-hand hit
-- applies to off-hand swings only, so adding it to the melee column would
-- claim a main-hand hit chance the player does not have. It is carried so
-- the cell tooltip can mention it.
--
-- MATCHING IS BY NAME: the reader (DC_Hit.ReadSelfTalents) hands in
-- { [talent name] = rank }, because GetTalentInfo returns names, and
-- tab/index positions shift whenever a tree is rebalanced while names
-- almost never do. A talent this file does not list is ignored; a talent
-- listed here that no longer exists simply never matches. Both directions
-- degrade to "no talent hit counted", never to a wrong number.
--
-- English client assumed, like the tooltip patterns in data/hit.lua: on a
-- localized client no name matches and the talent contribution reads 0.
--
-- WHAT IS DELIBERATELY EXCLUDED (each one grants "hit" in its tooltip text
-- but not hit that belongs in any column of this tab):
-- * PET hit -- hunter "Bestial Precision", warlock "Demonic Precision".
-- They raise the PET's hit chance (and its weapon skill / inherited
-- ratios); the player's own swings and casts are untouched.
-- * ENEMY DEBUFFS -- rogue "Blinding Haze" LOWERS the hit chance of
-- targets hit by Distract. It is the opposite sign and lands on
-- someone else.
-- * SINGLE-SPELL hit -- paladin "Improved Hand of Reckoning" buys hit
-- for one ability only. A column that claimed it would overstate holy
-- hit for every other spell the paladin casts.
-- * WEAPON-TYPE hit -- mage "Wand Specialization" (wands) and priest
-- "Wand Specialization" (wands and bows). Wand and bow attacks are
-- neither generic spell hit nor -- for the priest -- the generic
-- ranged-attack hit this tab's ranged column measures (that column
-- only exists for a real ranged ATTACK weapon, see
-- DC.RANGED_ATTACK_SUBTYPE), so counting them would misplace them.
-- * TEMPORARY procs -- e.g. the second half of the shaman's Elemental
-- Devastation, which grants spell hit for 10 sec after a melee crit.
-- Only the permanent, always-on half is counted; a scan is a snapshot
-- and a proc that happens to be up would make one row incomparable
-- with the next.
--
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
DopingControl = DopingControl or {}
local DC = DopingControl
-- ------------------------------------------------------------------
-- DC.TALENT_HIT -- [class token (UnitClass file token, e.g. "MAGE")] =
-- array of talent entries. Classes with no hit talent at all are absent.
-- ------------------------------------------------------------------
DC.TALENT_HIT = {
-- Generic hit: melee AND every spell school, from ONE talent. The
-- three entries below are the only talents in the game that raise
-- both halves at once, which is why hybrids can be capped on both
-- sides without spending a single item budget on it.
DRUID = {
{ name = "Natural Weapons", tree = "Balance", ranks = 3,
melee = { 1, 2, 3 }, spell = { 1, 2, 3 } },
},
PALADIN = {
{ name = "Precision", tree = "Protection", ranks = 3,
melee = { 1, 2, 3 }, spell = { 1, 2, 3 } },
},
SHAMAN = {
-- the talent's second half (melee crits grant 3/6/9% spell hit
-- for 10 sec) is a TEMPORARY proc and is not counted -- see the
-- exclusion list in the header
{ name = "Elemental Devastation", tree = "Elemental", ranks = 3,
melee = { 1, 2, 3 }, spell = { 1, 2, 3 } },
},
-- Melee only.
ROGUE = {
-- "with melee weapons": no ranged half, even though rogues can
-- carry a thrown weapon
{ name = "Precision", tree = "Combat", ranks = 5,
melee = { 1, 2, 3, 4, 5 } },
},
HUNTER = {
-- the talent says "hit chance" without naming a weapon, so the
-- base half feeds the melee AND the ranged column.
--
-- dwExtra is the "and by an additional X% while dual wielding"
-- half. The wording does not say which swings the EXTRA applies
-- to; a dual-wield condition is a melee-build condition, so it is
-- counted for MELEE ONLY. That is the reading that cannot
-- overstate a hunter's ranged hit -- the number that decides
-- whether the player is capped where it matters. Should the extra
-- turn out to be generic, this under-reports ranged by up to 3%,
-- which is the safe direction (same trade-off as the uncounted
-- set bonuses in scan/hit.lua).
{ name = "Surefooted", tree = "Survival", ranks = 3,
melee = { 1, 2, 3 }, ranged = { 1, 2, 3 },
dwExtra = { 1, 2, 3 } },
},
-- Off-hand only.
WARRIOR = {
-- off-hand swings only: NOT added to the melee column, carried
-- for the tooltip (see the `offhand` note in the header)
{ name = "Dual Wield Specialization", tree = "Fury", ranks = 5,
offhand = { 2, 4, 6, 8, 10 } },
},
-- School-specific spell hit.
MAGE = {
{ name = "Arcane Focus", tree = "Arcane", ranks = 5,
schools = { arcane = { 2, 4, 6, 8, 10 } } },
-- one talent, two schools, full value in each: it is not split
{ name = "Elemental Precision", tree = "Frost", ranks = 3,
schools = { frost = { 2, 4, 6 }, fire = { 2, 4, 6 } } },
},
PRIEST = {
{ name = "Shadow Focus", tree = "Shadow", ranks = 5,
schools = { shadow = { 2, 4, 6, 8, 10 } } },
-- "your Holy and Discipline spells": Discipline is a SPELL LINE,
-- not a magic school -- its damaging spells are holy school
-- anyway, so the whole talent maps onto holy. There is no
-- "discipline" column to put the other half in, and inventing one
-- would be a school this client's damage model does not have.
{ name = "Piercing Light", tree = "Discipline", ranks = 3,
schools = { holy = { 2, 4, 6 } } },
},
WARLOCK = {
-- "your Affliction spells": another SPELL-LINE talent rather than
-- a school talent. Affliction spells (Corruption, Curse of Agony,
-- Drain Life, Siphon Life, Death Coil, ...) are shadow school, so
-- the talent is mapped onto shadow. A warlock's fire spells
-- (Immolate, Shadowburn's destruction siblings) get nothing from
-- it, which is exactly why the fire and shadow columns of this tab
-- can differ by 10% for the same character.
{ name = "Suppression", tree = "Affliction", ranks = 5,
schools = { shadow = { 2, 4, 6, 8, 10 } } },
},
}