Module:Sandbox/User:Fjara/Sandbox/Shovel

From Illerai
Jump to navigation Jump to search

Documentation for this module may be created at Module:Sandbox/User:Fjara/Sandbox/Shovel/doc

local p = {}
	--0: csv "name (parenthetical)"-only

function createHeader(verbosity)
	local ret = mw.html.create('table'):addClass('wikitable sortable')
		:addClass('align-center-1 align-left-2 align-center-3 align-center-4')
	
	local header = mw.html.create('tr')
		:tag('th'):css('text-align', 'center'):attr('colspan', 2):wikitext('Monster'):done()
		:tag('th'):wikitext('[[File:Member icon.png|link=|Members]]'):done()
		:tag('th'):wikitext('[[File:Attack style icon.png|link=|Combat level]]'):done()

	if(tonumber(verbosity) > 1) then
		ret:addClass('align-center-5 align-center-6 align-center-7 align-center-8 align-center-9 align-center-10')
		header:tag('th'):wikitext('[[File:Hitpoints icon.png|link=|Hitpoints]]'):done()
			:tag('th'):wikitext('[[File:Attack icon.png|link=|Attack level]]'):done()
			:tag('th'):wikitext('[[File:Strength icon.png|link=|Strength level]]'):done()
			:tag('th'):wikitext('[[File:Defence icon.png|link=|Defence level]]'):done()
			:tag('th'):wikitext('[[File:Magic icon.png|link=|Magic level]]'):done()
			:tag('th'):wikitext('[[File:Ranged icon.png|link=|Ranged level]]'):done()
		
	elseif(tonumber(verbosity) > 2) then
		ret:addClass('align-center-11 align-center-12 align-center-13 align-center-14 align-center-15 align-center-16 align-center-17 align-center-18 align-center-19 align-center-20 align-center-21 align-center-22 align-center-23 align-center-24 align-center-25 align-center-26 align-center-27 align-center-28 align-center-29 align-center-30 align-center-31 align-center-32 align-center-33')
		header:tag('th'):wikitext('[[File:Attack icon.png|link=|Attack bonus]]'):done()
			:tag('th'):wikitext('[[File:Strength icon.png|link=|Strength bonus]]'):done()
			:tag('th'):wikitext('[[File:Magic icon.png|link=|Magic bonus]]'):done()
			:tag('th'):wikitext('[[File:Magic Damage icon.png|link=|Magic strength bonus]]'):done()
			:tag('th'):wikitext('[[File:Ranged icon.png|link=|Ranged bonus]]'):done()
			:tag('th'):wikitext('[[File:Ranged Strength icon.png|link=|Ranged Strength bonus]]'):done()
			:tag('th'):wikitext('[[File:White dagger.png|link=|Defensive stab bonus]]'):done()
			:tag('th'):wikitext('[[File:White scimitar.png|link=|Defensive slash bonus]]'):done()
			:tag('th'):wikitext('[[File:White warhammer.png|link=|Defensive crush bonus]]'):done()
			:tag('th'):wikitext('[[File:Magic icon.png|link=|Defensive Magic bonus]]'):done()
			:tag('th'):wikitext('[[File:Ranged icon.png|link=|Defensive Ranged bonus]]'):done()
			:tag('th'):wikitext('[[File:Poison hitsplat.png|link=|Poison immunity]]'):done()
			:tag('th'):wikitext('[[File:Venom hitsplat.png|link=|Venom immunity]]'):done()
			:tag('th'):wikitext('[[File:Slayer icon.png|link=|Slayer level]]'):done()
			:tag('th'):wikitext('[[File:Slayer icon.png|link=|Slayer experience]]Experience'):done()
			:tag('th'):css('text-align', 'left'):wikitext('Slayer categories'):done()
			:tag('th'):css('text-align', 'left'):wikitext('Assigned by'):done()
			:tag('th'):css('text-align', 'left'):wikitext('Experience bonus'):done()
			:tag('th'):css('text-align', 'left'):wikitext('[[Combat Style]]'):done()
			:tag('th'):css('text-align', 'center'):wikitext('[[Attack speed]]'):done()
			:tag('th'):css('text-align', 'center'):wikitext('Inflicts [[poison]]'):done()
			:tag('th'):css('text-align', 'center'):wikitext('Aggressiveness'):done()
			:tag('th'):css('text-align', 'center'):wikitext('Size'):done()
	end
	ret:node(header):done()
	return ret
end

-- Estimate the storage size of a serialized table of data
-- This function assumes somewhat consistent sizes of the elements
-- like for instance typical SMW data tables
function p.est_serial_size(tbl, datapoints)
	assert(type(tbl) == 'table')
	assert(type(datapoints) == 'number')

	local dp = {}

	for p = 1, datapoints do
		local pos = math.floor(#tbl * p / datapoints)
		local txt = mw.text.jsonEncode(tbl[pos])
		table.insert(dp, txt:len())
	end

	local avgsize = math.floor(enum.sum(dp) / datapoints)

	return #tbl * avgsize
end

-- Filter data with exclusion lists
function p.filterData(indata, disco, quest, dmm, cache)
	-- Fetch exclusion list
	local exlist = {}

	if not disco then
		table.insert(exlist, '[[Category:Monsters]] [[Category:Discontinued content]]')
	end
	if not quest then
		table.insert(exlist, '[[Category:Quest monsters]]')
	end
	if not dmm then
		table.insert(exlist, '[[Category:Monsters]] [[Category:Deadman Mode]]')
	end
	if not cache then
		table.insert(exlist, '[[Category:Monsters]] [[Category:Pages using information from game APIs or cache]]')
	end

	local pages_excl = pageswithcats(exlist)

	-- Post-process the data
	local data = {}

	for _, entry in ipairs(indata) do
		local process = true

		if enum.contains(pages_excl, entry['variantof']) or enum.contains(pages_excl, entry['name']) then
			process = false
		end

		if process then
			table.insert(data, entry)
		else
			--mw.log(string.format('Removed: %s', entry[1]))
		end
	end

	-- Statistics
	mw.log(string.format('Filter: exclusion list size: %i, start size: %i, end size: %i, removed %i.', 
		#pages_excl, #indata, #data, #indata - #data))

	return data
end

local yesNo = require('Module:Yesno')
local paramTest = require('Module:Paramtest')
local hasContent = require('Module:Paramtest').has_content
local enum = require('Module:Array')
local pagelisttools = require('Module:PageListTools').pageswithcats

local SlayerMasters = { 'Turael', 'Spria', 'Krystilia', 'Mazchna', 'Vannaka', 'Chaeldar', 'Konar quo Maten', 'Nieve', 'Steve', 'Duradel' }
local SlayerTasks = { 'All', 'Aberrant Spectres', 'Abyssal Demons', 'Ankou', 'Aviansies', 'Bandits', 'Banshees', 'Basilisks', 'Bats', 'Bears', 'Birds', 'Black Demons', 'Black Dragons', 'Black Knights', 'Bloodveld', 'Blue Dragons', 'Bosses', 'Brine Rats', 'Bronze Dragons', 'Catablepon', 'Cave Bugs', 'Cave Crawlers', 'Cave Horrors', 'Cave Kraken', 'Cave Slimes', 'Chaos Druids', 'Cockatrice', 'Cows', 'Crawling Hands', 'Crocodiles', 'Dagannoths', 'Dark Beasts', 'Dark Warriors', 'Dogs', 'Drakes', 'Dust Devils', 'Dwarves', 'Earth Warriors', 'Elves', 'Ents', 'Fever Spiders', 'Fire Giants', 'Flesh Crawlers', 'Fossil Island Wyverns', 'Gargoyles', 'Ghosts', 'Ghouls', 'Goblins', 'Greater Demons', 'Green Dragons', 'Harpie Bug Swarms', 'Hellhounds', 'Hill Giants', 'Hobgoblins', 'Hydras', 'Ice Giants', 'Ice Warriors', 'Icefiends', 'Infernal Mages', 'Iron Dragons', 'Jellies', 'Jungle Horrors', 'Kalphite', 'Killerwatts', 'Kurask', 'Lava Dragons', 'Lesser Demons', 'Lizardmen', 'Lizards', 'Magic Axes', 'Mammoths', 'Minotaurs', 'Mithril Dragons', 'Mogres', 'Molanisks', 'Monkeys', 'Moss Giants', 'Mutated Zygomites', 'Nechryael', 'Ogres', 'Otherworldly Beings', 'Pirates', 'Pyrefiends', 'Rats', 'Red Dragons', 'Revenants', 'Rockslugs', 'Rogues', 'Rune Dragons', 'Scabarites', 'Scorpions', 'Sea Snakes', 'Shades', 'Shadow Warriors', 'Skeletal Wyverns', 'Skeletons', 'Smoke Devils', 'Sourhogs', 'Spiders', 'Spiritual Creatures', 'Steel Dragons', 'Suqahs', 'Terror Dogs', 'Trolls', 'Turoth', 'TzHaar', 'Vampyres', 'Wall Beasts', 'Waterfiends', 'Werewolves', 'Wolves', 'Wyrms', 'Zombies' }
local Attributes = { 'demon', 'draconic', 'fiery', 'golem', 'kalphite', 'leafy', 'penance', 'shade', 'spectral', 'undead', 'vampyre', 'vampyre1', 'vampyre2', 'vampyre3', 'xerician' }
local MemberOptions = {'members', 'f2p', 'all'}
local AlphaOnlyPattern = '^%a$'

function shouldExclude(property, exclusionList)
	for _, exclusion in ipairs(exclusionList) do
		if((paramTest.has_content(exclusion)) and (ustring.find(string.lower(untable(property) or ''), string.lower(trim(exclusion))))) then
			return true
		end
	end
	return false
end

function tableConcat(mainTable, newTable, exclusionList)
    for i = 1, #newTable, 1 do
    	-- 
    	if(true) then
    		mainTable[#mainTable+1] = newTable[i]
    	end
    end
    return mainTable
end


function loadData(batchSize, limit, slayerOutput, members, assignedBy, slayerTask, attribute, fromLevel, toLevel, fromLetter, toLetter, cache, dmm, quest, discontinued)
	local query = {
		'[[Category:Monsters]]',
		'[[Combat level::≥' .. fromLevel .. ']] [[Combat level::≤' .. toLevel .. ']]', -- How bad is it to add this even if it doesnt filter
		'[[Combat level::≥' .. fromLevel .. ']] [[Combat level::≤' .. toLevel .. ']]', -- How bad is it to add this even if it doesnt filter
		limit = batchSize,
		offset = 0,
	}
	
	if(members == 'members') then
		table.insert(query, ' [[Is members only::true]]')
	elseif(members == 'f2p') then
		table.insert(query, ' [[Is members only::false]]')
	end
	
	if(assignedBy) then
		table.insert(query, ' [[Assigned by::' .. assignedBy .. ']]')
	end
	
	if(slayerTask) then
		if(slayerTask == 'All') then
			table.insert(query, ' [[Slayer category::+]]')
		else
			table.insert(query, ' [[Slayer category::' .. slayerTask .. ']]')
		end
	end
	
	if(attribute) then
		table.insert(query, ' [[Monster attribute::' .. attribute .. ']]')
	end
	
	if(cache) then
		 table.insert(query, ' [[Category:Pages using information from game APIs or cache]]')
	end
	
	if(dmm) then
		 table.insert(query, ' [[Category:Deadman Mode]]')
	end
	
	if(quest) then
		table.insert(query, ' [[Category:Quest monsters]]')
	end
	
	if(discontinued) then
		table.insert(query, ' [[Category:Discontinued content]]')
	end
	
	table.insert(query, '?=#-')
	
	table.insert(query, '?Image#-=image')
	table.insert(query, 'Is members only#-=members')
	table.insert(query, 'Combat level#-=level')
	table.insert(query, 'Hitpoints#-=hitpoints')
	table.insert(query, 'Attack level#-=attack')
	table.insert(query, 'Defence level#-=defence')
	table.insert(query, 'Magic level#-=magic')
	table.insert(query, 'Ranged level#-=ranged')
	table.insert(query, 'Is variant of#-=variantof')
	
	if(slayerOutput) then
		table.insert(query, 'Slayer level#-=slayerLevel')
		table.insert(query, 'Slayer experience#-=slayerXP')
		table.insert(query, 'Slayer category#-=slayerCat')
		table.insert(query, 'Assigned by#-=assignedBy')
	end

	local allData = {}
	for i = 0, limit, batchSize do
		query.limit = batchSize
		query.offset = i
		
		local t1 = os.clock()
		local smwData = mw.smw.ask(query)
		local t2 = os.clock()
		if(smwData == nil) then break end
		mw.log(string.format('SMW: entries %d, time elapsed: %.3f ms.', #smwData, (t2 - t1) * 1000))
		
		tableConcat(allData, smwData, exclusionList)
	end

	assert(allData ~= nil and #allData > 0, 'SMW query failed')

	for _, monster in ipairs(allData) do
		monster['name'] = monster[1]
		monster[1] = nil
		if(type(monster['image']) == 'table') then
			monster['image'] = monster['image'][1]
		end
		if(type(monster['slayerCat']) == 'table') then
			monster['slayerCat'] = table.concat(monster['slayerCat'], ', ')
		end
		if(type(monster['assignedBy']) == 'string') then
			monster['assignedBy'] = { monster['assignedBy'] }
		elseif type(dataline['assignedby']) == 'nil' then
			monster['assignedBy'] = { }
		end

	end

	return allData
end

function p._main(args)
	local batchSize = paramTest.default_to(tonumber(args.batchsize), 100)
	local limit = paramTest.default_to(tonumber(args.limit), 5000) -- Acting as a real limiter
	
	local csvOutput = yesNo(args.csv or '', true)
	local slayerOutput = yesNo(args.slayer or '', true)
	
	local members = contains(MemberOptions, args.members) and args.members or 'all'
	local assignedBy = contains(SlayerMasters, args.assignedby) and args.assignedby or ''
	local slayerTask = contains(SlayerTasks, args.slayertask) and args.slayertask or 'All' --force uppercase all words
	local attribute = contains(Attributes, args.attribute) and args.attribute or ''
	
	local levelSort = yesNo(args.levelsort or '', true) -- If false, alphabetical sort
	local fromLevel = paramTest.default_to(tonumber(args.fromlevel), 0)-- should these not default
	local toLevel = paramTest.default_to(tonumber(args.tolevel), 99999)-- should these not default
	local fromLetter = args.fromletter:match(AlphaOnlyPattern) and args.fromletter:upper() or 'A'-- should these not default
	local toLetter = args.toletter:match(AlphaOnlyPattern) and args.toletter:upper() or 'Z'-- should these not default
	
	assert(fromLevel > toLevel, 'fromlevel needs to be less then or equal to tolevel')
	assert(fromLetter > toLetter, 'fromletter needs to be the same or earlier than toletter')
	
	local cache = yesNo(args.cache or '', false)
	local dmm = yesNo(args.dmm or '', false)
	local quest = yesNo(args.quest or '', true)
	local discontinued = yesNo(args.discontinued or '', false)

	local data = loadData(batchSize, limit, slayerOutput, members, assignedBy, slayerTask, attribute, fromLevel, toLevel, fromLetter, toLetter, cache, dmm, quest, discontinued)
	data = filterData(data, ba['disco'], ba['quest'], ba['dmm'], ba['cache'])
	

	if ba['levelsort'] then
		table.sort(data, function(a, b) return a['level'] < b['level'] end)
	end

	-- Format the output page
	local div = mw.html.create('div')

	if #data == 0 then
		div:wikitext('Search yielded no results.')
		return div
	elseif ba['showstats'] then
		div:wikitext(string.format('Search yielded %i results.', #data))
		if smwstats['found'] == smwstats['limit'] then
			div:wikitext(string.format(' Your search might have been too large and have been truncated as a result.'))
		end
	end

	local tbl = div:tag('table'):addClass('wikitable sortable')
		:addClass('align-center-1 align-left-2 align-left-3 align-center-4 align-center-5')
		:addClass('align-center-6 align-center-7 align-center-8 align-center-9')

	if ba['verbose'] then
		tbl:addClass('align-center-10 align-center-11 align-left-12 align-left-13')
	end

	p.header(tbl, ba['verbose'])

	-- Render rows
	for e, entry in ipairs(data) do
		local name = mw.text.split(entry['name'], '#', true)
		if name[2] then name[2] = name[2]:gsub('_', ' ') else name[2] = '&nbsp;' end
		local tr = tbl:tag('tr')
			:tag('td'):css('height', '64px'):wikitext(entry['image'] and string.format('[[%s|link=|64x64px|%s]]', entry['image'], entry['name']) or ''):done()
			:tag('td'):wikitext(string.format('[[%s|%s]]<br/>\'\'%s\'\'', entry['name'], name[1], name[2])):done()
			:tag('td'):wikitext(entry['members'] and '[[File:Member icon.png|link=|Members]]' or '[[File:Free-to-play icon.png|link=|Free-to-play]]'):done()
			:tag('td'):wikitext(entry['level']):done()
			:tag('td'):wikitext(entry['hitpoints']):done()
			:tag('td'):wikitext(entry['attack']):done()
			:tag('td'):wikitext(entry['defence']):done()
			:tag('td'):wikitext(entry['magic']):done()
			:tag('td'):wikitext(entry['ranged']):done()
			
		if ba['verbose'] then
			tr:tag('td'):wikitext(entry['slaylvl'] or '1'):done()
			  :tag('td'):wikitext(entry['slayxp']):done()
			  :tag('td'):wikitext(entry['slaycat']):done()

			local td = tr:tag('td')
			for a, ab in ipairs(entry['assignedby']) do
				if enum.contains(slayer_masters, ab) then
					td:wikitext(string.format('[[File:%s chathead.png|48x64px|link=%s]]', ab, ab))
				else
					mw.log(string.format('unknown slayer master: %s', ab))
				end
			end
		end
	end

	return div
end

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

--[[ DEBUG COPYPASTA
mw.logObject( p.loadData('1', '1', 'A', 'A', true, 'All', false, '', '', '', false) )
mw.logObject( p.loadData('1', '1', 'A', 'A', false, 'All', false, '', '', '', false) )
= p._main({fromlevel='1', tolevel='1', verbose='yes'})
= p._main({fromletter='A', toletter='A', levelselect='no', levelsort='no', verbose='yes'})
--]]

return p