Module:Sandbox/Cqm/IM

Documentation for this module may be created at Module:Sandbox/Cqm/IM/doc

-- <pre>
-- ( ͡° ͜ʖ ͡°)
local p = {}

-- "imports"
local onmain = require('Module:Mainonly').on_main
local paramtest = require('Module:Paramtest')
local yesno = require('Module:Yesno')
local commas = require('Module:Addcommas')._add
local infobox = require('Module:Infobox')

--[[
 -- Mapping
--]]
local attack_styles = {
	melee = { image = 'Attack-icon', link = 'Melee' },
	ranged = { image = 'Ranged-icon', link = 'Ranged' },
	magic = { image = 'Magic-icon', link = 'Magic' },
	dragonfire = { image = 'Dragonfire icon', link = 'Dragonfire' },
	na = { image = 'Zero weakness icon', link = '' },
	typeless = { image = 'Zero weakness icon', link = '' }
}

local styles_map = {
	melee = 'melee',
	magic = 'magic',
	mage = 'magic',
	range = 'ranged',
	ranged = 'ranged',
	ranging = 'ranged',
	dragonfire = 'dragonfire',
	['dragon fire'] = 'dragonfire',
	dragonbreath = 'dragonfire',
	['dragon breath'] = 'dragonfire',
	['n/a'] = 'na',
	na = 'na',
	none = 'na'
}

local slay_masters = {
	turael = { text = '[[Turael]] or [[Spria]]', chathead = 'turael chathead', category = 'Monsters assigned by Turael or Spria' },
	mazchna = { text = '[[Mazchna]] or [[Achtryn]]', chathead = 'mazchna chathead', category = 'Monsters assigned by Mazchna or Achtryn' },
	chaeldar = { text = '[[Chaeldar]]', chathead = 'chaeldar chathead', category = 'Monsters assigned by Chaeldar' },
	sumona = { text = '[[Sumona]]', chathead = 'sumona chathead', category = 'Monsters assigned by Sumona' },
	vannaka = { text = '[[Vannaka]]', chathead = 'vannaka chathead', category = 'Monsters assigned by Vannaka' },
	duradel = { text = '[[Duradel]] or [[Lapalok]]', chathead = 'duradel chathead', category = 'Monsters assigned by Duradel or Lapalok' },
	kuradal = { text = '[[Kuradal]]', chathead = 'kuradal chathead', category = 'Monsters assigned by Kuradal' },
	morvran = { text = '[[Morvran]]', chathead = 'morvran chathead', category = 'Monsters assigned by Morvran' },
}

local slay_masters_map = {
	turael = 'turael',
	spria = 'turael',
	mazchna = 'mazchna',
	achtryn = 'mazchna',
	chaeldar = 'chaeldar',
	sumona = 'sumona',
	vannaka = 'vannaka',
	duradel = 'duradel',
	lapalok = 'duradel',
	kuradal = 'kuradal',
	morvran = 'morvran',
	no = 'none',
	none = 'none',
	['n/a'] = 'none'
}

local speeds = {
	[0] = 'N/A',
	[1] = '1 tick (0.6s)',
	[2] = '2 ticks (1.2s)',
	[3] = '3 ticks (1.8s)',
	[4] = '4 ticks (2.4s)',
	[5] = '5 ticks (3.0s)',
	[6] = '6 ticks (3.6s)',
	[7] = '7 ticks (4.2s)',
	[8] = '8 ticks (4.8s)',
	[9] = '9 ticks (5.4s)',
	[10] = '10 ticks (6s)'
}

local speed_map = {
	['1'] = 1,
	['2'] = 2,
	['3'] = 3,
	['4'] = 4,
	['5'] = 5,
	['6'] = 6,
	['7'] = 7,
	['8'] = 8,
	['9'] = 9,
	['10'] = 10,
	['n/a'] = 0,
	['0'] = 0
}

-- load weaknesses instead of remapping
local weaknesses = mw.loadData('Module:Weakness clickpic/data')

-- Main function called with invokes
function p.main(frame)
	local args = frame:getParent().args
	local ret = infobox.new(args)
	local yn_args = {
		'aggressive', 'immune_to_poison', 'immune_to_deflect',
		'immune_to_stun', 'immune_to_drain'
	}
	local num_args = {
		'slaylvl', 'slayxp',
		'max_melee', 'max_ranged', 'max_magic', 'max_spec',
		'acc_melee', 'acc_ranged', 'acc_magic', 'armour',
		'aff_weakness', 'aff_melee', 'aff_ranged', 'aff_magic' }
	for _, v in ipairs(yn_args) do
		ret:defineParams{ { name = v, func = boolargs } }
	end
	for _, v in ipairs(num_args) do
		ret:defineParams{ { name = v, func = { name = numberargs, params = { v, v }, flag = { 'p', 'r' } } }}
	end
	ret:defineParams{
		{ name = 'poisonous', func = poisonarg },
		{ name = 'level', func = combatarg },
		{ name = 'style', func = stylearg },
		{ name = 'lifepoints', func = numargcommas },
		{ name = 'experience', func = numargcommas },
		{ name = 'hpxp', func = { name = hpxparg, params = { 'experience' } } },
		{ name = 'wepxp', func = { name = wepxparg, params = { 'experience' } } },
		{ name = 'weakness', func = weaknessarg },
		{ name = 'explicit_weakness', func = { name = explicitwkarg, params = { 'weakness' }, flag = 'p' } },
		{ name = 'abilities', func = abilarg },
		{ name = 'assigned_by', func = slayerarg },
		{ name = 'not_assigned', func = { name = notassarg, params = { 'assigned_by' }, flag = 'p' } },
		{ name = 'release', func = 'release' },
		{ name = 'removal', func = 'removal' },
		{ name = 'members', func = 'has_content' },
		{ name = 'examine', func = 'has_content' },
		{ name = 'speed', func = speedarg }
	}
	
	ret:defineParams {
		{ name = 'name', func = 'name' },
		{ name = 'aka',
			func = function(arg) return paramtest.has_content(arg) and arg or false end },
		{ name = 'image', func = 'image' },
		{ name = 'slayercat',
			func = function(arg) return paramtest.has_content(arg) and arg or 'N/A' end }
	}
	ret:create()
	ret:cleanParams()
	ret:caption()
	ret:addClass('infobox-monster')
	ret:defineName('Infobox Monster')
	-- PARAMETER: image
	ret:addRow{
		{ tag = 'argd', content = 'image', class='infobox-image', colspan = '4' } }

	-- PARAMETER: release
	-- (update included automatically by infobox)
		:addRow{ { tag = 'th', content = 'Release' },
				{ tag = 'argd', content = 'release', css = { ['text-align'] = 'left' }, colspan = '3' } }

	-- PARAMETER: removal
	if ret:paramDefined('removal') then
		ret:addRow{ { tag = 'th', content = 'Removal' },
				{ tag = 'argd', content = 'removal', css = { ['text-align'] = 'left' }, colspan = '3' } }
	end

	-- PARAMETER: aka
	-- add only if it exists
	if ret:paramDefined('aka') then
		   ret:addRow{ { tag = 'th', content = 'AKA' },
					{ tag = 'argd', content = 'aka', css = { ['text-align'] = 'left' }, colspan = '3' } }
	end
	-- PARAMETER: members
	ret:addRow{ { tag = 'th', content = 'Members' },
				{ tag = 'argd', content = 'members', css = { ['text-align'] = 'left' }, colspan = '3' } }
		:addRow{ { tag = 'th', content = 'Examine', colspan = '4' } }
	-- PARAMETER: examine
		:addRow{ { tag = 'argd', content = 'examine', css = { ['text-align'] = 'left', ['max-width'] = '250px' }, colspan = '4' } }

	-- COMBAT INFO
		:addRow{ { tag = 'th', content = 'Combat info', css = { ['font-variant'] = 'small-caps' }, colspan = '4' } }
		-- PARAMETER: level | lifepoints | experience | hpxp
		:addRow{ { tag = 'th', content = '[[Combat level|Level]]', css = { width = '25%' } },
				{ tag = 'th', content = '[[Life points|LP]]', css = { width = '25%' } },
				{ tag = 'th', content = '[[File:Multicombat.png|link=|20px]] XP', css = { width = '25%' }, title = 'Combat style experience' },
				{ tag = 'th', content = '[[File:Constitution-icon.png|link=|20px]] XP', css = { width = '25%' }, title = 'Constitution experience' } }

		:addRow{ { tag = 'argd', content = 'level' },
				{ tag = 'argd', content = 'lifepoints' },
				{ tag = 'argd', content = 'experience' },
				{ tag = 'argd', content = 'hpxp' } }
		-- PARAMETER: wepxp
		:addRow{ { tag = 'th', content = '[[Equipment level|Equipment XP]] (2H/MH & Armour/OH)', colspan = '4' } }
		:addRow{ { tag = 'argd', content = 'wepxp', colspan = '4' } }
		-- PARAMETER: aggressive | poisonous
		:addRow{ { tag = 'th', content = '[[Aggressiveness|Aggressive]]', colspan = '2' },
				{ tag = 'th', content = 'Poisonous', colspan = '2' } }
		:addRow{ { tag = 'argd', content = 'aggressive', colspan = '2' },
				{ tag = 'argd', content = 'poisonous', colspan = '2' } }
	-- Slayer information
	-- dynamic size, dependent on whether or not the monster is assigned
	local slayer_level = ret:paramDefined('slaylvl','all')
	local not_assigned = ret:param('not_assigned','f')
	local _not_assigned = true
	if not_assigned.d == false then
		_not_assigned = false
	elseif not_assigned.switches then
		for _, v in ipairs(not_assigned.switches) do
			if v == false then
				_not_assigned = false
				break
			end
		end
	end
	-- do slayer row if a monster is assigned, or has a required level
	if _not_assigned == false or slayer_level == true then
		-- PARAMETER: slaylvl | slayxp | slayercat
		ret:addRow{ { tag = 'th', content = 'Slayer', class = 'slayer-header', colspan = '4' } }
			:addRow{ { tag = 'th', content = 'Level' },
					{ tag = 'th', content = 'XP' },
					{ tag = 'th', content = 'Category', colspan = '2' } }
			:addRow{ { tag = 'argd', content = 'slaylvl' },
					{ tag = 'argd', content = 'slayxp' },
					{ tag = 'argd', content = 'slayercat', colspan = '2' } }
			:addRow{ { tag = 'th', content = 'Assigned by', colspan = '4' } }
			:addRow{ { tag = 'argd', content = 'assigned_by',colspan = '4' } }
	end
	
	----------------
	-- OFFENSIVE STATS
	local max_disc = 'This value is the standard, non-legacy value. The number reflects the BASE maximum hit used by the monster in damage calculations. Damage-boosting mechanics such as enrage are not taken into consideration.' 
	ret:addRow{ { tag = 'th', content = 'Offensive', class = 'offensive-header', colspan = '4' } }
		:addRow{ { tag = 'th', content = '[[Maximum hit|Max hit]]', class = 'offensive-subheader', colspan = '4' } }
	-- PARAMETER: max_melee | max_ranged | max_magic | max_spec
		:addRow{ { tag = 'th', content = '[[File:Attack-icon.png|20px|link=]]', title = 'Maximum melee hit' },
				{ tag = 'th', content = '[[File:Ranged-icon.png|20px|link=]]', title = 'Maximum ranged hit' },
				{ tag = 'th', content = '[[File:Magic-icon.png|20px|link=]]', title = 'Maximum magic hit' },
				{ tag = 'th', content = '[[File:Special attack icon.png|20px|link=]]', title = 'Maximum typeless/special hit' } }

		:addRow{ { tag = 'argd', content = 'max_melee', title = max_disc },
				{ tag = 'argd', content = 'max_ranged', title = max_disc },
				{ tag = 'argd', content = 'max_magic', title = max_disc },
				{ tag = 'argd', content = 'max_spec', title = max_disc } }
	-- PARAMETER: style | speed
		:addRow{ { tag = 'th', content = 'Style', class = 'offensive-subheader', colspan = '2' },
				{ tag = 'th', content = '[[Attack speed|Speed]]', class = 'offensive-subheader', colspan = '2' } }

		:addRow{ { tag = 'argd', content = 'style', colspan = '2' },
				{ tag = 'argd', content = 'speed', colspan = '2' } }

	-- PARAMETER: acc_melee | acc_ranged | acc_magic
		:addRow{ { tag = 'th', content = '[[Hit chance|Accuracy]]', class = 'offensive-subheader', colspan = '4' } }

	--[[
		Accuracy parameters
		It's a big job because we need to have a nested table, so it's very hacky
	--]]
	local acc_melee,
		acc_ranged,
		acc_magic = ret:param('acc_melee','r') ,
					ret:param('acc_ranged','r'),
					ret:param('acc_magic','r')
		ret:tag('tr')
			:addClass('nestedinfo')
			:tag('td')
				:attr('colspan','4')
				:tag('table')
					:addClass('wikitable')
					:tag('tr')
						:tag('th')
							:attr('title','Melee accuracy as a function of 2.5 * f(x)')
							:css('width','33%')
							:wikitext('[[File:Attack-icon.png|20px|link=]]')
						:done()
						:tag('th')
							:attr('title','Ranged accuracy as a function of 2.5 * f(x)')
							:css('width','33%')
							:wikitext('[[File:Ranged-icon.png|20px|link=]]')
						:done()
						:tag('th')
							:attr('title','Magic accuracy as a function of 2.5 * f(x)')
							:css('width','33%')
							:wikitext('[[File:Magic-icon.png|20px|link=]]')
						:done()
					:done()
					:tag('tr')
						-- PARAMETER: melee accuracy
						:tag('td')
							:css('width','33%')
							:wikitext(acc_melee)
							:attr('data-attr-param','acc_melee')
						:done()
						-- PARAMETER: ranged accuracy
						:tag('td')
							:css('width','33%')
							:wikitext(acc_ranged)
							:attr('data-attr-param','acc_ranged')
						:done()
						-- PARAMETER: magic accuracy
						:tag('td')
							:css('width','33%')
							:wikitext(acc_magic)
							:attr('data-attr-param','acc_magic')
						:done()
					:done()
				:done()
			:done()
		:done()
	-- ACCURACY DONE (thank god!)
	-- PARAMETER: abilities
	-- only add if they exist
	local abilities = ret:param('abilities','f')
	local _abilities = false
	if abilities.d and abilities.d ~= 'None' then
		_abilities = true
	end
	if abilities.switches then
		for _, v in ipairs(abilities.switches) do
			if v ~= _nil then
				_abilities = true
				break
		   end
		end
	end
	if _abilities then
		ret:addRow{ { tag = 'th', content = 'Abilities used', class = 'offensive-subheader', colspan = '4' } }
			:addRow{ { tag = 'argd', content = 'abilities', colspan = '4' } }

	end
	-- DEFENSIVE STATS
	ret:addRow{ { tag = 'th', content = 'Defensive', class = 'defensive-header', colspan = '4' } }
	-- PARAMETER: armour | weakness
		:addRow{ { tag = 'th', content = 'Armour', class = 'defensive-subheader', colspan = '2' },
				{ tag = 'th', content = '[[Weakness]]', class = 'defensive-subheader', colspan = '2' } }
		:addRow{ { tag = 'argd', content = 'armour', colspan = '2' },
				{ tag = 'argd', content = 'weakness', colspan = '2' } }

	-- PARAMETER: explicit_weakness
	-- PARAMETER: aff_weakness | aff_melee | aff_ranged | aff_magic
		:addRow{ { tag = 'th', content = '[[Affinity|Affinities]]', class = 'defensive-subheader', colspan = '4' } }
		:addRow{ { tag = 'argh', content = 'explicit_weakness', title = 'Affinity value of the monster\'s explicit weakness' },
				{ tag = 'th', content = '[[File:Attack-icon.png|20px|link=]]', title = 'Affinity value of the monster against melee attacks' },
				{ tag = 'th', content = '[[File:Ranged-icon.png|20px|link=]]', title = 'Affinity value of the monster against ranged attacks' },
				{ tag = 'th', content = '[[File:Magic-icon.png|20px|link=]]', title = 'Affinity value of the monster against magic attacks' } }
		:addRow{ { tag = 'argd', content = 'aff_weakness' },
				{ tag = 'argd', content = 'aff_melee' },
				{ tag = 'argd', content = 'aff_ranged' },
				{ tag = 'argd', content = 'aff_magic' } }

	-- PARAMETER: immune_to_poison | immune_to_deflect | immune_to_stun | immune_to_drain
		:addRow{ { tag = 'th', content = 'Immunities', class = 'defensive-subheader', colspan = '4' } }
		:addRow{ { tag = 'th', content = '[[File:Immune to poison.png|link=]]', title = 'Immune to poison' },
				{ tag = 'th', content = '[[File:Immune to deflect.png|link=]]', title = 'Immune to deflect' },
				{ tag = 'th', content = '[[File:Immune to stun.png|link=]]', title = 'Immune to stun' },
				{ tag = 'th', content = '[[File:Immune to drain.png|link=]]', title = 'Immune to stat drain' } }
		:addRow{ { tag = 'argd', content = 'immune_to_poison' },
				{ tag = 'argd', content = 'immune_to_deflect' },
				{ tag = 'argd', content = 'immune_to_stun' },
				{ tag = 'argd', content = 'immune_to_drain' } }
	ret:finish()
	if onmain() then
		local a1 = ret:param('all')
		local a2 = ret:categoryData()
		ret:wikitext(addcategories(a1,a2))
	end
	return ret:tostring()
end

-- For numerical args
function numberargs(arg,v)
	local arg_v = (arg or ''):find('%S') and string.gsub(arg,',','') or -1
	local arg_i
	if arg_v == -1 then
		arg_i = nil
	elseif string.lower(arg_v) == 'n/a' then
		arg_i = 'N/A'
	elseif string.lower(arg_v) == 'varies' then
		arg_i = 'Varies'
	else
		arg_i = tonumber(arg_v:gsub(',',''),10)
		if not arg_i then
			arg_i = badarg(v,'should be a single numerical value.')
		end
	end
	return arg_i
end

-- For combat level
function combatarg(arg)
	local arg_v = (arg or ''):find('%S') and string.gsub(arg,',','') or -1
	local arg_i
	if arg_v == -1 then
		arg_i = nil
	elseif string.lower(arg_v) == 'n/a' or tonumber(arg_v) == 0 then
		arg_i = 'N/A'
	else
		arg_i = tonumber(arg_v:gsub(',',''),10)
		if not arg_i then
			arg_i = badarg('level','should be a single numerical value.')
		end
	end
	return arg_i
end

-- For numbers (adds commas)
function numargcommas(arg)
	local ret = numberargs(arg)
	if type(ret) == 'number' then
		return commas(ret)
	else
		return ret
	end
end

-- For hp xp
function hpxparg(arg)
	local xp = string.gsub(arg or '',',','')
	if string.lower(xp) == 'n/a' then
		return 'N/A'
	else
		xp = tonumber(xp)
	end
	if type(xp) == 'number' then
		return commas(math.floor(xp*3.3)/10)
	else
		return nil
	end
end

-- weapon xp
function wepxparg(arg)
	local xp = string.gsub(arg or '',',','')
	if string.lower(xp) == 'n/a' then
		return 'N/A'
	else
		xp = tonumber(xp)
	end
	if type(xp) == 'number' then
		local th,mh,oh
		th = math.floor(xp * .06)
		mh = math.floor(xp * .04)
		oh = math.floor(xp * .02)
		return string.format('%s / %s / %s',th,mh,oh)
	else
		return nil
	end
end
-- For true/false
function boolargs(arg)
	local arg_v = (arg or ''):find('%S') and arg or -1
	local arg_i
	if arg_v == -1 then
		arg_i = nil
	else
		arg_v = yesno(arg_v)
		if arg_v then
			arg_i = '[[File:Yes check.svg|20px|alt=Yes|link=]]'
		else
			arg_i = '[[File:X mark.svg|20px|alt=No|link=]]'
		end
	end
	return arg_i
end

-- Poison
function poisonarg(arg)
	arg = string.gsub(arg or '',',','')
	arg = string.lower(arg)

	if arg == 'yes' then
		arg = '<span title="This monster is poisonous but has no base value defined. Change the |poisonous parameter to a number">[[File:Yes check.svg|20px|alt=Yes|link=]] ???</span>'
	elseif tonumber(arg) then
		arg = '<span title="Base value at which poison damage starts">[[File:Yes check.svg|20px|alt=Yes|link=]] '..tonumber(arg)..'</span>'
	else
		arg = '[[File:X mark.svg|20px|alt=No|link=]]'
	end

	return arg
end

-- style
function stylearg(arg)
	-- split by commas
	local atts = mw.text.split(string.lower(arg or ''),'%s*,%s*')
	local _atts = {}
	-- remake the list as a table, remove anything that's blank/doesn't exist
	for _, v in ipairs(atts) do
		local att_x = styles_map[v]
		if att_x then
			table.insert(_atts,attack_styles[att_x])
		end
	end
	local p_att
	if #_atts == 0 then
		p_att = nil
	else
		p_att = {}
		for _, v in ipairs(_atts) do
			table.insert(p_att,string.format('[[File:%s.png|25px|link=%s]]',v.image,v.link))
		end
		p_att = table.concat(p_att,' ')
	end
	return p_att
end

-- weakness
function weaknessarg(arg)
	-- split by commas
	local wk = mw.text.split(string.lower(arg or ''),'%s*,%s*')
	local _wk = {}

	-- remake the list as a table, remove anything that's blank/doesn't exist
	for _, v in ipairs(wk) do
		local wk_x = weaknesses[v]
		if wk_x then
			table.insert(_wk,wk_x)
		end
	end
	local p_wk
	if #_wk == 0 then
		p_wk = nil
	else
		p_wk = {}
		for _, v in ipairs(_wk) do
			table.insert(p_wk,string.format('[[File:%s|25px|link=%s]]',v.image,v.link))
		end
		p_wk = table.concat(p_wk,' ')
	end
	return p_wk
end

-- Explicit weakness
-- weakness / explicit weakness
function explicitwkarg(arg)
	-- split by commas
	local wk = mw.text.split(string.lower(arg or ''),'%s*,%s*')
	local _wk = weaknesses[wk[1]]
	if _wk then
		return string.format('[[File:%s|20px|link=]]',_wk.image)
	else
		return nil
	end
end

function wkcats(arg,tbl)
	for _, v in pairs(weaknesses) do
		if arg:find(v.image) then
			if v.category then
				table.insert(tbl,v.category)
			end
		end
	end
end

function slaycats(arg,tbl)
	for _, v in pairs(slay_masters) do
		if arg:find(v.chathead) then
			if v.category then
				table.insert(tbl,v.category)
			end
		end
	end
end

-- Slayer assigners
function slayerarg(_arg)
	local arg = _arg
	if arg then
		arg = mw.text.split(string.lower(arg),'%s*,%s*')
		local slayer_master_list = {
			turael = false,
			spria = false,
			mazchna = false,
			achtryn = false,
			vannaka = false,
			chaeldar = false,
			sumona = false,
			duradel = false,
			lapalok = false,
			kuradal = false,
			morvran = false,
			none = false
		}
		for _, v in ipairs(arg) do
			if slay_masters_map[v] then
				slayer_master_list[slay_masters_map[v]] = true
			end
		end
		if slayer_master_list.none then
			arg = 'Not assigned'
		else
			arg = {}
			for n, v in pairs(slayer_master_list) do
				if v then
					--table.insert(params.assigned_by, slay_masters[n].text)
					table.insert(arg, 1, string.format('[[File:%s.png|30px|link=%s]]',slay_masters[n].chathead,n))
				end
			end
			arg = table.concat(arg,' ')
		end			
	else
		arg = nil
	end
	return arg
end

-- Not assigned
function notassarg(arg)
	if arg then
		if arg:find('%?action=edit') or not arg:find('%S') then
			return true
		end
		arg = mw.text.split(string.lower(arg),'%s*,%s*')
		for _, v in ipairs(arg) do
			if slay_masters_map[v] == 'none' then
				return true
			end
		end
	end
	return false
end

-- Abilities used
function abilarg(arg)
	arg = paramtest.default_to(arg,false)
	if not arg then
		arg = 'None'
	elseif arg:find('clickpic') then
		arg = mw.getCurrentFrame():preprocess(arg)
	else
		arg = 'None'
	end
	return arg
end

-- Attacks speed
function speedarg(arg)
	if paramtest.is_empty(arg) then
		return nil
	end
	_,_,arg = string.find(arg:lower()..' ','^(.-)%s')
	arg = speeds[speed_map[arg]]
					or badarg('speed',' is not a valid attack speed')
	return arg
end

-- red ERR span with title hover for explanation
function badarg(argname, argmessage)
	return '<span '..
			'title="The parameter «'..argname..'» '..argmessage..'" '..
			'style="color:red; font-weight:bold; cursor:help; border-bottom:1px dotted red;">'..
			'ERR</span>'
end

function addcategories(args,catargs)
	local ret = { 'Bestiary' }
	local cat_map = {
		-- Added if the parameter has content
		defined = {
			aka = 'Pages with AKA'
			},
		-- Added if the parameter has no content
		notdefined = {
			image = 'Needs image',
			members = 'Needs members status',
			release = 'Needs release date',
			examine = 'Needs examine added',
			level = 'Needs combat level',
			immune_to_stun = 'Missing immunity information',
			immune_to_poison = 'Missing immunity information',
			immune_to_deflect = 'Missing immunity information',
			immune_to_drain = 'Missing immunity information'
			},
		-- Parameters that are either true or false
		-- map a category to a boolean value
		yesno = {
			members = { [true] = '', [false] = 'F2P bestiary' },
			abilities = { [true] = 'Monsters that use abilities', [false] = '' },
			immune_to_stun = { [true] = 'Stun-immune', [false] = '' },
		}
	}
 
	-- Run and add mapped categories
	for n, v in pairs(cat_map.defined) do
		if catargs[n] and catargs[n].one_defined then
			table.insert(ret,v)
		end
	end
 
	for n, v in pairs(cat_map.notdefined) do
		if catargs[n] and catargs[n].all_defined == false then
			table.insert(ret,v)
		end
	end
 
	local levels = { args.level.d }
	if args.level.switches then
		for _, v in ipairs(args.level.switches) do
			if v ~= infobox.nil_param() then
				table.insert(levels,v)
			end
		end
	end

	for _, v in ipairs(levels) do
		if v == 'N/A' then
			table.insert(ret,'Monsters with no combat level')
		elseif tonumber(v) then
			table.insert(ret,string.format('Combat level %s monsters',v))
		end
	end
		
	if args.members.d:find('[Nn]o') then
		table.insert(ret,'F2P bestiary')
	elseif args.members.switches then
		for _, v in ipairs(args.members.switches) do
			if v:find('[Nn]o') then
				table.insert(ret,'F2P bestiary')
				break
			end
		end
	end
	if args.abilities.d:find('clickpic') then
		table.insert(ret,'Monsters that use abilities')
	elseif args.abilities.switches then
		for _, v in ipairs(args.abilities.switches) do
			if v:find('clickpic') then
				table.insert(ret,'Monsters that use abilities')
				break
			end
		end
	end
	if args.weakness.d then
		wkcats(args.weakness.d,ret)
	end
	if args.weakness.switches then
		for _, v in ipairs(args.weakness.switches) do
			if v ~= _nil then
				wkcats(v,ret)
			end
		end
	end

	if args.assigned_by.d then
		slaycats(args.assigned_by.d,ret)
	end
	if args.assigned_by.switches then
		for _, v in ipairs(args.assigned_by.switches) do
			if v ~= _nil then
				slaycats(v,ret)
			end
		end
	end
	-- combine table and format category wikicode
	for i, v in ipairs(ret) do
		ret[i] = string.format('[[Category:%s]]',v)
	end

	return table.concat(ret,'')
end

return p