Module:Skill
Overview
Module for handling skills with semantic media wiki support
Skill templates
Module:Item2
All templates defined in Module:Skill:
{{Skill}}
{{Skill progression}}
Module:Skill link
All templates defined in Module:Skill link:
The above documentation is transcluded from Module:Skill/doc.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
local p = {}
local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local m_game = require('Module:Game')
local mwlanguage = mw.language.getContentLanguage()
--
-- Data
--
local i18n = {
skill_icon = 'File:%s skill icon.png',
args = {
-- skills
skill_id = 'skill_id',
is_support_gem = 'is_support_gem',
support_gem_letter = 'support_gem_letter',
cast_time = 'cast_time',
gem_description = 'gem_description',
active_skill_name = 'active_skill_name',
item_class_restriction = 'item_class_restriction',
projectile_speed = 'projectile_speed',
stat_text = 'stat_text',
quality_stat_text = 'quality_stat_text',
has_percentage_mana_cost = 'has_percentage_mana_cost',
has_reservation_mana_cost = 'has_reservation_mana_cost',
radius = 'radius',
radius_description = 'radius_description',
radius_secondary = 'radius_secondary',
radius_secondary_description = 'radius_secondary_description',
radius_tertiary = 'radius_tertiary',
radius_tertiary_description = 'radius_tertiary_description',
-- skill_levels
level_requirement = 'level_requirement',
dexterity_requirement = 'dexterity_requirement',
intelligence_requirement = 'intelligence_requirement',
strength_requirement = 'strength_requirement',
mana_multiplier = 'mana_multiplier',
critical_strike_chance = 'critical_strike_chance',
mana_cost = 'mana_cost',
damage_effectiveness = 'damage_effectiveness',
stored_uses = 'stored_uses',
cooldown = 'cooldown',
vaal_souls_requirement = 'vaal_souls_requirement',
vaal_stored_uses = 'vaal_stored_uses',
damage_multiplier = 'damage_multiplier',
experience = 'experience',
stat_text = 'stat_text',
quality_stat_text = 'quality_stat_text',
-- skill_stats_per_level
id = 'id',
value = 'value',
-- prefixes
prefix_level = 'level',
prefix_static = 'static_',
prefix_quality_stat = 'quality_',
infix_stat = 'stat'
},
}
--
-- Data
--
local data = {}
data.cast = {}
data.cast.wrap = function(f)
return function(tpl_args, frame, value)
if value == nil then
return nil
else
return f(value)
end
end
end
local map = {}
map.stats = {
{
prefix_in = '',
out = 'stats',
quality = false,
},
{
prefix_in = i18n.args.prefix_quality_stat,
out = 'quality_stats',
quality = true,
},
}
map.static = {
table = 'skill',
fields = {
-- GrantedEffects.dat
skill_id = {
name = i18n.args.skill_id,
field = 'skill_id',
type = 'String',
func = nil,
},
--[[is_support_gem = {
name = i18n.args.is_support_gem,
field = 'is_support_gem',
type = 'Boolean',
func = data.cast.wrap(m_util.cast.boolean),
},
support_gem_letter = {
name = i18n.args.support_gem_letter,
field = 'support_gem_letter',
type = 'String (size=2)',
func = nil,
},-]]--
-- Active Skills.dat
cast_time = {
name = i18n.args.cast_time,
field = 'cast_time',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
},
gem_description = {
name = i18n.args.gem_description,
field = 'description',
type = 'Text',
func = nil,
},
active_skill_name = {
name = i18n.args.active_skill_name,
field = 'active_skill_name',
type = 'String',
func = nil,
},
skill_icon = {
name = i18n.args.skill_icon,
fields = 'skill_icon',
type = 'Page',
func = function(tpl_args, frame)
tpl_args.skill_icon = string.format(i18n.skill_icon, tpl_args.active_skill_name)
end
},
item_class_restriction = {
name = i18n.args.item_class_restriction,
field = 'item_class_restriction',
type = 'List (,) of String',
func = function(tpl_args, frame, value)
if value == nil then
return nil
end
value = m_util.string.split(value, ', ')
for _, v in ipairs(value) do
local result = m_util.table.find_in_nested_array{value=v,key='full', tbl=m_game.constants.item.class}
if result == nil then
error(string.format('Invalid item class: %s', v))
end
end
return value
end,
},
-- Projectiles.dat - manually mapped to the skills
projectile_speed = {
name = i18n.args.projectile_speed,
field = 'projectile_speed',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
},
-- Misc data derieved from stats
stat_text = {
name = i18n.args.stat_text,
field = 'stat_text',
type = 'Text',
func = nil,
},
quality_stat_text = {
name = i18n.args.quality_stat_text,
field = 'quality_stat_text',
type = 'Text',
func = nil,
},
-- Misc data currently not from game data
has_percentage_mana_cost = {
name = i18n.args.has_percentage_mana_cost,
field = 'has_percentage_mana_cost',
type = 'Boolean',
func = data.cast.wrap(m_util.cast.boolean),
},
has_reservation_mana_cost = {
name = i18n.args.has_reservation_mana_cost,
field = 'has_reservation_mana_cost',
type = 'Boolean',
func = data.cast.wrap(m_util.cast.boolean),
},
radius = {
name = i18n.args.radius,
field = 'radius',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
},
radius_description = {
name = i18n.args.radius_description,
field = 'radius_description',
type = 'Text',
func = nil,
},
radius_secondary = {
name = i18n.args.radius_secondary,
field = 'radius_secondary',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
},
radius_secondary_description = {
name = i18n.args.radius_secondary_description,
field = 'radius_secondary_description',
type = 'Text',
func = nil,
},
-- not sure if any skill actually has 3 radius componets
radius_tertiary = {
name = i18n.args.radius_tertiary,
field = 'radius_tertiary',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
},
radius_tertiary_description = {
name = i18n.args.radius_tertiary_description,
field = 'radius_tertiary_description',
type = 'Text',
func = nil,
},
-- Set manually
max_level = {
field = 'max_level',
type = 'Integer',
},
},
}
map.progression = {
table = 'skill_levels',
fields = {
level = {
name = nil,
field = 'level',
type = 'Integer',
func = nil,
header = nil,
},
level_requirement = {
name = i18n.args.level_requirement,
field = 'level_requirement',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('[[Image:Level_up_icon_small.png|link=|Lvl.]]', 'Required Level', 'nounderline'),
},
dexterity_requirement = {
name = i18n.args.dexterity_requirement,
field = 'dexterity_requirement',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('[[Image:DexterityIcon_small.png|link=|dexterity]]', 'Required Dexterity', 'nounderline'),
},
strength_requirement = {
name = i18n.args.strength_requirement,
field = 'strength_requirement',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('[[Image:StrengthIcon_small.png|link=|strength]]', 'Required Strength', 'nounderline'),
},
intelligence_requirement = {
name = i18n.args.intelligence_requirement,
field = 'intelligence_requirement',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('[[Image:IntelligenceIcon_small.png|link=|intelligence]]', 'Required Intelligence', 'nounderline'),
},
mana_multiplier = {
name = i18n.args.mana_multiplier,
field = 'mana_multiplier',
type = 'Float',
func = data.cast.wrap(m_util.cast.number),
header = 'Mana<br>Multiplier',
fmt = '%s%%',
},
critical_strike_chance = {
name = i18n.args.critical_strike_chance,
field = 'critical_strike_chance',
type = 'Float',
func = data.cast.wrap(m_util.cast.number),
header = 'Critical<br>Strike<br>Chance',
fmt = '%s%%',
},
mana_cost = {
name = i18n.args.mana_cost,
field = 'mana_cost',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
header = function (skill_data)
if skill_data["Has reservation mana cost"] then
return 'Mana<br>Reserved'
else
return 'Mana<br>Cost'
end
end,
fmt = function (tpl_args, frame, value)
local str
if tpl_args.has_percentage_mana_cost then
str = '%s%%'
else
str = '%s'
end
return string.format(str, value)
end,
},
damage_effectiveness = {
name = i18n.args.damage_effectiveness,
field = 'damage_effectiveness',
type = 'Float',
func = data.cast.wrap(m_util.cast.number),
header = 'Damage<br>Effectiveness',
fmt = '%s%%',
},
stored_uses = {
name = i18n.args.stored_uses,
field = 'stored_uses',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
header = 'Stored<br>Uses',
},
cooldown = {
name = i18n.args.cooldown,
field = 'cooldown',
type = 'Float',
func = data.cast.wrap(m_util.cast.number),
header = 'Cooldown',
fmt = '%ss',
},
vaal_souls_requirement = {
name = i18n.args.vaal_souls_requirement,
field = 'vaal_souls_requirement',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
header = 'Vaal<br>souls',
},
vaal_stored_uses = {
name = i18n.args.vaal_stored_uses,
field = 'vaal_stored_uses',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
header = 'Stored<br>Uses',
},
damage_multiplier = {
name = i18n.args.damage_multiplier,
field = 'damage_multiplier',
type = 'Float',
func = data.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('Damage<br>Multiplier', 'Deals x% of Base Damage'),
fmt = '%s%%',
},
-- from gem experience, optional
experience = {
name = i18n.args.experience,
field = 'experience',
type = 'Integer',
func = data.cast.wrap(m_util.cast.number),
hide = true,
},
stat_text = {
name = i18n.args.stat_text,
field = 'stat_text',
type = 'Text',
func = nil,
hide = true,
},
quality_stat_text = {
name = i18n.args.quality_stat_text,
field = 'quality_stat_text',
type = 'Text',
func = nil,
hide = true,
},
}
}
map.skill_stats_per_level = {
table = 'skill_stats_per_level',
fields = {
level = {
field = 'level',
type = 'Integer',
},
id = {
field = 'id',
type = 'String',
},
value = {
field = 'value',
type = 'Integer',
},
is_quality_stat = {
field = 'is_quality_stat',
type = 'Boolean',
},
},
}
--
-- Helper functions
--
local h = {}
function h.map_to_arg(tpl_args, frame, properties, prefix_in, map, level)
for key, row in pairs(map.fields) do
if row.name then
local val = tpl_args[prefix_in .. row.name]
if row.func ~= nil then
val = row.func(tpl_args, frame, val)
end
if val ~= nil then
if level ~= nil then
tpl_args.skill_levels[level][key] = val
-- Nuke variables since they're remapped to skill_levels
tpl_args[prefix_in .. row.name] = nil
else
tpl_args[row.name] = val
end
properties[row.field] = val
end
end
end
end
function h.stats(tpl_args, frame, prefix_in, level)
for _, stat_type in ipairs(map.stats) do
local type_prefix = string.format('%s%s%s', prefix_in, stat_type.prefix_in, i18n.args.infix_stat)
tpl_args.skill_levels[level][stat_type.out] = {}
for i=1, 8 do
local stat_id_key = string.format('%s%s_%s', type_prefix, i, i18n.args.id)
local stat_val_key = string.format('%s%s_%s', type_prefix, i, i18n.args.value)
local stat = {
id = tpl_args[stat_id_key],
value = tonumber(tpl_args[stat_val_key]),
}
if stat.id ~= nil and stat.value ~= nil then
tpl_args.skill_levels[level][stat_type.out][#tpl_args.skill_levels[level][stat_type.out]+1] = stat
m_util.cargo.store(frame, {
_table = map.skill_stats_per_level.table,
[map.skill_stats_per_level.fields.level.field] = level,
[map.skill_stats_per_level.fields.id.field] = stat.id,
[map.skill_stats_per_level.fields.value.field] = stat.value,
[map.skill_stats_per_level.fields.is_quality_stat.field] = stat_type.quality,
})
-- Nuke variables since they're remapped to skill levels
tpl_args[stat_id_key] = nil
tpl_args[stat_val_key] = nil
end
end
end
end
function h.na(tr)
tr
:tag('td')
:attr('class', 'table-na')
:wikitext('N/A')
:done()
end
function h.int_value_or_na(tpl_args, frame, tr, value, pdata)
value = tonumber(value)
if value == nil then
h.na(tr)
else
value = mwlanguage:formatNum(value)
if pdata.fmt ~= nil then
if type(pdata.fmt) == 'string' then
value = string.format(pdata.fmt, value)
elseif type(pdata.fmt) == 'function' then
value = pdata.fmt(tpl_args, frame, value)
end
end
tr
:tag('td')
:wikitext(value)
:done()
end
end
--
-- For Template:Skill
--
p.table_skills = m_util.cargo.declare_factory{data=map.static}
p.table_skill_levels = m_util.cargo.declare_factory{data=map.progression}
p.table_skill_stats_per_level = m_util.cargo.declare_factory{data=map.skill_stats_per_level}
function p.skill(frame, tpl_args)
if tpl_args == nil then
tpl_args = getArgs(frame, {
parentFirst = true
})
end
frame = m_util.misc.get_frame(frame)
--
-- Args
--
local properties
tpl_args.skill_levels = {
[0] = {},
}
-- Handle level progression
for i=1, 30 do
local prefix = i18n.args.prefix_level .. i
if m_util.cast.boolean(tpl_args[prefix]) == true then
-- Don't need this anymore
tpl_args[prefix] = nil
tpl_args.skill_levels[i] = {}
prefix = prefix .. '_'
if tpl_args[prefix .. i18n.args.experience] ~= nil then
tpl_args.max_level = i
end
properties = {
_table = map.progression.table,
[map.progression.fields.level.field] = i
}
h.map_to_arg(tpl_args, frame, properties, prefix, map.progression, i)
m_util.cargo.store(frame, properties)
h.stats(tpl_args, frame, prefix, i)
end
end
-- Handle static arguments
properties = {
_table = map.static.table,
[map.static.fields.max_level.field] = tpl_args.max_level
}
h.map_to_arg(tpl_args, frame, properties, '', map.static)
h.map_to_arg(tpl_args, frame, properties, i18n.args.prefix_static, map.progression, 0)
h.stats(tpl_args, frame, i18n.args.prefix_static, 0)
m_util.cargo.store(frame, properties)
end
function p.progression(frame)
local tpl_args = getArgs(frame, {
parentFirst = true
})
frame = m_util.misc.get_frame(frame)
--
tpl_args.stat_format = {}
local prefix
for i=1, 9 do
prefix = 'c' .. i .. '_'
local statfmt = {
header = tpl_args[prefix .. 'header'],
abbr = tpl_args[prefix .. 'abbr'],
pattern_extract = tpl_args[prefix .. 'pattern_extract'],
pattern_value = tpl_args[prefix .. 'pattern_value'],
counter = 0,
}
if m_util.table.has_all_value(statfmt, {'header', 'abbr', 'pattern_extract', 'pattern_value'}) then
break
end
if m_util.table.has_one_value(statfmt, {'header', 'abbr', 'pattern_extract', 'pattern_value'}) then
error(string.format('All formatting keys must be specified for index "%s"', i))
end
statfmt.header = m_util.html.abbr(statfmt.abbr, statfmt.header)
statfmt.abbr = nil
tpl_args.stat_format[#tpl_args.stat_format+1] = statfmt
end
local result
local query
local page
local skill_data
if tpl_args.skill_id then
query = {}
query[#query+1] = string.format('[[Is skill id::%s]]', tpl_args.skill_id)
query[#query+1] = '?Has reservation mana cost#-'
query['limit'] = 1
result = m_util.smw.query(query, frame)
if #result == 0 or #result[1] == 0 then
error('Couldn\'t find a page for the specified skill id')
end
page = result[1][1]
skill_data = result[1]
else
if tpl_args.page then
page = tpl_args.page
else
page = mw.title.getCurrentTitle().prefixedText
end
query = {}
query[#query+1] = string.format('[[%s]]', page)
query[#query+1] = '?Has reservation mana cost#-'
result = m_util.smw.query(query, frame)
if #result == 0 then
error('Couldn\'t find the queried data on the skill page')
end
skill_data = result[1]
end
skill_data["Has reservation mana cost"] = data.cast.wrap(m_util.cast.boolean)(skill_data["Has reservation mana cost"])
query = {}
query[#query+1] = string.format('[[-Has subobject::%s]]', page)
for _, pdata in ipairs(map.progression) do
query[#query+1] = '?' .. pdata.property .. '#-'
end
query[#query+1] = '?Is skill level#-'
query[#query+1] = '?Has stat text'
query[#query+1] = '?Has stat id'
query[#query+1] = '?Has stat value#-'
query[#query+1] = '?Has quality stat id'
query[#query+1] = '?Has quality stat value#-'
query.sort = 'Is skill level'
result = m_util.smw.query(query, frame)
if #result == 0 then
error('No gem level progression data found')
end
headers = {}
for i, row in ipairs(result) do
for k, v in pairs(row) do
if v ~= "" then
headers[k] = true
end
end
end
local tbl = mw.html.create('table')
tbl:attr('class', 'wikitable skill-progression-table')
local head = tbl:tag('tr')
head
:tag('th')
:wikitext('Level')
:done()
for _, pdata in ipairs(map.progression) do
-- TODO should be nil?
if pdata.hide == nil and headers[pdata.property] then
local text
if type(pdata.header) == 'function' then
text = pdata.header(skill_data)
else
text = pdata.header
end
head
:tag('th')
:wikitext(text)
:done()
end
end
for _, statfmt in ipairs(tpl_args.stat_format) do
head
:tag('th')
:wikitext(statfmt.header)
:done()
end
if headers['Has experience requirement'] then
head
:tag('th')
:wikitext(m_util.html.abbr('Exp.', 'Experience Needed to Level Up'))
:done()
:tag('th')
:wikitext(m_util.html.abbr('Total Exp.', 'Total experience needed'))
:done()
end
local tblrow
local lastexp = 0
local experience
for i, row in ipairs(result) do
tblrow = tbl:tag('tr')
tblrow
:tag('th')
:wikitext(row['Is skill level'])
:done()
for _, pdata in ipairs(map.progression) do
if pdata.hide == nil and headers[pdata.property] then
h.int_value_or_na(tpl_args, frame, tblrow, row[pdata.property], pdata)
end
end
-- stats
stats = m_util.string.split(row['Has stat text'], '<br>')
for _, statfmt in ipairs(tpl_args.stat_format) do
local match = {}
for j, stat in ipairs(stats) do
match = {string.match(stat, statfmt.pattern_extract)}
if #match > 0 then
-- TODO maybe remove stat here to avoid testing against in future loops
break
end
end
if #match == 0 then
h.na(tblrow)
else
-- used to find broken progression (i.e. due to game updates)
statfmt.counter = statfmt.counter + 1
tblrow
:tag('td')
:wikitext(string.format(statfmt.pattern_value, match[1], match[2], match[3], match[4], match[5]))
:done()
end
end
-- TODO: Quality stats, afaik no gems use this atm
if headers['Has experience requirement'] then
experience = tonumber(row['Has experience requirement'])
if experience ~= nil then
h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
lastexp = experience
else
h.na(tblrow)
end
h.int_value_or_na(tpl_args, frame, tblrow, experience, {})
end
end
local empty = ''
for _, statfmt in ipairs(tpl_args.stat_format) do
if statfmt.counter == 0 then
empty = '[[Category:Pages with broken skill progression tables]]'
break
end
end
return empty .. tostring(tbl)
end
function p.map(arg)
for key, data in pairs(map[arg].fields) do
mw.logObject(key)
end
end
return p