Documentation for this module may be created at Module:DroppedBy/doc
-- Module:DroppedBy
local p = {}
local ITEM_NAME_REMAP = {
['Dead Frog'] = 'Dead Frog (Item)',
['Dead Snake'] = 'Dead Snake (Item)',
['Skull'] = 'Skull (Item)',
}
local function remapItemName(name)
return ITEM_NAME_REMAP[name] or name
end
local function formatPct(pct)
if pct >= 20 then
return string.format('%d%%', math.floor(pct + 0.5))
elseif pct >= 1 then
return string.format('%.1f%%', pct)
elseif pct >= 0.1 then
return string.format('%.2f%%', pct)
else
return string.format('%.3f%%', pct)
end
end
local function formatAvg(avgVal)
if avgVal >= 1 then
return string.format('%.2f', avgVal)
elseif avgVal >= 0.1 then
return string.format('%.3f', avgVal)
elseif avgVal >= 0.01 then
return string.format('%.4f', avgVal)
else
return string.format('%.5f', avgVal)
end
end
local function formatNum(n)
return mw.language.new('en'):formatNum(n)
end
local function parseCreatureList(raw)
local creatures = {}
for name in raw:gmatch('[^\n,]+') do
name = name:match('^%s*(.-)%s*$')
if name ~= '' then
table.insert(creatures, name)
end
end
return creatures
end
local RARITY_THRESHOLDS = {
{ tier = 'common', min = 20 },
{ tier = 'uncommon', min = 5 },
{ tier = 'semi-rare', min = 1 },
{ tier = 'rare', min = 0.1 },
{ tier = 'very rare', min = 0 },
}
local function getRarity(pct)
for _, r in ipairs(RARITY_THRESHOLDS) do
if pct >= r.min then return r.tier end
end
return 'very rare'
end
local function findItemInBlock(block, kills, targetName)
for entry in block:gmatch('|%s*([A-Z][^\n|]+)') do
if not entry:match('^%s*%a[%a%s]*%s*=') then
local name = entry:match('^%s*(.-)%s*,')
local times = tonumber(entry:match('times%s*:%s*(%d+)'))
local amount = entry:match('amount%s*:%s*([%d%-]+)')
local total = tonumber(entry:match('total%s*:%s*(%d+)'))
if name then
name = remapItemName(name)
if name:lower() == targetName:lower() then
local effectiveTotal = amount and total or times
return {
pct = times and (times / kills * 100) or 0,
amount = amount or '1',
avg = effectiveTotal and (effectiveTotal / kills) or 0,
}
end
end
end
end
return nil
end
local function getCreatureDropData(creatureName, targetItem)
local statsPage = mw.title.new('Loot_Statistics:' .. creatureName)
if not statsPage then return nil end
local content = statsPage:getContent()
if not content then return nil end
local block = content:match('%{%{Loot2%a*%s*(.-)%}%}')
if not block then return nil end
local kills = tonumber(block:match('|%s*kills%s*=%s*(%d+)'))
if not kills or kills == 0 then return nil end
local itemData = findItemInBlock(block, kills, targetItem)
if not itemData then return nil end
local creaturePage = mw.title.new(creatureName)
local creatureContent = creaturePage and creaturePage:getContent() or ''
local isBoss = (creatureContent:match('|%s*isboss%s*=%s*(%a+)') or ''):lower() == 'yes'
local threshold = isBoss and 50 or 500
return {
name = creatureName,
isBoss = isBoss,
kills = kills,
reliable = kills >= threshold,
pct = itemData.pct,
amount = itemData.amount,
avg = itemData.avg,
}
end
local function renderTable(frame, entries, isBossTable)
local rows = {}
local showQuantity = false
for _, entry in ipairs(entries) do
if entry.amount ~= '1' then
showQuantity = true
break
end
end
local creatureHeader = isBossTable and 'Boss' or 'Creature'
rows[#rows+1] = '{| class="wikitable sortable" style="text-align:center; width:auto; float:left; margin-right:2em;"'
if showQuantity then
rows[#rows+1] = string.format('! !! %s !! Quantity !! Avg !! data-sort-type="number" | %%', creatureHeader)
else
rows[#rows+1] = string.format('! !! %s !! Avg !! data-sort-type="number" | %%', creatureHeader)
end
for _, entry in ipairs(entries) do
local tier = getRarity(entry.pct)
local cellClass = entry.reliable and ('loot-' .. tier:gsub(' ', '-')) or ''
local avg = entry.avg > 0 and formatAvg(entry.avg) or ''
local icon = frame:preprocess('{{ilink|' .. entry.name .. '}}')
if showQuantity then
rows[#rows+1] = string.format(
'|-\n| %s || [[%s]] || %s || %s || class="%s" style="text-align:center;" data-sort-value="%f" | %s',
icon, entry.name, entry.amount, avg,
cellClass, entry.pct, formatPct(entry.pct)
)
else
rows[#rows+1] = string.format(
'|-\n| %s || [[%s]] || %s || class="%s" style="text-align:center;" data-sort-value="%f" | %s',
icon, entry.name, avg,
cellClass, entry.pct, formatPct(entry.pct)
)
end
end
rows[#rows+1] = '|}'
return table.concat(rows, '\n')
end
function p.render(frame)
local item = frame.args.item or mw.title.getCurrentTitle().text
local raw = frame.args[1] or ''
local creatures = parseCreatureList(raw)
if #creatures == 0 then
return string.format('<p class="no-results">No creatures drop %s.</p>', item)
end
local normalList = {}
local bossList = {}
for _, name in ipairs(creatures) do
local data = getCreatureDropData(name, item)
if data then
if data.isBoss then
table.insert(bossList, data)
else
table.insert(normalList, data)
end
end
end
table.sort(normalList, function(a, b) return a.pct > b.pct end)
table.sort(bossList, function(a, b) return a.pct > b.pct end)
if #normalList == 0 and #bossList == 0 then
return string.format('<p class="no-results">No loot statistics data found for %s.</p>', item)
end
local tables = {}
if #normalList > 0 then
table.insert(tables, renderTable(frame, normalList, false))
end
if #bossList > 0 then
table.insert(tables, renderTable(frame, bossList, true))
end
return table.concat(tables, '\n')
end
return p