750 lines
34 KiB
Lua
750 lines
34 KiB
Lua
-- Vampify -- shared constants and the pinned formula descriptor.
|
|
--
|
|
-- The formula was MEASURED in-game (a companion telemetry addon's instrumentation, 2026-08-07..09), it is not derived from tooltips
|
|
-- or forum lore. It lives in a descriptor rather than inside the arithmetic so that a resolved
|
|
-- open question (spec 4.3) is a one-line change here, not a refactor of model.lua.
|
|
|
|
VampifyConst = {}
|
|
local C = VampifyConst
|
|
|
|
C.VERSION = "0.4.0"
|
|
|
|
-- Empty a list buffer for reuse. THE table.setn IS THE POINT: in Lua 5.0 table.insert maintains an
|
|
-- `n` field, and nil-ing the indices by hand does not reset it -- so the next round of inserts
|
|
-- lands BEHIND the stale n, leaving nil holes at 1..n. That crashed the options window
|
|
-- ("table.concat: table contains non-strings") on its second refresh and silently grew the tooltip
|
|
-- line buffer on every gear scan. One helper, so the trap exists in exactly one place.
|
|
function C.resetList(t)
|
|
for i = table.getn(t), 1, -1 do t[i] = nil end
|
|
table.setn(t, 0)
|
|
return t
|
|
end
|
|
|
|
-- Vampirism source spell ids: items are ranks 1-5, enchants are bracer/boots at +1% each.
|
|
C.SPELL_IDS = { 45420, 45421, 45422, 45423, 45424, 57146, 57148 }
|
|
|
|
-- ---- error capture -----------------------------------------------------------------------------
|
|
--
|
|
-- Two problems, one mechanism. A Lua error in an OnUpdate repeats every frame, which floods the
|
|
-- chat and makes the game unpleasant; and the developer only learns about it if the player thinks
|
|
-- to mention it. So: every error is written to <WoW>\imports\vampify_errors.txt where tooling
|
|
-- can read it, and only the FIRST occurrence of each distinct message reaches the chat. Repeats are
|
|
-- counted, not shown.
|
|
--
|
|
-- Lives here rather than in its own file purely so it loads first, before anything that could
|
|
-- throw, without costing a client restart to add a new file to the toc.
|
|
--
|
|
-- Deliberately chains to the previous handler for that first occurrence: swallowing errors outright
|
|
-- would trade a visible problem for an invisible one.
|
|
|
|
C._errSeen = {}
|
|
C._errOrder = {}
|
|
C._errPrev = nil
|
|
|
|
local function writeErrors()
|
|
if not ExportFile then return end -- SuperWoW only; harmless without it
|
|
local lines = {}
|
|
table.insert(lines, "addon=Vampify version=" .. tostring(C.VERSION))
|
|
for i = 1, table.getn(C._errOrder) do
|
|
local m = C._errOrder[i]
|
|
table.insert(lines, "[x" .. tostring(C._errSeen[m]) .. "] " .. m)
|
|
end
|
|
-- ExportFile appends .txt itself -- passing "vampify_errors.txt" would yield a double
|
|
-- extension, which is a known in-game gotcha.
|
|
ExportFile("vampify_errors", table.concat(lines, "\n"))
|
|
end
|
|
|
|
function C.installErrorCapture()
|
|
if C._errInstalled then return end
|
|
C._errInstalled = true
|
|
C._errPrev = geterrorhandler and geterrorhandler() or nil
|
|
|
|
seterrorhandler(function(msg)
|
|
local show, write = C.recordError(msg)
|
|
if write then writeErrors() end
|
|
if show and C._errPrev then C._errPrev(msg) end
|
|
end)
|
|
end
|
|
|
|
-- Pure counting half, split out so it can be tested offline: WoW's error handler cannot be, and a
|
|
-- bug in the thing that reports bugs is the worst kind. Returns (showInChat, writeToFile).
|
|
function C.recordError(msg)
|
|
msg = tostring(msg)
|
|
local firstTime = C._errSeen[msg] == nil
|
|
if firstTime then
|
|
C._errSeen[msg] = 1
|
|
table.insert(C._errOrder, msg)
|
|
else
|
|
C._errSeen[msg] = C._errSeen[msg] + 1
|
|
end
|
|
-- Show it once, then stay quiet: the player has been told, and a per-frame repeat adds nothing
|
|
-- but noise. Write on the first sighting and every 50th repeat, so a runaway loop stays visible
|
|
-- in the file without writing on every single frame.
|
|
local write = firstTime or math.mod(C._errSeen[msg], 50) == 0
|
|
return firstTime, write
|
|
end
|
|
|
|
-- Installed at file scope, not from an event: an error thrown while a later file is still loading
|
|
-- would otherwise be missed, and those are exactly the errors worth catching.
|
|
if seterrorhandler then C.installErrorCapture() end
|
|
|
|
-- ---- spells that do NOT trigger Vampirism -------------------------------------------------------
|
|
--
|
|
-- Damage shields were MEASURED not to trigger (Thorns 236 triggers, Thorium Shield Spike 27 -- zero
|
|
-- healing from either). Excluding the DAMAGE_SHIELD_SELF event is not enough: the client also
|
|
-- reports shield procs as ordinary SPELL_DAMAGE_EVENT_SELF, so they slipped in and were credited
|
|
-- with healing that never happened (spotted in-game via the per-ability breakdown, which showed
|
|
-- Thorium Shield Spike at 12.4%).
|
|
--
|
|
-- There is no reliable structural signal that marks a proc as a damage shield, so this is a list.
|
|
-- It is extendable at runtime with /vf exclude <id> for anything found later, and excluded spells
|
|
-- are dropped from BOTH the healing and the damage side -- counting damage that cannot trigger
|
|
-- would drag the headline percentage below the truth.
|
|
|
|
C.NO_TRIGGER = {
|
|
[16624] = "Thorium Shield Spike", -- id read from the live client 2026-08-10
|
|
}
|
|
|
|
function C.triggersVampirism(spellId)
|
|
if not spellId then return true end -- auto attacks always trigger
|
|
if C.NO_TRIGGER[spellId] then return false end
|
|
local cfg = VampifyConfig and VampifyConfig.getChar and VampifyConfig.getChar()
|
|
if cfg and cfg.noTrigger and cfg.noTrigger[spellId] then return false end
|
|
return true
|
|
end
|
|
|
|
-- ---- runtime exclusion management (cfg.noTrigger) -----------------------------------------------
|
|
--
|
|
-- cfg.noTrigger is the SAME mechanism as NO_TRIGGER above, made runtime-extendable: a new damage
|
|
-- shield is only ever found once it shows up wrongly in the breakdown (there is no structural
|
|
-- signal, see the comment above), so the list has to grow without a client restart. These three
|
|
-- functions are the one place that reads/writes cfg.noTrigger, so core/commands.lua's slash
|
|
-- handlers stay thin wiring and the union logic in triggersVampirism above is never duplicated.
|
|
--
|
|
-- New exclusions apply forward-only: they change what counts as triggering from the moment they
|
|
-- are added, but do not reach back into totals already recorded for earlier hits. See the /vf
|
|
-- exclude add command in commands.lua for the reasoning (kept there, next to where it is user-
|
|
-- visible); /vf reset remains the explicit way to discard a tainted session.
|
|
|
|
-- Adds spellId to the character-scoped runtime set. Returns false (and changes nothing) if the id
|
|
-- is already covered -- either hardcoded or already added -- with a reason string so the caller
|
|
-- can tell the player why, rather than silently re-adding.
|
|
function C.addNoTrigger(spellId, cfg)
|
|
if C.NO_TRIGGER[spellId] then return false, "builtin" end
|
|
if not cfg then return false, "no-config" end
|
|
if not cfg.noTrigger then cfg.noTrigger = {} end
|
|
if cfg.noTrigger[spellId] then return false, "already" end
|
|
cfg.noTrigger[spellId] = true
|
|
return true
|
|
end
|
|
|
|
-- Removes spellId from the runtime set. The hardcoded list is not reachable through this path --
|
|
-- it is measured fact, not a preference -- so this only ever touches cfg.noTrigger.
|
|
function C.removeNoTrigger(spellId, cfg)
|
|
if not (cfg and cfg.noTrigger and cfg.noTrigger[spellId]) then return false end
|
|
cfg.noTrigger[spellId] = nil
|
|
return true
|
|
end
|
|
|
|
-- Combined listing for /vf exclude list: hardcoded entries first, then runtime ones, each tagged
|
|
-- so the caller can say which is which. `out` is reused like every other list buffer in this addon
|
|
-- (see resetList above) so a repeated /vf exclude list stays allocation-free.
|
|
function C.listNoTrigger(cfg, out)
|
|
out = out or {}
|
|
C.resetList(out)
|
|
local n = 0
|
|
for id in pairs(C.NO_TRIGGER) do
|
|
n = n + 1
|
|
out[n] = { id = id, builtin = true }
|
|
end
|
|
if cfg and cfg.noTrigger then
|
|
for id in pairs(cfg.noTrigger) do
|
|
n = n + 1
|
|
out[n] = { id = id, builtin = false }
|
|
end
|
|
end
|
|
table.setn(out, n)
|
|
return out
|
|
end
|
|
|
|
-- ---- spell id -> name --------------------------------------------------------------------------
|
|
--
|
|
-- nampower hands us numeric spell ids; the tooltip has to show something a player recognises. 1.12
|
|
-- has no GetSpellInfo, but this client has two independent routes that do work, and the result is
|
|
-- cached because a tooltip refresh must not re-resolve on every frame.
|
|
--
|
|
-- Explicitly NOT tried: GameTooltip:SetHyperlink("spell:<id>"). It is documented broken on
|
|
-- 1.12/TurtleWoW -- it fails SILENTLY and leaves the tooltip empty -- so attempting it would cost
|
|
-- work per unknown id and yield nothing.
|
|
--
|
|
-- (Belongs in its own file; it is here so it loads first and needs no toc change, which would cost
|
|
-- a client restart. Move it when something else forces a restart anyway.)
|
|
|
|
C._spellNames = {}
|
|
C.spellNameSource = "none"
|
|
|
|
function C.spellName(id)
|
|
-- -1 is the aggregate's sentinel for auto attacks (VampifyAggregate.MELEE), which have no id.
|
|
if type(id) ~= "number" or id < 0 then return "Melee" end
|
|
local cached = C._spellNames[id]
|
|
if cached then return cached end
|
|
|
|
local name
|
|
|
|
-- 1. SuperWoW's SpellInfo(spellId). It reads the client's spell DBC by numeric id rather than
|
|
-- walking the spellbook, so it resolves foreign and NPC spells too, not just our own.
|
|
if SpellInfo then
|
|
local ok, n = pcall(SpellInfo, id)
|
|
if ok and type(n) == "string" and n ~= "" then
|
|
name = n
|
|
C.spellNameSource = "SpellInfo"
|
|
end
|
|
end
|
|
|
|
-- 2. nampower's own resolver, an independent second route in case SuperWoW is absent.
|
|
if not name and GetSpellNameAndRankForId then
|
|
local ok, n = pcall(GetSpellNameAndRankForId, id)
|
|
if ok and type(n) == "string" and n ~= "" then
|
|
name = n
|
|
C.spellNameSource = "nampower"
|
|
end
|
|
end
|
|
|
|
if not name then name = "Spell #" .. id end
|
|
C._spellNames[id] = name
|
|
return name
|
|
end
|
|
|
|
-- ---- spell id -> icon ---------------------------------------------------------------------------
|
|
--
|
|
-- The UI redesign's per-ability rows want an icon next to the name, not just text. SpellInfo(id)
|
|
-- already returns this as its 3rd value (name, rank, texture, minRange, maxRange) -- C.spellName
|
|
-- above discards it -- so this is a second thin reader over the SAME SuperWoW call, cached exactly
|
|
-- like C.spellName (a tooltip/bar refresh must not re-resolve on every frame), just with its own
|
|
-- cache table because the two calls can return a texture where the name lookup failed or vice versa
|
|
-- (unlikely, but nothing guarantees the two are correlated) and sharing a cache would only save one
|
|
-- pcall while entangling two independently-failing things for no reason.
|
|
--
|
|
-- Unlike C.spellName, a miss here has no readable-string fallback to manufacture -- "no icon" is a
|
|
-- real, displayable answer (the caller substitutes its own placeholder texture), so this returns nil
|
|
-- rather than inventing a path that does not exist on disk for a genuine SPELL id.
|
|
--
|
|
-- A.MELEE (-1) and any non-number id are NOT a SpellInfo lookup at all. Two revisions on
|
|
-- this, both 2026-08-24, in order:
|
|
-- (1) first: read the equipped main-hand weapon's texture directly (GetInventoryItemTexture) --
|
|
-- superseded below;
|
|
-- (2) then (this version): WoW's OWN spellbook already carries this mapping. Every character has
|
|
-- a base "Attack" entry in their spellbook, and its icon IS the equipped weapon's icon,
|
|
-- maintained by the client itself across a weapon swap (confirmed from an in-game spellbook
|
|
-- screenshot). Reading it through GetSpellTexture on the "Attack" entry therefore needs no
|
|
-- swap-invalidation machinery of its own -- WoW already does that job; see C.meleeIcon below
|
|
-- for exactly what is and is not independently confirmed about this here.
|
|
C.MELEE_ICON = "Interface\\Icons\\Ability_MeleeDamage" -- fixed fallback only, see C.meleeIcon
|
|
|
|
-- The "Attack" entry's SPELLBOOK INDEX, found once by NAME and cached -- an index is class- and
|
|
-- level-dependent (talent respecs, new abilities learned into earlier tabs can shift it), so it is
|
|
-- never hardcoded, exactly the same reasoning gui/display.lua's own buildSpellbookSet gives for
|
|
-- never assuming a fixed spell id. `_attackIndexKnown` distinguishes "not searched yet" (nil, look
|
|
-- again) from "searched, genuinely not found" (also nil, but do NOT re-scan every call -- that would
|
|
-- turn a one-time cost into a per-frame one for a locale/build where no "Attack" entry exists).
|
|
C._attackIndex = nil
|
|
C._attackIndexKnown = false
|
|
|
|
-- Invalidates the cached index -- wired to SPELLS_CHANGED/LEARNED_SPELL_IN_TAB below, the same two
|
|
-- events Blizzard's own FrameXML\SpellBookFrame.lua registers in 1.12.1 -- a newly learned ability
|
|
-- can insert into an earlier tab and shift every
|
|
-- later index, which is exactly the class of change that would silently point this at the wrong
|
|
-- spellbook row if never re-checked. Exposed (not local) so an offline test can simulate the event
|
|
-- without needing a real WoW event frame.
|
|
function C.invalidateAttackIndex()
|
|
C._attackIndex, C._attackIndexKnown = nil, false
|
|
end
|
|
|
|
-- Iterates the spellbook by index from 1, same loop shape as gui/display.lua's buildSpellbookSet
|
|
-- (GetSpellName(i, bookType) until nil) -- the established pattern in this codebase for "walk the
|
|
-- whole spellbook", not a second invention of it. pcall-wrapped like every other WoW API read in
|
|
-- this file: GetSpellName may be absent entirely (offline tests, or a hypothetical client without
|
|
-- it) and must degrade to "not found" rather than error.
|
|
local function findAttackIndex()
|
|
if not GetSpellName then return nil end
|
|
local bookType = BOOKTYPE_SPELL or "spell" -- literal fallback matches SpellBookFrame.lua's own
|
|
-- BOOKTYPE_SPELL = "spell" in case load order ever
|
|
-- left the global unset when this runs
|
|
local i = 1
|
|
while true do
|
|
local ok, name = pcall(GetSpellName, i, bookType)
|
|
if not ok or not name then break end
|
|
if name == "Attack" then return i end
|
|
i = i + 1
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- The "Attack" spellbook entry's current icon. The INDEX is cached (found by name, invalidated on
|
|
-- the events above); the TEXTURE itself is read FRESH on every call via GetSpellTexture, which is
|
|
-- what carries a weapon swap through without this file needing its own swap-invalidation -- WoW's
|
|
-- client is claimed to keep that entry's icon in sync with the equipped weapon on its own (confirmed
|
|
-- from an in-game screenshot of the "Attack" spellbook row).
|
|
--
|
|
-- NOT INDEPENDENTLY VERIFIED HERE: this file (and the FrameXML source it was checked against) can
|
|
-- only confirm the API SURFACE -- GetSpellName(i, bookType)/GetSpellTexture(i, bookType), bookType
|
|
-- BOOKTYPE_SPELL == "spell", both used exactly this way by Blizzard's own SpellBookFrame.lua. Whether
|
|
-- the "Attack" entry's texture is genuinely weapon-linked is a client-side (C++) behavior no Lua
|
|
-- source exposes -- that rests on in-game observation and is still open for an in-game
|
|
-- double-check (see this feature's own verdict).
|
|
--
|
|
-- Falls back to C.MELEE_ICON when there is no client API at all (offline tests), no "Attack" entry
|
|
-- was found (unknown locale/build), or GetSpellTexture itself returns nothing for it.
|
|
function C.meleeIcon()
|
|
if not C._attackIndexKnown then
|
|
C._attackIndex, C._attackIndexKnown = findAttackIndex(), true
|
|
end
|
|
if C._attackIndex and GetSpellTexture then
|
|
local bookType = BOOKTYPE_SPELL or "spell"
|
|
local ok, tex = pcall(GetSpellTexture, C._attackIndex, bookType)
|
|
if ok and type(tex) == "string" and tex ~= "" then return tex end
|
|
end
|
|
return C.MELEE_ICON
|
|
end
|
|
|
|
if CreateFrame then
|
|
local f = CreateFrame("Frame", "VampifyConstSpellbookWatcher")
|
|
f:RegisterEvent("SPELLS_CHANGED")
|
|
f:RegisterEvent("LEARNED_SPELL_IN_TAB")
|
|
f:SetScript("OnEvent", C.invalidateAttackIndex)
|
|
end
|
|
|
|
C._spellIcons = {}
|
|
|
|
function C.spellIcon(id)
|
|
if type(id) ~= "number" or id < 0 then return C.meleeIcon() end
|
|
local cached = C._spellIcons[id]
|
|
if cached ~= nil then
|
|
if cached == false then return nil end -- a cached MISS, not "never looked up"
|
|
return cached
|
|
end
|
|
|
|
local icon
|
|
if SpellInfo then
|
|
-- pcall-wrapped like C.spellName: SuperWoW may be absent, or SpellInfo may throw for an id
|
|
-- the client's DBC does not know. Either way this must degrade to nil, never propagate an
|
|
-- error into the hot damage/tooltip path.
|
|
local ok, _, _, tex = pcall(SpellInfo, id)
|
|
if ok and type(tex) == "string" and tex ~= "" then icon = tex end
|
|
end
|
|
|
|
-- Cache a MISS too (as `false`, distinguishable from "not yet looked up" = nil), so an id that
|
|
-- keeps failing (no SuperWoW, or genuinely no icon) is not re-queried every frame -- the whole
|
|
-- point of caching at all.
|
|
C._spellIcons[id] = icon or false
|
|
return icon
|
|
end
|
|
|
|
function C.errorCount()
|
|
local n = 0
|
|
for _, c in pairs(C._errSeen) do n = n + c end
|
|
return n, table.getn(C._errOrder)
|
|
end
|
|
|
|
C.FORMULA = {
|
|
-- AoE damping. Measured 0.7021 on Flame Wave, independent of the number of targets hit.
|
|
-- An earlier 0.86 estimate was wrong and is retracted.
|
|
aoeFactor = 0.7,
|
|
|
|
-- Per-hit floor equals the NUMBER OF SOURCES (3 items -> min 3 HP, 2 items -> exactly 2.000).
|
|
-- A fixed floor of 3, and no floor at all, are both refuted by measurement.
|
|
floorMode = "sources",
|
|
|
|
-- Settled 2026-08-10 (spec 4a): of 7239 own hits during active Vampirism epochs, 14 carried
|
|
-- mitigation and 9 were decisive after discarding full-health windows -- 7 pointed clearly at
|
|
-- net, one favoured gross by 0.6 HP (noise), one was a full-block window too noisy to count.
|
|
-- The gross error tracked mitigation * sumPercent almost exactly, which is the shape the
|
|
-- mechanism predicts. The server pays Vampirism on damage already reduced by block/absorb/
|
|
-- resist, not on the logged (pre-mitigation) amount.
|
|
damageBase = "net",
|
|
|
|
-- Three-valued. "undecided" behaves as "off" and makes the displayed total a LOWER BOUND.
|
|
channels = {
|
|
melee = "on", -- measured
|
|
spell = "on", -- measured
|
|
aoe = "on", -- measured, with aoeFactor
|
|
proc = "on", -- measured: Tidal Wave 12/12 windows, 0/36 control
|
|
dmgshield = "off", -- refuted: Thorns 236 + Thorium Spike 27 triggers -> 0 healing
|
|
dot = "on", -- measured 2026-08-10: ticks trigger per tick, no special
|
|
-- handling, measured in-game over two Flame Shock sequences.
|
|
-- The old "never occurred in a Vampirism epoch" note predated
|
|
-- that measurement.
|
|
totempet = "off", -- the developer's read from play; undecidable as instrumented
|
|
pvp = "undecided", -- no Vampirism-geared BG epoch with own damage
|
|
},
|
|
}
|
|
|
|
-- ---- item proc origins (spell id -> item name) --------------------------------------------------
|
|
--
|
|
-- C.ORIGINS itself is generated (see the block below) from the server-source
|
|
-- item_template + spell_template dump, in two layers: DIRECT spelltrigger_N == 2 ("chance on hit")
|
|
-- ids, which appear in the combat log exactly as-is; and CHAIN ids, reached by following item
|
|
-- spells at trigger 1 (on-equip proc auras) OR 2 one spell_template effect hop via
|
|
-- effectTriggerSpell1/2/3. The chain layer exists because a real proc can sit one hop away from
|
|
-- the item: spell 16614 ("Lightning Strike") never appears as an item_template spellid_N at all --
|
|
-- only its equip-aura wrapper (spell 16615, trigger==1) does, and 16615's own spell_template
|
|
-- effect is what fires 16614. A direct-only lookup would silently miss exactly that case. Plain
|
|
-- on-use procs (trigger 0) are still excluded -- they answer "what did this item grant", a
|
|
-- different question than a damage-origin lookup needs.
|
|
--
|
|
-- Nil-safe by construction: returns nil both when spellId has no known item origin AND when the
|
|
-- generator has never been run yet (C.ORIGINS absent entirely), so callers never need a separate
|
|
-- guard for "table not generated yet" vs. "id not a proc".
|
|
function C.itemProcOrigin(spellId)
|
|
if not C.ORIGINS then return nil end
|
|
return C.ORIGINS[spellId]
|
|
end
|
|
|
|
-- BEGIN GENERATED ORIGINS (generated by an offline generator script -- do not edit by hand)
|
|
--
|
|
-- Generated by an offline generator script. Two sources, unioned, two layers:
|
|
-- Pass 1 (item->spell edges): UNION of a live game-database dump of item-to-spell
|
|
-- links and a local item_template dump. Conflict rule: for an
|
|
-- item entry known to both, the live database wins outright (current server state); the local
|
|
-- dump contributes ONLY items the live database does not know at all. See this generator's file
|
|
-- header ("TWO SOURCES, UNION not replacement") for the full rule and why.
|
|
-- Pass 2 (spell effect chain): local spell_template dump -- the live database
|
|
-- has no equivalent effectTriggerSpell field, see this generator's file header.
|
|
-- 1. DIRECT: item-attached spell at trigger == 2 ("chance on hit") --
|
|
-- these appear in the combat log exactly as-is.
|
|
-- 2. CHAIN: item-attached spell at trigger == 1 (on-equip proc auras) OR == 2,
|
|
-- followed ONE spell_template effect hop via effectTriggerSpell1/2/3.
|
|
-- This exists because a real proc (spell 16614 "Lightning Strike") never
|
|
-- appears as an item-attached spell at all -- only the equip-aura wrapper
|
|
-- (16615) does, at trigger==1, and 16615's own spell_template effect is what
|
|
-- fires 16614.
|
|
-- A direct-only lookup silently misses cases exactly like this one.
|
|
-- An id reachable through both layers gets one entry with item names unioned.
|
|
--
|
|
-- Maps a proc spell id to the item(s) that carry it, so the addon can answer
|
|
-- "is this spell id an item proc, and from what" via a plain table lookup.
|
|
--
|
|
-- Lives in const.lua (not its own file) for the same reason C.spellName does: a new
|
|
-- .lua file needs a Vampify.toc entry, which costs a full client restart, while
|
|
-- const.lua loads first as the very first module and a table literal here needs only
|
|
-- a /reload. Re-run the generator and replace this block; do not hand-edit it.
|
|
--
|
|
-- Regenerate: run the offline generator script
|
|
C.ORIGINS = {
|
|
[56] = "Carved Ragetotem and others",
|
|
[89] = "Fire Sword of Crippling",
|
|
[695] = "Betrayer",
|
|
[744] = "Gift of the Spider God",
|
|
[772] = "Serrated Handaxe",
|
|
[871] = "Glaive of the Defender",
|
|
[1054] = "Lesser Firestone",
|
|
[2606] = "Stormfist",
|
|
[2912] = "Sentinel's Moonslicer",
|
|
[3264] = "Bloodhowler",
|
|
[3271] = "Chilton Wand",
|
|
[3396] = "The Ripper, Vile Sting",
|
|
[3424] = "Gift of the Spider God",
|
|
[3742] = "Gahz'rilla Fang, Aura Proc Damage Sword",
|
|
[5597] = "The Ripper",
|
|
[6647] = "The Ripper",
|
|
[6751] = "Venom Infused Blade",
|
|
[7712] = "Fiery Retributer and others",
|
|
[7714] = "Embergem Cuffs and others",
|
|
[8191] = "Sword of Zeal",
|
|
[8277] = "Cursed Shinbone",
|
|
[8313] = "Bite of Serra'kis",
|
|
[8348] = "Julie's Dagger",
|
|
[8552] = "Staff of Horrors, Cursed Thornblade",
|
|
[9057] = "Red Whelp Gloves, Helmet of the Scarlet Avenger",
|
|
[9159] = "Green Whelp Armor",
|
|
[9329] = "Soulstring and others",
|
|
[9632] = "Ravager, The Cruel Blade",
|
|
[9633] = "Ravager, The Cruel Blade",
|
|
[9777] = "Truesilver Breastplate",
|
|
[9796] = "Blight",
|
|
[9800] = "Truesilver Champion",
|
|
[9806] = "Phantom Blade",
|
|
[10342] = "Guardian Talisman",
|
|
[10351] = "Blade of the Basilisk",
|
|
[10368] = "Uther's Strength",
|
|
[10370] = "Mutilator",
|
|
[10371] = "Obedient Whacker",
|
|
[10373] = "Pendulum of Doom",
|
|
[11657] = "Jang'thraze the Protector",
|
|
[11658] = "Sul'thraze the Lasher",
|
|
[11790] = "Toxic Revenger",
|
|
[11791] = "Digmaster 5000, Vibroblade, Nail on a Plank",
|
|
[11879] = "Shoni's Disarming Tool, The Murkfisher",
|
|
[12484] = "Einhorn's Skinner",
|
|
[12685] = "Stealthblade",
|
|
[12686] = "Ragehammer",
|
|
[12731] = "Stoneslayer",
|
|
[13049] = "Dragon's Call",
|
|
[13318] = "Barman Shanker, Blood Talon, Killmaim",
|
|
[13438] = "Excavator's Brand",
|
|
[13439] = "Winter's Bite and others",
|
|
[13440] = "Grimclaw and others",
|
|
[13441] = "Orb of Fire",
|
|
[13442] = "Meteor Shard, Baron Charr's Sceptre, Scroll of Cow Portal",
|
|
[13480] = "Night Reaver",
|
|
[13482] = "The Ziggler, Electrocutioner Leg",
|
|
[13486] = "Bloodletter Scalpel, Fleshrender",
|
|
[13490] = "Howling Blade",
|
|
[13491] = "Iron Knuckles",
|
|
[13496] = "Mug O' Hurt",
|
|
[13518] = "Blackvenom Blade, Fang of the Broodmother",
|
|
[13519] = "Shortsword of Vengeance, Fishbringer",
|
|
[13524] = "Stalvan's Reaper",
|
|
[13526] = "Strike of the Hydra, Hookfang Shanker",
|
|
[13527] = "Supercharger Battle Axe",
|
|
[13528] = "Sword of Decay",
|
|
[13530] = "Tainted Pierce",
|
|
[13532] = "The Hand of Antu'sul",
|
|
[13533] = "The Jackhammer, Carved Grimtotem",
|
|
[13534] = "The Shatterer",
|
|
[13752] = "Dazzling Longsword",
|
|
[14106] = "The Black Knight",
|
|
[14118] = "Drakefang Butcher",
|
|
[14119] = "Phytoblade, Thunder 45, Thunderhorn",
|
|
[14126] = "Grim Reaper, Rusty Coghammer",
|
|
[15280] = "Dark Iron Sunderer",
|
|
[15283] = "Dark Iron Pulverizer",
|
|
[15494] = "Ironfoe",
|
|
[15592] = "Skaldrenox's Rage",
|
|
[15595] = "Force of Will",
|
|
[15601] = "Hand of Justice",
|
|
[15602] = "Lord General's Sword",
|
|
[15661] = "Terrorblade Glaive",
|
|
[15662] = "Smoldering Claw",
|
|
[16393] = "Glutton's Cleaver",
|
|
[16400] = "Widow's Kiss and others",
|
|
[16401] = "Poison-tipped Bone Spear",
|
|
[16403] = "The Goldtusk and others",
|
|
[16405] = "Ripsaw and others",
|
|
[16406] = "Gutwrencher, Hameya's Slayer",
|
|
[16407] = "Edge of Winter",
|
|
[16408] = "Darkwater Talwar",
|
|
[16409] = "Ghoulfang and others",
|
|
[16411] = "Deathblow",
|
|
[16413] = "Firebreather, Searing Blade",
|
|
[16414] = "Wraith Scythe, Scythe of the Harvest",
|
|
[16415] = "Taran Icebreaker",
|
|
[16433] = "Bloodfist and others",
|
|
[16454] = "Searing Needle",
|
|
[16528] = "Keris of Zul'Serak",
|
|
[16549] = "Blackhand Doomsaw",
|
|
[16551] = "Felstriker",
|
|
[16559] = "Flame Wrath",
|
|
[16560] = "Flame Wrath",
|
|
[16602] = "Blackblade of Shahram",
|
|
[16603] = "Demonfork",
|
|
[16608] = "Demon Forged Breastplate, Breastplate of the Dark Reaver",
|
|
[16614] = "Storm Gauntlets and others",
|
|
[16621] = "Invulnerable Mail",
|
|
[16782] = "Charged Servo Arm and others",
|
|
[16783] = "Totem of Infliction, Girdle of Reprisal",
|
|
[16784] = "Vile Protector",
|
|
[16871] = "Bleakwood Hew",
|
|
[16898] = "Blazing Rapier",
|
|
[16908] = "Serenity",
|
|
[16916] = "Arcanite Champion",
|
|
[16921] = "Masterwork Stormhammer, Thunderforge Lance",
|
|
[16927] = "Frostguard",
|
|
[16928] = "Annihilator",
|
|
[16939] = "Darkspear",
|
|
[17144] = "Stormpike",
|
|
[17148] = "Brain Hacker",
|
|
[17152] = "Destiny",
|
|
[17153] = "Kang the Decapitator",
|
|
[17154] = "The Green Tower",
|
|
[17196] = "Seeping Willow",
|
|
[17308] = "Femur Club and others",
|
|
[17315] = "Bashguuder, Rivenspike",
|
|
[17331] = "Fang of the Crystal Spider",
|
|
[17351] = "Argent Defender",
|
|
[17352] = "Argent Avenger",
|
|
[17407] = "The Nicker",
|
|
[17483] = "Demonshear",
|
|
[17484] = "Skullforge Reaver",
|
|
[17496] = "Crest of Retribution",
|
|
[17500] = "Malown's Slam",
|
|
[17504] = "Bloodrazor",
|
|
[17505] = "The Cruel Hand of Timmy",
|
|
[17506] = "Soul Breaker",
|
|
[17509] = "Dark Reaver",
|
|
[17510] = "Sword of Corruption",
|
|
[17511] = "Serpent Slicer, Ichor Spitter, Toxic Ripper",
|
|
[17936] = "Firestone",
|
|
[17940] = "Greater Firestone",
|
|
[17942] = "Major Firestone",
|
|
[18077] = "Venom Web Fang",
|
|
[18078] = "Bloody Pick and others",
|
|
[18081] = "Gryphon Rider's Stormhammer",
|
|
[18082] = "Volcanic Hammer",
|
|
[18083] = "Galgann's Firehammer",
|
|
[18084] = "Fist of the Damned",
|
|
[18086] = "Teebu's Blazing Longsword, Force of Magma",
|
|
[18088] = "Blade of the Wretched, Corruption",
|
|
[18089] = "Linken's Sword of Mastery",
|
|
[18090] = "Gutrender, Fisher's Harpoon, Ancient Hakkari Flayer",
|
|
[18091] = "Archeus",
|
|
[18092] = "Shiver Blade, Coldheart Icicle",
|
|
[18104] = "Axe of the Deep Woods",
|
|
[18107] = "Gut Ripper, Fleshslasher",
|
|
[18112] = "Ashbringer",
|
|
[18138] = "Black Duskwood Staff, Shadowblade, Deathbringer",
|
|
[18187] = "Pan of Po'rool, Overloaded Heating Coil, Fists of the Red Dawn",
|
|
[18197] = "Serpent's Kiss, Stinging Viper",
|
|
[18199] = "Burning War Axe",
|
|
[18200] = "Bloodspiller",
|
|
[18202] = "Bloodpike, Gargoyle Shredder Talons, Jaw of the Ancient",
|
|
[18203] = "Venomspitter",
|
|
[18204] = "Cobalt Crusher",
|
|
[18205] = "Black Malice and others",
|
|
[18206] = "Diabolic Skiver",
|
|
[18208] = "Scorpion Sting",
|
|
[18211] = "Nightblade, Doombringer, Ebon Hand",
|
|
[18214] = "Witchfury, Sun's Tail",
|
|
[18217] = "Duskbringer",
|
|
[18276] = "Darrowspike, Bonechill Hammer",
|
|
[18278] = "Silent Fang",
|
|
[18289] = "Gravestone War Axe",
|
|
[18350] = "Black Grasp of the Destroyer",
|
|
[18381] = "Cursed Felblade",
|
|
[18398] = "Sliverblade, Glacial Blade",
|
|
[18543] = "Everflame Torch",
|
|
[18633] = "Frightskull Shaft",
|
|
[18652] = "Barovian Family Sword",
|
|
[18656] = "Ebon Hilt of Marduk, Shadowbringer",
|
|
[18796] = "Fiery War Axe",
|
|
[18797] = "Flurry Axe",
|
|
[18798] = "Freezing Band",
|
|
[18803] = "Hand of Edward the Odd",
|
|
[18817] = "Skullflame Shield",
|
|
[18818] = "Skullflame Shield",
|
|
[18819] = "Archaic Slicer",
|
|
[18828] = "Wall of the Dead",
|
|
[18833] = "Alcor's Sunrazor",
|
|
[18946] = "The Lion Horn of Stormwind",
|
|
[18980] = "Electrified Gloves",
|
|
[19260] = "Chillpike",
|
|
[19755] = "Frightalon",
|
|
[19874] = "Shimmering Platinum Warhammer",
|
|
[20586] = "Windreaper",
|
|
[20869] = "Glacial Stone",
|
|
[20883] = "Joonho's Mercy",
|
|
[21140] = "Vis'kag the Bloodletter, Drake Talon Cleaver",
|
|
[21151] = "Gutgore Ripper",
|
|
[21152] = "Earthshaker",
|
|
[21153] = "Bonereaver's Edge",
|
|
[21159] = "Sulfuron Hammer",
|
|
[21162] = "Sulfuras, Hand of Ragnaros",
|
|
[21165] = "Empyrean Demolisher, Steamrigged Servohammer",
|
|
[21170] = "Shadowstrike",
|
|
[21179] = "Thunderstrike",
|
|
[21186] = "Spinal Reaper",
|
|
[21330] = "Eye of the Abyss",
|
|
[21898] = "Cowl of Terror",
|
|
[21919] = "Thrash Blade, Chronobreaker, Letashaz's Right Claw",
|
|
[21949] = "Gatorbite Axe",
|
|
[21951] = "Fist of Stone, Energized Spear",
|
|
[21952] = "Claw of Celebras",
|
|
[21961] = "Princess Theradras' Scepter, Carved Runetotem",
|
|
[21970] = "Mark of the Chosen",
|
|
[21992] = "Thunderfury, Blessed Blade of the Windseeker",
|
|
[22600] = "Force Reactive Disk",
|
|
[22619] = "Force Reactive Disk",
|
|
[22639] = "Eskhandar's Left Claw",
|
|
[22640] = "Eskhandar's Right Claw",
|
|
[22850] = "Quel'Serrar",
|
|
[22863] = "Sprinter's Sword",
|
|
[23267] = "Perdition's Blade",
|
|
[23454] = "Ironfist and others",
|
|
[23592] = "Electrified Dagger",
|
|
[23604] = "Black Amnesty",
|
|
[23605] = "Nightfall",
|
|
[23682] = "Darkmoon Card: Heroism",
|
|
[23684] = "Darkmoon Card: Blue Dragon",
|
|
[23687] = "Darkmoon Card: Maelstrom",
|
|
[23719] = "The Untamed Blade",
|
|
[24241] = "Halberd of Smiting",
|
|
[24251] = "Zulian Slicer",
|
|
[24254] = "Sceptre of Smiting",
|
|
[24257] = "Jeklik's Crusher",
|
|
[24362] = "Feralkin Necklace, Ancient Hakkari Flayer, Devilsaur Claws",
|
|
[24388] = "The Lobotomizer",
|
|
[24405] = "Glacial Spike, Scale of the Blue Drake",
|
|
[24585] = "Ancient Hakkari Manslayer",
|
|
[24993] = "Emerald Dragonfang",
|
|
[25768] = "Staff of the Qiraji Prophets",
|
|
[25907] = "Wrath of Cenarius",
|
|
[26108] = "Dark Edge of Insanity",
|
|
[26415] = "Kalimdor's Revenge",
|
|
[26693] = "Neretzek, The Blood Drinker, Shadowbringer, Pulseseeker",
|
|
[27039] = "Beastmaster's Cap, Deathmist Mask, Pendant of Kindred Spirit",
|
|
[27042] = "Beastmaster's Gloves, Deathmist Wraps",
|
|
[27205] = "Beastmaster's Boots, Deathmist Sandals, Charm of Dark Domination",
|
|
[27208] = "Beastmaster's Tunic, Deathmist Robe",
|
|
[27559] = "Hushblade, Jagged Obsidian Shield",
|
|
[27648] = "Thunderfury, Blessed Blade of the Windseeker",
|
|
[27655] = "Heart of Wyrmthalak",
|
|
[27657] = "Inflatable Woman",
|
|
[27860] = "Blade of Eternal Darkness",
|
|
[27868] = "Icemail Jerkin",
|
|
[28414] = "Corrupted Ashbringer",
|
|
[28441] = "Corrupted Ashbringer",
|
|
[28701] = "Tempest's Rage",
|
|
[29151] = "Misplaced Servo Arm",
|
|
[29155] = "Corrupted Ashbringer",
|
|
[29164] = "Stygian Buckler",
|
|
[29502] = "Hurricane",
|
|
[29638] = "Bow of Searing Arrows",
|
|
[29639] = "Dwarven Hand Cannon",
|
|
[29640] = "Heartseeking Crossbow, Lodestone",
|
|
[29641] = "Dark Iron Rifle",
|
|
[29644] = "Galgann's Fireblaster",
|
|
[29646] = "Quillshooter",
|
|
[29647] = "Shell Launcher Shotgun",
|
|
[29653] = "Venomstrike",
|
|
[29655] = "Verdant Keeper's Aim",
|
|
[45076] = "Aspect of Seradane",
|
|
[45416] = "Vial of Potent Venoms",
|
|
[45522] = "Idol of the Emerald Rot",
|
|
[45841] = "Rod of Resuscitation",
|
|
[45843] = "Mana Binding Signet",
|
|
[45848] = "Fist of the Forgotten Order",
|
|
[45849] = "Totem of Crackling Thunder",
|
|
[45856] = "Shawl of the Castellan",
|
|
[45858] = "Breath of Solnius",
|
|
[45860] = "Chromie's Broken Pocket Watch",
|
|
[45862] = "Libram of the Faithful",
|
|
[45867] = "Crystal of Vengeance",
|
|
[45869] = "Concentrated Power of Will",
|
|
[45873] = "Black Widow Eggs",
|
|
[45875] = "Vampire Heart",
|
|
[46104] = "Tempered Runeblade",
|
|
[46318] = "Demoralization Club",
|
|
[46319] = "Horde Defender's Axe",
|
|
[46431] = "Idol of Evergrowth",
|
|
[47354] = "Breastplate of Beast Mastery",
|
|
[48004] = "Dream's Herald",
|
|
[48005] = "Frostbound Slasher",
|
|
[48006] = "Pauldron of Deflection",
|
|
[48008] = "Bloodletter Razor",
|
|
[48048] = "Ornate Bloodstone Dagger",
|
|
[48101] = "Totem of the Stonebreaker",
|
|
[48102] = "Towerforge Demolisher",
|
|
[49369] = "Modrag'zan, Heart of the Mountain",
|
|
[51001] = "Ornate Pyrium Gauntlets",
|
|
[51144] = "Shar'tateth, the Shattered Edge",
|
|
[51250] = "Splinterspear Mace",
|
|
[51251] = "Claw of the Mageweaver",
|
|
[51266] = "Stonewrought Vambraces",
|
|
[51277] = "Crystalvein Breastplate",
|
|
[51740] = "Treant's Bane",
|
|
[52843] = "Bloodcaller's Decapitator",
|
|
[52853] = "Draenethyst Blade",
|
|
[52854] = "Draenethyst Juggernaut",
|
|
}
|
|
-- END GENERATED ORIGINS
|