更新发送到功能
更新仇恨计算方式 还在开发 更新其他细节
This commit is contained in:
452
Parser.lua
Normal file
452
Parser.lua
Normal file
@@ -0,0 +1,452 @@
|
||||
local NanamiDPS = NanamiDPS
|
||||
local DataStore = NanamiDPS.DataStore
|
||||
|
||||
local Parser = CreateFrame("Frame")
|
||||
NanamiDPS.Parser = Parser
|
||||
|
||||
local validUnits = { ["player"] = true }
|
||||
for i = 1, 4 do validUnits["party" .. i] = true end
|
||||
for i = 1, 40 do validUnits["raid" .. i] = true end
|
||||
|
||||
local validPets = { ["pet"] = true }
|
||||
for i = 1, 4 do validPets["partypet" .. i] = true end
|
||||
for i = 1, 40 do validPets["raidpet" .. i] = true end
|
||||
|
||||
Parser.validUnits = validUnits
|
||||
Parser.validPets = validPets
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Spell threat coefficients (vanilla / classic values, max rank)
|
||||
-- Reference: classic-warrior wiki, warcrafttavern.com
|
||||
-- bonus = fixed threat added ON TOP of damage
|
||||
-- mult = multiplier applied to the DAMAGE portion (default 1.0)
|
||||
-----------------------------------------------------------------------
|
||||
local spellThreatData = {
|
||||
["Heroic Strike"] = { bonus = 173 },
|
||||
["Shield Slam"] = { bonus = 254 },
|
||||
["Revenge"] = { bonus = 270, mult = 2.25 },
|
||||
["Shield Bash"] = { bonus = 156, mult = 1.5 },
|
||||
["Cleave"] = { bonus = 100 },
|
||||
["Execute"] = { mult = 1.25 },
|
||||
["Hamstring"] = { bonus = 135, mult = 1.25 },
|
||||
["Thunder Clap"] = { mult = 2.5 },
|
||||
["Overpower"] = { mult = 0.75 },
|
||||
["Disarm"] = { bonus = 99 },
|
||||
["Sunder Armor"] = { bonus = 261 },
|
||||
["Maul"] = { mult = 1.75 },
|
||||
["Swipe"] = { mult = 1.75 },
|
||||
["Faerie Fire (Feral)"] = { bonus = 108 },
|
||||
["Mind Blast"] = { mult = 2.0 },
|
||||
["Holy Nova"] = { mult = 0 },
|
||||
["Earth Shock"] = { mult = 2.0 },
|
||||
["Searing Pain"] = { mult = 2.0 },
|
||||
["Distracting Shot"] = { bonus = 600 },
|
||||
["Scorpid Poison"] = { bonus = 5 },
|
||||
["Intimidation"] = { bonus = 580 },
|
||||
["Life Tap"] = { mult = 0 },
|
||||
["Counterspell"] = { bonus = 300 },
|
||||
["Mocking Blow"] = { bonus = 250 },
|
||||
}
|
||||
|
||||
do
|
||||
local cnAliases = {
|
||||
["\232\139\177\229\139\135\230\137\147\229\135\187"] = "Heroic Strike",
|
||||
["\231\155\190\231\137\140\231\140\155\229\135\187"] = "Shield Slam",
|
||||
["\229\164\141\228\187\135"] = "Revenge",
|
||||
["\231\155\190\229\135\187"] = "Shield Bash",
|
||||
["\233\161\186\229\138\136\230\150\169"] = "Cleave",
|
||||
["\230\150\169\230\157\128"] = "Execute",
|
||||
["\230\150\173\231\173\139"] = "Hamstring",
|
||||
["\233\155\183\233\156\134\228\184\128\229\135\187"] = "Thunder Clap",
|
||||
["\229\142\139\229\136\182"] = "Overpower",
|
||||
["\231\188\180\230\162\176"] = "Disarm",
|
||||
["\231\160\180\231\148\178\230\148\187\229\135\187"] = "Sunder Armor",
|
||||
["\230\167\152\229\135\187"] = "Maul",
|
||||
["\230\140\165\229\135\187"] = "Swipe",
|
||||
["\231\178\190\231\129\181\228\185\139\231\129\171(\233\135\142\233\135\145)"] = "Faerie Fire (Feral)",
|
||||
["\229\191\131\231\129\181\233\156\135\231\136\134"] = "Mind Blast",
|
||||
["\231\165\158\229\156\163\230\150\176\230\152\159"] = "Holy Nova",
|
||||
["\229\156\176\233\156\135\230\156\175"] = "Earth Shock",
|
||||
["\231\129\188\231\131\173\228\185\139\231\151\155"] = "Searing Pain",
|
||||
["\230\137\176\228\185\177\229\176\132\229\135\187"] = "Distracting Shot",
|
||||
["\232\157\157\230\175\146"] = "Scorpid Poison",
|
||||
["\232\131\129\232\191\171"] = "Intimidation",
|
||||
["\231\148\159\229\145\189\229\136\134\230\181\129"] = "Life Tap",
|
||||
["\229\143\141\229\136\182\233\173\148\230\179\149"] = "Counterspell",
|
||||
["\229\152\178\229\188\132\230\137\147\229\135\187"] = "Mocking Blow",
|
||||
}
|
||||
for cn, en in pairs(cnAliases) do
|
||||
if spellThreatData[en] then
|
||||
spellThreatData[cn] = spellThreatData[en]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local classThreatMod = {
|
||||
ROGUE = 0.8,
|
||||
}
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Stance / form / buff threat modifier detection
|
||||
-- Scans UnitBuff textures to detect known threat-altering states.
|
||||
-- Cached per-unit for 2 seconds to avoid excessive buff scanning.
|
||||
-----------------------------------------------------------------------
|
||||
local stanceScanPatterns = {
|
||||
{ pat = "DefensiveStance", mod = 1.3 },
|
||||
{ pat = "OffensiveStance", mod = 0.8 },
|
||||
{ pat = "Racial_Avatar", mod = 0.8 },
|
||||
{ pat = "BerserkStance", mod = 0.8 },
|
||||
{ pat = "BearForm", mod = 1.3 },
|
||||
{ pat = "CatForm", mod = 0.8 },
|
||||
}
|
||||
|
||||
local buffScanPatterns = {
|
||||
{ pat = "SealOfSalvation", mod = 0.7 },
|
||||
}
|
||||
|
||||
local function ScanUnitThreatMod(unit)
|
||||
if not unit or not UnitExists(unit) then return 1.0 end
|
||||
local stanceMod = 1.0
|
||||
local buffMod = 1.0
|
||||
for i = 1, 32 do
|
||||
local texture = UnitBuff(unit, i)
|
||||
if not texture then break end
|
||||
for _, s in ipairs(stanceScanPatterns) do
|
||||
if string.find(texture, s.pat) then
|
||||
stanceMod = s.mod
|
||||
end
|
||||
end
|
||||
for _, b in ipairs(buffScanPatterns) do
|
||||
if string.find(texture, b.pat) then
|
||||
buffMod = buffMod * b.mod
|
||||
end
|
||||
end
|
||||
end
|
||||
return stanceMod * buffMod
|
||||
end
|
||||
|
||||
local stanceModCache = {}
|
||||
local STANCE_CACHE_TTL = 2
|
||||
|
||||
function Parser:GetUnitThreatMod(name)
|
||||
local now = GetTime()
|
||||
local cached = stanceModCache[name]
|
||||
if cached and (now - cached.time) < STANCE_CACHE_TTL then
|
||||
return cached.mod
|
||||
end
|
||||
local unit = self:UnitByName(name)
|
||||
local mod = ScanUnitThreatMod(unit)
|
||||
stanceModCache[name] = { mod = mod, time = now }
|
||||
return mod
|
||||
end
|
||||
|
||||
function Parser:CalculateSpellThreat(source, spell, damage)
|
||||
if not damage or damage <= 0 then return 0 end
|
||||
|
||||
local threat = damage
|
||||
local coeff = spell and spellThreatData[spell]
|
||||
|
||||
if coeff then
|
||||
threat = damage * (coeff.mult or 1.0) + (coeff.bonus or 0)
|
||||
end
|
||||
|
||||
local class = DataStore:GetClass(source)
|
||||
if class and classThreatMod[class] then
|
||||
threat = threat * classThreatMod[class]
|
||||
end
|
||||
|
||||
threat = threat * self:GetUnitThreatMod(source)
|
||||
|
||||
return math.max(threat, 0)
|
||||
end
|
||||
|
||||
function Parser:CalculateHealThreat(source, amount)
|
||||
if not amount or amount <= 0 then return 0 end
|
||||
|
||||
local threat = amount * 0.5
|
||||
|
||||
local class = DataStore:GetClass(source)
|
||||
if class and classThreatMod[class] then
|
||||
threat = threat * classThreatMod[class]
|
||||
end
|
||||
|
||||
threat = threat * self:GetUnitThreatMod(source)
|
||||
|
||||
return threat
|
||||
end
|
||||
|
||||
local unit_cache = {}
|
||||
function Parser:UnitByName(name)
|
||||
if unit_cache[name] and UnitName(unit_cache[name]) == name then
|
||||
return unit_cache[name]
|
||||
end
|
||||
for unit in pairs(validUnits) do
|
||||
if UnitName(unit) == name then
|
||||
unit_cache[name] = unit
|
||||
return unit
|
||||
end
|
||||
end
|
||||
for unit in pairs(validPets) do
|
||||
if UnitName(unit) == name then
|
||||
unit_cache[name] = unit
|
||||
return unit
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Parser:ScanName(name)
|
||||
if not name then return nil end
|
||||
|
||||
for unit in pairs(validUnits) do
|
||||
if UnitExists(unit) and UnitName(unit) == name then
|
||||
if UnitIsPlayer(unit) then
|
||||
local _, class = UnitClass(unit)
|
||||
DataStore:SetClass(name, class)
|
||||
return "PLAYER"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- SuperWoW pet format: "PetName (OwnerName)"
|
||||
local match, _, owner = string.find(name, "%((.*)%)", 1)
|
||||
if match and owner then
|
||||
if Parser:ScanName(owner) == "PLAYER" then
|
||||
DataStore:SetClass(name, owner)
|
||||
return "PET"
|
||||
end
|
||||
end
|
||||
|
||||
for unit in pairs(validPets) do
|
||||
if UnitExists(unit) and UnitName(unit) == name then
|
||||
if strsub(unit, 0, 3) == "pet" then
|
||||
DataStore:SetClass(name, UnitName("player"))
|
||||
elseif strsub(unit, 0, 8) == "partypet" then
|
||||
DataStore:SetClass(name, UnitName("party" .. strsub(unit, 9)))
|
||||
elseif strsub(unit, 0, 7) == "raidpet" then
|
||||
DataStore:SetClass(name, UnitName("raid" .. strsub(unit, 8)))
|
||||
end
|
||||
return "PET"
|
||||
end
|
||||
end
|
||||
|
||||
if NanamiDPS.config and NanamiDPS.config.trackAllUnits then
|
||||
DataStore:SetClass(name, DataStore:GetClass(name) or "__other__")
|
||||
return "OTHER"
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function Parser:ResolveSource(source)
|
||||
source = NanamiDPS.trim(source)
|
||||
|
||||
local sourceType = self:ScanName(source)
|
||||
if not sourceType then return nil, nil end
|
||||
|
||||
if NanamiDPS.config and NanamiDPS.config.mergePets and sourceType == "PET" then
|
||||
local ownerClass = DataStore:GetClass(source)
|
||||
if ownerClass and ownerClass ~= "__other__" and NanamiDPS.validClasses[DataStore:GetClass(ownerClass)] then
|
||||
return ownerClass, "Pet: " .. source
|
||||
elseif ownerClass and ownerClass ~= "__other__" then
|
||||
return ownerClass, "Pet: " .. source
|
||||
end
|
||||
end
|
||||
|
||||
return source, nil
|
||||
end
|
||||
|
||||
function Parser:ProcessDamage(source, spell, target, amount, school)
|
||||
if type(source) ~= "string" or not tonumber(amount) then return end
|
||||
source = NanamiDPS.trim(source)
|
||||
amount = tonumber(amount)
|
||||
|
||||
if source == target then return end
|
||||
|
||||
local resolvedSource, petPrefix = self:ResolveSource(source)
|
||||
if not resolvedSource then return end
|
||||
|
||||
local finalSpell = petPrefix and (petPrefix .. ": " .. (spell or "")) or spell
|
||||
|
||||
if not DataStore.inCombat then
|
||||
DataStore:StartCombat()
|
||||
end
|
||||
|
||||
DataStore:AddDamage(resolvedSource, finalSpell, target, amount, school)
|
||||
|
||||
local targetType = self:ScanName(target)
|
||||
if targetType == "PLAYER" then
|
||||
DataStore:AddDamageTaken(target, finalSpell, resolvedSource, amount, school)
|
||||
end
|
||||
|
||||
DataStore:AddThreat(source, self:CalculateSpellThreat(source, spell, amount))
|
||||
DataStore:UpdateActivity(resolvedSource, GetTime())
|
||||
|
||||
if targetType == "PLAYER" then
|
||||
self:FeedDeathLog(target, {
|
||||
time = GetTime(),
|
||||
source = resolvedSource,
|
||||
spell = finalSpell,
|
||||
amount = -amount,
|
||||
type = "damage",
|
||||
hp = self:GetUnitHP(target),
|
||||
})
|
||||
end
|
||||
|
||||
self:ThrottledRefresh()
|
||||
end
|
||||
|
||||
function Parser:ProcessHealing(source, spell, target, amount, school)
|
||||
if type(source) ~= "string" or not tonumber(amount) then return end
|
||||
source = NanamiDPS.trim(source)
|
||||
amount = tonumber(amount)
|
||||
|
||||
local resolvedSource, petPrefix = self:ResolveSource(source)
|
||||
if not resolvedSource then return end
|
||||
|
||||
local finalSpell = petPrefix and (petPrefix .. ": " .. (spell or "")) or spell
|
||||
|
||||
local effective = amount
|
||||
local unitstr = self:UnitByName(target)
|
||||
if unitstr then
|
||||
effective = math.min(UnitHealthMax(unitstr) - UnitHealth(unitstr), amount)
|
||||
end
|
||||
|
||||
if not DataStore.inCombat then
|
||||
DataStore:StartCombat()
|
||||
end
|
||||
|
||||
DataStore:AddHealing(resolvedSource, finalSpell, target, amount, effective)
|
||||
|
||||
DataStore:AddThreat(source, self:CalculateHealThreat(source, amount))
|
||||
DataStore:UpdateActivity(resolvedSource, GetTime())
|
||||
|
||||
local targetType = self:ScanName(target)
|
||||
if targetType == "PLAYER" then
|
||||
self:FeedDeathLog(target, {
|
||||
time = GetTime(),
|
||||
source = resolvedSource,
|
||||
spell = finalSpell,
|
||||
amount = effective,
|
||||
type = "heal",
|
||||
hp = self:GetUnitHP(target),
|
||||
})
|
||||
end
|
||||
|
||||
self:ThrottledRefresh()
|
||||
end
|
||||
|
||||
function Parser:ProcessDeath(name)
|
||||
if not name then return end
|
||||
local nameType = self:ScanName(name)
|
||||
if nameType ~= "PLAYER" then return end
|
||||
|
||||
local log = self:FlushDeathLog(name)
|
||||
DataStore:AddDeath(name, {
|
||||
time = GetTime(),
|
||||
timeStr = date("%H:%M:%S"),
|
||||
events = log,
|
||||
})
|
||||
|
||||
self:ThrottledRefresh()
|
||||
end
|
||||
|
||||
function Parser:ProcessDispel(source, spell, target, aura)
|
||||
if type(source) ~= "string" then return end
|
||||
source = NanamiDPS.trim(source)
|
||||
|
||||
local resolvedSource = self:ResolveSource(source)
|
||||
if not resolvedSource then return end
|
||||
|
||||
DataStore:AddDispel(resolvedSource, spell or NanamiDPS.L["Unknown"], target, aura)
|
||||
DataStore:UpdateActivity(resolvedSource, GetTime())
|
||||
|
||||
self:ThrottledRefresh()
|
||||
end
|
||||
|
||||
function Parser:ProcessInterrupt(source, spell, target, interrupted)
|
||||
if type(source) ~= "string" then return end
|
||||
source = NanamiDPS.trim(source)
|
||||
|
||||
local resolvedSource = self:ResolveSource(source)
|
||||
if not resolvedSource then return end
|
||||
|
||||
DataStore:AddInterrupt(resolvedSource, spell or NanamiDPS.L["Unknown"], target, interrupted)
|
||||
DataStore:UpdateActivity(resolvedSource, GetTime())
|
||||
|
||||
self:ThrottledRefresh()
|
||||
end
|
||||
|
||||
function Parser:GetUnitHP(name)
|
||||
local unit = self:UnitByName(name)
|
||||
if unit then
|
||||
return UnitHealth(unit), UnitHealthMax(unit)
|
||||
end
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- Throttle refresh callbacks to avoid excessive UI updates
|
||||
Parser.lastRefreshTime = 0
|
||||
local REFRESH_INTERVAL = 0.25
|
||||
|
||||
function Parser:ThrottledRefresh()
|
||||
local now = GetTime()
|
||||
if now - self.lastRefreshTime >= REFRESH_INTERVAL then
|
||||
self.lastRefreshTime = now
|
||||
NanamiDPS:FireCallback("refresh")
|
||||
end
|
||||
end
|
||||
|
||||
-- Death log buffer: last 15 seconds of events per player
|
||||
Parser.deathLogBuffer = {}
|
||||
local DEATH_LOG_WINDOW = 15
|
||||
|
||||
function Parser:FeedDeathLog(name, entry)
|
||||
if not self.deathLogBuffer[name] then
|
||||
self.deathLogBuffer[name] = {}
|
||||
end
|
||||
table.insert(self.deathLogBuffer[name], entry)
|
||||
|
||||
-- Trim old entries
|
||||
local now = GetTime()
|
||||
local buf = self.deathLogBuffer[name]
|
||||
while table.getn(buf) > 0 and (now - buf[1].time) > DEATH_LOG_WINDOW do
|
||||
table.remove(buf, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function Parser:FlushDeathLog(name)
|
||||
local log = self.deathLogBuffer[name] or {}
|
||||
self.deathLogBuffer[name] = {}
|
||||
return log
|
||||
end
|
||||
|
||||
-- Combat state detection
|
||||
local combatFrame = CreateFrame("Frame", "NanamiDPSCombatState", UIParent)
|
||||
combatFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
|
||||
combatFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
|
||||
|
||||
local pendingCombatEnd = false
|
||||
local combatEndTimer = 0
|
||||
|
||||
combatFrame:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_REGEN_DISABLED" then
|
||||
pendingCombatEnd = false
|
||||
if not DataStore.inCombat then
|
||||
DataStore:StartCombat()
|
||||
end
|
||||
elseif event == "PLAYER_REGEN_ENABLED" then
|
||||
pendingCombatEnd = true
|
||||
combatEndTimer = GetTime() + 2
|
||||
end
|
||||
end)
|
||||
|
||||
combatFrame:SetScript("OnUpdate", function()
|
||||
if (this.tick or 1) > GetTime() then return end
|
||||
this.tick = GetTime() + 1
|
||||
|
||||
if pendingCombatEnd and GetTime() >= combatEndTimer then
|
||||
pendingCombatEnd = false
|
||||
DataStore:StopCombat()
|
||||
end
|
||||
end)
|
||||
Reference in New Issue
Block a user