RuneScape Wiki
mNo edit summary
mNo edit summary
Line 343: Line 343:
 
else
 
else
 
f = ''
 
f = ''
for w in string.gmatch(d, '([^,]+)') do
+
for w in string.gmatch(v, '([^,]+)') do
 
w = w:gsub("^%s*(.-)%s*$", "%1")
 
w = w:gsub("^%s*(.-)%s*$", "%1")
if w == 'passive' then
+
w2 = string.lower(w)
  +
if w2 == 'passive' then
 
f = f .. '\n* [[Passive effect]]'
 
f = f .. '\n* [[Passive effect]]'
elseif w == 'time' then
+
elseif w2 == 'time' then
 
f = f .. '\n* Time remaining'
 
f = f .. '\n* Time remaining'
elseif w == 'degrade' then
+
elseif w2 == 'degrade' then
 
f = f .. '\n* [[Degradation]]'
 
f = f .. '\n* [[Degradation]]'
elseif w == 'items' then
+
elseif w2 == 'items' then
 
f = f .. '\n* Items consumed'
 
f = f .. '\n* Items consumed'
elseif w == 'urn' then
+
elseif w2 == 'urn' then
 
f = f .. '\n* Percentage filled'
 
f = f .. '\n* Percentage filled'
 
else
 
else

Revision as of 18:20, 13 November 2017

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

-- <pre>
-- ( ͡° ͜ʖ ͡°)

local p = {}

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

-- Main function called with invokes
function p.main(frame)
	local args = frame:getParent().args
	local ret = infobox.new(args)

	-- Parameter definitions
	ret:defineParams{
		{ name = 'name', func = 'name' },
		{ name = 'aka', func = 'has_content' },
		{ name = 'image', func = 'image' },

		-- release and removal
		-- removal only shown if it exists
		{ name = 'release', func = 'release' },
		{ name = 'removal', func = 'removal' },
		{ name = 'removaldisp', func = { name = removaldisp, params = { 'removal' } }, dupes = true },

		{ name = 'members', func = 'has_content' },
		{ name = 'tooltip', func = tooltiparg },
		{ name = 'examine', func = 'has_content' },
		{ name = 'quest', func = 'has_content' },
		{ name = 'tradeable', func = tradeablearg },
		{ name = 'equipable', func = 'has_content' },

		-- bankable; only show if "No"; default to "Yes"
		{ name = 'bankable', func = { name = 'has_content', params = { 'bankable', 'Yes'}, flag = { 'd', 'r' } } },
		{ name = 'bankabledisp', func = { name = yesnodisp, params = { 'bankable', 'no' }, flag = { 'd', 'r' } }, dupes = true },

		-- stacksinbank; only show if "No"; default to "Yes"
		{ name = 'stacksinbank', func = { name = 'has_content', params = { 'stacksinbank', 'Yes'}, flag = { 'd', 'r' } } },
		{ name = 'stacksinbankdisp', func = { name = yesnodisp, params = { 'stacksinbank', 'no' }, flag = { 'd', 'r' } }, dupes = true },

		-- lendable; only show if "Yes"; default to "No"
		{ name = 'lendable', func = { name = 'has_content', params = { 'lendable', 'No'}, flag = { 'd', 'r' } } },
		{ name = 'lendabledisp', func = { name = yesnodisp, params = { 'lendable' } }, dupes = true },

		{ name = 'stackable', func = 'has_content' },
		{ name = 'disassembly', func = disassemblyarg },

		-- edible; only show if "Yes"; default to "No"
		{ name = 'edible', func = { name = 'has_content', params = { 'edible', 'No'}, flag = { 'd', 'r' } } },
		{ name = 'edibledisp', func = { name = yesnodisp, params = { 'edible' } }, dupes = true },

		-- noteable; only show if "Yes"; default to "No"
		{ name = 'noteable', func = { name = 'has_content', params = { 'noteable', 'No'}, flag = { 'd', 'r' } } },
		{ name = 'noteabledisp', func = { name = yesnodisp, params = { 'noteable' } }, dupes = true },
		{ name = 'destroy', func = 'has_content' },
		{ name = 'store', func = storearg },
		{ name = 'val', func = { name = valraw, params = { 'value' }, flag = 'p' }, dupes = true },
		{ name = 'value', func = { name = valuearg, params =  { 'val', 'convert' }, flag = { 'd', 'p' } } },
		-- gemw prices
		-- only displayed if they exist
		--dupes must exist for individual prices to have them display properly
		{ name = 'gemw', func = { name = gemwarg, params = { 'exchange', 'tradeable' }, flag = {'p', 'd'} } },
		{ name = 'gemwname', func = { name = gemwnamearg, params = { 'name', 'gemwname' } } },
		{ name = 'gemwprice', func = { name = gemwpricearg, params = { 'gemw', 'gemwname' } }, dupes = true },
		{ name = 'exchange', func = { name = exchangearg, params = { 'gemwprice', 'gemwname' } }, dupes = true },
		{ name = 'graph', func = { name = gemwgrapharg, params = { 'gemwprice', 'gemwname' } }, dupes = true },
		{ name = 'buylimit', func = { name = buylimitarg, params = { 'gemwprice', 'gemwname' } }, dupes = true },
		-- used for both exchange and graphs
		{ name = 'gemwdisp', func = { name = gemwdisp, params = { 'gemwprice' } }, dupes = true },
		{ name = 'kept', func = { name = keptondeatharg, params = { 'kept', 'ikod', 'reclaim', 'sacrifice', 'val', 'gemwprice' }, flag = { 'p', 'p', 'p', 'd', 'd' } } },

		-- alchemy
		{ name = 'alchable', func = { name = alchablearg, params = { 'alchable', 'high', 'low' }, flag = 'p' }, dupes = true },
		{ name = 'alchmultiplier', func = multiplierarg },
		{ name = 'high', func = { name = alchvalues, params = { 'val', 'high', 'alchmultiplier', 1, 'alchable' }, flag = { 'd', 'p', 'd', 'r', 'd' } } },
		{ name = 'low', func = { name = alchvalues, params = { 'val', 'low', 'alchmultiplier', 2/3, 'alchable' }, flag = { 'd', 'p', 'd', 'r', 'd' } } },

		{ name = 'weight', func = weightarg },
		{ name = 'weightraw', func = { name = weightargraw, params = { 'weight' }, flag = 'p' } },

		-- not used; only for categories
		{ name = 'id', func = 'numbers' },
		{ name = 'rscid', func = 'numbers' }
	}

	ret:setMaxButtons(7)
	ret:create()
	ret:cleanParams()

	-- parameter linkings for hidden rows
	ret:linkParams{
		{ 'removal', 'removaldisp' },
		{ 'exchange', 'gemwdisp' },
		{ 'graph', 'gemwdisp' },
		{ 'buylimit', 'gemwdisp' },
		{ 'bankable', 'bankabledisp' },
		{ 'stacksinbank', 'stacksinbankdisp' },
		{ 'lendable', 'lendabledisp' },
		{ 'noteable', 'noteabledisp' },
		{ 'edible', 'edibledisp' }
	}

	ret:defineLinks({ { 'Template:%s/FAQ', 'FAQ' },
			{ 'Template:%s/doc', 'doc' } })

	ret:useSMW({
		id = 'Item ID',
		val = 'Value',
		weightraw = 'Weight',
	})

	ret:caption()
	ret:defineName('Infobox Item')
	ret:addClass('infobox-item')
	-- PARAMETER: image
	ret:addRow{
		{ tag = 'argd', content = 'image', class = 'infobox-image', colspan = '2' } }

	-- PARAMETER: release
	-- (update included automatically by infobox)
		:addRow{ { tag = 'th', content = 'Release' },
				{ tag = 'argd', content = 'release' } }
 
	-- PARAMETER: removal
	if ret:paramDefined('removal') then
		ret:addRow{ { tag = 'th', content = 'Removal' },
				{ tag = 'argd', content = 'removal' } }
	end

	-- PARAMETER: aka
	-- add only if it exists
	if ret:paramDefined('aka') then
		   ret:addRow{ { tag = 'th', content = '[[Slang dictionary|AKA]]' },
				{ tag = 'argd', content = 'aka' } }
	end

	-- PARAMETER: members
	ret:addRow{
		{ tag = 'th', content = '[[Members]]' },
		{ tag = 'argd', content = 'members' } }

	-- PARAMETER: quest
	:addRow{
		{ tag = 'th', content = '[[Quest items|Quest item]]' },
		{ tag = 'argd', content = 'quest' } }

	-- PARAMETER: tradeable
	:addRow{
		{ tag = 'th', content = '[[Items#Tradeability|Tradeable]]' },
		{ tag = 'argd', content = 'tradeable' } }

	-- PARAMETER: bankable
	if ret:paramGrep('bankable','no') then
		ret:addRow{
			{ tag = 'th', content = '[[Bank]]able' },
			{ tag = 'argd', content = 'bankable' } }
	end

	-- PARAMETER: stacksinbank
	if ret:paramGrep('stacksinbank','no') then
		ret:addRow{
			{ tag = 'th', content = 'Stacks in bank' },
			{ tag = 'argd', content = 'stacksinbank' } }
	end

	-- PARAMETER: lendable
	if ret:paramGrep('lendable','yes') then
		ret:addRow{
			{ tag = 'th', content = '[[Item Lending|Lendable]]' },
			{ tag = 'argd', content = 'lendable' } }
	end

	-- PARAMETER: equipable
	ret:addRow{
		{ tag = 'th', content = '[[Equipment|Equipable]]' },
		{ tag = 'argd', content = 'equipable' } }

	-- PARAMETER: stackable
	:addRow{
		{ tag = 'th', content = '[[Stackable items|Stackable]]' },
		{ tag = 'argd', content = 'stackable' } }

	-- PARAMETER: disassembly
	:addRow{
		{ tag = 'th', content = '[[Disassemble|Disassembly]]' },
		{ tag = 'argd', content = 'disassembly' } }

	-- PARAMETER: noteable
	if ret:paramGrep('noteable','yes') then
		ret:addRow{
			{ tag = 'th', content = '[[Note|Noteable]]' },
			{ tag = 'argd', content = 'noteable' } }
	end

	-- PARAMETER: edible
	if ret:paramGrep('edible','yes') then
		ret:addRow{
			{ tag = 'th', content = '[[Food|Edible]]' },
			{ tag = 'argd', content = 'edible' } }
	end

	-- PARAMETER: value
	ret:addRow{
		{ tag = 'th', content = '[[Value]]' },
		{ tag = 'argd', content = 'value' } }

	-- PARAMETER: alchable | high | low
	-- find if any version is alchable
	local anyalchable = ret:paramGrep('alchable',true)

	-- if any are alchable, add both rows
	if anyalchable == true then
		ret:addRow{
			{ tag = 'th', content = '[[High Level Alchemy|High alch]]' },
			{ tag = 'argd', content = 'high' } }
		:addRow{ 
			{ tag = 'th', content = '[[Low Level Alchemy|Low alch]]' },
			{ tag = 'argd', content = 'low' } }
	else
	-- otherwise add a single "no alch" row
		ret:addRow{
			{ tag = 'th', content = '[[Alchemy]]' },
			{ tag = 'td', content = 'Not alchemisable' } }
	end

	-- PARAMETER: destroy
	ret:addRow{
		{ tag = 'th', content = '[[Destroy (action)|Destroy]]' },
		{ tag = 'argd', content = 'destroy', css = { ['max-width'] = '200px' } } }

	-- PARAMETER: death
	ret:addRow{
		{ tag = 'th', content = '[[Items Kept on Death|On death]]' },
		{ tag = 'argd', content = 'kept' } }

	-- find if we have any exchange prices
	local anygemw = ret:paramGrep('gemwprice', function(_arg) return _arg > 0 end)

	-- if we have any on the ge, add the gemw row
	if anygemw == true then
		-- PARAMETER: exchange | gemwname
		ret:addRow{
			{ tag = 'th', content = '[[RuneScape:Grand Exchange Market Watch|Exchange]]' },
			{ tag = 'argd', content = 'exchange' } }
			:addRow{
			{ tag = 'th', content = '[[Grand Exchange#Trade restrictions|Buy limit]]' },
			{ tag = 'argd', content = 'buylimit' } }
	end

	-- PARAMETER: weight
	ret:addRow{
		{ tag = 'th', content = '[[Weight]]' },
		{ tag = 'argd', content = 'weight' } }

	-- PARAMETER: tooltip
	if ret:paramDefined('tooltip') then
	    ret:addRow{
    		{ tag = 'th', content = 'Tooltip' },
    		{ tag = 'argd', content = 'tooltip' } }
    end

	-- PARAMETER: examine
	ret:addRow{
		{ tag = 'th', content = '[[Examine]]' },
		{ tag = 'argd', content = 'examine', css = { ['max-width'] = '200px' } } }

	-- PARAMETER: graph
	if anygemw == true then
		ret:addRow{ { tag = 'argd', content = 'graph', colspan = '2', css = { ['text-align'] = 'center', ['padding'] = '0', ['background-color'] = 'white' } },
			meta = { addClass = 'hidden' } }
	end

	if onmain() then
		local a1 = ret:param('all')
		local a2 = ret:categoryData()
		ret:wikitext(addcategories(a1,a2))
	end

	return ret:tostring()
end

-- Store price
function storearg(store, currency, seller)
	-- remove any commas
	store = string.gsub(store or '',',','')

	-- no for not sold
	if string.lower(store) == 'no' then
		return 'Not sold'
	else
		store = tonumber(store,10)
	end

	if type(store) == 'number' then
		return store
	else
		return nil
	end
end

-- tradeable
-- tradeablearg(value)
function tradeablearg(v)
	v = string.lower(v or '')
	if v == 'yes' or v == 'no' then
		v = mw.text.split(v,'')
		v[1] = string.upper(v[1])
		return table.concat(v,'')
	elseif v == 'restricted' then
		return '[[Restricted trade items|Restricted]]'
	else
		return nil
	end
end

-- disassembly
-- disassemblyarg(yes/no)
function disassemblyarg(d)
	d = string.lower(d or '')
	if d == 'yes' then
		return '[[#DisassemblyT|Yes]]' -- Unique anchor ID created by {{Disassembly}}
	elseif d == 'no' then
		return 'No'
	elseif d == 'restricted' then
		return '[[:Category:Location restricted disassembly|Restricted]]'
	elseif d == 'na' or d == 'n/a' or d == 'discontinued' or d == 'irrelevant' then
		return 'N/A'
	else
		return nil
	end
end

-- tooltip
-- tooltiparg(v)
function tooltiparg(v)
	d = string.lower(v or '')
	if d == 'no' or d == '' then
	    return null
	else
	    f = ''
        for w in string.gmatch(v, '([^,]+)') do
            w = w:gsub("^%s*(.-)%s*$", "%1")
            w2 = string.lower(w)
            if w2 == 'passive' then
                f = f .. '\n* [[Passive effect]]'
            elseif w2 == 'time' then
                f = f .. '\n* Time remaining'
        	elseif w2 == 'degrade' then
        		f = f .. '\n* [[Degradation]]'
        	elseif w2 == 'items' then
        	    f = f .. '\n* Items consumed'
        	elseif w2 == 'urn' then
        	    f = f .. '\n* Percentage filled'
        	else
        	    f = f .. '\n* ' .. w
        	end
        end
        if f == '' then
            return null
        else
            return f
        end
	end
end

-- value
-- separate number storage for operation
-- valraw(value)
function valraw(v)
	local _ v = v or ''
	_v = string.gsub(v,',','')

	return tonumber(v,10)
end

-- value
-- valuearg(value, convert)
-- actual value already parsed
function valuearg(v,c)
	-- replace commas and turn into a number
	if paramtest.has_content(c) then
		c = string.gsub(c,',','')
		c = tonumber(c,10)
	else
		c = nil
	end

	v = tonumber(v,10)

	-- if both are defined, show both, value first
	if v and c then
		return string.format('%s<br/><b>Cash out:</b><br/>%s',plural('coin',v),plural('coin',c))
	-- if only value is defined, show just that
	elseif v and not c then
		return plural('coin',v)
	-- if only convert is defined, show just that
	-- may need to change this so that value is requested
	elseif c and not v then
		return string.format('<b>Cash out:</b><br/>%s',plural('coin',c))
	else
		return nil
	end
end
			
-- Alchables
-- alchablearg(alchable, high, low)
function alchablearg(a,h,l)
	-- not alchable if both are false, or if "alchable" is false
	if string.lower(a or '') == 'no' then
		return false
	elseif string.lower(h or '') == 'no' and string.lower(l or '') == 'no' then
		return false
	else
		return true
	end
end

-- alch multiplier arg
-- only accepts numbers
-- defaults to .6
function multiplierarg(v)
	return tonumber(v) or .6
end

-- high/low alch
-- alchvalues(value, override value, multiplier, alchable)
-- actual value already parsed
-- value, template param, alch multiplier, override multiplier, not alchable bool
function alchvalues(v,_p,a,m,n)
	-- remove commas and turn into a number
	v = tonumber(v,10)
	if paramtest.has_content(_p) then
		_p = string.gsub(_p,',','')
		_p = tonumber(_p,10)
	else
		_p = nil
	end

	-- return override always
	if type(_p) == 'number' then
		return plural('coin',_p)
	end

	-- if you can't alch it, return this
	-- used in the case of 1 version being alchable and the other not
	if n == false then
		return 'Not alchemisable'
	end
	-- otherwise try the value and multiply it
	if v then
		local r = math.floor(killRoundingError(v * m * a))
		return plural('coin',r)
	end

	return nil
end

-- weight
-- weightarg(weight)
function weightarg(w)
	if paramtest.has_content(w) then
		-- replace all "kg" and spaces here
		w = string.gsub(w or '','[kg ]','')

		-- replace hyphen with minus sign
		w = string.gsub(w,'-','&minus;')

		-- use non-breaking spaces and html entities for display
		-- still necessary to convert the "kg" to html?
		return string.format('%s&nbsp;&#107;&#103;',w)
	end
	return nil
end

-- weightargraw(weight)
function weightargraw(w)
	if paramtest.has_content(w) then
		-- replace all "kg" and spaces here
		w = string.gsub(w or '','[kg ]','')
		return tonumber(w)
	end
	return nil
end

-- on ge or not
-- only accepts "gemw"
-- gemwarg(exchange,tradeable)
function gemwarg(arg,arg2)
	g = string.lower(arg or '')
	return arg2 == 'Yes' and g == 'gemw'
end

-- gemw names
-- gemwnamearg(name, override name)
function gemwnamearg(n,a)
	-- return override
	if a and a:find('%S') then
		return string.gsub(a,'</?span>','')
	-- otherwise use the "name" parameter
	elseif n and n:find('%S') then
		return string.gsub(n,'</?span>','')
	-- default to page name
	else
		return mw.title.getCurrentTitle().fullText
	end
end

-- separate thing to hold all the prices as raw numbers
-- gemwpricearg(gemw, name)
function gemwpricearg(g,n)
	if g == true then
		-- return price if page is found
		-- -1 for errors
		if exchange._exists(n) then
			return tonumber(exchange._price(n),10) or -1
		else
			return -1
		end
	-- 0 for no price
	else
		return 0
	end
end

-- exchange display
-- exchangearg(gemw price, name)
-- uses the already fetched ge price to operate
-- values less than 1 are used for parsing instructions
function exchangearg(g,n)
	if type(g) ~= 'number' then
		g = tonumber(g,10) or 0
	end
	-- 0 for not sold
	if g == 0 then
		return '<span class="infobox-quantity" data-val-each="0">Not sold</span>'
	-- -1 for no page found
	elseif g == -1 then 
		return badarg('exchange','was set to «gemw» but no page was found for «'..n..'».')
	-- all other numbers 
	elseif g > 0 then
		-- plural done in format because we need a span around the value
		return string.format('<span class="infobox-quantity" data-val-each="%s"><span class="infobox-quantity-replace">%s</span> coin%s ([[Exchange:%s|info]])</span>',g,commas(g),g>1 and 's' or '',n)
	-- not a number = nil
	-- shouldn't be used, but it's a fallback
	else
		return 0
	end
end

-- ge graphs
-- gemwgrapharg(gemw price, name)
-- uses the already fetched ge price to operate
-- values less than 1 are used for parsing instructions
function gemwgrapharg(g,n)
	if type(g) ~= 'number' then
		g = tonumber(g,10) or 0
	end

	-- 0 for not sold an
	-- -1 for error
	-- call to hide the graph
	if g == 0 or g == -1 then
		return 'No data to display'
	-- all other numbers 
	elseif g > 0 then
		-- TODO: Change Module:ExchangeData and give it a proper graph function
		-- remove reliance on frame
		return mw.getCurrentFrame():preprocess(string.format('{{GEChart|%s|size=small}}',n))
	-- not a number = nil
	-- shouldn't be used, but it's a fallback
	else
		return nil
	end
end

-- ge buy limits
-- buylimitarg(gemw price, name)
-- uses the already fetched ge price to operate
-- values less than 1 are used for parsing instructions
function buylimitarg(g,n)
	if type(g) ~= 'number' then
		g = tonumber(g,10) or 0
	end

	-- 0 for not sold an
	-- -1 for error
	-- call to hide the graph
	if g == 0 or g == -1 then
		return '-'
	-- all other numbers 
	elseif g > 0 then
		local ret = exchange._limit(n)
		if ret == nil then
			return '-'
		else
			return commas(ret)
		end
	-- not a number = nil
	-- shouldn't be used, but it's a fallback
	else
		return nil
	end
end

-- class names based on value
-- gemwdisp(price)
function gemwdisp(_p)
	if _p == 0 then
		return ''
	else
		return 'infobox-cell-shown'
	end
end

-- Shows if the param matches the alt
-- alt defaults to yes
-- Everything else = hide
function yesnodisp(arg, alt)
	arg = string.lower(arg or '')
	alt = string.lower(alt or 'yes')
	if arg == alt then
		return 'infobox-cell-shown'
	else
		return ''
	end
end

-- Shows if has a date
function removaldisp(arg)
	if string.find(arg or '','%[%[') then
		return 'infobox-cell-shown'
	else
		return ''
	end
end

-- death
-- keptondeatharg(death, override value, override reclaim, override sacrifice, value, gemwprice)
function keptondeatharg(d,_i,_r,_s,v,g)
	d = string.lower(d or '')
	v = tonumber(v,10)
	-- items always lost
	if d == 'never' then
		return 'Always lost'
	-- items always kept unless in the wilderness
	elseif d == 'always' then
		return 'Always kept outside [[Wilderness|Wild]]'
	elseif d == 'alwaysinclwild' then
		return 'Always kept'
	elseif d == 'dropped' then
		return 'Dropped on death'
	elseif d == 'safe' then
		return 'Always a safe death'
	-- returns a nil value to force it to use gemw
	elseif d == 'gemwdegrade' then
		return nil
	-- reclaimable items
	elseif d == 'reclaimable' then
		local i
		-- all other items display reclaim and sacrifice
		-- if overridden ikod value, display that as well
		-- default is to show this
		local ret = { 'Reclaimable' }

		-- replace commas and convert to number
		if paramtest.has_content(_i) then
			i = string.gsub(_i,',','')
			i = tonumber(i,10)
		else
			i = nil
		end

		-- if i is defined, show that value
		if i then
			table.insert(ret,string.format('<b>Value:</b> %s',commas(i)))
		-- test geprice next
		elseif (tonumber(g,10) or 0) > 0 then
			i = g
		-- test value next
		elseif v then
			i = v
		end

		-- otherwise give up
		if not tonumber(i,10) then
			return i
		end

		-- reclaim cost is based on the death value of an item
		-- sacrifice is 4 x reclaim
		-- sacrifice value is a function of reclaim and not the original number
		-- this is evident by the fact that items with a death value of 1 have a sacrifice value of 4

		-- reclaim, sacrifice
		local r,s

		-- test for overridden reclaim
		if paramtest.has_content(_r) then
			_r = string.gsub(_r,',','')
			r = tonumber(_r)
		end

		-- if no number, use formula
		if not r then
			-- less than 10,000: .15x
			if i < 10000 then
				r = math.floor(i * .15)
				-- all items cost at least 1 coin, so round back up if 0
				if r == 0 then
					r = 1
				end

			--[[
				Death formula
			]]
			-- 10,000 to 49,999: 500 + 0.1x
			elseif i >= 10000 and i < 50000 then
				r = 500 + i * 0.1

			-- 50,000 to 249,999: 3000 + 0.05x
			elseif i >= 50000 and i < 250000 then
				r = 3000 + i * 0.05

			-- 250,000 to 999,999: 10500 + 0.02x
			elseif i >= 250000 and i < 1000000 then
				r = 10500 + i * 0.02

			-- 1,000,000 to 9,999,999: 20500 + 0.01x
			elseif i >= 1000000 and i < 10000000 then
				r = 20500 + i * 0.01

			-- 10,000,000 and greater: 70,500 + 0.005x
			elseif i >= 10000000 then
				r = 70500 + i * 0.005

			-- this shouldn't happen, but fallback to 1
			else
				r = 1
			end
		end

		r = math.floor(killRoundingError(r))

		if paramtest.has_content(_s) then
			_s = string.gsub(_s,',','')
			_s = tonumber(_s,10)
		end

		-- if overridden, use _s for sacrifice
		if tonumber(_s,10) then
			s = _s
		else
			-- otherwise include sacrifice as 4r
			s = r * 4
		end

		table.insert(ret,string.format('<b>Reclaim:</b> %s',commas(r)))
		table.insert(ret,string.format('<b>Sacrifice:</b> %s',commas(s)))

		return table.concat(ret,'<br/>')
	end
	-- otherwise unknown
	return nil
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

-- plural
-- returns number with the word
function plural(arg,n,alt)
	local _n = commas(tonumber(n,10) or 1)
	if tonumber(n,10) == 1 then
		return string.format('%s %s',_n,arg)
	elseif alt then
		return string.format('%s %s',_n,alt)
	else
		return string.format('%s %ss',_n,arg) 
	end
end

-- Does exactly what's on the tin
function killRoundingError(n)
	return math.floor(n*1000+0.0000099)/1000
end

-- Categories
-- oman this is still blatant copy pasta
function addcategories(args,catargs)
	local ret = { 'Items' }
	local cat_map = {
		-- Added if the parameter has content
		defined = {
			aka = 'Pages with AKA',
			tooltip = 'Items that have a tooltip'
			},
		-- 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',
			weight = 'Needs weight added',
			value = 'Items missing value',
			quest = 'Items missing quest',
			destroy = 'Missing destroy text',
			kept = 'Items missing death info',
			disassembly = 'Items missing disassembly info'
			},
		-- Parameters that have text
		-- map a category to a value
		grep = {
			members = { yes = 'Members\' items', no = 'Free-to-play items' },
			stackable = { yes = 'Stackable items' },
			lendable = { yes = 'Lendable items' },
			equipable = { yes = 'Equipable items' },
			gemw = { ['true'] = 'Grand Exchange items' },
			tradeable = { yes = 'Tradeable items', no = 'Untradeable items', restricted = 'Restricted trade items' },
			disassembly = { yes = 'Items that can be disassembled', no = 'Items that cannot be disassembled', restricted = 'Location restricted disassembly', ['n/a'] = 'Items with N/A disassembly' },
			kept = { ['always lost'] = 'Items that are never kept on death', ['always kept outside'] = 'Items that are always kept outside the Wilderness on death', reclaimable = 'Items that are reclaimable on death' }
		}
	}

	-- Run and add mapped categories

	-- defined 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

	-- undefined categories
	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

	-- quest items
	-- just look for a link
	if args.quest.d:find('%[%[') then
		table.insert(ret,'Quest items')
	elseif args.quest.switches then
		for _, v in ipairs(args.quest.switches) do
			if v:find('%[%[') then
				table.insert(ret,'Quest items')
				break
			end
		end
	end

	-- extra func for death
	-- searching for 'always kept' would match more than what we want
	if args.kept.d == 'Always kept' then
		table.insert(ret,'Items that are always kept on death')
	elseif args.kept.switches then
		for _, v in ipairs(args.kept.switches) do
			if v == 'Always kept' then
				table.insert(ret,'Items that are always kept on death')
				break
			end
		end
	end
	-- ids
	if not catargs.id.all_defined then
		-- rsc ids have no id
		if catargs.rscid.all_defined then
			-- do nothing
		else
			table.insert(ret,'Needs ID')
		end
	end

	-- store
	if string.lower(args.store.d or '') ~= 'not sold' and not string.lower(args.store.d or 'edit'):find('edit') then
		table.insert(ret,'Pages that use Store')
	elseif args.store.switches then
		for _, v in ipairs(args.store.switches) do
			if string.lower(v or '') ~= 'not sold' and string.lower(v or '') ~= infobox.nil_param() and not string.lower(v or 'edit'):find('edit') then
				table.insert(ret,'Pages that use Store')
			end
		end
	end

	-- alchemy
	-- non alchable
	if args.alchable.d == false then
		table.insert(ret,'Items that cannot be alchemised')
	elseif args.alchable.switches then
		for _, v in ipairs(args.alchable.switches) do
			if v == false then
				table.insert(ret,'Items that cannot be alchemised')
				break
			end
		end
	end

	-- gemw
	-- if item is both (not untradeable) and (not GEMW) then add Non-GE items
	if not args.gemw.d and string.lower(args.tradeable.d) ~= 'no' then
		table.insert(ret, 'Non-GE items')
	end
	-- switches; if tradeable switches exist, if gemwX and tradeableX are as above, add Non-GE items
	--						if no switches, gemwX and tradeable (default)
	if args.gemw.switches then
		if args.tradeable.switches then
			for i, v in ipairs(args.gemw.switches) do
				if not v and string.lower(args.tradeable.switches[i] or args.tradeable.d) ~= 'no' then
					table.insert(ret, 'Non-GE items')
				end
			end
		else
			for i, v in ipairs(args.gemw.switches) do
				if not v and string.lower(args.tradeable.d) ~= 'no' then
					table.insert(ret, 'Non-GE items')
				end
			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