Module:Infobox Monster

From WIDEVERSE Wiki
Revision as of 15:47, 26 October 2021 by en>Gaz Lloyd (no longer necessary)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Documentation for this module may be created at Module:Infobox Monster/doc

-- <nowiki>
--------------------------
-- Module for [[Template:Infobox Monster new]]
-- Please test changes to this module at [[Module:Infobox Monster/sandbox]] first
------------------------
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')
local attack_speed_bar = require('Module:Attack speed bar').monster
local slayer_info = require('Module:Slayer assignments')
local slayer_race = mw.loadData('Module:NPC races/Slayer')
local race_info = mw.loadData('Module:NPC races')

--[[
 -- 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 primary_styles = {
	melee = { 'inquisitor', '[[File:Inquisitor staff.png|25px|link=Inquisitor staff]]'},
	ranged = { 'terrasaur', '[[File:Terrasaur maul.png|25px|link=Terrasaur maul]]'},
	magic = { 'hexhunter', '[[File:Hexhunter bow.png|25px|link=Hexhunter bow]]'},
}

local slay_masters = {
	turael = { text = '[[Turael]] or [[Spria]]', chathead = 'turael chathead', category = 'Monsters assigned by Turael or Spria' },
	jacquelyn = { text = '[[Jacquelyn]]', chathead = 'jacquelyn chathead', category = 'Monsters assigned by Jacquelyn' },
	mazchna = { text = '[[Mazchna]] or [[Achtryn]]', chathead = 'mazchna chathead', category = 'Monsters assigned by Mazchna or Achtryn' },
	vannaka = { text = '[[Vannaka]]', chathead = 'vannaka chathead', category = 'Monsters assigned by Vannaka' },
	chaeldar = { text = '[[Chaeldar]]', chathead = 'chaeldar chathead', category = 'Monsters assigned by Chaeldar' },
	sumona = { text = '[[Sumona]]', chathead = 'sumona chathead', category = 'Monsters assigned by Sumona' },
	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' },
	mandrith = { text = '[[Mandrith]]', chathead = 'Mandrith chathead', category = 'Monsters assigned by Mandrith' },
	laniakea = { text = '[[Laniakea]]', chathead = 'Laniakea chathead', category = 'Monsters assigned by Laniakea' },
}

local slay_masters_map = {
	turael = 'Turael',
	spria = 'Turael',
	jacquelyn = 'Jacquelyn',
	mazchna = 'Mazchna',
	achtryn = 'Mazchna',
	vannaka = 'Vannaka',
	chaeldar = 'Chaeldar',
	sumona = 'Sumona',
	duradel = 'Duradel',
	lapalok = 'Duradel',
	kuradal = 'Kuradal',
	morvran = 'Morvran',
	mandrith = 'Mandrith',
	laniakea = 'Laniakea',
	no = 'none',
	none = 'none',
	['n/a'] = 'none'
}

local slay_masters_order = { 'Turael', 'Jacquelyn', 'Mazchna', 'Vannaka', 'Chaeldar', 'Sumona', 'Duradel', 'Kuradal', 'Morvran', 'Mandrith', 'Laniakea' }

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,
	['random'] = 'random',
	['0'] = 0
}

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

-- location restriction
local restriction_map = {
	surface = 'surface',
	dungeoneering = 'dungeoneering',
	dg = 'dungeoneering',
	daemonheim = 'dungeoneering',
	quest = 'quest',
	minigame = 'minigame',
	activity = 'minigame',
	gone = 'removed',
	removed = 'removed',
	limited = 'limited'
}

-- Main function called with invokes
function p.main(frame)
	local args = frame:getParent().args
	local ret = infobox.new(args)
	local yn_args = {
		'aggressive'
	}
	local immune_args = {
		'immune_to_poison', 'immune_to_deflect',
		'immune_to_stun', 'immune_to_drain'
	}
	local num_args = {
		'slaylvl', 'slayxp',
		'attack', 'strength', 'defence', 'ranged', 'magic',
		'max_melee', 'max_ranged', 'max_magic', 'max_spec',
		'acc_melee', 'acc_ranged', 'acc_magic', 'armour',
		'aff_melee', 'aff_ranged', 'aff_magic' } --aff_weakness has a special func

	for _, v in ipairs(yn_args) do
		ret:defineParams{ { name = v, func = { name = boolargs, params = { v, v }, flag = { 'p', 'r' } } }}
		ret:defineParams{ { name = v..'_smw', func = { name = boolsmwargs, params = { v }, flag = { 'd' } } }}
	end
	for _, v in ipairs(immune_args) do
		ret:defineParams{ { name = v, func = { name = immuneargs, params = { v, v }, flag = { 'p', 'r' } } }}
		ret:defineParams{ { name = v..'_smw', func = { name = immunesmwargs, params = { v }, flag = { 'd' } } }}
	end

	for _, v in ipairs(num_args) do
		ret:defineParams{
			{ name = v, func = { name = numargcommas, params = { v }, flag = { 'p' } } },
			{ name = v..'_smw', func = { name = numargraw, params = { v }, flag = 'd' } }
		}
	end
	ret:defineParams{
		{ name = 'vanchor', func = { name = 'has_content', params = { 'version' }, flag = 'p' } },
		{ name = 'poisonous', func = { name = poisonarg, params = { 'poisonous' }, flag = 'p' } },
		{ name = 'poisonous_smw', func = { name = boolsmwargs, params = { 'poisonous' }, flag = 'd' } },
		{ name = 'level', func = combatarg },
		{ name = 'level_smw', func =  { name = numargraw2, params = { 'level' }, flag = 'p' } },
		{ name = 'style', func = stylearg },
		{ name = 'styleimg', func = { name = styleimgarg, params = { 'style' }, flag = 'p', dupes = true } },
		{ name = 'styletxt', func = { name = styletxtarg, params = { 'style' }, flag = 'p', dupes = true } },
		{ name = 'primarystyle_smw', func = { name = primarystylesmwarg, params = { 'primarystyle' }, flag = 'p', dupes = true } },
		{ name = 'lifepoints', func = numargcommas },
		{ name = 'lpraw', func = { name = numargraw, params = { 'lifepoints' }, flag = 'p' } },
		{ name = 'experience', func = numargcommas },
		{ name = 'xpraw', func = { name = numargraw, params = { 'experience' },  flag = 'p' } },
		{ name = 'hpxp', func = { name = hpxparg, params = { 'experience' } } },
		{ name = 'wepxp2h', func = numargcommas },
		{ name = 'wepxpmhandarmour', func = numargcommas },
		{ name = 'wepxpoh', func = numargcommas },
		{ name = 'wepxp', func = { name = wepxparg, params = { 'experience', 'wepxp2h', 'wepxpmhandarmour', 'wepxpoh' } } },
		{ name = 'weakness', func = weaknessarg },
		{ name = 'generalweakness', func = { name = generalweaknessarg, params = { 'aff_melee', 'aff_ranged', 'aff_magic' } } },
		{ name = 'weaknessimg', func = { name = weaknessimgarg, params = { 'weakness' }, flag = 'p', dupes = true } },
		{ name = 'weaknesstxt', func = { name = weaknesstxtarg, params = { 'weakness' }, flag = 'p', dupes = true } },
		{ name = 'explicit_weakness', func = { name = explicitwkarg, params = { 'weakness' }, flag = 'p' } },
		{ name = 'aff_weakness', func = { name = aff_weakness_arg , params = { 'aff_weakness', 'aff_weakness', 'weakness' }, flag = { 'p', 'r', 'p' } } },
		{ name = 'aff_weakness_smw', func = { name = aff_weakness_smw_arg , params = { 'aff_weakness', }, flag = { 'd' } } },
		{ name = 'susceptibility_smw', func = { name = susceptarg_smw, params = { 'susceptibility', 'weakness', 'primarystyle' }, flag = 'p' } },
		{ name = 'susceptibility', func = { name = susceptarg, params = { 'susceptibility_smw' }, flag = 'd' } },
		{ name = 'abilities', func = abilarg },
		{ name = 'abilities_sep', func = abilseparg },
		{ name = 'slayercat', func = slayercatarg },
		{ name = 'slayercat_disp', func = { name = slayercatdisparg, params = { 'slayercat' }, flag = 'd' } },
		{ name = 'slayercat_smw', func = { name = slayercatsmwarg, params = { 'slayercat' }, flag = 'd' } },
		{ name = 'race', func = { name = racearg, params = { 'race', 'slayercat' }, flag = {'p', 'd'} } },
		{ name = 'assigned_by_raw', func = { name = slayerarg, params={ 'slayercat', 'assigned_by_override', 'assigned_by' }, flag = 'd' }  },
		{ name = 'assigned_by_img', func = { name = slayerimgarg, params = { 'assigned_by_raw' }, flag = 'd', dupes = true } },
		{ name = 'not_assigned', func = { name = notassarg, params = { 'assigned_by_raw' }, flag = 'd' } },
		{ name = 'release', func = 'release' },
		{ name = 'removal', func = 'removal' },
		{ name = 'members', func = 'has_content' },
		{ name = 'actions', func = { name = actionarg, params = { 'actions' }, flag = { 'd' } } },
		{ name = 'actions_smw', func = { name = actionsmw, params = { 'actions' }, flag = { 'p' } }, dupes = true },
		{ name = 'examine', func = 'has_content' },
		{ name = 'voice', func = 'has_content' },
		{ name = 'speed', func = speedarg },
		{ name = 'name', func = 'name' },
		{ name = 'aka', func = 'has_content' },
		{ name = 'image', func = 'image' },
		{ name = 'icon', func = 'image' },
		{ name = 'icon_cell', func = { name = icon, params = { 'name', 'icon' }, flag='d' } },
		{ name = 'chathead', func = 'image' },
		{ name = 'restriction', func = restrictionarg },
		{ name = 'restrictionsurface', func = { name = restrsurfarg , params = { 'restriction', 'restriction' }, flag = { 'd', 'p' } } },
		{ name = 'thievelvl', func = { name = numberargs, params = { 'thievelvl', 'thievelvl' }, flag = { 'p', 'r' } } },
		{ name = 'thievelvl_smw', func =  { name = numargraw2, params = { 'thievelvl' }, flag = 'p' } },
 
		-- not used; only for categories
		{ name = 'id', func = iddisp },
		{ name = 'id_smw', func = { name = idsmw, params = { 'id' }, flag = 'p' } },
		{ name = 'chisel_links', func = { name = make_chisel_links, params = { 'id_smw', 'name' }, flag = 'd' } },
		{ name = 'rscid', func = 'numbers' },
		{ name = 'dropversion', func = 'has_content' },
		{ name = 'SMWarg', func = { name = SMWarg, params = { 'name', 'version', 'id', 'members', 'release', 'removal', 'examine', 'level', 'style', 'lpraw', 'weakness', 'xpraw', 'slaylvl', 'slayxp', 'assigned_by_raw', 'slayercat',
			'poisonous', 'abilities_sep', 'restriction', 'aggressive', 'immune_to_stun', 'immune_to_drain', 'immune_to_poison', 'immune_to_deflect',
			'attack', 'magic', 'ranged', 'defence', 'max_melee', 'max_magic', 'max_ranged', 'max_spec', 'acc_melee', 'acc_magic', 'acc_ranged', 'armour', 'aff_melee', 'aff_magic', 'aff_ranged', 'aff_weakness' }, flag = 'd', dupes = true } }
	}

	ret:useSMWOne({
		id_smw = 'All NPC ID'
	})
	
	ret:useSMWSubobject({
		id_smw = 'NPC ID',
		name = 'Monster name',
		vanchor = 'Version anchor',
		level_smw = 'Combat level',
		xpraw = 'Combat experience',
		thievelvl_smw = 'Thieving level',
		members = 'Is members only',
		actions_smw = 'Actions',
		release = 'Release date',
		update = 'Release update',
		removal = 'Removal date',
		removalupdate = 'Removal update',
		styleimg = 'NPC attack style',
		styletxt = 'NPC attack style text',
		primarystyle_smw = 'NPC primary attack style',
		lpraw = 'NPC life points',
		weaknessimg = 'Weakness',
		weaknesstxt = 'Weakness text',
		generalweakness = 'Weakness by class',
		susceptibility_smw = 'Susceptible to',
		slayercat_smw = 'Slayer category',
		slaylvl_smw = 'Slayer level',
		slayxp_smw = 'Slayer experience',
		assigned_by_img = 'Assigned by',
		restriction = 'Location restriction',
		attack_smw = 'NPC Attack level',
		magic_smw = 'NPC Magic level',
		ranged_smw = 'NPC Ranged level',
		defence_smw = 'NPC Defence level',
		max_melee_smw = 'NPC melee max hit',
		max_magic_smw = 'NPC magic max hit',
		max_ranged_smw = 'NPC ranged max hit',
		max_spec_smw = 'NPC special max hit',
		acc_melee_smw = 'NPC melee accuracy',
		acc_magic_smw = 'NPC magic accuracy',
		acc_ranged_smw = 'NPC ranged accuracy',
		armour_smw = 'NPC armour',
		aff_melee_smw = 'NPC melee affinity',
		aff_magic_smw = 'NPC magic affinity',
		aff_ranged_smw = 'NPC ranged affinity',
		aff_weakness_smw = 'NPC weakness affinity',
		aggressive_smw = 'Is NPC aggressive',
		poisonous_smw = 'Is NPC poisonous',
		immune_to_poison_smw = 'NPC susceptible to poison',
		immune_to_deflect_smw = 'NPC susceptible to deflect',
		immune_to_stun_smw = 'NPC susceptible to stun',
		immune_to_drain_smw = 'NPC susceptible to drain',
		SMWarg = 'Monster JSON',
	})

	ret:defineLinks({
		links = {
			{ 'Template:%s/FAQ', 'FAQ' },
			{ 'Template:Infobox Monster new/doc', 'doc' }
		},
		colspan = 12
	})
	ret:customButtonPlacement(true)
	ret:create()
	ret:cleanParams()

	ret:addClass('infobox-monster')
	ret:defineName('Infobox Monster')
	
	
	-- use catargs
	if onmain() then
		local a2 = ret:categoryData()
		if not a2['restriction'].all_defined then
			ret:useSMWSubobject({
				restrictionsurface = 'Location restriction'
			})
		end
	end

	ret:addButtonsRow{
		colspan = 12
	}
	
	-- PARAMETER: chathead
	if ret:paramDefined('chathead', 'all') then
		ret:addRow{
			{ tag = 'argd', content = 'chathead', class='infobox-image', colspan = '12' }
		}
	end
	
	-- PARAMETER: image
	ret:addRow{
		{ tag = 'argd', content = 'image', class='infobox-image bordered-image', colspan = '12' }
	}
	
	ret:addRow{
		{ tag = 'argh', content = 'name', class='infobox-header', colspan = '12' }
	}
	
	-- PARAMETER: icon
	if ret:paramDefined('icon') then
		ret:addRow{
			{ tag = 'argd', content = 'icon', class='infobox-image', colspan = '12' }
		}
	end
	-- PARAMETER: release
	-- (update included automatically by infobox)
	ret:addRow{ { tag = 'th', content = 'Release', colspan = '3' },
				{ tag = 'argd', content = 'release', css = { ['text-align'] = 'left' }, colspan = '9' } }

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

	-- PARAMETER: aka
	-- add only if it exists
	if ret:paramDefined('aka') then
		   ret:addRow{ { tag = 'th', content = 'AKA', colspan = '3' },
					{ tag = 'argd', content = 'aka', css = { ['text-align'] = 'left', ['max-width'] = '100px' }, colspan = '9' } }
	end
	-- PARAMETER: members
	ret:addRow{ { tag = 'th', content = 'Members', colspan = '3' },
				{ tag = 'argd', content = 'members', css = { ['text-align'] = 'left' }, colspan = '9' } }
			
	-- PARAMETER: actions
	ret:addRow{ { tag = 'th', content = 'Actions', colspan = '3' },
				{ tag = 'argd', content = 'actions', css = { ['text-align'] = 'left' }, colspan = '9' } }
	
	-- PARAMETER: race
	ret:addRow{ { tag = 'th', content = '[[Races|Race]]', colspan = '3' },
				{ tag = 'argd', content = 'race', css = { ['text-align'] = 'left' }, colspan = '9' } }
	
	-- PARAMETER: examine
	:addRow{ { tag = 'th', content = 'Examine', class = 'infobox-subheader', colspan = '12' } }
	:addRow{ { tag = 'argd', content = 'examine', css = { ['text-align'] = 'center', ['max-width'] = '250px' }, colspan = '12' } }

	-- COMBAT INFO
		:addRow{ { tag = 'th', content = 'Combat info', colspan = '12', class = 'combat-info-header infobox-subheader' } }
		-- PARAMETER: level | lifepoints | experience | hpxp
		:addRow{ { tag = 'th', content = '[[Combat level|Level]]', class = 'combat-subheader', colspan = '3' },
				{ tag = 'th', content = '[[Life points|LP]]', class = 'combat-subheader', colspan = '3' },
				{ tag = 'th', content = '[[File:Multicombat.png|link=|20px]] XP', class = 'combat-subheader', colspan = '3', title = 'Combat style experience' },
				{ tag = 'th', content = '[[File:Constitution-icon.png|link=|20px]] XP', class = 'combat-subheader', colspan = '3', title = 'Constitution experience' } }

		:addRow{ { tag = 'argd', content = 'level', colspan = '3' },
				{ tag = 'argd', content = 'lifepoints', colspan = '3' },
				{ tag = 'argd', class='mob-cb-xp', content = 'experience', colspan = '3' },
				{ tag = 'argd', class='mob-hp-xp', content = 'hpxp', colspan = '3' } }
		-- PARAMETER: wepxp
		:addRow{ { tag = 'th', content = '[[Equipment level|Equipment XP]] (2H/MH & Armour/OH)', class = 'combat-subheader', colspan = '12' } }
		:addRow{ { tag = 'argd', content = 'wepxp', class='mob-eq-xp', colspan = '12' } }
		-- PARAMETER: aggressive | poisonous
		:addRow{ { tag = 'th', content = '[[Aggressiveness|Aggressive]]', class = 'combat-subheader', colspan = '6' },
				{ tag = 'th', content = 'Poisonous', class = 'combat-subheader', colspan = '6' } }
		:addRow{ { tag = 'argd', content = 'aggressive', colspan = '6' },
				{ tag = 'argd', content = 'poisonous', colspan = '6' } }
	-- 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 = '12' } }
			:addRow{ { tag = 'th', content = 'Level', class = 'slayer-subheader', colspan = '3' },
					{ tag = 'th', content = 'XP', class = 'slayer-subheader', colspan = '3' },
					{ tag = 'th', content = 'Category', class = 'slayer-subheader', colspan = '6' } }
			:addRow{ { tag = 'argd', content = 'slaylvl', colspan = '3' },
					{ tag = 'argd', class='mob-slay-xp', content = 'slayxp', colspan = '3' },
					{ tag = 'argd', content = 'slayercat_disp', colspan = '6' } }
			:addRow{ { tag = 'th', content = 'Assigned by', class = 'slayer-subheader', colspan = '12' } }
			:addRow{ { tag = 'argd', content = 'assigned_by_raw',colspan = '12' } }
	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 = '12' } }
	
		:addRow{ { tag = 'th', content = '[[Maximum hit|Max hit]]', class = 'offensive-subheader', colspan = '12' } }
	-- PARAMETER: max_melee | max_ranged | max_magic | max_spec
		:addRow{ { tag = 'td', content = '[[File:Attack-icon.png|20px|link=]]', title = 'Maximum melee hit', colspan = '3' },
				{ tag = 'td', content = '[[File:Ranged-icon.png|20px|link=]]', title = 'Maximum ranged hit', colspan = '3' },
				{ tag = 'td', content = '[[File:Magic-icon.png|20px|link=]]', title = 'Maximum magic hit', colspan = '3' },
				{ tag = 'td', content = '[[File:Weapon Special attack.png|20px|link=]]', title = 'Maximum typeless/special hit', colspan = '3' } }
		:addRow{ { tag = 'argd', content = 'max_melee', title = max_disc, colspan = '3' },
				{ tag = 'argd', content = 'max_ranged', title = max_disc, colspan = '3' },
				{ tag = 'argd', content = 'max_magic', title = max_disc, colspan = '3' },
				{ tag = 'argd', content = 'max_spec', title = max_disc, colspan = '3' } }

	-- PARAMETER: style | rate (formely speed)
		:addRow{ { tag = 'th', content = 'Style', class = 'offensive-subheader', colspan = '6' },
				{ tag = 'th', content = '[[Attack rate|Rate]]', class = 'offensive-subheader', colspan = '6' } }

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

	-- PARAMETER: acc_melee | acc_ranged | acc_magic
		:addRow{ { tag = 'th', content = 'Combat levels', class = 'offensive-subheader', colspan = '12' } }


	-- Accuracy parameters
	local levels_disc = "This is the monster's level in this combat skill. It functions in the same way a player's does - it affects the accuracy or armour in the same way (but not damage)."
	
	ret:addRow {
		{ tag = 'td', content = '[[File:Attack-icon.png|20px|link=]]', title = 'Attack level', colspan = '4' },
		{ tag = 'td', content = '[[File:Ranged-icon.png|20px|link=]]', title = 'Ranged level', colspan = '4' },
		{ tag = 'td', content = '[[File:Magic-icon.png|20px|link=]]', title = 'Magic level', colspan = '4' }
	}

	:addRow {
		{ tag = 'argd', content = 'attack', title = levels_disc, colspan = '4' },
		{ tag = 'argd', content = 'ranged', title = levels_disc, colspan = '4' },
		{ tag = 'argd', content = 'magic', title = levels_disc, colspan = '4' }
	}

	:addRow {
		{ tag = 'th', content = '[[Hit chance|Accuracy]]', class = 'offensive-subheader', colspan = '12' }
	}

	:addRow {
		{ tag = 'td', content = '[[File:Attack-icon.png|20px|link=]]', title = 'Melee accuracy as a function of 2.5 * f(x)', colspan = '4' },
		{ tag = 'td', content = '[[File:Ranged-icon.png|20px|link=]]', title = 'Ranged accuracy as a function of 2.5 * f(x)', colspan = '4' },
		{ tag = 'td', content = '[[File:Magic-icon.png|20px|link=]]', title = 'Magic accuracy as a function of 2.5 * f(x)', colspan = '4' }
	}

	:addRow {
		{ tag = 'argd', content = 'acc_melee', colspan = '4' },
		{ tag = 'argd', content = 'acc_ranged', colspan = '4' },
		{ tag = 'argd', content = 'acc_magic', colspan = '4' }
	}

	-- 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 = '12' } }
			:addRow{ { tag = 'argd', content = 'abilities', colspan = '12' } }

	end
	-- DEFENSIVE STATS
	ret:addRow{ { tag = 'th', content = 'Defensive', class = 'defensive-header', colspan = '12' } }
	-- PARAMETER: armour | defence | weakness
		:addRow{ { tag = 'th', content = 'Armour', class = 'defensive-subheader', css = { ['padding-left'] = '0', ['padding-right'] = '0' }, colspan = '2' },
				{ tag = 'th', content = '[[File:Defence-icon.png|link=]]', class = 'defensive-subheader', title = 'Defence level', colspan = '2' },
				{ tag = 'th', content = '[[Weakness]]', class = 'defensive-subheader', colspan = '3' },
				{ tag = 'th', content = '[[Susceptibility|Susceptible]]', class = 'defensive-subheader', colspan = '5' } }
		:addRow{ { tag = 'argd', content = 'armour', colspan = '2' },
				{ tag = 'argd', content = 'defence', title = levels_disc, colspan = '2' },
				{ tag = 'argd', content = 'weakness', colspan = '3' },
				{ tag = 'argd', content = 'susceptibility', colspan = '5', class = "infobox-monster-susceptibility-cell" } }

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

	-- PARAMETER: immune_to_poison | immune_to_deflect | immune_to_stun | immune_to_drain
		:addRow{ { tag = 'th', content = 'Immunities', class = 'defensive-subheader', colspan = '12' } }
		:addRow{ { tag = 'td', content = '[[File:Poison immunity icon.png|27x27px|frameless|link=]]', title = 'Immune to poison?', colspan = '3' },
				{ tag = 'td', content = '[[File:Deflect immunity icon.png|27x27px|frameless|link=]]', title = 'Immune to deflect?', colspan = '3' },
				{ tag = 'td', content = '[[File:Stun immunity icon.png|27x27px|frameless|link=]]', title = 'Immune to stun?', colspan = '3' },
				{ tag = 'td', content = '[[File:Drain immunity icon.png|27x27px|frameless|link=]]', title = "Immune to stat drain?", colspan = '3' } }
		:addRow{ meta = { addClass = 'infobox-monster-immunites-row' }, 
				{ tag = 'argd', content = 'immune_to_poison', colspan = '3' },
				{ tag = 'argd', content = 'immune_to_deflect', colspan = '3' },
				{ tag = 'argd', content = 'immune_to_stun', colspan = '3' },
				{ tag = 'argd', content = 'immune_to_drain', colspan = '3' } }
	
	-- advanced data
	ret	:addRow{
			{ tag = 'th', content = 'Advanced data', class = 'infobox-subheader', colspan = '12' },
			meta = {addClass = 'advanced-data'}
		}
		:addRow{
			{ tag = 'th', content = 'NPC ID', colspan = '4' },
			{ tag = 'argd', content = 'id', colspan = '6' },
			meta = {addClass = 'advanced-data'}
		}
		:addRow{
			{ tag = 'th', content = 'Links', colspan = '4' },
			{ tag = 'argd', content = 'chisel_links', colspan = '6' },
			meta = {addClass = 'advanced-data'}
		}
	
	ret:addDropLevelVars('combat', 'level_smw')
	
	-- For infobox hit calc
	--ret:addRow{ { tag = 'th', content = 'Hit calculator', class = 'rsw-ihc-header infobox-header', colspan = '12' } }
	--	:addRow{ meta = { addClass = 'infobox-hitcalc' },
	--		{ tag = 'td', content = 'JavaScript is required for the hit calculator to function!', colspan = '12' } }
	
	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.')
		else
			arg_i = commas(arg_i)
		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 numbers
function numargraw(arg)
	if not arg then return 0 end
	if type(arg) ~= 'number' then
		arg = tostring(arg)
		arg = arg:gsub(',', '')
	end
	return tonumber(arg) or 0
end
function numargraw2(arg)
	if not arg then return nil end
	if type(arg) ~= 'number' then
		arg = tostring(arg)
		arg = arg:gsub(',', '')
	end
	return tonumber(arg)
end

-- For the icon
function icon(name, icon)
	if not icon then
		return name
	else
		return icon..' '..name
	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(combatxp, wepxp2h, wepxpmhandarmour, wepxpoh)
	local xp = string.gsub(combatxp or '',',','')
	local xp2h = string.gsub(wepxp2h or '',',','')
	local xpmh = string.gsub(wepxpmhandarmour or '',',','')
	local xpoh = string.gsub(wepxpoh or '',',','')
	
	if string.lower(xp) == 'n/a' then
		return 'N/A'
	else
		xp = tonumber(xp)
	end
	
	if string.lower(xp2h) ~= '' then
		xp2h = tonumber(xp2h)
	end
	
	if string.lower(xpmh) ~= '' then
		xpmh = tonumber(xpmh)
	end
	
	if string.lower(xpoh) ~= '' then
		xpoh = tonumber(xpoh)
	end
	
	if type(xp) == 'number' then
		local th,mh,oh
		
		if type(xp2h) == 'number' then
		    th = xp2h
		else
		    th = math.floor(xp * .06)
		end
		
		if type(xpmh) == 'number' then
		    mh = xpmh
		else
		    mh = math.floor(xp * .04)
		end
		
		if type(xpoh) == 'number' then
		    oh = xpoh
		else
		    oh = math.floor(xp * .02)
		end
		
		return string.format('%s / %s / %s', commas(th), commas(mh), commas(oh))
	else
		return nil
	end
end
-- For true/false
function boolargs(arg,argr)
	local arg_v = (arg or ''):find('%S') and arg or -1

	local arg_ret

	if arg_v == -1 then
		arg_ret = nil
	else
		arg_v = yesno(arg_v)
		local argf = '<span title="This monster is%s %s.">%s</span>'
		local f1,f3
		local f2 = string.gsub(argr,'_',' ')

		if arg_v then
			f1 = ''
			f3 = '[[File:Yes check.svg|20px|alt=Yes|link=]]'
		else
			f1 = ' not'
			f3 = '[[File:X mark.svg|20px|alt=No|link=]]'
		end

		arg_ret = string.format(argf,f1,f2,f3)
	end

	return arg_ret
end
function boolsmwargs(arg)
	if infobox.isDefined(arg) then
		arg = string.lower(arg)
		if arg:find('yes check') then
			return 'true'
		elseif arg:find('x mark') then
			return 'false'
		end
	end
	return nil
end

function immuneargs(arg,argr)
	local arg_v = (arg or ''):find('%S') and arg or -1

	local arg_ret

	if arg_v == -1 then
		arg_ret = nil
	else
		arg_v = yesno(arg_v)
		local argf = '<span class="infobox-monster-%s infobox-monster-%s monster-%s-%s" title="This monster is%s %s.">%s</span>'
		local f1,f3,f4
		local f2 = string.gsub(argr,'_',' ')
		local f5 = string.gsub(argr,'immune_to_','')

		if arg_v then
			f1 = ''
			f3 = 'Immune'
			f4 = 'immune'
		else
			f1 = ' not'
			f3 = 'Not immune'
			f4 = 'susceptible'
		end

		arg_ret = string.format(argf, f4, f5, f5, f4, f1, f2, f3)
	end

	return arg_ret
end

function immunesmwargs(arg)
	if infobox.isDefined(arg) then
		arg = string.lower(arg)
		if arg:find('is not immune') then
			return 'true'
		elseif arg:find('is immune') then
			return 'false'
		end
	end
	return nil
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="This monster is poisonous. The number shown is the base value at which poison damage starts.">[[File:Yes check.svg|20px|alt=Yes|link=]] '..tonumber(arg)..'</span>'
	else
		arg = '<span title="This monster is not poisonous.">[[File:X mark.svg|20px|alt=No|link=]]</span>'
	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

function styleimgarg(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
		for _, v in ipairs(_atts) do
			table.insert(p_att,string.format('File:%s.png',v.image))
		end
	end
	return table.concat(p_att, infobox.splitpoint)
end

function styletxtarg(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,att_x)
		end
	end
	return table.concat(_atts, infobox.splitpoint)
end

function primarystylearg(ps)
	if infobox.isDefined(ps) then
		ps = mw.text.trim(ps:lower())
	end
	if primary_styles[ps] then
		return primary_styles[ps]
	end
	return nil
end

function primarystylesmwarg(ps)
	if infobox.isDefined(ps) then
		ps = mw.text.trim(ps:lower())
	end
	if primary_styles[ps] or ps == 'melee' then
		return ps
	end
	return nil
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 and not wk_x._suscept 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

function weaknessimgarg(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 = {}
	for _, v in ipairs(_wk) do
		table.insert(p_wk,string.format('File:%s',v.image))
	end
	return table.concat(p_wk, infobox.splitpoint)
end

function weaknesstxtarg(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,v)
		end
	end
	return table.concat(_wk, infobox.splitpoint)
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 aff_weakness_arg(aff,aff_r,weakness)
	-- split by commas
	local wk = mw.text.split(string.lower(weakness or ''),'%s*,%s*')
	local _wk = weaknesses[wk[1]]
	if _wk and _wk.text == 'Nothing' then
		return '-'
	else
		return numberargs(aff,aff_r)
	end
end
function aff_weakness_smw_arg(aff)
	if tonumber(aff) then
		return tonumber(aff)
	end
	return nil
end

-- generally weak to (class)
--    looks for the class that the mob is weakest to
--    if multiple equally weak, then none
function generalweaknessarg(aff_melee, aff_ranged, aff_magic)
	aff_melee = tonumber(aff_melee) or 0
	aff_ranged = tonumber(aff_ranged) or 0
	aff_magic = tonumber(aff_magic) or 0
	local max = math.max(aff_melee, aff_magic, aff_ranged)
	if max == 0 then
		return 'none'
	end
	local ret = {}
	
	if aff_melee == max then
		table.insert(ret, 'melee')
	end
	if aff_ranged == max then
		table.insert(ret, 'ranged')
	end
	if aff_magic == max then
		table.insert(ret, 'magic')
	end
	if #ret == 1 then
		return ret[1]
	end
	return 'none'
end

function wkcats(arg,tbl)
	for v in mw.text.gsplit(arg, infobox.splitpoint) do
		if weaknesses[v] then
			if weaknesses[v].category then
				table.insert(tbl, weaknesses[v].category)
			end
		elseif arg == 'inquisitor' then
			table.insert(tbl, 'Susceptible to inquisitor staff')
		elseif arg == 'terrasaur' then
			table.insert(tbl, 'Susceptible to terrasaur maul')
		elseif arg == 'hexhunter' then
			table.insert(tbl, 'Susceptible to hexhunter bow')
		end
	end
end
	
function susceptarg_smw(sus, wk, ps)
	local sus_check = {}
	local suscepts = {}
	if infobox.isDefined(ps) then
		ps = mw.text.trim(ps:lower())
	end
	if primary_styles[ps] then
		sus_check[primary_styles[ps][1]] = true
		table.insert(suscepts, primary_styles[ps][1])
	end
	if infobox.isDefined(wk) then
		if wk:find('darklight') or wk:find('silverlight') then
			wk = wk..',demon'
		end
		sus = string.lower(tostring(sus)..','..wk)
	end
	for v in mw.text.gsplit(sus, '%s*,%s*') do
		local u = weaknesses[v]
		if u and u._suscept and not sus_check[u] then
			sus_check[u] = true
			table.insert(suscepts, u.name or v)
		end
	end
	if #suscepts == 0 then
		return nil
	end
	return table.concat(suscepts, infobox.splitpoint)
end
function susceptarg(sus_smw)
	local suscepts = {}
	if not infobox.isDefined(sus_smw) then
		return "None"
	end
	for v in mw.text.gsplit(sus_smw, infobox.splitpoint) do
		mw.log(v)
		local u = weaknesses[v]
		if u then
			table.insert(suscepts, string.format('[[File:%s|25px|frameless|link=%s]]', u.image, u.link))
		end
	end
	if #suscepts == 0 then
		return "None"
	end
	return table.concat(suscepts, ' ')
end

function slayercatarg(arg)
	if string.match(arg or '','%S') then
		local sl = string.lower(tostring(arg))
		if sl == 'n/a' or sl == 'no' or sl == 'none' then
			return 'N/A'
		end
		local out = {}
		for i in mw.text.gsplit(arg, '%s*,%s*') do
			table.insert(out, mw.text.trim(i))
		end
		return table.concat(out, ', ')
	end
	return nil
end

function slayercatdisparg(arg)
	if infobox.isDefined(arg) then
		if arg == 'N/A' then
			return 'N/A'
		end
		local out = {}
		for i in mw.text.gsplit(arg, ', ') do
			table.insert(out, string.format('[[%s (Slayer assignment)|%s]]', i, i))
		end
		return table.concat(out, ', ')
	end
	return nil
end

function slayercatsmwarg(cat)
	if infobox.isDefined(cat) then
		cat = string.lower(tostring(cat))
		if cat == 'n/a' or cat == 'no' or cat == 'none' then
			return nil
		end
		cat = string.gsub(cat, '[][]', '')
		cat = string.gsub(cat, ',', infobox.splitpoint)
		return cat
	end
	return nil
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(cat, assignedbyoverride, assignedby)
	local arg = nil
	
	-- check for +override & fallback
	if infobox.isDefined(assignedbyoverride) then
		arg = assignedbyoverride
	elseif not infobox.isDefined(cat) and infobox.isDefined(assignedby) then
		arg = assignedby
	end
	local slayer_master_list
	local clean_cats
	if arg then
		arg = mw.text.split(string.lower(arg),'%s*,%s*')
		slayer_master_list = {
			turael = false,
			spria = false,
			jacquelyn = false,
			mazchna = false,
			achtryn = false,
			vannaka = false,
			chaeldar = false,
			sumona = false,
			duradel = false,
			lapalok = false,
			kuradal = false,
			morvran = false,
			mandrith = 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
	else
		if not infobox.isDefined(cat) then
			return nil
		end
		clean_cats = mw.text.split(cat, ', ')
		slayer_master_list = {none={}, all_none=true}
		for _,v in ipairs(clean_cats) do
			local master_amounts = slayer_info.getAllAmounts(v)
			slayer_master_list.none[v] = master_amounts.none
			slayer_master_list.all_none = slayer_master_list.all_none and master_amounts.none
			for i,j in ipairs(slay_masters_order) do
				if master_amounts[j] then
					if not slayer_master_list[j] then
						slayer_master_list[j] = {}
					end
					slayer_master_list[j][v] = master_amounts[j]
				end
			end
		end
	end
	local out
	if slayer_master_list.all_none then
		out = 'Not assigned'
	else
		out = {}
		for _, n in ipairs(slay_masters_order) do
			local v = slayer_master_list[n]
			local _n = string.lower(n)
			if v then
				local amtstr
				if v == true then -- literal true testing for override/fallback method
					amtstr = 'assigned quantities unknown'
				else
					amtstr = {}
					for _,u in ipairs(clean_cats) do
						if v[u] then
							table.insert(amtstr, string.format('%s–%s %s', v[u].min, v[u].max, u))
						end
					end
					amtstr = table.concat(amtstr, '; ')
				end
				table.insert(out, string.format('[[File:%s.png|30px|link=%s|%s: %s]]',slay_masters[_n].chathead,n,n,amtstr))
			end
		end
		out = table.concat(out,' ')
	end
	return out
end

function slayerimgarg(arg)
	local out = {}
	for i,v in string.gmatch(arg, '%[%[(File:.-\.png)\|') do
		table.insert(out, v)
	end
	if #out == 0 then
		return nil
	else
		return table.concat(out, infobox.splitpoint)
	end
end

-- Race
function racearg(racearg, slayercats)
	if infobox.isDefined(racearg) then
		local ret = { '' }
		local race = string.lower(racearg)
		for _, v in ipairs(race_info) do
			if v.race == race then
			 	table.insert(ret, v.racelink)
		    end
		end
		
		for i, v in ipairs(ret) do
		if (v ~= '') then
			ret[i] = string.format('[[%s]]', v)
		end
		end
		
		r =  table.concat(ret, '')
		if r ~= '' then
			return r
		else
			--Do not return invalid categories
			--return arg
		end
	end
	
	-- Based on slayer cats
	local races = {}
	for i in mw.text.gsplit(slayercats, ', ') do
		if slayer_race[i] then
			races[slayer_race[i]] = true
		end
	end
	local ret = {}
	for _, v in ipairs(race_info) do
		if races[v.race] then
			if v.racelink and v.racelink ~= '' then
		 		table.insert(ret, string.format('[[%s]]', v.racelink))
		 	end
	    end
	end
	
	if ret[1] then
		return table.concat(ret, ', ')
	end
	
	return nil
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

function abilseparg(arg)
	arg = paramtest.default_to(arg,false)
	if not arg then
		arg = ''
	elseif arg:find('clickpic') then
		arg = arg:gsub('{{[aA]bility clickpic|',''):gsub('}}',''):gsub('|',',')
	else
		arg = ''
	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 = tostring(attack_speed_bar(speed_map[arg])) or badarg('speed',' is not a valid attack rate')
	return arg
end

function restrictionarg(arg)
	if paramtest.is_empty(arg) then
		return nil
	end
	return restriction_map[string.lower(arg)]
end
function restrsurfarg(cleaned, passed)
	if infobox.isDefined(cleaned) then
		return nil
	end
	return restriction_map.surface
end

function iddisp(id)
	if infobox.isDefined(id) then
		return string.gsub(id, ', *', ', ')
	end
	return nil
end
function idsmw(id)
	if infobox.isDefined(id) then
		if string.lower(tostring(id)) == 'no' then
			return nil
		end
		local r = string.gsub(id, ', *', infobox.splitpoint)
		return r
	end
	return nil
end

function actionarg(arg)
	if infobox.isDefined(arg) then
		if string.lower(arg) == 'none' or string.lower(arg) == 'examine' then
			return '<ul><li>Examine</li></ul>'
		end
		local has_examine = false
		local ret = mw.html.create('ul')
		for x in mw.text.gsplit(arg, ',') do
			ret:tag('li'):wikitext(mw.text.trim(x))
			has_examine = has_examine or (string.find(x, 'Examine') ~= nil)
		end
		if not has_examine then
			ret:tag('li'):wikitext('Examine')
		end
		ret = tostring(ret)
		ret = mw.ustring.gsub(ret, '%[*[Ww]alk [Hh]ere%]*', '[[Walk here|Walk here]]')
		ret = mw.ustring.gsub(ret, '%[*[Ee]xamine%]*', 'Examine')
		return ret
	end
	return nil
end

function actionsmw(arg)
	if infobox.isDefined(arg) then
		if string.lower(arg) == 'none' or string.lower(arg) == 'examine' then
			return 'Examine'
		end
		local r = string.gsub(arg, ',%s*', infobox.splitpoint)
		r = mw.ustring.gsub(r, '%[*[Ww]alk [Hh]ere%]*', 'Walk here')
		r = mw.ustring.gsub(r, '%[*[Ee]xamine%]*', 'Examine')
		if not r:find('Examine') then
			r = r .. infobox.splitpoint .. 'Examine'
		end
		return r
	end
	return nil
end

function make_chisel_links(id, name)
	local link1 = 'https://chisel.weirdgloop.org/bestiary/'
	local link2 = 'https://chisel.weirdgloop.org/gazproj/mrnd'
	if infobox.isDefined(id) then
		local ids = mw.text.split(id, infobox.splitpoint)
		link1 = link1 .. table.concat(ids, '%20')
		id1 = tonumber(ids[1])
		if id1 then
			if #ids == 1 then
				link2 = string.format('%s?%s#%s-%s', link2, id1, id1-15, id1+15)
			else
				for i,j in ipairs(ids) do
					if i == 1 then
						link2 = string.format('%s?%s#%s', link2, j, j)
					else
						link2 = link2 .. '@' .. j
					end
				end
			end
		else
			link2 = string.format('%sid#%s', link2, ids[1])
		end
	else
		local _name = name:gsub(' ', '%%20')
		link1 = link1 .. _name
		link2 = string.format('%s#%s', link2, _name)
	end
	return string.format('[%s bestiary]&nbsp;&bull;&nbsp;[%s MRND]', link1, link2)
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

-- SMW
-- 'name', 'version', 'id', 'members', 'release', 'removal', 'examine', 'level', 'style', 'lpraw', 'weakness', 'xpraw', 'slaylvl', 'slayxp', 'assigned_by', 'slayercat', 'restriction',
-- 'poisonous', 'abilities_sep', 'aggressive', 'immune_to_stun', 'immune_to_drain', 'immune_to_poison', 'immune_to_deflect',
-- 'attack', 'magic', 'ranged', 'defence', 'max_melee', 'max_magic', 'max_ranged', 'max_spec', 'acc_melee', 'acc_magic', 'acc_ranged', 'armour', 'aff_melee', 'aff_magic', 'aff_ranged', 'aff_weakness'
function SMWarg(name, version, id, members, release, removal, examine, level, style, lp, weakness, xp, slaylvl, slayxp, assignedby, slayercat, poisonous, abilities, restriction,
				aggressive, immune_to_stun, immune_to_drain, immune_to_poison, immune_to_deflect,
				attack, magic, ranged, defence, max_melee, max_magic, max_ranged, max_spec, acc_melee, acc_magic, acc_ranged, armour, aff_melee, aff_magic, aff_ranged, aff_weakness)
	local toJSON = {
		name = name,
		version = version,
		id = id,
		members = members,
		examine = examine,
		level = level,
		style = style:gsub('|', '¦'),
		lifepoints = lp,
		weakness = weakness:gsub('|', '¦'),
		experience = xp,
		slayer = slaylvl,
		slayer_experience = slayxp,
		assigned_by = assignedby:gsub('|', '¦'),
		slayer_category = slayercat
	}
	
	local yn = {
		aggressive = aggressive,
		immune_to_stun = immune_to_stun,
		immune_to_drain = immune_to_drain,
		immune_to_poison = immune_to_poison,
		immune_to_deflect = immune_to_deflect
	}
	local num = {
		attack = attack,
		magic = magic,
		ranged = ranged,
		defence = defence,
		max_melee = max_melee,
		max_magic = max_magic,
		max_ranged = max_ranged,
		max_spec = max_spec,
		acc_melee = acc_melee,
		acc_magic = acc_magic,
		acc_ranged = acc_ranged,
		armour = armour,
		aff_melee = aff_melee,
		aff_magic = aff_magic,
		aff_ranged = aff_ranged,
		aff_weakness = aff_weakness
	}
	
	for k,v in pairs(yn) do
		if v:find('[Yy]es check') then
			toJSON[k] = true
		elseif v:find('[Xx] mark') then
			toJSON[k] = false
		end
	end
	
	for k,v in pairs(num) do
		if type(v) == 'string' then
			if v:lower() == 'n/a' or v:lower() == 'varies' then
				toJSON[k] = v
			end
		elseif type(v) == 'number' then
			toJSON[k] = v
		end
	end
	
	if paramtest.is_empty(restriction) and tostring(restriction):find('action=edit') then
		toJSON.restriction = restriction_map.surface
	else
		toJSON.restriction = restriction
	end
	
	if poisonous then
		if poisonous:find('[Yy]es check') then
			toJSON.poisonous = true
			local m = poisonous:match(' (%d+)<')
			if m then
				toJSON.poison_damage = m
			end
		elseif poisonous:find('[Xx] mark') then
			toJSON.poisonous = false
		end
	end
	
	local rel, upd, rem, updr
	rel, upd = release:match('(.-) %(%[%[Update:(.-)|Update%]%]%)')
	if rel == nil then
		rel = release:match('(.-) %(Update unknown%)')
	end
	if rel then
		toJSON.release_date = rel:gsub('%[',''):gsub('%]','')
		if upd then
			toJSON.release_update_post = upd
		end
	end
	
	rem, updr = removal:match('(.-) %(%[%[Update:(.-)|Update%]%]%)')
	if rem == nil then
		rem = removal:match('(.-) %(Update unknown%)')
	end
	if rem then
		toJSON.removal_date = rem:gsub('%[',''):gsub('%]','')
		if updr then
			toJSON.removal_update_post = updr
		end
	end
			
	
	for k,v in pairs(toJSON) do
		if v == '' or (type(v) == 'string' and string.find(v,'action=edit')) then
			toJSON[k] = nil
		end
	end
	
	return mw.text.nowiki(mw.text.jsonEncode(toJSON))
	
end

function addcategories(args,catargs)
	local ret = { 'Cache NPCs', '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 non-player character image',
			members = 'Needs members status',
			release = 'Needs release date',
			examine = 'Needs examine added',
			actions = 'Needs actions added',
			level = 'Needs combat level',
			experience = 'Needs XP per kill',
			primarystyle_smw = 'Missing primary attack style',
			immune_to_stun = 'Missing stun immunity information',
			immune_to_poison = 'Missing poison immunity information',
			immune_to_deflect = 'Missing deflect immunity information',
			immune_to_drain = 'Missing drain immunity information',
			attack = 'Missing combat skill levels',
			magic = 'Missing combat skill levels',
			ranged = 'Missing combat skill levels',
			defence = 'Missing combat skill levels',
			id = 'Needs ID'
		},
		-- regex
		grep = {
			poisonous = { ['yes check%.svg'] = 'Poisonous monsters' },
			aggressive = { ['yes check%.svg'] = 'Aggressive monsters' },
			immune_to_stun = { ['is immune'] = 'Stun-immune monsters', ['is not immune'] = 'Stun-susceptible monsters' },
			immune_to_poison = { ['is immune'] = 'Poison-immune monsters', ['is not immune'] = 'Poison-susceptible monsters' },
			immune_to_deflect = { ['is immune'] = 'Deflect-immune monsters', ['is not immune'] = 'Deflect-susceptible monsters' },
			immune_to_drain = { ['is immune'] = 'Drain-immune monsters', ['is not immune'] = 'Drain-susceptible monsters' },
		},
	}
 
	-- 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

	-- searches
	for n, v in pairs(cat_map.grep) do
		for m, w in pairs(v) do
			if args[n] then
				if string.find(string.lower(tostring(args[n].d) or ''),m) then
					table.insert(ret,w)
				end
				if args[n].switches then
					for _, x in ipairs(args[n].switches) do
						if string.find(string.lower(tostring(x)),m) then
							table.insert(ret,w)
						end
					end
				end
			end
		end
	end

	if args.vanchor.switches then
		local verstable = {}
		for _, v in ipairs(args.vanchor.switches) do
			if verstable[v] then
				table.insert(ret,'Pages with a duplicate subobject name')
				break
			else
				verstable[v] = true
			end
		end
	end
	
	local levels = { args.level.d }
	if args.level.switches then
		for _, v in ipairs(args.level.switches) do
			if v ~= infobox.nilParam() 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.members.d:find('[Yy]es') then
		table.insert(ret,'P2P bestiary')
	elseif args.members.switches then
		for _, v in ipairs(args.members.switches) do
			if v:find('[Yy]es') then
				table.insert(ret,'P2P 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.weaknesstxt.d then
		wkcats(args.weaknesstxt.d,ret)
	end
	if args.weaknesstxt.switches then
		for _, v in ipairs(args.weaknesstxt.switches) do
			if v ~= _nil then
				wkcats(v,ret)
			end
		end
	end
	if args.susceptibility_smw.d then
		wkcats(args.susceptibility_smw.d,ret)
	end
	if args.susceptibility_smw.switches then
		for _, v in ipairs(args.susceptibility_smw.switches) do
			if v ~= _nil then
				wkcats(v,ret)
			end
		end
	end

	if args.assigned_by_raw.d then
		slaycats(args.assigned_by_raw.d,ret)
	end
	if args.assigned_by_raw.switches then
		for _, v in ipairs(args.assigned_by_raw.switches) do
			if v ~= _nil then
				slaycats(v,ret)
			end
		end
	end

	if not args.voice.d:find('%?action=edit') and args.voice.d:find('[Yy]es') then
		table.insert(ret,'Voice acted NPCs')
	elseif args.voice.switches then
		for _, v in ipairs(args.voice.switches) do
			if not v:find('%?action=edit') and v:find('[Yy]es') then
				table.insert(ret,'Voice acted NPCs')
				break
			end
		end
	end
	
	-- Add the associated race category if matched
	local hasrace = false
	local defrace = false
	local races = {}
	if not args.race.d:find('%?action=edit') and args.race.d ~= infobox.nilParam() then
		for i in mw.text.gsplit(args.race.d, ', ') do
			local racestr = string.gsub(string.gsub(i, '%]%]', ''), '%[%[', '')
			races[racestr] = true
			hasrace = true
			defrace = true
		end
	end
	if 	args.race.switches then
		for _,v in ipairs(args.race.switches) do
			if not v:find('%?action=edit') and v ~= infobox.nilParam() then
				for i in mw.text.gsplit(v, ', ') do
					local racestr = string.gsub(string.gsub(i, '%]%]', ''), '%[%[', '')
					races[racestr] = true
					hasrace = true
				end
			elseif not defrace then
				table.insert(ret, 'Needs race')
			end
		end
	end
	if hasrace then
		for _,v in ipairs(race_info) do
			if races[v.racelink] then
				table.insert(ret, v.racecat)
				if v.addcat then
		    		for _,k in ipairs(v.addcat) do
		    			table.insert(ret, k)
		    		end
		    	end
			end
		end
	else
		table.insert(ret, 'Needs race')
	end

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

	return table.concat(ret,'')
end

return p
-- </nowiki>