Files
2026-08-03 09:47:19 +02:00

320 lines
15 KiB
Lua

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