Module:User stats

Documentation for this module may be created at Module:User stats/doc

local experience = require('Module:Experience')
local hiscore = require('Module:Hiscores')
local yesno = require('Module:Yesno')
local paramtest = require('Module:Paramtest')
local hc = paramtest.has_content
local dt = paramtest.default_to
local scm = require('Module:Skill clickpic')._main
require('Module:Mw.html extension')
local lang = mw.getContentLanguage()

local p = {}

function combat_level(att, str, def, ran, mag, con, pra, sum)
	sum = sum or 0
	local ret = {
		level = (math.max(att+str, 2*mag, 2*ran) * 1.3 + def + con + math.floor(pra/2) + math.floor(sum/2))/4,
		level_nosum = (math.max(att+str, 2*mag, 2*ran) * 1.3 + def + con + math.floor(pra/2))/4,
	}
	ret.sum_add = ret.level - ret.level_nosum
	return ret
end
function combat_level_stats(user_stats, is_rs3)
	local args = {}
	local l = {'attack', 'strength', 'defence', 'ranged', 'magic', 'constitution', 'prayer'}
	if is_rs3 then
		table.insert(l, 'summoning')
	end
	for i,v in ipairs(l) do
		args[i] = user_stats[v].level
		if not args[i] then
			return nil
		end
	end
	local lvl = combat_level(unpack(args))
	local ret
	if is_rs3 then
		ret = string.format('<span title="%s + %s">%s</span>', lvl.level_nosum, lvl.sum_add, lvl.level)
	else
		ret = lvl.level_nosum
	end
	return ret
end

function normalise_args(args)
	local out = {}
	for k,v in pairs(args) do
		local key = string.lower(k)
		local add_key = true
		if key ~= k then
			if args[key] then
				add_key = false
			end
		end
		if add_key then
			out[key] = v
		end
	end
	return out
end

function fnum(x)
	x = tonumber(x)
	if x then
		return lang:formatNum(x)
	end
	return ''
end

local stat_override_names = {
	[true] = {
		'overall',
		'attack',
		'defence',
		'strength',
		'constitution',
		'ranged',
		'prayer',
		'magic',
		'cooking',
		'woodcutting',
		'fletching',
		'fishing',
		'firemaking',
		'crafting',
		'smithing',
		'mining',
		'herblore',
		'agility',
		'thieving',
		'slayer',
		'farming',
		'runecrafting',
		'hunter',
		'construction',
		'summoning',
		'dungeoneering',
		'divination',
		'invention',
		'archaeology',
		'runescore'
	},
	[false] = {
		'overall',
		'attack',
		'defence',
		'strength',
		'constitution',
		'ranged',
		'prayer',
		'magic',
		'cooking',
		'woodcutting',
		'fletching',
		'fishing',
		'firemaking',
		'crafting',
		'smithing',
		'mining',
		'herblore',
		'agility',
		'thieving',
		'slayer',
		'farming',
		'runecrafting',
		'hunter',
		'construction'
	}
}
local rs3_modes = {
	['rs3'] = true,
	['rs3-ironman'] = true,
	['rs3-hardcore'] = true
}

local elite_skills = {
	invention=true
}
local skills_max = {
	herblore = 120,
	farming = 120,
	slayer = 120,
	dungeoneering = 120,
	invention = 120,
	archaeology = 120
}
local skills_virtual_max = {
	invention = 150
}

local not_skills = {
	runescore=true,
	overall=true
}

local hiscore_links = {
	['rs3'] = 'm=hiscore/compare.ws',
	['rs3-ironman'] = 'm=hiscore_ironman/compare.ws',
	['rs3-hardcore'] = 'm=hiscore_hardcore_ironman/compare.ws',
	['osrs'] = 'm=hiscore_oldschool/compare.ws',
	['osrs-ironman'] = 'm=hiscore_oldschool_ironman/hiscorepersonal',
	['osrs-ultimate'] = 'm=hiscore_oldschool_ultimate/hiscorepersonal',
	
	-- not currently supported by #hs:
	['osrs-hardcore'] = 'm=hiscore_oldschool_hardcore_ironman/hiscorepersonal',
	['osrs-deadman'] = 'm=hiscore_oldschool_deadman/hiscorepersonal',
	['osrs-seasonal'] = 'm=hiscore_oldschool_seasonal/hiscorepersonal',
	['osrs-tournament'] = 'm=hiscore_oldschool_tournament/hiscorepersonal'
}

local ranks_string = {
	['rs3'] = 'Ranks',
	['rs3-ironman'] = 'Ranks',
	['rs3-hardcore'] = 'Ranks',
	['osrs'] = 'Ranks',
	['osrs-ironman'] = 'Ranks',
	['osrs-ultimate'] = 'Ranks',
	
	-- not currently supported by #hs:
	['osrs-hardcore'] = 'Ranks',
	['osrs-deadman'] = 'Ranks and XP',
	['osrs-seasonal'] = 'Ranks and XP',
	['osrs-tournament'] = 'Ranks and XP'
}

local game_string = {
	['rs3'] = 'RuneScape',
	['rs3-ironman'] = 'RS Ironman',
	['rs3-hardcore'] = 'RS Hardcore Ironman',
	['osrs'] = 'Old School RuneScape',
	['osrs-ironman'] = 'OSRS Ironman',
	['osrs-ultimate'] = 'OSRS Ultimate Ironman',
	
	-- not currently supported by #hs:
	['osrs-hardcore'] = 'OSRS Hardcore Ironman',
	['osrs-deadman'] = 'OSRS Deadman mode',
	['osrs-seasonal'] = 'OSRS League',
	['osrs-tournament'] = 'OSRS Tournament'
}

function hs(name, game, title)
	if not hc(name) then
		return title or 'Hiscores'
	end
	local url = hiscore_links[game] or hiscore_links.rs3
	return string.format('<span class="plainlinks">[http://services.runescape.com/%s?user1=%s %s]</span>', url, mw.uri.encode(name), title or name)
end

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

function p._main(raw_args)
	local args = normalise_args(raw_args)
	local user_stats = {}
	local virtual = yesno(args.virtual, false)
	local game = string.lower(args.game or 'rs3')
	local game_is_rs3 = rs3_modes[game] or false

	function set_level(skill)
		local val = tonumber(args[skill])
		if not val then
			user_stats[skill] = {level=args[skill]}
			return false
		end
		if not_skills[skill] then
			user_stats[skill]={
				score=val
			}
			return false
		end
		local xp, lvl
		local iselite = game_is_rs3 and elite_skills[skill]
		if val > 150 then
			xp = val
			lvl = experience._level_at_xp(val, iselite)
		else
			lvl = val
			xp = experience._xp_at_level(val, iselite)
		end
		if game_is_rs3 then
			lvl = math.min(lvl, skills_max[skill] or 99)
		else
			lvl = math.min(lvl, 99)
		end
		user_stats[skill] = {
			level = lvl,
			xp = xp
		}
		return true
	end
	function add_virtual(skill)
		local val = user_stats[skill].xp
		if not val then
			return 0
		end
		local virtlvl = experience._level_at_xp_unr(val, game_is_rs3 and elite_skills[skill])
		virtlvl = math.min(virtlvl, skills_virtual_max[skill] or 126)
		user_stats[skill].virtual = virtlvl
		return virtlvl
	end
	args.margin = yesno(args.margin)
	args.overall = dt(args.overall, args.total)
	args.achievement = dt(args.achievement, args.task)
	
	if hc(args.name) then
		local is_ok, pc_user_stats = pcall(hiscore.get_stats, args.name, game, true)
		if is_ok then
			user_stats = pc_user_stats
		end
	end
	local ei = 29
	if not game_is_rs3 then
		ei = 24
	end
	local has_override = false
	local running_lvl, running_xp, running_virt = 0, 0, 0
	for i = 2, ei, 1 do
		local s = stat_override_names[game_is_rs3][i]
		if not user_stats[s] and not hc(args[s]) then
			-- neither defined, force 1
			args[s] = 1
		end
		if hc(args[s]) then
			local overridden = set_level(s)
			has_override = has_override or overridden
		end
		if not hc(args.overall) then
			running_xp = (type(user_stats[s].xp) == 'number' and user_stats[s].xp or 0) + running_xp
			running_lvl = (type(user_stats[s].level) == 'number' and user_stats[s].level or 0) + running_lvl
			running_virt = running_virt + add_virtual(s)
		end
	end
	local overall_rank_str = ''
	if hc(args.overall) then
		user_stats['overall'] = {
			level = args.overall
		}
	elseif has_override then
		user_stats['overall'] = {
			xp = running_xp,
			level = running_lvl,
			virtual = running_virt
		}
	else
		user_stats.overall.virtual = running_virt
		if user_stats.overall.rank then
			overall_rank_str = '@NL@Rank: '..fnum(user_stats.overall.rank)
		end
	end
	if game_is_rs3 then
		if not user_stats.runescore then
			user_stats.runescore = {}
		end
		if hc(args.runescore) then
			user_stats.runescore = {score=args.runescore}
		end
	end

	local cmb
	if hc(args.combat) then
		cmb = args.combat
	else
		cmb = combat_level_stats(user_stats, game_is_rs3)
	end


	function make_cell(td, skill)
		td:css('width', '33%')
		td:wikitext(scm(skill), '&nbsp;')
		local vals = user_stats[skill]
		if not vals then
			td:wikitext('----')
			return
		end
		local rank_str = ''
		if vals.rank then
			rank_str = '@NL@Rank: ' .. fnum(vals.rank)
		end
			
		if virtual and vals.virtual > vals.level then
			td	:wikitext(vals.virtual)
				:attr('title', string.format('Real level: %s@NL@XP: %s%s', vals.level, fnum(vals.xp), rank_str))
		else
			td	:wikitext(vals.level)
				:attr('title', string.format('XP: %s%s', fnum(vals.xp), rank_str))
		end
	end
	function make_row(tr, s1, s2, s3)
		tr:done():newline()
		make_cell(tr:tag('td'), s1)
		make_cell(tr:tag('td'), s2)
		make_cell(tr:tag('td'), s3)
	end
	function make_cell_simple(td, stat, val)
		td:css('width', '33%')
		td:wikitext(scm(stat), '&nbsp;')
		if hc(val) then
			if tonumber(val) then
				td:wikitext(fnum(tonumber(val)))
			else
				td:wikitext(val)
			end
		else
			td:wikitext('----')
		end
	end

	local ret = mw.html.create('table')
	ret:addClass('rsw-infobox infobox-user-stats userstats')
		:css({['text-align']='center', float=dt(args.align, 'right')})
		:cssText(args['extra-css'])
	
	ret	:tag('tr')
			:tag('th')
				:addClass('infobox-header')
				:wikitext(hs(args.name, game, args.box_title or args.name or 'Hiscores'))
				:attr('colspan', 3)
			:done()
		:newline()
		:tag('tr')
			:tag('th')
				:addClass('infobox-subheader')
				:wikitext('Total level:&nbsp;', user_stats.overall.level)
				:wikitextIf(virtual, string.format('&nbsp;(%s)', user_stats.overall.virtual or ''))
				:attr('colspan', 3)
				:attrIf(user_stats.overall.xp, {title=string.format('Total experience: %s%s', fnum(user_stats.overall.xp), overall_rank_str)})

	make_row(ret:tag('tr'), 'attack', 'constitution', 'mining')
	make_row(ret:tag('tr'), 'strength', 'agility', 'smithing')
	make_row(ret:tag('tr'), 'defence', 'herblore', 'fishing')
	make_row(ret:tag('tr'), 'ranged', 'thieving', 'cooking')
	make_row(ret:tag('tr'), 'prayer', 'crafting', 'firemaking')
	make_row(ret:tag('tr'), 'magic', 'fletching', 'woodcutting')
	make_row(ret:tag('tr'), 'runecrafting', 'slayer', 'farming')
	if game_is_rs3 then
		make_row(ret:tag('tr'), 'construction', 'hunter', 'summoning')
		make_row(ret:tag('tr'), 'dungeoneering', 'divination', 'invention')
	
		local tr = ret:tag('tr')
		ret:newline()
		make_cell(tr:tag('td'), 'archaeology')
		make_cell_simple(tr:tag('td'), 'combat', cmb)
		make_cell_simple(tr:tag('td'), 'quest', args.quest)

		tr = ret:tag('tr')
		ret:newline()
		make_cell_simple(tr:tag('td'), 'music', args.music)
		make_cell_simple(tr:tag('td'), 'task', args.achievement)
		local rs_td = tr:tag('td')
		make_cell_simple(rs_td, 'runescore', user_stats.runescore.score)
		if user_stats.runescore.rank then
			rs_td:attr('title', 'Rank: '..fnum(user_stats.runescore.rank))
		end
	else
		local tr = ret:tag('tr')
		make_cell(tr:tag('td'), 'construction')
		make_cell(tr:tag('td'), 'hunter')
		tr:tag('td') -- this is where i'd put warding, IF I HAD IT
		
		tr = ret:tag('tr')
		ret:newline()
		make_cell_simple(tr:tag('td'), 'combat', cmb)
		make_cell_simple(tr:tag('td'), 'quest', args.quest)
		make_cell_simple(tr:tag('td'), 'music', args.music)
		
	end
	
	if hc(args.date) then
		ret:tag('tr')
			:tag('th')
				:attr('colspan', 3)
				:wikitext('As of ', args.date)
	end
	if not yesno(args.hide_inforow) then
		ret:tag('tr'):tag('td')
			:attr('colspan', 3)
			:wikitext(string.format('Hover for XP and rank. %s for %s.', ranks_string[game] or 'Ranks', game_string[game] or 'RuneScape'))
	end
	
	-- mw.html escapes & (but scribunto/wikitext does not) so we gotta do a gsub on the entire string
	-- note: &#10; is a new line that will work in a title attribute
	ret = tostring(ret)
	ret = ret:gsub('@NL@', '&#10;')
	return ret
end

return p