Module:Bestiary
Module documentation
This documentation is transcluded from Template:No documentation/doc. [edit] [history] [purge]
This module does not have any documentation. Please consider adding documentation at Module:Bestiary/doc. [edit]
Module:Bestiary's function main is invoked by Template:Bestiarytable.
Module:Bestiary requires Module:Array.
Module:Bestiary requires Module:PageListTools.
Module:Bestiary requires Module:Paramtest.
Module:Bestiary requires Module:Yesno.
Module:Bestiary requires strict.
require('strict')
local p = {}
local yesno = require('Module:Yesno')
local pt = require('Module:Paramtest')
local arr = require('Module:Array')
local pagelisttools = require('Module:PageListTools')
local pageswithcats = pagelisttools.pageswithcats
local smwstats = {}
-- Formats a table header
function p.header(tbl, verbose)
local tr = tbl:tag('tr')
:tag('th'):attr('colspan', '2'):wikitext('Monster'):done()
:tag('th'):wikitext('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:White dagger.png|link=|Stab defence]][[File:Defence icon.png|link=|alt=]]'):done()
:tag('th'):wikitext('[[File:White scimitar.png|link=|Slash defence]][[File:Defence icon.png|link=|alt=]]'):done()
:tag('th'):wikitext('[[File:White warhammer.png|link=|Crush defence]][[File:Defence icon.png|link=|alt=]]'):done()
:tag('th'):wikitext('[[File:Magic icon.png|link=|Magic defence]][[File:Defence icon.png|link=|alt=]]'):done()
:tag('th'):wikitext('[[File:Steel dart.png|link=|Light Ranged defence]][[File:Defence icon.png|link=|alt=]]'):done()
:tag('th'):wikitext('[[File:Steel arrow.png|link=|Standard Ranged defence]][[File:Defence icon.png|link=|alt=]]'):done()
:tag('th'):wikitext('[[File:Steel bolts.png|link=|Heavy Ranged defence]][[File:Defence icon.png|link=|alt=]]'):done()
if verbose then
tr:tag('th'):wikitext('[[File:Slayer icon.png|link=|Slayer level]]'):done()
:tag('th'):wikitext('<abbr 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 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 arr.contains(mambo, mw.text.trim(args['mems'])) then
error('mems parameter has invalid value')
end
if pt.has_content(args['assignedby']) and not arr.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'])
mw.logObject(fulldata)
local data = p.filterData(fulldata, ba['disco'], ba['quest'], ba['dmm'], ba['cache'])
mw.logObject(data)
local fulldata = nil
if ba['levelsort'] then
pcall(function()
table.sort(data, function(a, b)
return a['level'] < b['level']
-- example fix in case you were directed to the line above by an error:
-- [[Special:Diff/14325110]]
end)
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')
if ba['verbose'] then
tbl:addClass('align-center-10 align-center-11 align-left-12 align-left-13')
end
p.header(tbl, ba['verbose'])
-- Render rows
local function makeRow(entry)
local name = mw.text.split(entry['name'], '#', true)
if name[2] then
name[2] = name[2]:gsub('_', ' ')
end
local tr = mw.html.create('tr')
:tag('td'):css('height', '64px'):wikitext(entry['image'] and string.format('[[%s|link=|64x64px|%s%s]]', entry['image'], name[1], (name[2] and ' - ' .. name[2]) or '') or '' ):done()
:tag('td'):wikitext( string.format('[[%s|%s]]%s', entry['name'], name[1], name[2] and '<br/><i>' .. name[2] .. '</i>' or '') ):done()
:tag('td'):wikitext(entry['members'] and '[[File:Member icon.png|center|link=Members|alt=Members]]' or '[[File:Free-to-play icon.png|center|link=Free-to-play|alt=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(entry['stab defence']):done()
:tag('td'):wikitext(entry['slash defence']):done()
:tag('td'):wikitext(entry['crush defence']):done()
:tag('td'):wikitext(entry['magic defence']):done()
:tag('td'):wikitext(entry['light ranged defence']):done()
:tag('td'):wikitext(entry['standard ranged defence']):done()
:tag('td'):wikitext(entry['heavy ranged defence']):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')
local slaymtbl = {}
for a, ab in ipairs(entry['assignedby']) do
if arr.contains(slayer_masters, ab) then
table.insert(slaymtbl, ab)
else
mw.log(string.format('Unknown Slayer master: %s', ab))
end
end
td:wikitext(mw.text.listToText( slaymtbl, ', ', ', and ' ))
end
return tr
end
local errors = {}
for e, entry in ipairs(data) do
local isnoterr, ret = pcall(makeRow, entry)
if isnoterr then
tbl:node(ret)
else
table.insert(errors, 'Error message: '..ret..'<br>Affected entry: <code>'..mw.text.nowiki(mw.text.jsonEncode(entry))..'</code>')
end
end
if #errors > 0 then
local errtag = div:tag('div'):addClass('error'):wikitext('Errors with this query shown below:[[Category:Pages with script errors]]')
for i,v in ipairs(errors) do
errtag:newline():wikitext('* ',v)
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(arr.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 arr.contains(pages_excl, entry['variantof']) or arr.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',
['Stab defence bonus'] = 'stab defence',
['Slash defence bonus'] = 'slash defence',
['Crush defence bonus'] = 'crush defence',
['Magic defence bonus'] = 'magic defence',
['Light range defence bonus'] = 'light ranged defence',
['Standard range defence bonus'] = 'standard ranged defence',
['Heavy range defence bonus'] = 'heavy ranged defence',
['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 1000 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