Module:Bestiary/Sandbox
Jump to navigation
Jump to search
Documentation for this module may be created at Module:Bestiary/Sandbox/doc
local p = {}
local yesno = require('Module:Yesno')
local pt = require('Module:Paramtest')
local enum = require('Module:Enum')
local pagelisttools = require('Module:PageListTools')
local avg = require('Module:Average_drop_value/Sandbox')
local curr = require('Module:Currency')._amount
local pageswithcats = pagelisttools.pageswithcats
local smwstats = {}
-- Formats a table header
function p.header(tbl, verbose)
local tr = tbl:tag('tr')
:tag('th'):addClass('unsortable'):node(''):done()
:tag('th'):css('text-align', 'left'):wikitext('Monster'):done()
:tag('th'):wikitext('[[File:Member icon.png|link=|Members]]'):done()
:tag('th'):wikitext('[[File:Attack style icon.png|link=|Combat level]]'):done()
:tag('th'):wikitext('[[File:Hitpoints icon.png|link=|Hitpoints]]'):done()
:tag('th'):wikitext('[[File:Attack icon.png|link=|Attack level]]'):done()
:tag('th'):wikitext('[[File:Defence icon.png|link=|Defence level]]'):done()
:tag('th'):wikitext('[[File:Magic icon.png|link=|Magic level]]'):done()
:tag('th'):wikitext('[[File:Ranged icon.png|link=|Ranged level]]'):done()
:tag('th'):wikitext('[[File:Coins 10000.png|link=|Average drop value]]'):done()
if verbose then
tr:tag('th'):wikitext('[[File:Slayer icon.png|link=|Slayer level]]'):done()
:tag('th'):wikitext('<span title="Slayer experience">XP</span>'):done()
:tag('th'):css('text-align', 'left'):wikitext('Slayer categories'):done()
:tag('th'):css('text-align', 'left'):wikitext('Assigned by'):done()
end
end
-- Main entrypoint
function p.main(frame)
local args = frame:getParent().args
return p._main(args)
end
function p._main(args)
local frame = mw.getCurrentFrame()
local mambo = {'Members', 'F2P', 'All'}
local slayer_masters = { 'Spria', 'Turael', 'Krystilia', 'Mazchna', 'Vannaka', 'Chaeldar', 'Konar quo Maten', 'Nieve', 'Steve', 'Duradel' }
local pattern_alpha = '^%a$'
-- Fetch and validate all input parameters
local fromlevel = tonumber(pt.default_to(args['fromlevel'], '1'))
local tolevel = tonumber(pt.default_to(args['tolevel'], '1'))
local fromletter = string.upper(pt.default_to(args['fromletter'], 'A'))
local toletter = string.upper(pt.default_to(args['toletter'], 'A'))
local mems = pt.default_to(args['mems'], 'All')
local assignedby = pt.default_to(args['assignedby'], '')
local onlycat = pt.default_to(args['onlycat'], '')
local attribute = pt.default_to(args['attribute'], '')
local ba = {
levelselect = true,
levelsort = true,
slayeronly = false,
verbose = false,
showstats = false,
disco = false,
quest = true,
dmm = true,
cache = false
}
for k, b in pairs(ba) do
if pt.has_content(args[k]) then
ba[k] = yesno(args[k])
end
end
if pt.has_content(args['mems']) and not enum.contains(mambo, mw.text.trim(args['mems'])) then
error('mems parameter has invalid value')
end
if pt.has_content(args['assignedby']) and not enum.contains(slayer_masters, mw.text.trim(args['assignedby'])) then
error('assignedby parameter has invalid value')
end
if ba['levelselect'] then
if pt.is_empty(args['fromlevel']) or not fromlevel then
error('parameter fromlevel is invalid')
end
if pt.is_empty(args['tolevel']) or not tolevel then
error('parameter tolevel is invalid')
end
assert(tolevel >= fromlevel, 'parameter tolevel must be greater than or equal to fromlevel')
else
if pt.is_empty(args['fromletter']) or not fromletter:match(pattern_alpha) then
error('parameter fromletter is invalid')
end
if pt.is_empty(args['toletter']) or not toletter:match(pattern_alpha) then
error('parameter toletter is invalid')
end
assert(toletter >= fromletter, 'parameter toletter must be greater than or equal to fromletter')
end
-- Gather the data
local fulldata = p.loadData(fromlevel, tolevel, fromletter, toletter, ba['levelselect'], mems, ba['slayeronly'], onlycat, attribute, assignedby, ba['verbose'])
local data = p.filterData(fulldata, ba['disco'], ba['quest'], ba['dmm'], ba['cache'])
local fulldata = nil
if ba['levelsort'] then
table.sort(data, function(a, b) return a['level'] < b['level'] end)
end
-- Format the output page
local div = mw.html.create('div')
if #data == 0 then
div:wikitext('Search yielded no results.')
return div
elseif ba['showstats'] then
div:wikitext(string.format('Search yielded %i results.', #data))
if smwstats['found'] == smwstats['limit'] then
div:wikitext(string.format(' Your search might have been too large and have been truncated as a result.'))
end
end
local tbl = div:tag('table'):addClass('wikitable sortable')
:addClass('align-center-1 align-left-2 align-left-3 align-center-4 align-center-5')
:addClass('align-center-6 align-center-7 align-center-8 align-center-9 align-center-10')
if ba['verbose'] then
tbl:addClass('align-center-11 align-center-12 align-left-13 align-left-14')
end
p.header(tbl, ba['verbose'])
-- Render rows
local storedavgdata = {}
for e, entry in ipairs(data) do
local name = mw.text.split(entry['name'], '#', true)
--we save up smw queries for performance, e.g. all barbarians in generic barbarian drop table get the same results so they are computed once.
local avgdata = storedavgdata[name[1]] or avg.getDropData({entry['name']})
--fallback case for average drop value
if (not avgdata) or avgdata == {} or avgdata == '' then
avgdata = avg.getDropData({name[1]}) or {}
storedavgdata[name[1]] = avgdata
end
if name[2] then
name[2] = name[2]:gsub('_', ' ')
else name[2] = ' '
end
local tr = tbl:tag('tr')
:tag('td'):css('height', '64px'):wikitext(entry['image'] and string.format('[[%s|link=|64x64px|%s]]', entry['image'], entry['name']) or ''):done()
:tag('td'):wikitext(string.format('[[%s|%s]]<br/>\'\'%s\'\'', entry['name'], name[1], name[2])):done()
:tag('td'):wikitext(entry['members'] and '[[File:Member icon.png|link=|Members]]' or '[[File:Free-to-play icon.png|link=|Free-to-play]]'):done()
:tag('td'):wikitext(entry['level']):done()
:tag('td'):wikitext(entry['hitpoints']):done()
:tag('td'):wikitext(entry['attack']):done()
:tag('td'):wikitext(entry['defence']):done()
:tag('td'):wikitext(entry['magic']):done()
:tag('td'):wikitext(entry['ranged']):done()
:tag('td'):wikitext(curr(avg.totalvalfromdata(avgdata), 'coins')):done()
if ba['verbose'] then
tr:tag('td'):wikitext(entry['slaylvl'] or '1'):done()
:tag('td'):wikitext(entry['slayxp']):done()
:tag('td'):wikitext(entry['slaycat']):done()
local td = tr:tag('td')
for a, ab in ipairs(entry['assignedby']) do
if enum.contains(slayer_masters, ab) then
td:wikitext(string.format('[[File:%s chathead.png|48x64px|link=%s]]', ab, ab))
else
mw.log(string.format('unknown slayer master: %s', ab))
end
end
end
end
return div
end
-- Estimate the storage size of a serialized table of data
-- This function assumes somewhat consistent sizes of the elements
-- like for instance typical SMW data tables
function p.est_serial_size(tbl, datapoints)
assert(type(tbl) == 'table')
assert(type(datapoints) == 'number')
local dp = {}
for p = 1, datapoints do
local pos = math.floor(#tbl * p / datapoints)
local txt = mw.text.jsonEncode(tbl[pos])
table.insert(dp, txt:len())
end
local avgsize = math.floor(enum.sum(dp) / datapoints)
return #tbl * avgsize
end
-- Filter data with exclusion lists
function p.filterData(indata, disco, quest, dmm, cache)
-- Fetch exclusion list
local exlist = {}
if not disco then
table.insert(exlist, '[[Category:Monsters]] [[Category:Discontinued content]]')
end
if not quest then
table.insert(exlist, '[[Category:Quest monsters]]')
end
if not dmm then
table.insert(exlist, '[[Category:Monsters]] [[Category:Deadman Mode]]')
end
if not cache then
table.insert(exlist, '[[Category:Monsters]] [[Category:Pages using information from game APIs or cache]]')
end
local pages_excl = pageswithcats(exlist)
-- Post-process the data
local data = {}
for _, entry in ipairs(indata) do
local process = true
if enum.contains(pages_excl, entry['variantof']) or enum.contains(pages_excl, entry['name']) then
process = false
end
if process then
table.insert(data, entry)
else
--mw.log(string.format('Removed: %s', entry[1]))
end
end
-- Statistics
mw.log(string.format('Filter: exclusion list size: %i, start size: %i, end size: %i, removed %i.',
#pages_excl, #indata, #data, #indata - #data))
return data
end
-- Sends a query to SMW and returns the data
function p.loadData(fromlevel, tolevel, fromletter, toletter, levelselect, mems, slayeronly, onlycat, attribute, assignedby, verbose)
local props = {
['Image'] = 'image',
['Is members only'] = 'members',
['Combat level'] = 'level',
['Hitpoints'] = 'hitpoints',
['Attack level'] = 'attack',
['Defence level'] = 'defence',
['Magic level'] = 'magic',
['Ranged level'] = 'ranged',
['Is variant of'] = 'variantof'
}
local extprops = {
['Slayer level'] = 'slaylvl',
['Slayer experience'] = 'slayxp',
['Slayer category'] = 'slaycat',
['Assigned by'] = 'assignedby'
}
local query = {}
-- Conditions part of query
local condition = { '[[Uses infobox::Monster]]' }
if mems == 'Members' then
table.insert(condition, ' [[Is members only::true]]')
elseif mems == 'F2P' then
table.insert(condition, ' [[Is members only::false]]')
end
if onlycat:len() > 0 or slayeronly then
if onlycat:len() > 0 then
table.insert(condition, string.format(' [[Slayer category::%s]]', onlycat))
else
table.insert(condition, ' [[Slayer category::+]]')
end
end
if attribute:len() > 0 then
table.insert(condition, string.format(' [[Monster attribute::%s]]', attribute))
end
if assignedby:len() > 0 then
table.insert(condition, string.format(' [[Assigned by::%s]]', assignedby))
end
if levelselect then
table.insert(condition, string.format(' [[Combat level::≥%i]] [[Combat level::≤%i]]', fromlevel, tolevel))
else
if (toletter == 'Z') then
table.insert(condition, string.format(' [[Name::≥%s]]', fromletter))
else
table.insert(condition, string.format(' [[Name::≥%s]] [[Name::<<%s]]', fromletter, string.char(toletter:byte() + 1)))
end
end
table.insert(query, table.concat(condition))
-- Printouts part of query
table.insert(query, '?=#-')
for k, pr in pairs(props) do
table.insert(query, string.format('?%s #- = %s', k, pr))
end
if verbose then
for k, pr in pairs(extprops) do
table.insert(query, string.format('?%s #- = %s', k, pr))
end
end
-- Parameters part of query
query.offset = 0
query.limit = verbose and 500 or 580
-- Fetch the data
-- mw.logObject(query)
local t1 = os.clock()
local smw = mw.smw.ask(query)
local t2 = os.clock()
if not smw or #smw == 0 then
smw = {}
end
-- Post-process the data
local data = {}
for _, entry in ipairs(smw) do
local dataline = entry
dataline['name'] = dataline[1]
dataline[1] = nil
if type(dataline['image']) == 'table' then
dataline['image'] = dataline['image'][1]
end
if type(dataline['slaycat']) == 'table' then
dataline['slaycat'] = table.concat(entry['slaycat'], ', ')
end
if type(dataline['assignedby']) == 'string' then
dataline['assignedby'] = { entry['assignedby'] }
elseif type(dataline['assignedby']) == 'nil' then
dataline['assignedby'] = { }
end
table.insert(data, dataline)
end
-- Statistics
smwstats = {
offset = query.offset,
limit = query.limit,
found = #smw,
time = (t2 - t1) * 1000,
size = p.est_serial_size(smw, 5)
}
mw.log(string.format('SMW: Found %i, offset %i, limit %i, time elapsed %.3f ms, est. serial size: %.0f KiB.',
smwstats.found, smwstats.offset, smwstats.limit, smwstats.time, smwstats.size / 1024))
return data
end
--[[ DEBUG COPYPASTA
mw.logObject( p.loadData('1', '1', 'A', 'A', true, 'All', false, '', '', '', false) )
mw.logObject( p.loadData('1', '1', 'A', 'A', false, 'All', false, '', '', '', false) )
= p._main({fromlevel='1', tolevel='1', verbose='yes'})
= p._main({fromletter='A', toletter='A', levelselect='no', levelsort='no', verbose='yes'})
--]]
return p