tibia

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