Mobius Final Fantasy Wiki
mNo edit summary
m (Use full page name instead.)
 
(19 intermediate revisions by the same user not shown)
Line 1: Line 1:
  +
local mArguments -- lazy load
  +
local cargo = require("Module:Cargo")
  +
local cargo_store = cargo.store
  +
local mm = require("Module:Math")
  +
local yesno = require('Module:Yesno')
  +
 
local p = {}
 
local p = {}
  +
local black = '★'
 
local white = '☆'
+
local BLACK = mw.ustring.char(0x2605)
  +
local WHITE = mw.ustring.char(0x2606)
  +
local NAMESPACE = mw.title.getCurrentTitle().namespace
  +
local FULLPAGENAME = mw.title.getCurrentTitle().fullText
  +
 
local multis = {1.0, 1.1, 1.2, 1.3, 1.4, 1.7, 2.0, 2.3, 2.6, 3.0}
 
local multis = {1.0, 1.1, 1.2, 1.3, 1.4, 1.7, 2.0, 2.3, 2.6, 3.0}
  +
local cooldowns = {8, 7, 6, 5, 4, 3, 2, 2, 1, 1}
   
local function round(n)
+
local i18n = {
  +
categories = {
return math.floor(n + 0.5)
 
  +
-- Tracking categories
end
 
  +
non_support_with_no_atk = "Non-support abilities with no attack",
  +
non_support_with_no_brk = "Non-support abilities with no break power",
  +
non_support_with_fixed_atk = "Non-support abilities with fixed attack",
  +
non_support_with_fixed_brk = "Non-support abilities with fixed break power",
  +
non_support_with_cooldown = "Non-support abilities with cooldown",
  +
support_with_varied_atk = "Support abilities with attack",
  +
support_with_varied_brk = "Support abilities with break power",
  +
support_with_fixed_atk = "Support abilities with fixed attack",
  +
support_with_fixed_brk = "Support abilities with fixed break power",
  +
support_with_no_cooldown = "Support abilities with no cooldown",
  +
},
  +
errors = {
  +
rarity_empty = "No rarity specified.",
  +
rarity_not_a_number = "Rarity is not a number: %q",
  +
rarity_out_of_bounds = "Rarity is outside the range of 1-5: %d",
  +
}
  +
}
   
local function count_stars(s)
+
local function number_to_stars(n)
local total = tonumber(s)
+
local n = tonumber(n) or 0
  +
return string.rep(WHITE, n / 10) .. string.rep(BLACK, n % 10)
local _, black_count, white_count
 
if total == nil then
 
_, black_count = string.gsub(s, black, '')
 
_, white_count = string.gsub(s, white, '')
 
total = black_count + white_count * 10
 
end
 
return total
 
 
end
 
end
   
local function number_to_stars(n)
+
local function parse_number(s)
local total = tonumber(n)
+
return tonumber(s) or 0
local w_count = math.floor(total / 10)
 
local b_count = total % 10
 
return string.rep(white, w_count) .. string.rep(black, b_count)
 
 
end
 
end
   
local function jobtype_icon(s)
+
local function parse_stars(s)
local s = mw.ustring.lower(s)
+
local s = tostring(s)
  +
:gsub("★", BLACK)
local fmt = '[[File:%s_Icon.png|link=|20px]]'
 
  +
:gsub("★", BLACK)
if (s == 'warrior' or
 
  +
:gsub("★", BLACK)
s == 'mage' or
 
  +
:gsub("☆", WHITE)
s == 'ranger' or
 
  +
:gsub("☆", WHITE)
s == 'monk') then
 
  +
:gsub("☆", WHITE)
return fmt:format(s)
 
  +
local pattern = "^%s*(["..BLACK..WHITE.."]*)%s*x?%s*(%d*)%s*$"
  +
local stars, digit = mw.ustring.match(s, pattern)
  +
if digit == "" and stars == "" then
  +
mw.log(string.format("Count not find stars or numbers: %q", s))
  +
return 0
  +
elseif digit == "" and stars ~= "" then
  +
local _, bcount, wcount
  +
_, bcount = string.gsub(stars, BLACK, "")
  +
_, wcount = string.gsub(stars, WHITE, "")
  +
return bcount + wcount * 10
  +
elseif digit ~= "" and (stars == "" or stars == BLACK) then
  +
return tonumber(digit)
  +
elseif digit ~= "" and stars ~= "" then
  +
mw.log(string.format("Number and multiple stars specified: %q", s))
  +
-- Stars were specified multiple times AND digits also follow.
  +
return 0
 
end
 
end
 
end
 
end
   
function p.ultimate(frame)
+
function p.get_levels(args)
local pframe = frame:getParent()
+
local rarity = tostring(args.rarity or '')
  +
if mw.text.trim(rarity) == '' then
local config = frame.args
 
  +
error(i18n.errors.rarity_empty)
local args = pframe.args
 
return p._ultimate(args)
 
end
 
 
function p._ultimate(args)
 
local gauge = {160, 155, 150, 140, 135, 130, 125, 110, 100, 80}
 
local name = args.name or ''
 
local jobtype = args.jobtype or ''
 
local info = args.description or ''
 
local attack = args.attack or ''
 
local breakpower = args.breakpower or ''
 
local critchance = args.critchance or ''
 
local extra = args.extra or ''
 
 
if attack == '' then attack = 0 end
 
if breakpower == '' then breakpower = 0 end
 
 
local total = count_stars(critchance)
 
local critstars = number_to_stars(total)
 
 
local tbl = {}
 
table.insert(tbl, {
 
'Lv',
 
'Ultimate Gauge',
 
'Attack (%)',
 
'Break Power (%)',
 
'Crit Chance',
 
})
 
for level=1,10 do
 
local m = multis[level]
 
local row = {
 
level,
 
gauge[level],
 
attack * m,
 
breakpower * m,
 
critstars .. ' (' .. tostring(total * 5) .. '%)',
 
}
 
table.insert(tbl, row)
 
 
end
 
end
  +
local jobtype = tostring(args.jobtype):lower()
  +
local is_support = jobtype == 'support' or jobtype == 'healer'
   
local html = mw.html.create('')
+
local rarities = {}
  +
local is_fast
html
 
  +
for digit, plus in rarity:gmatch('(%d+)(%+?)') do
:wikitext(jobtype_icon(jobtype))
 
  +
digit = tonumber(digit)
:tag('b'):wikitext(name):done():newline()
 
  +
if digit < 1 or digit > 5 then
:wikitext(": ''\"" .. info .. "\"''"):newline()
 
  +
error(i18n.errors.rarity_out_of_bounds:format(digit))
local wikitable = html:tag('table'):addClass('wikitable')
 
for i,row in ipairs(tbl) do
 
local tr = wikitable:tag('tr')
 
local tagtype = i == 1 and 'th' or 'td'
 
for j,col in ipairs(row) do
 
tr
 
:tag(tagtype)
 
:wikitext(col)
 
:done()
 
 
end
 
end
  +
rarities[#rarities+1] = digit
tr:done():newline()
 
  +
is_fast = is_fast or plus == '+'
 
end
 
end
  +
if #rarity == 0 then
wikitable:done():newline()
 
  +
error(i18n.errors.rarity_not_a_number:format(rarity))
if extra ~= '' then
 
html:tag('b'):wikitext('Added Effects: '):done():newline():wikitext(extra)
 
 
end
 
end
  +
local min_rarity = mm._min(unpack(rarities))
return tostring(html)
 
  +
local max_rarity = mm._max(unpack(rarities))
  +
local m, c
  +
if is_support then
  +
m, c = 1, 1
  +
else
  +
m, c = 2, 0
  +
end
  +
local start = 1
  +
local stop = m * max_rarity + c
  +
local step = 1
  +
if is_fast then
  +
start = m * min_rarity + c
  +
step = m
  +
end
  +
local levels = {}
  +
for x=start, stop, step do
  +
levels[#levels+1] = x
  +
end
  +
return levels
 
end
 
end
   
 
function p._main(args)
 
function p._main(args)
local args = args
+
-- Load args into locals
local rarity = args.rarity or ''
+
local jobtype = tostring(args.jobtype):lower()
local attack = args.attack or ''
+
local base_atk = parse_number(args.atk or args.attack)
local breakpower = args.breakpower or ''
+
local base_brk = parse_number(args.brk or args.break_power or args.breakpower)
local critchance = args.critchance or ''
+
local base_crt = parse_stars(args.crt or args.crit_chance or args.critchance)
  +
local const_attack = (args.const_attack or '') == 'y'
 
local const_breakpower = (args.const_breakpower or '') == 'y'
+
local is_fixed_atk = yesno(args.const_atk or args.const_attack)
local has_cooldown = (args.has_cooldown or '') == 'y'
+
local is_fixed_brk = yesno(args.const_brk or args.const_breakpower)
  +
local is_support = jobtype == 'support'
if rarity == '' then
 
  +
local has_cooldown = args.has_cooldown
mw.log('ability stat: "rarity" argument is null or empty')
 
  +
if has_cooldown == nil then -- If there is a support with no-cooldown, then this would be "false"
return ''
 
  +
has_cooldown = is_support
  +
else
  +
has_cooldown = yesno(has_cooldown)
 
end
 
end
  +
local cooldowns = {8, 7, 6, 5, 4, 3, 2, 2, 1, 1}
 
  +
local lvls = p.get_levels(args)
  +
local atks = {}
  +
local brks = {}
  +
local crts = {}
  +
local cds = {}
 
 
  +
-- Handle fixed and varying stats.
local fast = false
 
  +
for i=1,10 do
local rarities = {}
 
  +
local atk
for a,b in string.gmatch(rarity, '(-?%d+)(%+?)') do
 
  +
if is_fixed_atk then
a = tonumber(a)
 
  +
atk = base_atk
if a < 1 or 5 < a then
 
  +
else
error("Rarity outside the range of 1~5: "..a..b)
 
  +
local atk_i = args['atk'..i] or args['attack'..i]
elseif b == '+' then
 
  +
local atk_m = base_atk * multis[i]
fast = true
 
  +
atk = atk_i or atk_m
  +
end
  +
atks[i] = mm._round(atk)
  +
local brk
  +
if is_fixed_brk then
  +
brk = base_brk
  +
else
  +
local brk_i = args['brk'..i] or args['breakpower'..i] or args['break_power'..i]
  +
local brk_m = base_brk * multis[i]
  +
brk = brk_i or brk_m
  +
end
  +
brks[i] = mm._round(brk)
  +
crts[i] = base_crt
  +
if has_cooldown then
  +
cds[i] = args['cd'..i] or args['cooldown'..i] or cooldowns[i]
 
end
 
end
table.insert(rarities, a)
 
 
end
 
end
  +
table.sort(rarities)
 
  +
-- If the stat set contains only one value, then it is a fixed stat.
if attack == '' then
 
  +
local atk_set = {}
attack = 0
 
  +
local atk_len = 0
else
 
  +
local brk_set = {}
attack = tonumber(attack)
 
  +
local brk_len = 0
end
 
  +
-- We're only checking the values at each level.
if breakpower == '' then
 
  +
for i,lvl in ipairs(lvls) do
breakpower = 0
 
  +
-- Before insert, check if it's already defined.
else
 
  +
local k = atks[lvl]
breakpower = tonumber(breakpower)
 
  +
if not atk_set[k] then
end
 
  +
atk_len = atk_len + 1
if critchance ~= '' then
 
  +
atk_set[k] = true
local total = count_stars(critchance)
 
critchance = number_to_stars(total)
 
end
 
-- If attack is 0, then it's a support ability.
 
-- If break is 0, then it's a support ability.
 
local support = attack == 0 or breakpower == 0
 
local show_attack = attack > 0
 
local show_breakpower = breakpower > 0
 
local show_critchance = critchance ~= ''
 
local show_cooldown = has_cooldown or support or (fast and critchance == '')
 
 
local columns = {}
 
local rows = {}
 
local level
 
local m, c -- y = mx + c
 
if support then
 
m = 1
 
c = 1
 
else
 
m = 2
 
c = 0
 
end
 
local levels = {}
 
if fast then
 
for i, rarity in ipairs(rarities) do
 
table.insert(levels, rarity * m + c)
 
 
end
 
end
  +
k = brks[lvl]
else
 
  +
if not brk_set[k] then
for level=1, rarities[#rarities] * m + c do
 
  +
brk_len = brk_len + 1
table.insert(levels, level)
 
  +
brk_set[k] = true
 
end
 
end
 
end
 
end
  +
-- no value, or the only value is "0".
table.insert(columns, 'ALv.')
 
  +
local has_no_atk = atk_len == 0 or (atk_len == 1 and atk_set[0])
if show_attack then table.insert(columns, 'Attack') end
 
  +
local has_no_brk = atk_len == 0 or (atk_len == 1 and atk_set[0])
if show_breakpower then table.insert(columns, 'Break Power') end
 
  +
-- If only one value, and is not "0".
if show_critchance then table.insert(columns, 'Crit Chance') end
 
  +
local has_fixed_atk = atk_len == 1 and not atk_set[0]
if show_cooldown then table.insert(columns, 'Cooldown') end
 
  +
local has_fixed_brk = brk_len == 1 and not brk_set[0]
for i,level in ipairs(levels) do
 
  +
-- If there are 2 or more different values.
local row = {}
 
  +
-- Is "0" a valid value?
local m = multis[level]
 
  +
local has_varied_atk = atk_len > 1 and not atk_set[0]
table.insert(row, level)
 
  +
local has_varied_brk = brk_len > 1 and not brk_set[0]
if show_attack then
 
  +
-- Create data rows for storage and display.
if const_attack then
 
  +
local data_rows = {}
table.insert(row, attack)
 
  +
for i,lvl in ipairs(lvls) do
else
 
  +
local data_row = {
table.insert(row, round(attack * m))
 
  +
level = lvl,
  +
attack = atks[lvl],
  +
break_power = brks[lvl],
  +
crit_chance = crts[lvl],
  +
cooldown = cds[lvl],
  +
}
  +
data_rows[#data_rows+1] = data_row
  +
end
  +
  +
-- Storage
  +
if NAMESPACE == 0 or args.demospace == "main" then
  +
local abilities = mw.ext.cargo.query("abilities", "_pageName", {
  +
where = string.format("_pageName = %q", FULLPAGENAME),
  +
})
  +
if #abilities > 0 then
  +
for i,data_row in ipairs(data_rows) do
  +
cargo.store("ability_stats", data_row)
 
end
 
end
 
end
 
end
if show_breakpower then
 
if const_breakpower then
 
table.insert(row, breakpower)
 
else
 
table.insert(row, round(breakpower * m))
 
end
 
end
 
if show_critchance then
 
table.insert(row, critchance)
 
end
 
if show_cooldown then
 
local cooldown = cooldowns[level]
 
table.insert(row, cooldown)
 
end
 
table.insert(rows, row)
 
 
end
 
end
 
local wikitable, tr
 
 
 
  +
-- Display
wikitable = mw.html.create('table'):addClass('wikitable'):newline()
 
tr = mw.html.create('tr')
+
local root = mw.html.create("table")
  +
root:addClass("wikitable")
for i,th in ipairs(columns) do
 
  +
local header = root:tag("tr")
tr:tag('th'):wikitext(th)
 
  +
header:tag("th"):wikitext("ALv."):done()
  +
header:tag("th"):wikitext("Attack"):done()
  +
header:tag("th"):wikitext("Break Power"):done()
  +
header:tag("th"):wikitext("Crit Chance"):done()
  +
header:tag("th"):wikitext("Cooldown"):done()
  +
for i,data_row in ipairs(data_rows) do
  +
local lvl = data_row.level
  +
local atk = data_row.attack or 0
  +
local brk = data_row.break_power or 0
  +
local crt = data_row.crit_chance
  +
local stars = number_to_stars(crt)
  +
crt = string.format("%s (%d%%)", stars, crt * 5)
  +
local cd = data_row.cooldown or 0
  +
  +
local tbl_row = root:tag('tr')
  +
tbl_row:tag("td"):wikitext(lvl):done()
  +
tbl_row:tag("td"):wikitext(atk):done()
  +
tbl_row:tag("td"):wikitext(brk):done()
  +
tbl_row:tag("td"):wikitext(crt):done()
  +
tbl_row:tag("td"):wikitext(cd):done()
 
end
 
end
  +
wikitable:node(tr):newline()
 
  +
-- Categories
for i,row in ipairs(rows) do
 
  +
local cats = {}
tr = mw.html.create('tr')
 
  +
if NAMESPACE == 0 or args.demospace == "main" then
for j,td in ipairs(row) do
 
  +
if is_support then
tr:tag('td'):wikitext(td)
 
  +
if has_fixed_atk then
  +
cats[#cats+1] = i18n.categories.support_with_fixed_atk
  +
elseif has_varied_atk then
  +
cats[#cats+1] = i18n.categories.support_with_varied_atk
  +
end
  +
if has_fixed_brk then
  +
cats[#cats+1] = i18n.categories.support_with_fixed_brk
  +
elseif has_varied_brk then
  +
cats[#cats+1] = i18n.categories.support_with_varied_brk
  +
end
  +
if not has_cooldown then
  +
cats[#cats+1] = i18n.categories.support_with_no_cooldown
  +
end
  +
else
  +
if has_no_atk then
  +
cats[#cats+1] = i18n.categories.non_support_with_no_atk
  +
elseif has_fixed_atk then
  +
cats[#cats+1] = i18n.categories.non_support_with_fixed_atk
  +
end
  +
if has_no_brk then
  +
cats[#cats+1] = i18n.categories.non_support_with_no_brk
  +
elseif has_fixed_brk then
  +
cats[#cats+1] = i18n.categories.non_support_with_fixed_brk
  +
end
  +
if has_cooldown then
  +
cats[#cats+1] = i18n.categories.non_support_with_cooldown
  +
end
 
end
 
end
wikitable:node(tr):newline()
 
 
end
 
end
  +
for i,cat in ipairs(cats) do
return tostring(wikitable)
 
  +
cats[i] = string.format('[[Category:%s]]', cat)
  +
end
  +
  +
-- Output
  +
return tostring(root) .. table.concat(cats)
 
end
 
end
   
 
function p.main(frame)
 
function p.main(frame)
  +
mArguments = require("Module:Arguments")
local pframe = frame:getParent()
 
local config = frame.args
+
local args = mArguments.getArgs(frame)
local args = pframe.args
 
 
return p._main(args)
 
return p._main(args)
 
end
 
end

Latest revision as of 11:21, 5 February 2019

Tests

YesY All tests passed.

Name Expected Actual
YesY testFastSupportLevels
YesY testFastWarriorLevels
YesY testNormalSupportLevels
YesY testNormalWarriorLevels

local mArguments -- lazy load
local cargo = require("Module:Cargo")
local cargo_store = cargo.store
local mm = require("Module:Math")
local yesno = require('Module:Yesno')

local p = {}

local BLACK = mw.ustring.char(0x2605)
local WHITE = mw.ustring.char(0x2606)
local NAMESPACE = mw.title.getCurrentTitle().namespace
local FULLPAGENAME = mw.title.getCurrentTitle().fullText

local multis = {1.0, 1.1, 1.2, 1.3, 1.4, 1.7, 2.0, 2.3, 2.6, 3.0}
local cooldowns = {8, 7, 6, 5, 4, 3, 2, 2, 1, 1}

local i18n = {
	categories = {
		-- Tracking categories
		non_support_with_no_atk = "Non-support abilities with no attack",
		non_support_with_no_brk = "Non-support abilities with no break power",
		non_support_with_fixed_atk = "Non-support abilities with fixed attack",
		non_support_with_fixed_brk = "Non-support abilities with fixed break power",
		non_support_with_cooldown = "Non-support abilities with cooldown",
		support_with_varied_atk = "Support abilities with attack",
		support_with_varied_brk = "Support abilities with break power",
		support_with_fixed_atk = "Support abilities with fixed attack",
		support_with_fixed_brk = "Support abilities with fixed break power",
		support_with_no_cooldown = "Support abilities with no cooldown",
	},
	errors = {
		rarity_empty = "No rarity specified.",
		rarity_not_a_number = "Rarity is not a number: %q",
		rarity_out_of_bounds = "Rarity is outside the range of 1-5: %d",
	}
}

local function number_to_stars(n)
	local n = tonumber(n) or 0
	return string.rep(WHITE, n / 10) .. string.rep(BLACK, n % 10)
end

local function parse_number(s)
	return tonumber(s) or 0
end

local function parse_stars(s)
	local s = tostring(s)
		:gsub("&#9733;",  BLACK)
		:gsub("&#x2605;", BLACK)
		:gsub("&starf;",  BLACK)
		:gsub("&#9734;",  WHITE)
		:gsub("&#x2606;", WHITE)
		:gsub("&star;",   WHITE)
	local pattern = "^%s*(["..BLACK..WHITE.."]*)%s*x?%s*(%d*)%s*$"
	local stars, digit = mw.ustring.match(s, pattern)
	if digit == "" and stars == "" then
		mw.log(string.format("Count not find stars or numbers: %q", s))
		return 0
	elseif digit == "" and stars ~= "" then
		local _, bcount, wcount
		_, bcount = string.gsub(stars, BLACK, "")
		_, wcount = string.gsub(stars, WHITE, "")
		return bcount + wcount * 10
	elseif digit ~= "" and (stars == "" or stars == BLACK) then
		return tonumber(digit)
	elseif digit ~= "" and stars ~= "" then
		mw.log(string.format("Number and multiple stars specified: %q", s))
		-- Stars were specified multiple times AND digits also follow.
		return 0
	end
end

function p.get_levels(args)
	local rarity = tostring(args.rarity or '')
	if mw.text.trim(rarity) == '' then
		error(i18n.errors.rarity_empty)
	end
	local jobtype = tostring(args.jobtype):lower()
	local is_support = jobtype == 'support' or jobtype == 'healer'

	local rarities = {}
	local is_fast
	for digit, plus in rarity:gmatch('(%d+)(%+?)') do
		digit = tonumber(digit)
		if digit < 1 or digit > 5 then
			error(i18n.errors.rarity_out_of_bounds:format(digit))
		end
		rarities[#rarities+1] = digit
		is_fast = is_fast or plus == '+'
	end
	if #rarity == 0 then
		error(i18n.errors.rarity_not_a_number:format(rarity))
	end
	local min_rarity = mm._min(unpack(rarities))
	local max_rarity = mm._max(unpack(rarities))
	local m, c
	if is_support then
		m, c = 1, 1
	else
		m, c = 2, 0
	end
	local start = 1
	local stop = m * max_rarity + c
	local step = 1
	if is_fast then
		start = m * min_rarity + c
		step = m
	end
	local levels = {}
	for x=start, stop, step do
		levels[#levels+1] = x
	end
	return levels
end

function p._main(args)
	-- Load args into locals
	local jobtype = tostring(args.jobtype):lower()
	local base_atk = parse_number(args.atk or args.attack)
	local base_brk = parse_number(args.brk or args.break_power or args.breakpower)
	local base_crt = parse_stars(args.crt or args.crit_chance or args.critchance)

	local is_fixed_atk = yesno(args.const_atk or args.const_attack)
	local is_fixed_brk = yesno(args.const_brk or args.const_breakpower)
	local is_support = jobtype == 'support'
	local has_cooldown = args.has_cooldown
	if has_cooldown == nil then -- If there is a support with no-cooldown, then this would be "false"
		has_cooldown = is_support
	else
		has_cooldown = yesno(has_cooldown)
	end

	local lvls = p.get_levels(args)
	local atks = {}
	local brks = {}
	local crts = {}
	local cds = {}
	
	-- Handle fixed and varying stats.
	for i=1,10 do
		local atk
		if is_fixed_atk then
			atk = base_atk
		else
			local atk_i = args['atk'..i] or args['attack'..i]
			local atk_m = base_atk * multis[i]
			atk = atk_i or atk_m
		end
		atks[i] = mm._round(atk)
		local brk
		if is_fixed_brk then
			brk = base_brk
		else
			local brk_i = args['brk'..i] or args['breakpower'..i] or args['break_power'..i]
			local brk_m = base_brk * multis[i]
			brk = brk_i or brk_m
		end
		brks[i] = mm._round(brk)
		crts[i] = base_crt
		if has_cooldown then
			cds[i] = args['cd'..i] or args['cooldown'..i] or cooldowns[i]
		end
	end

	-- If the stat set contains only one value, then it is a fixed stat.
	local atk_set = {}
	local atk_len = 0
	local brk_set = {}
	local brk_len = 0
	-- We're only checking the values at each level.
	for i,lvl in ipairs(lvls) do
		-- Before insert, check if it's already defined.
		local k = atks[lvl]
		if not atk_set[k] then
			atk_len = atk_len + 1
			atk_set[k] = true
		end
		k = brks[lvl]
		if not brk_set[k] then
			brk_len = brk_len + 1
			brk_set[k] = true
		end
	end
	-- no value, or the only value is "0".
	local has_no_atk = atk_len == 0 or (atk_len == 1 and atk_set[0])
	local has_no_brk = atk_len == 0 or (atk_len == 1 and atk_set[0])
	-- If only one value, and is not "0".
	local has_fixed_atk = atk_len == 1 and not atk_set[0]
	local has_fixed_brk = brk_len == 1 and not brk_set[0]
	-- If there are 2 or more different values.
	-- Is "0" a valid value?
	local has_varied_atk = atk_len > 1 and not atk_set[0]
	local has_varied_brk = brk_len > 1 and not brk_set[0]
	-- Create data rows for storage and display.
	local data_rows = {}
	for i,lvl in ipairs(lvls) do
		local data_row = {
			level = lvl,
			attack = atks[lvl],
			break_power = brks[lvl],
			crit_chance = crts[lvl],
			cooldown = cds[lvl],
		}
		data_rows[#data_rows+1] = data_row
	end
	
	-- Storage
	if NAMESPACE == 0 or args.demospace == "main" then
		local abilities = mw.ext.cargo.query("abilities", "_pageName", {
			where = string.format("_pageName = %q", FULLPAGENAME),
		})
		if #abilities > 0 then
			for i,data_row in ipairs(data_rows) do
				cargo.store("ability_stats", data_row)
			end
		end
	end
	
	-- Display
	local root = mw.html.create("table")
	root:addClass("wikitable")
	local header = root:tag("tr")
	header:tag("th"):wikitext("ALv."):done()
	header:tag("th"):wikitext("Attack"):done()
	header:tag("th"):wikitext("Break Power"):done()
	header:tag("th"):wikitext("Crit Chance"):done()
	header:tag("th"):wikitext("Cooldown"):done()
	for i,data_row in ipairs(data_rows) do
		local lvl = data_row.level
		local atk = data_row.attack or 0
		local brk = data_row.break_power or 0
		local crt = data_row.crit_chance
		local stars = number_to_stars(crt)
		crt = string.format("%s (%d%%)", stars, crt * 5)
		local cd = data_row.cooldown or 0

		local tbl_row = root:tag('tr')
		tbl_row:tag("td"):wikitext(lvl):done()
		tbl_row:tag("td"):wikitext(atk):done()
		tbl_row:tag("td"):wikitext(brk):done()
		tbl_row:tag("td"):wikitext(crt):done()
		tbl_row:tag("td"):wikitext(cd):done()
	end
	
	-- Categories
	local cats = {}
	if NAMESPACE == 0 or args.demospace == "main" then
		if is_support then
			if has_fixed_atk then
				cats[#cats+1] = i18n.categories.support_with_fixed_atk
			elseif has_varied_atk then
				cats[#cats+1] = i18n.categories.support_with_varied_atk
			end
			if has_fixed_brk then
				cats[#cats+1] = i18n.categories.support_with_fixed_brk
			elseif has_varied_brk then
				cats[#cats+1] = i18n.categories.support_with_varied_brk
			end
			if not has_cooldown then
				cats[#cats+1] = i18n.categories.support_with_no_cooldown
			end
		else
			if has_no_atk then
				cats[#cats+1] = i18n.categories.non_support_with_no_atk
			elseif has_fixed_atk then
				cats[#cats+1] = i18n.categories.non_support_with_fixed_atk
			end
			if has_no_brk then
				cats[#cats+1] = i18n.categories.non_support_with_no_brk
			elseif has_fixed_brk then
				cats[#cats+1] = i18n.categories.non_support_with_fixed_brk
			end
			if has_cooldown then
				cats[#cats+1] = i18n.categories.non_support_with_cooldown
			end
		end
	end
	for i,cat in ipairs(cats) do
		cats[i] = string.format('[[Category:%s]]', cat)
	end
	
	-- Output
	return tostring(root) .. table.concat(cats)
end

function p.main(frame)
	mArguments = require("Module:Arguments")
	local args = mArguments.getArgs(frame)
	return p._main(args)
end

return p