Module:Infotable Bonuses

From Roat Pkz
Jump to navigation Jump to search

Documentation for this module may be created at Module:Infotable Bonuses/doc

local p = {}

local trim = mw.text.trim
local gsplit = mw.text.gsplit
local split = mw.text.split
local listToText = mw.text.listToText

local yesNo = require('Module:Yesno')
local paramTest = require('Module:Paramtest')
local contains = require('Module:Array').contains
local pageListCheck = require('Module:PageListTools').pagelistchecks
local equipmentStats = require('Module:FetchItemStats').equipmentStats
local onMain = require('Module:Mainonly').on_main

-- Sorting keys
local sortKey = {
	'astab', 'aslash', 'acrush', 'amagic', 'arange',
	'dstab', 'dslash', 'dcrush', 'dmagic', 'drange',
	'str',   'mdmg',   'rstr',   'prayer', 'weight',
	'membs'
}

-- Sorting orders
local sortOrder = {
	'ascending', 'asc',
	'descending', 'desc', 'reverse',
	'random', 'rand'
}

function totalsFooter(tabl, totals, useComments)
	local row = tabl:tag('tr'):addClass('sortbottom'):tag('th'):attr('colspan', '2'):wikitext('Totals'):done()
	
	for i = 1, #sortKey, 1 do
		local cell = row:tag('td'):css('text-align', 'right')
		if(sortKey[i] == 'mdmg') then
			cell:wikitext(totals[i] .. '%'):done()
		elseif(sortKey[i] == 'weight') then
			local zeroTrimmedWeight, _ = tostring(totals[i]):gsub("%.0+$", "")
			cell:wikitext(zeroTrimmedWeight):done()
		elseif(sortKey[i] == 'membs') then
			cell:addClass('table-na nohighlight'):css('text-align', 'center'):wikitext('<small>N/A</small>')
		else
			cell:wikitext(totals[i])
		end
	end

	if(useComments) then
		row:tag('td'):done()
	end
end

function buildRow(pageData, totals, columnWidth, itemWidth, useComments, comment)
	local row = mw.html.create('tr')
		:tag('td'):cssText(columnWidth):wikitext(pageData['image'] and '[[' .. pageData['image'] .. '|link=|' .. split(pageData['name'], '#', true)[1] .. ']]' or ''):done()
		:tag('td'):cssText(itemWidth):wikitext('[[' .. split(pageData['name'], '#', true)[1] .. ']]'):done()

	for i = 1, #sortKey, 1 do
		local attribute = pageData[sortKey[i]]
		local cell = row:tag('td'):cssText(columnWidth)
		
		if(sortKey[i] == 'membs') then
				cell:wikitext(yesNo(attribute, false) and '[[File:Member icon.png|link=Members]]' or '[[File:Free-to-play icon.png|link=Free-to-play]]'):done()
		else
			if(not attribute) then
				cell:addClass('table-no'):addClass('nohighlight'):attr('data-sort-value', 0):wikitext('?'):done()
			else
				if(sortKey[i] == 'mdmg') then
					cell:wikitext(attribute .. '%'):done()
				elseif(sortKey[i] == 'weight') then
					local zeroTrimmedWeight, _ = tostring(attribute):gsub("%.0+$", "")
					cell:wikitext(zeroTrimmedWeight):done()
				else
					cell:wikitext(attribute):done()
				end
				totals[i] = totals[i] + tonumber(attribute)
			end
		end
	end
	
	if(useComments) then
		local cell = row:tag('td'):wikitext(comment):done()
	end
	
	return row, totals
end

function createHeader(tabl, useComments)
	local header = tabl:tag('tr'):tag('th'):attr({ colspan = '2', rowspan = '2' }):wikitext('Item'):done()
		:tag('th'):attr('colspan', '5'):wikitext('Attack Bonuses'):done()
		:tag('th'):attr('colspan', '5'):wikitext('Defence Bonuses'):done()
		:tag('th'):attr('colspan', '6'):wikitext('Other'):done()
		
	if(useComments) then
		header:tag('th'):attr('rowspan', '2'):wikitext('Comment'):done()
	end

	tabl:tag('tr'):tag('th'):wikitext('[[File:White dagger.png|link=|Stab attack]]'):done()
		:tag('th'):wikitext('[[File:White scimitar.png|link=|Slash attack]]'):done()
		:tag('th'):wikitext('[[File:White warhammer.png|link=|Crush attack]]'):done()
		:tag('th'):wikitext('[[File:Magic icon.png|link=|Magic attack]]'):done()
		:tag('th'):wikitext('[[File:Ranged icon.png|link=|Ranged attack]]'):done()
		:tag('th'):wikitext('[[File:White dagger.png|link=|Stab defence]]'):done()
		:tag('th'):wikitext('[[File:White scimitar.png|link=|Slash defence]]'):done()
		:tag('th'):wikitext('[[File:White warhammer.png|link=|Crush defence]]'):done()
		:tag('th'):wikitext('[[File:Magic icon.png|link=|Magic defence]]'):done()
		:tag('th'):wikitext('[[File:Ranged icon.png|link=|Ranged defence]]'):done()
		:tag('th'):wikitext('[[File:Strength icon.png|link=|Melee strength]]'):done()
		:tag('th'):wikitext('[[File:Magic Damage icon.png|link=|Magic damage]]'):done()
		:tag('th'):wikitext('[[File:Ranged Strength icon.png|link=|Ranged strength]]'):done()
		:tag('th'):wikitext('[[File:Prayer icon.png|link=|Prayer bonus]]'):done()
		:tag('th'):wikitext('[[File:Weight icon.png|link=|Weight]]'):done()
		:tag('th'):wikitext('[[File:Member icon.png|link=|Members]]'):done()
end

-- Is there a faster way to do this?
function sortPagesToInputOrder(pages, data)
	local pageLoc = {}
	for i, page in ipairs(pages) do
		pageLoc[page] = i
	end
	local ret = {}
	for i, v in ipairs(data) do
		if(pageLoc[v.name] == nil) then
			error(v.name .. ' is spelled wrong or does not match any input')
		else
			ret[pageLoc[v.name]] = v
		end
	end
	return ret
end

function p._main(args)
	
	local pages = {}
	for _, page in ipairs(args) do -- Iternates through unnamed args
		if(string.find(page, "_")) then
			page = string.gsub(page, '_', " ")
		end
		table.insert(pages, trim(page))
	end
	assert(#pages > 0, 'You must specify at least one item')

	local keys = {}
	if(paramTest.has_content(args['sort'])) then
		for key in gsplit(args['sort'], ',', true) do
			table.insert(keys, trim(key))
		end
	end
	if(yesNo(keys[1], true)) then
		for _, key in ipairs(keys) do
			assert(contains(sortKey, key), 'Invalid sorting key:"' .. key .. '"' .. tostring(yesNo(keys[1])))
		end
	end

	local orders = {}
	if(paramTest.has_content(args.order)) then
		for order in gsplit(args.order, ',', true) do
			table.insert(orders, trim(order))
		end
	end
	for _, order in ipairs(orders) do
		assert(contains(sortOrder, order), 'Invalid sorting order:' .. order)
	end
	
	assert(#orders == #keys or #orders == 0 or #keys == 0, 'The number of sort orders must match the number of sort keys or either can be zero')

	local useComments = false
	local comments = {}
	for i = 1, #pages, 1 do
		comment = args['comment'..tostring(i)]
		if(paramTest.has_content(comment)) then
			comments[i] = comment
			useComments = true
		end
	end

	local noHeader = yesNo(paramTest.default_to(args.noheader, false), false)
	local noTotals = yesNo(paramTest.default_to(args.nototals, false), false)
	local expensive = yesNo(paramTest.default_to(args.expensive, false), false)
	local columnWidth = paramTest.has_content(args.colwidth) and 'width:' .. args.colwidth or nil
	local itemWidth = paramTest.has_content(args.itemwidth) and 'width:' .. args.itemwidth or nil
	
	-- Checks if any input pages are redirects, invalid (red links), or duplicates of other page inputs.
	-- !!This is a resource expensive test, only use temporarily or site-wide for the purpose of maintenance.
	if(args.expensive) then
		local check = pageListCheck(pages)

		if((#check.invalid > 0) or (#check.redirect > 0) or (#check.duplicate > 0)) then
			local msg = string.format('Of the %d pages requested %d are non-existent (%s), %d are redirects (%s), and %d are duplicates (%s).',
				#pages,
				#check.invalid, (#check.invalid > 0) and listToText(check.invalid, ', ', ' and ') or '',
				#check.redirect, (#check.redirect > 0) and listToText(check.redirect, ', ', ' and ') or '',
				#check.duplicate, (#check.duplicate > 0) and listToText(check.duplicate, ', ', ' and ') or '')

			return error(msg .. '[[Category:Infotable Bonuses with multi-variant items]]')
		end
	end

	-- Fetch the data
	local data = equipmentStats(pages, keys, orders)

	-- Check for pages that are missing from the data. Sorting in SMW can lead to pages being removed from the results
	--  due to the page not having the property that is being sorted, or the property is set to a nil value.
	if(#data < #pages) then
		-- Find all pages not found by equipmentStats
		local pageListing = {}
		for _, page in ipairs(pages) do
			pageListing[page] = ''
		end
		for _, pageData in ipairs(data) do
			if(pageListing[pageData.name]) then
				pageListing[pageData.name] = nil
			end
		end
		local pageList = ''
		for key, _ in pairs(pageListing) do
			pageList = pageList .. key .. ' '
		end
	
		local msg = string.format('Of the %i pages requested, there is %i missing.(%s)%s',
			#pages, #pages - #data, pageList,
			(#keys > 0) and ' Try temporarily disabling sorting to see which items might have multiple variants.' or '')

		error(msg .. '[[Category:Infotable Bonuses with multi-variant items]]')
	end

	-- Check for items with multiple variants
	for _, pageData in ipairs(data) do
		-- Hijacking this loop to clean up the name parameter on page's with subobjects that contain underscores (smw adds an underscore)
		--  This is faster than gsub on every name
		if(string.find(pageData.name, "_")) then
			pageData.name = string.gsub(pageData.name, '_', " ")
		end
		if(pageData['subobj']) then
			local msg = string.format('Item \'[[%s]]\' have multiple variants; please specify one of them: %s',
				pageData['name'], listToText(pageData['subobj'], ', ', ' or '))
			error(msg .. tostring('[[Category:Infotable Bonuses with multi-variant items]]'))
		end
	end

	if((#orders == 0) and (#keys == 0)) then
		data = sortPagesToInputOrder(pages, data)
	end

	local ret = mw.html.create('table'):addClass('wikitable sortable infotable-bonuses align-center-1 align-left-2 align-right-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')

	if(not noHeader) then
		createHeader(ret, useComments)
	end

	-- See sortKey for labels, members is excluded
	local totals = {
		0, 0, 0, 0, 0,
		0, 0, 0, 0, 0,
		0, 0, 0, 0, 0,
	}

	local rowCount = 0
	for i, pageData in ipairs(data) do
		row, totals = buildRow(pageData, totals, columnWidth, itemWidth, useComments, comments[i])
		ret:node(row)
		rowcount = rowCount + 1
	end
	if(rowCount == #pages) then
		error('The number of inputs does not match the output. ' .. math.max(#pages, rowCount) - math.min(#pages, rowCount) .. ' items affected.')
	end
		
	if(not noTotals) then
		totalsFooter(ret, totals, useComments)
	end

	return ret
end

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

--[[ DEBUG =
mw.logObject( p.loadData({'Beach boxing gloves#Yellow', 'Boxing gloves#Red'}, {}, {}) )
mw.logObject( p.loadData({'Iron pickaxe', 'Steel pickaxe'}, {'arange', 'drange'}, {'desc', 'desc'}) )
= p._main({'Verac\'s brassard#Undamaged', 'Verac\'s flail#Undamaged', 'Verac\'s helm#Undamaged', 'Verac\'s plateskirt#Undamaged'})
= p._main({'3rd age full helmet', '3rd age platebody', '3rd age platelegs', '3rd age kiteshield', '3rd age longsword', sort='dstab,str', order='asc,asc'})
--]]

return p