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