Module:CharacterTier/Dev

From Granblue Fantasy Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:CharacterTier/Dev/doc

local p = {}
local ROW_GRANULARITY = 0.5
local LIST_COLUMNS = {race=1, weapon=1, obtain=1}
local getArgs = require('Module:Arguments').getArgs

function p.render(frame)
	mw.log('Module:CharacterTier: render start: ' .. os.clock())

	-- make a list of filters
	local filter = {}
	for ix,fil in pairs({'rarity','element','type','race','rating'}) do
		if frame.args[fil] then
			filter[fil] = frame.args[fil]
		end
	end

	-- fetch character ratings
	mw.log('Module:CharacterTier: fetch characters start: ' .. os.clock())
	local data = p.fetchCharacters(frame, filter)
	mw.log('Module:CharacterTier: fetch characters end: ' .. os.clock())
	
	-- do we have multiple elements?
	local elements = {}
	local elementcount = 0
	for ix,character in pairs(data) do
		if (character.element ~= nil) and not (elements[character.element]) then
			elements[character.element] = true
			elementcount = elementcount + 1
		end
	end

	local result = {}

	if frame.args.template ~= nil then
		for key,character in pairs(data) do
			table.insert(result, frame:expandTemplate{title = frame.args.template, args = character})
		end
	else
		if elementcount > 1 then
			table.insert(result, '<table class="wikitable tierlist tierlist-multi"><tr><th>Rating</th><th data-filter-element="fire">Fire</th><th data-filter-element="water">Water</th><th data-filter-element="earth">Earth</th><th data-filter-element="wind">Wind</th><th data-filter-element="light">Light</th><th data-filter-element="dark">Dark</th><th data-filter-element="any">Any</th></tr>')
			table.insert(result, p.makeRatingTable(data, 'elements', 70))
			table.insert(result, '</table>')
		else
			table.insert(result, '<table class="wikitable tierlist tierlist-single">')
			table.insert(result, '<tr><th>Rating</th><th>Characters</th></tr>')
			table.insert(result, p.makeRatingTable(data, nil, 100))
			table.insert(result, '</table>')
		end

		table.insert(result, '<table class="wikitable tierlist-details" style="max-width: 725px;"><tr><th>Icon</th><th>Name</th><th>Type</th><th>Rating</th><th style="min-width: 475px">Remarks</th></tr>')
		table.insert(result, p.makeRemarksTable(data))
		table.insert(result, '</table>')
	end
	
	mw.log('Module:CharacterTier: render end: ' .. os.clock())
	return table.concat(result, '')
end

function p.renderCharacterTemplate(frame)
	local args = getArgs(frame)
	local id = mw.text.trim(args['id'] or '')

	local template = frame.args.template
	if template == nil then
		return 'Error: template undefined'
	end

	if string.len(id) > 0 then
		local characters = p.fetchCharacters(frame, {id=id})
		local character = characters and characters[1]
		if character ~= nil then
			return frame:expandTemplate{title = template, args = character}
		end
	end
	local name = frame.args.name
	local link = frame.args.link
	if (name == nil) and (link == nil) then
		return 'Error: name undefined'
	end
	if link == nil then
		link = name
	end
	if name == nil then
		local pos = link:find('(', 1, true)
		if pos ~= nil then
			name = string.sub(link, 1, pos-1)
		else
			name = link
		end
	end

	local character = p.fetchCharacter(frame, name, link)
	if character == nil then
		return 'Error: Character "'..link..'" not found!'
	end
	
	return frame:expandTemplate{title = template, args = character}
end

function p.fetchCharacter(f, name, link)
	local res = p.fetchCharacters(f, {link=link, name=(not link) and name or nil})
	return res and res[1]
end

function p.fetchCharacters(f, filter)
	mw.log('Module:CharacterTier: fetchCharacters start: ' .. os.clock())

	local queryFields = 'characters.id=id,characters._pageName=link,rarity,type,element,race,name,type,race,weapon,gw_rating=rating,gw_reasons=reasons,gw_last_update=last_update,gw_preliminary=preliminary'
	local queryArgs = {limit = 1000, join = 'characters.id=character_ratings.id'}
	if type(filter) == "table" then
		local where = nil
		for _, k in pairs({'id','element','type','race','rarity','link','name'}) do
			local v = filter[k]
			if type(v) == "string" and not v:match('["\\]') then
				where = where or {}
				if k == 'id' then
					where[#where+1] = ('character_ratings.id=%s'):format(v)
				else
					where[#where+1] = ('%s %s "%s"'):format(k == "link" and "characters._pageName" or k, LIST_COLUMNS[k] and "HOLDS" or "=", v)
				end
			end
		end
		
		queryArgs.where = where and table.concat(where, " AND ") or nil
	end

	local result = {}
	for _, row in ipairs(mw.ext.cargo.query('characters,character_ratings', queryFields, queryArgs)) do
		result[#result+1] = row

		-- General housekeeping
		local args = row
		args.ix = #result
		if args.race == nil then
			args.race = 'none'
		end
		if args.link == nil then
			args.link = args.name
		end
		if (args.preliminary ~= nil) and (string.len(args.preliminary) > 0) then
			local year, month, day = args.preliminary:match("(%d%d%d%d)%-(%d%d)%-(%d%d)")
			local difftime = year and os.difftime(os.time(), os.time({year=year+0, month=month+0, day=day+0})) or math.huge
			-- ratings are preliminary for 10 days
			args.preliminary = (difftime < (10 * 24 * 60 * 60))
		else
			args.preliminary = false
		end
		args.element = (args.element or ""):lower()
		args.weapon = args.weapon or ''
		args.shortid = args.id and args.id:sub(3,3) .. args.id:sub(5,7) or ''
	end

	mw.log('Module:Character.fetchCharacters end: ' .. os.clock())
	return result
end

function p.makeRatingTable(data, columns, iconsize) 
	local result = {}
	local ratings = {}
	local elements = {}
	if columns == 'elements' then
		elements = {'fire','water','earth','wind','light','dark','any'}
	elseif #data > 0 then
		elements = {data[1].element}
	end

	-- index data by rating
	local rating_keys = {}
	for _, charInfo in pairs(data) do
		local realRating = tonumber(charInfo.rating)
		if realRating then
			local rating
			if #elements > 1 then
				rating = realRating - realRating % ROW_GRANULARITY
			else
				rating = realRating
			end
			if ratings[rating] == nil then
				ratings[rating] = {}
				rating_keys[#rating_keys + 1] = rating
			end
			charInfo.numRating = realRating
			table.insert(ratings[rating], charInfo)
		end
	end
	table.sort(rating_keys)
	
	-- print each rating row
	for i=#rating_keys, 1, -1 do
		local characters = ratings[rating_keys[i]]
		table.sort(characters, function(a,b)
			if a.numRating ~= b.numRating then
				return a.numRating > b.numRating
			end
			return a.name < b.name
		end)
		table.insert(result, '<tr style="vertical-align: top;">')
		local label, hasRatingRange = "?", false
		if characters[1].rating ~= characters[#characters].rating then
			label, hasRatingRange = characters[1].rating .. " - " .. characters[#characters].rating, true
		elseif (characters[1].rating or 0) ~= 0 then
			label = characters[1].rating
		end
		table.insert(result, '<td style="font-weight: bold;vertical-align: middle;text-align: center;">' .. label .. '</td>')
		for ix2,element in pairs(elements) do
			table.insert(result, '<td data-filter-element="' .. element .. '">')
			local lastRating = false
			for key,character in pairs(characters) do
				if character.element == element then
					if hasRatingRange and lastRating ~= character.numRating then
						if lastRating then
							table.insert(result, '<hr class="filterable-divider" title="' .. character.rating .. '">')
						end
						lastRating = character.numRating
					end
					table.insert(result, '<span data-short-id="' .. character.shortid .. '" data-filter-rarity="' .. character.rarity:lower() .. '">[[File:' .. character.link .. ' iconA.jpg|'..iconsize..'px|link=#ref' .. character.ix .. '|' .. character.name .. ']]</span>')
				end
			end
			table.insert(result, '</td>\n')
		end
		table.insert(result, '</tr>\n')
	end
	
	return table.concat(result, '')
end

function p.makeRemarksTable(data)
	table.sort(data, function(a,b)
		if a.link == nil then
			return 1
		elseif b.link == nil then
			return -1
		else
			return a.link < b.link
		end
	end)
	local result = {}
	local frame = mw.getCurrentFrame()
	local weaponsCache = {}
	for ix,character in pairs(data) do
		if (character ~= nil) and (character.name ~= nil) then
			local name = character.name
			local link = character.link
			local remarks = '-'
			if character.reasons ~= nil then
				remarks = character.reasons
			end
			if character.preliminary then
				remarks = '<span style="color: red; font-weight: bold;">Preliminary rating.</span> Please allow a few days for it to settle.<br />' .. remarks
			end
			local race = character.race:gsub("^%l", string.upper)
			local typ = character.type:gsub("^%l", string.upper)
			local weapons = weaponsCache[character.weapon]
			if not weapons then
				weapons = frame:expandTemplate{ title = 'CharacterTierWeapons', args = { character.weapon } }
				weaponsCache[character.weapon] = weapons
			end

			table.insert(result, '<tr style="text-align: center;" data-filter-element="' .. character.element .. '">')
			table.insert(result, '<td><div id="ref'..character.ix..'">[[File:' .. link .. ' iconA.jpg|80px|link=' .. link .. ']]</div></td>')
			table.insert(result, '<td style="white-space: nowrap;">[['..link..'|'..name..']]</td>')
			table.insert(result, '<td>[[File:Label_Type_'..typ..'.png|100px|link=]]<br />[[File:Label_Race_'..race..'.png|100px|link=]]<br />'..weapons..'</td>')
			table.insert(result, '<td>'..character.rating..'</td>')
			table.insert(result, '<td style="text-align: left; vertical-align: top; min-width: 475px;">\n'..remarks..'\n</td>')
			table.insert(result, '</tr>')
		end
	end
	return table.concat(result, '\n')
end

function p.testElements()
	local frame = mw.getCurrentFrame()
	return p.render(frame)
end

function p.testOneElement()
	local frame = mw.getCurrentFrame()
	frame.args.element = 'fire'
	frame.args.rarity = 'ssr'
	return p.render(frame)
end

function p.testSingle()
	local frame = mw.getCurrentFrame()
	local character = p.fetchCharacter(frame, 'Lucio', 'Lucio')
	if character ~= nil then
		return 'Character: ' .. character.name .. ', link: ' .. character.link .. ', rating: ' .. character.rating .. ', prel: ' .. tostring(character.preliminary)
	else
		return 'Character not found'
	end
end

function p.testMultiTemplate()
	local frame = mw.getCurrentFrame()
	frame.args.template = 'CharacterTier'
	return p.render(frame)
end

function p.testSingleTemplate()
	local frame = mw.getCurrentFrame()
	frame.args.link = 'Lucius'
	frame.args.template = 'CharacterTier'
	return p.renderCharacterTemplate(frame)
end

function p.testSingleTemplateMini()
	local frame = mw.getCurrentFrame()
	frame.args.id = '3040231000'
	frame.args.template = 'CharacterTierMini'
	return p.renderCharacterTemplate(frame)
end

function p.testRender()
	local frame = mw.getCurrentFrame()
	return p.render(frame)
end

return p