Module documentation
This documentation is transcluded from Template:No documentation/doc. [edit] [history] [purge]
This module does not have any documentation. Please consider adding documentation at Module:Slottable/doc. [edit]
Module:Slottable's function main is invoked by Template:Slottable.

local p = {}

require('Module:Mw.html extension')

local yesNo = require('Module:Yesno')
local paramTest = require('Module:Paramtest')
local contains = require('Module:Array').contains
local minimum = require('Module:Array').min
local pagesWithCats = require('Module:PageListTools').pageswithcats
local pagesWithConditions = require('Module:PageListTools').pageswithconditions
local editbtn = require('Module:Edit button')

local memberOptions = {'members', 'f2p', 'all'}
local slotOptions = {'head', 'cape', 'neck', 'ammo', 'weapon', 'shield', 'body', 'legs', 'hands', 'feet', 'ring', '2h'}
local statOptions = {'astab', 'aslash', 'acrush', 'amagic', 'arange', 'dstab', 'dslash', 'dcrush', 'dmagic', 'drange', 'str', 'rstr', 'mdmg', 'prayer'}

function statFormat(_arg, default)
	local arg = tonumber(_arg)
	if(not arg) then return (default or _arg) end
	if(arg < 0) then return tostring(arg) end
	return '+' .. arg
end

function buildRow(item, attackSpeedColumn, uim)
	local row = mw.html.create('tr')
		:tag('td'):wikitext(item.image and '[[' .. item.image .. '|link=|' .. item.name .. ']]' or ''):done()
		:tag('td'):wikitext('[[' .. item.name .. ']]'):done()
		:tag('td'):wikitext(item.members and '[[File:Member icon.png|link=|Members]]' or '[[File:Free-to-play icon.png|link=|Free-to-play]]'):done()

	for _, stat in ipairs(statOptions) do
		local statNum = tonumber(item[stat])
		row:tag('td'):wikitext((statFormat(item[stat]) or editbtn("'''?''' (edit)", item.name)) .. (stat == 'mdmg' and '%' or ''))
			:addClassIf(statNum and (statNum > 0), 'table-positive')
			:addClassIf(statNum and (statNum < 0), 'table-negative')
	end
	
	local weightNum = tonumber(item.weight)
	row:tag('td'):wikitext(item.weight)

	if(attackSpeedColumn) then
		-- if((item.speed == nil) or (item.speed < 0)) then
		-- 	row:tag('td'):addClass('table-na nohighlight'):css('text-align', 'center'):wikitext('<small>N/A</small>')
		-- else
		-- 	row:tag('td'):wikitext(item.speed):done()
		-- end
		if item.speed == nil or (tonumber(item.speed) and tonumber(item.speed) < 0) then
			row:tag('td'):addClass('table-na nohighlight'):css('text-align', 'center'):wikitext('<small>N/A</small>')
		else
			row:tag('td'):wikitext(item.speed):done()
		end
	end
	
	if(uim) then
		local storeability = 'Shop'
		if(item.emote) then
			storeability = 'STASH'
		elseif(item.costume) then
			storeability = 'POH'
		end
		row:tag('td'):wikitext(storeability):done()
	end

	return row
end

function createHeader(slotName, attackSpeedColumn, uim)
	local tabl = mw.html.create('table'):addClass('wikitable lighttable sortable sticky-header align-center-1 align-left-2 align-center-3 align-right-4 align-right-5 align-right-6 align-right-7 align-right-8 align-right-9 align-right-10 align-right-11 align-right-12 align-right-13 align-right-14 align-right-15 align-right-16 align-right-17 align-center-18'):done()
	local header = tabl:tag('tr')
	header:tag('th'):attr('colspan', '2'):wikitext('Name'):done()
		:tag('th'):wikitext('[[File:Member icon.png|link=|Members]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:White dagger.png|link=|Stab attack]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:White scimitar.png|link=|Slash attack]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:White warhammer.png|link=|Crush attack]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Magic icon.png|link=|Magic attack]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Ranged icon.png|link=|Ranged attack]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:White dagger.png|link=|Stab defence]]<sup>[[File:Defence icon.png|link=]]</sup>'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:White scimitar.png|link=|Slash defence]]<sup>[[File:Defence icon.png|link=]]</sup>'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:White warhammer.png|link=|Crush defence]]<sup>[[File:Defence icon.png|link=]]</sup>'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Magic icon.png|link=|Magic defence]]<sup>[[File:Defence icon.png|link=]]</sup>'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Ranged icon.png|link=|Ranged defence]]<sup>[[File:Defence icon.png|link=]]</sup>'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Strength icon.png|link=|Strength]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Ranged Strength icon.png|link=|Ranged Strength]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Magic Damage icon.png|link=|Magic Damage]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Prayer icon.png|link=|Prayer]]'):done()
		:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Weight icon.png|link=|Weight]]'):done()
		
	if(attackSpeedColumn) then
		header:tag('th'):attr('data-sort-type', 'number'):wikitext('[[File:Watch.png|link=|Speed]]'):done()
	end
	
	if(uim) then
		header:tag('th'):wikitext('[[File:Marble magic wardrobe icon.png|link=]]Stored/Buy'):done()
	end
	
	return tabl
end

function filterData(data, slotName, options)

	if(slotName == 'ammo') then slotName = 'Ammunition' end -- hacky shit until ammo/ammunition is resolved
	slotCat = '[[Category:' .. paramTest.ucfirst(slotName) .. ' slot items]]'
	if slotName == '2h' then
		slotCat = '[[Category:Two-handed slot items]]'	
	end
	local exclusionListCategories = {
		{ options.beta,			slotCat .. '[[Category:Beta items]]' },
		{ options.discontinued,	slotCat .. '[[Category:Discontinued content]]' },
		{ options.dmm,			slotCat .. '[[Category:Deadman seasonal items]]' },
		{ options.emir,			slotCat .. '[[Category:Emir\'s Arena]]' },
		{ options.failedPoll,	slotCat .. '[[Category:Pages containing information from failed polls]]' },
		{ options.gauntlet,		slotCat .. '[[Category:The Gauntlet]]' },
		{ options.lms,			slotCat .. '[[Category:Last Man Standing]]' },
		{ options.quest,		slotCat .. '[[Category:Quest items]]' },
	}

	local exclusionList = { }
	for _, excl in ipairs(exclusionListCategories) do
		if(not excl[1]) then
			table.insert(exclusionList, excl[2])
		end
	end
	local pagesToExclude = #exclusionList > 0 and pagesWithCats(exclusionList) or {}
	local emotePagesToInclude, costumePagesToInclude
	if(options.uim) then
		emotePagesToInclude = pagesWithCats({ slotCat .. '[[Category:Items needed for an emote clue]]' }) or {}
		costumePagesToInclude = pagesWithCats({ slotCat .. '[[Category:Items storable in the costume room]]' }) or {}
	end

	-- Filter the data
	local retData = {} 

	for _, item in ipairs(data) do 
		local keep = true

		if(((options.members == 'members') and (item['members'] == false)) or
			((options.members == 'f2p') and (item['members'] == true)) or
			((contains(pagesToExclude, item['variantof'])) or (contains(pagesToExclude, item['name'])))) then
			keep = false
		end

		if(options.uim) then
			if((not (contains(emotePagesToInclude, item['variantof']) or (contains(emotePagesToInclude, item['name'])))) and
				(not (contains(costumePagesToInclude, item['variantof']) or (contains(costumePagesToInclude, item['name'])))) and
				(next(pagesWithConditions('[[Sells item::' .. item.name .. ']]')) == nil)) then
				keep = false
			elseif(contains(costumePagesToInclude, item['variantof']) or (contains(costumePagesToInclude, item['name']))) then
				item.costume = true
			elseif(contains(emotePagesToInclude, item['variantof']) or (contains(emotePagesToInclude, item['name']))) then
				item.emote = true
			end
		end

		if(keep) then
			table.insert(retData, item)
		end
		
	end
	
	mw.log(string.format('Filter: exclusion list size: %i, start size: %i, end size: %i, removed %i.', #pagesToExclude, #data, #retData, #data - #retData))

	return retData
end

function loadData(slotName, attackSpeed, members)
	local query = {
		'[[Equipment slot::' .. slotName .. ']]',
		'?=#-',
		'?Stab attack bonus#-=astab',
		'?Slash attack bonus#-=aslash',
		'?Crush attack bonus#-=acrush',
		'?Magic attack bonus#-=amagic',
		'?Range attack bonus#-=arange',
		'?Stab defence bonus#-=dstab',
		'?Slash defence bonus#-=dslash',
		'?Crush defence bonus#-=dcrush',
		'?Magic defence bonus#-=dmagic',
		'?Range defence bonus#-=drange',
		'?Strength bonus#-=str',
		'?Magic Damage bonus#-=mdmg',
		'?Ranged Strength bonus#-=rstr',
		'?Prayer bonus#-=prayer',
		'?Weight#-=weight',
		'?Is members only#-=members',
		'?Image#-=image',
		'?Is variant of#-=variantof',
		offset = 0,
		limit = 1000,
	}
	
	if(attackSpeed) then
		table.insert(query, '?Weapon attack speed#-=speed')
	end

	local t1 = os.clock()
	local smwData = mw.smw.ask(query)
	local t2 = os.clock()
	assert(smwData ~= nil and #smwData > 0, 'SMW query failed')

	for _, item in ipairs(smwData) do 
		-- Rename the first parameter to name for clarity and ease of use
		item['name'] = item[1]
		item[1] = nil
		
		if(item['image'] == nil) then
			local hasDefaultFile = mw.title.new(item['name'] .. '.png', 'File'):getContent()
			item['image'] = hasDefaultFile and 'File:' .. item['name'] .. '.png' or ''
		elseif(type(item['image']) == 'table') then
			item['image'] = item['image'][1]
		end
		-- Fix members values by defaulting when running into issue
		if(type(item.members) == 'boolean') then
			-- Short circuit
		elseif(item.members == nil) then
			item.members = false
		elseif(type(item.members) == 'table') then
			for _, mems in ipairs(item.members) do
				if(mems == false) then
					item.members = false
					break
				end
			end
		end
		-- Fix weights with multiple values (Max cape), this may do nothing and is precautionary
		if(item.weight == nil) then
			item.weight = 0
		elseif(type(item.weight) == 'table') then
			item.weight, _ = minimum(item.weight)
		end
	end

	mw.log(string.format('SMW: entries %d, time elapsed: %.3f ms.', #smwData, (t2 - t1) * 1000))

	return smwData
end

-- JSON data dump entry-point
function p.dumpData(frame)
	local args = frame:getParent().args
	return p._dumpData(args)
end

function p._dumpData(args)
	local slot = args.slot
	assert(contains(slotOptions, slot), 'Invalid slot specified')

	local data = loadData(slot, true)

	-- Tests indicate that JSON pretty-printing increases the size by a factor of 1.78.
	-- Removing indenting results in an increase in size by a factor of 1.15.
	-- The latter is a very reasonable trade-off to make the files more wiki-friendly.
	local prefix = string.format('-- Data for item slot \'%s\' @ %s.\n-- Generated by Module:Slottable, function dumpData()\nreturn mw.text.jsonDecode([=[\n', slot, os.date('%F %T', os.time()))
	local rawjson = mw.text.jsonEncode(data, mw.text.JSON_PRETTY)
	local jsondata, subst = rawjson:gsub('\n%s+', '\n')
	local postfix = '\n]=])\n'

	mw.log(string.format('Dumping JSON data for item slot \'%s\'. Raw size: %d bytes, formatted size: %d bytes (factor: %.2f).', slot, rawjson:len(), jsondata:len(), jsondata:len() / rawjson:len()))

	return prefix .. jsondata .. postfix
end


-- Turtle data dump entry-point
function p.dumpDataTTL(frame)
	local args = frame:getParent().args
	return p._dumpDataTTL(args)
end

function p._dumpDataTTL(args)
	local slot = args.slot
	assert(contains(slotOptions, slot), 'Invalid slot specified')

	local data = loadData(slot, true)

    local slot_
    if(slot == "2h") then
    	slot_ = "zweihander"
    else
    	slot_ = slot
    end

	local prefix = string.format('# Data for item slot \'%s\' @ %s.\n# Generated by Module:Slottable, function dumpDataTTL()\n\n', slot, os.date('%F %T', os.time()))
	prefix = prefix .. "@prefix " .. slot_ .. ": <http://oldschool.runescape.wiki/rdf/" .. slot_ .. "/> .\n"
	prefix = prefix .. "@prefix prop: <http://oldschool.runescape.wiki/rdf/prop/> .\n\n"

	local ttlData = ""
	for _, i in ipairs(data) do
		ttlData = ttlData .. slot_ .. ":" .. _ .. " " .. "prop:slot \"" .. slot_ .. "\"; "
		local outl = {}
		for k, v in pairs(i) do
			if((k == "image") or (k == "name") or (k == "variantof")) then
				table.insert(outl, "prop:" .. k .. " \"" .. tostring(v) .. "\"")
			else
				table.insert(outl, "prop:" .. k .. " " .. tostring(v))
			end
		end
		ttlData = ttlData .. table.concat(outl, "; ") .. " .\n"
	end

	local postfix = '\n\n'
	mw.log(string.format('Dumping Turtle data for item slot \'%s\'. Size: %d bytes.', slot, ttlData:len()))

	return prefix .. ttlData .. postfix
end

function p._main(args)
	local slot = string.lower(paramTest.default_to(args.slot, ''))
	local members = string.lower(paramTest.default_to(args.members, 'all'))

	assert(contains(slotOptions, slot), 'Invalid slot specified')
	assert(contains(memberOptions, members), 'Invalid members status specified')

	local beta = yesNo(paramTest.default_to(args.beta, false), false)
	local discontinued = yesNo(paramTest.default_to(args.discontinued, false), false)
	local dmm = yesNo(paramTest.default_to(args.dmm, false), false)
	local emir = yesNo(paramTest.default_to(args.emir, false), false)
	local failedPoll = yesNo(paramTest.default_to(args.failedpoll, false), false)
	local gauntlet = yesNo(paramTest.default_to(args.beta, false), false)
	local lms = yesNo(paramTest.default_to(args.lms, false), false)
	local quest = yesNo(paramTest.default_to(args.quest, true), true)

	-- UIM specific tables, that only display items that can be:
	--  purchased, stored in the POH, or stored in STASH untsi
	local uim = yesNo(paramTest.default_to(args.uim, false), false)

	local attackSpeed = false
	if((slot == '2h') or (slot == 'weapon')) then
		attackSpeed = true
	end

	local data = loadData(slot, attackSpeed, members, uim)
	data = filterData(data, slot, {members = members, uim = uim, beta = beta, discontinued = discontinued, dmm = dmm, emir = emir, failedPoll = failedPoll, gauntlet = gauntlet, lms = lms,  quest = quest })
	
	local ret = createHeader(slot, attackSpeed, uim)
	for _, item in ipairs(data) do
		ret:node(buildRow(item, attackSpeed, uim))
	end

	return ret
end

function p.main(frame)
	local args = frame:getParent().args
	return p._main(args)
end

--[[ DEBUG
mw.logObject(p.getData('weapon'))
p._main({slot='weapon', members='all'})
p._dumpData({slot='weapon'})
p._dumpDataTTL({slot='weapon'})
--]]

return p