Module:Skill cap: Difference between revisions

From HorizonXI Wiki
(add cap_to_rank function to convert a level 75 skill cap number (276) to a letter-grade rank (A+))
m (job skill caps table: transparent caption background)
 
(11 intermediate revisions by the same user not shown)
Line 21: Line 21:


local p = {}
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


--[[
--[[
Line 27: Line 58:
Get the skill cap, given the skill rank (A+ through F) as the first parameter,
Get the skill cap, given the skill rank (A+ through F) as the first parameter,
and the level as the second 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:
Example:
{{#invoke:Skill cap|get cap|A+|75}}
{{#invoke:Skill cap|get cap|A+|37}}
{{#invoke:Skill cap|get cap|276|37}}
]]
]]


function p.get_cap(frame)
local function lookup_cap(rank, level)
     local skill_rank = frame.args[1]
     if tonumber(rank) then -- numerical alias for letter-grade
    local level = tonumber(frame.args[2])
    rank = lookup_rank(tonumber(rank))
    end


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


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


return skill_cap_table[skill_rank][level]
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
end


Line 54: Line 97:
Get the minimum level required, given the skill rank (A+ through F) as the first
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.
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:
Example:
{{#invoke:Skill cap|find minimum level|A+|250}}
{{#invoke:Skill cap|find minimum level|A+|250}}
{{#invoke:Skill cap|find minimum level|276|250}}
]]
]]


Line 62: Line 109:
     local skill_rank = frame.args[1]
     local skill_rank = frame.args[1]
     local skill_level = tonumber(frame.args[2])
     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
if not skill_rank or not skill_cap_table[skill_rank] then
Line 94: Line 145:


--[[
--[[
cap_to_rank
job_skill_caps_table


Get the letter-grade rank, given the level 75 skill cap as the first parameter.
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:
Example:
{{#invoke:Skill cap|cap to rank|276}}
{{#invoke:Skill cap|job skill caps table|Warrior}}
]]
]]


function p.cap_to_rank(frame)
function p.job_skill_caps_table(frame)
     local search_value = tonumber(frame.args[1])
     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()
      
      
    if not search_value then
for _, skill_list in ipairs(skill_sets) do
    return "invalid rank"
for i, s in ipairs(skill_list) do
     end
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")


    for rank, skill_caps in pairs(skill_cap_table) do
        row
        if skill_caps[75] == search_value then
        :tag("td")
            return rank
        :css("position", "sticky")
        end
        :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
     end
 
   
     return "invalid rank"
    root:done()
   
     return tostring(root)
end
end


p["cap to rank"] = p.cap_to_rank -- alias
p["job skill caps table"] = p.job_skill_caps_table -- alias


return p
return p

Latest revision as of 00:23, 11 October 2025

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