Module:Skill cap

From HorizonXI Wiki

Documentation for this module may be created at Module:Skill cap/doc

--[[

A module for functions relating to skill caps.

]]

local skill_cap_table = {
	-- Level  1  2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32   33   34   35   36   37   38   39   40   41   42   43   44   45   46   47   48   49   50   51   52   53   54   55   56   57   58   59   60   61   62   63   64   65   66   67   68   69   70   71   72   73   74   75
	["A+"] = {6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123, 126, 129, 132, 135, 138, 141, 144, 147, 150, 153, 158, 163, 168, 173, 178, 183, 188, 193, 198, 203, 207, 212, 217, 222, 227, 232, 236, 241, 246, 251, 256, 261, 266, 271, 276},
	["A-"] = {6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123, 126, 129, 132, 135, 138, 141, 144, 147, 150, 153, 158, 163, 168, 173, 178, 183, 188, 193, 198, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, 244, 249, 254, 259, 264, 269},
	["B+"] = {5, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 65, 68, 71, 74, 77, 80, 83, 86, 89, 92, 94,  97, 100, 103, 106, 109, 112, 115, 118, 121, 123, 126, 129, 132, 135, 138, 141, 144, 147, 151, 156, 161, 166, 171, 176, 181, 186, 191, 196, 199, 203, 207, 210, 214, 218, 221, 225, 229, 233, 237, 242, 246, 251, 256},
	["B" ] = {5, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 65, 68, 71, 74, 77, 80, 83, 86, 89, 92, 94,  97, 100, 103, 106, 109, 112, 115, 118, 121, 123, 126, 129, 132, 135, 138, 141, 144, 147, 151, 156, 161, 166, 171, 176, 181, 186, 191, 196, 199, 202, 205, 208, 212, 215, 218, 221, 225, 228, 232, 236, 241, 245, 250},
	["B-"] = {5, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 65, 68, 71, 74, 77, 80, 83, 86, 89, 92, 94,  97, 100, 103, 106, 109, 112, 115, 118, 121, 123, 126, 129, 132, 135, 138, 141, 144, 147, 151, 156, 161, 166, 171, 176, 181, 186, 191, 196, 198, 201, 204, 206, 209, 212, 214, 217, 220, 223, 226, 229, 233, 236, 240},
	["C+"] = {5, 7, 10, 13, 16, 19, 21, 24, 27, 30, 33, 35, 38, 41, 44, 47, 49, 52, 55, 58, 61, 63, 66, 69, 72, 75, 77, 80, 83, 86, 89, 91,  94,  97, 100, 103, 105, 108, 111, 114, 117, 119, 122, 125, 128, 130, 133, 136, 139, 142, 146, 151, 156, 161, 166, 170, 175, 180, 185, 190, 192, 195, 197, 200, 202, 205, 207, 210, 212, 215, 218, 221, 224, 227, 230},
	["C" ] = {5, 7, 10, 13, 16, 19, 21, 24, 27, 30, 33, 35, 38, 41, 44, 47, 49, 52, 55, 58, 61, 63, 66, 69, 72, 75, 77, 80, 83, 86, 89, 91,  94,  97, 100, 103, 105, 108, 111, 114, 117, 119, 122, 125, 128, 130, 133, 136, 139, 142, 146, 151, 156, 161, 166, 170, 175, 180, 185, 190, 192, 194, 196, 199, 201, 203, 205, 208, 210, 212, 214, 217, 219, 222, 225},
	["C-"] = {5, 7, 10, 13, 16, 19, 21, 24, 27, 30, 33, 35, 38, 41, 44, 47, 49, 52, 55, 58, 61, 63, 66, 69, 72, 75, 77, 80, 83, 86, 89, 91,  94,  97, 100, 103, 105, 108, 111, 114, 117, 119, 122, 125, 128, 130, 133, 136, 139, 142, 146, 151, 156, 161, 166, 170, 175, 180, 185, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220},
	["D" ] = {4, 6,  9, 12, 14, 17, 20, 22, 25, 28, 31, 33, 36, 39, 41, 44, 47, 49, 52, 55, 58, 60, 63, 66, 68, 71, 74, 76, 79, 82, 85, 87,  90,  93,  95,  98, 101, 103, 106, 109, 112, 114, 117, 120, 122, 125, 128, 130, 133, 136, 140, 145, 150, 154, 159, 164, 168, 173, 178, 183, 184, 186, 188, 190, 192, 194, 195, 197, 199, 201, 202, 204, 206, 208, 210},
	["E" ] = {4, 6,  9, 11, 14, 16, 19, 21, 24, 26, 29, 31, 34, 36, 39, 41, 44, 46, 49, 51, 54, 56, 59, 61, 64, 66, 69, 71, 74, 76, 79, 81,  84,  86,  89,  91,  94,  96,  99, 101, 104, 106, 109, 111, 114, 116, 119, 121, 124, 126, 130, 135, 139, 144, 148, 153, 157, 162, 166, 171, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200},
	["F" ] = {4, 6,  8, 10, 13, 15, 17, 20, 22, 24, 27, 29, 31, 33, 36, 38, 40, 43, 45, 47, 50, 52, 54, 56, 59, 61, 63, 66, 68, 70, 73, 75,  77,  79,  82,  84,  86,  89,  91,  93,  96,  98, 100, 102, 105, 107, 109, 112, 114, 116, 120, 124, 128, 133, 137, 141, 146, 150, 154, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189}
}

local p = {}

--[[
cap_to_rank

Get the letter-grade rank, given the level 75 skill cap as the first parameter.

Example:
{{#invoke:Skill cap|cap to rank|276}}
]]

local function lookup_rank(search_value)
    if not search_value then
    	return "invalid rank"
    end

    for rank, skill_caps in pairs(skill_cap_table) do
        if skill_caps[75] == search_value then
            return rank
        end
    end

    return "invalid rank"
end

function p.cap_to_rank(frame)
    local search_value = tonumber(frame.args[1])

	return lookup_rank(search_value)
end

p["cap to rank"] = p.cap_to_rank -- alias

--[[
get_cap

Get the skill cap, given the skill rank (A+ through F) as the first parameter,
and the level as the second parameter.

You may also use the level 75 skill cap as an alias for the letter-grade rank.
For example, 276 is an alias for A+.

Example:
{{#invoke:Skill cap|get cap|A+|37}}
{{#invoke:Skill cap|get cap|276|37}}
]]

local function lookup_cap(rank, level)
    if tonumber(rank) then -- numerical alias for letter-grade
    	rank = lookup_rank(tonumber(rank))
    end

	if not rank or not skill_cap_table[rank] then
	    return "invalid rank"
	end

	if not level or not skill_cap_table[rank][level] then
	    return "invalid level"
	end

	return skill_cap_table[rank][level]
end

function p.get_cap(frame)
    local rank = frame.args[1]
    local level = tonumber(frame.args[2])

	return lookup_cap(rank, level)
end

p["get cap"] = p.get_cap -- alias

--[[
find_minimum_level

Get the minimum level required, given the skill rank (A+ through F) as the first
parameter, and the desired skill level as the second parameter.

You may also use the level 75 skill cap as an alias for the letter-grade rank.
For example, 276 is an alias for A+.

Example:
{{#invoke:Skill cap|find minimum level|A+|250}}
{{#invoke:Skill cap|find minimum level|276|250}}
]]

function p.find_minimum_level(frame)
    local skill_rank = frame.args[1]
    local skill_level = tonumber(frame.args[2])

    if tonumber(skill_rank) then -- numerical alias for letter-grade
    	skill_rank = lookup_rank(tonumber(skill_rank))
    end

	if not skill_rank or not skill_cap_table[skill_rank] then
	    return "invalid rank"
	end

	if not skill_level then
	    return "invalid level"
	end

    local arr = skill_cap_table[skill_rank]

    -- Do a binary search for the first number in the the skill cap table
    -- greater than or equal to skill_level
    local left, right = 1, 75
    local result = nil

    while left <= right do
        local mid = math.floor((left + right) / 2)
        if arr[mid] >= skill_level then
            result = mid  -- potential candidate
            right = mid - 1  -- search left side for earlier occurrence
        else
            left = mid + 1  -- search right side
        end
    end
    
    return result
end

p["find minimum level"] = p.find_minimum_level -- alias

--[[
job_skill_caps_table

Displays a job's skill caps for every skill and every level, given the job name
as a parameter. Uses the Cargo table JobSkills, which holds data set by the
{{job skills}} template on each job page.

Example:
{{#invoke:Skill cap|job skill caps table|Warrior}}
]]

function p.job_skill_caps_table(frame)
    local job = frame.args[1]

	if not job then
	    return "invalid job"
	end

	local cargo_result = mw.ext.cargo.query(
		"JobSkills",
		"Skill,SkillCap",
		{ where = "Job='" .. job .. "'" }
	)

	if not cargo_result then
		return "invalid job"
	end

	local skills = {}

	--[[
	condense the Cargo result into a single table indexed by skill name
	cargo_result:
	{ { Skill="Archery", SkillCap=269 }, { Skill="Axe", SkillCap=240 }, ... }
	skills:
	{ Archery=269, Axe=240, ... }
	]]
	for _, cargo_row in ipairs(cargo_result) do
        skills[cargo_row["Skill"]] = cargo_row["SkillCap"]
	end

	local skill_sets =
		{
			{"Hand-to-Hand", "Dagger", "Sword", "Great Sword", "Axe",
				"Great Axe", "Scythe", "Polearm", "Katana", "Great Katana",
				"Club", "Staff"
			},
			{"Archery", "Marksmanship", "Throwing"},
			{"Guarding", "Evasion", "Shield", "Parrying"},
			{"Divine", "Healing", "Enhancing", "Enfeebling", "Elemental",
				"Dark", "Summon", "Ninjutsu", "Singing", "String", "Wind",
				"Blue"
			}
		}

	-- prune empty columns from skill_sets, iterating backwards
	for i = #skill_sets, 1, -1 do
		for j = #skill_sets[i], 1, -1 do
			if not skills[skill_sets[i][j]] then
				table.remove(skill_sets[i], j)
			end
		end
		if #skill_sets[i] == 0 then
			table.remove(skill_sets, i)
		end
	end

    -- render the table
	local root = mw.html.create("table")
		:attr("id", "skill-caps")
        :addClass("horizon-table jobs-table mw-collapsible mw-collapsed sortable")
		:css("display", "block")
		:css("margin", "auto")
		:css("border", "none")
        :css("width", "fit-content")
        :css("max-height", "20lh")
        :css("overflow", "auto")
        :css("text-align", "center")
        :css("background", "transparent")

	-- Table caption
	root
		:tag("caption")
		:css("margin", "auto")
		:css("width", "20ch")
		:wikitext("Skill Caps")
		:done()

    -- Table header row
    local header_row = root:tag("tr")

	-- Level header
    header_row
		:tag("th")
		:attr("scope", "col")
		:css("position", "sticky")
		:css("top", "0")
		:css("left", "0")
		:css("z-index", "1")
		:wikitext("Level")
		:done()
    
	for _, skill_list in ipairs(skill_sets) do
		for i, s in ipairs(skill_list) do
			local header = header_row
				:tag("th")
				:attr("scope", "col")
				:addClass("unsortable")
				:css("position", "sticky")
				:css("top", "0")
				:css("line-height", "1.6")
				:css("writing-mode", "sideways-lr")
				:css("vertical-align", "bottom")
				:css("text-align", "left")

			if i == 1 then
				header:css("border-left-width", "3px")
			end

			if s == "Blue" then
				header:addClass("toau")
			end

			header:wikitext(s)
			header:done()
		end
	end
    header_row:done()

    -- Table rows
    for level=1,75 do
        local row = root:tag("tr")

        row
        	:tag("td")
        	:css("position", "sticky")
        	:css("left", "0")
        	:wikitext(level)
        	:done()
        
		for _, skill_list in ipairs(skill_sets) do
			for i, s in ipairs(skill_list) do
				local cell = row:tag("td")
				
				if i == 1 then
					cell:css("border-left-width", "3px")
				end
				if s == "Blue" then
					cell:addClass("toau")
				end
				cell:wikitext(lookup_cap(skills[s], level))
				
				cell:done()
			end
		end
		
		row:done()
    end
    
    root:done()
    
    return tostring(root)
end

p["job skill caps table"] = p.job_skill_caps_table -- alias

return p