Module:InfoboxHero: Difference between revisionsGive feedback
Jump to navigation
Jump to search
mNo edit summary |
add tooltips (the fancy template style) |
||
| Line 317: | Line 317: | ||
{ param = 'pronouns', label_dict = 'Pronouns' }, | { param = 'pronouns', label_dict = 'Pronouns' }, | ||
{ param = 'voice_actor', label_dict = 'VoiceActor' }, | { param = 'voice_actor', label_dict = 'VoiceActor' }, | ||
{ param = 'codenames', label_dict = 'Codenames' }, | { param = 'codenames', label_dict = 'Codenames', tooltip = 'Codenames used for this hero in the game files' }, | ||
} | } | ||
| Line 616: | Line 616: | ||
local tr = tbl:tag('tr') | local tr = tbl:tag('tr') | ||
local label = dict_module.translate(row.label_dict) | local label = dict_module.translate(row.label_dict) | ||
if row.tooltip then | |||
label = ctx.frame:expandTemplate{ title = 'Tooltip', args = { label, row.tooltip } } | |||
end | |||
if row.link then | if row.link then | ||
tr:tag('td'):wikitext(string.format('[[%s|<span>%s</span>]]:', | tr:tag('td'):wikitext(string.format('[[%s|<span>%s</span>]]:', | ||
Revision as of 19:45, 16 July 2026
Documentation for this module may be created at Module:InfoboxHero/doc
--------------------------------------------------------------------------------
-- Module:InfoboxHero
--
-- Renders the hero infobox. Called from [[Template:Infobox hero]], which passes
-- the editor's parameters through as the parent frame:
--
-- {{#invoke:InfoboxHero|render}}
--
-- May also be invoked directly for testing, in which case parameters are read
-- from the invoke itself:
--
-- {{#invoke:InfoboxHero|render|key=hero_atlas}}
--
-- Stat data comes from [[Data:HeroData.json]]; per-hero colors, backgrounds and
-- logo sizes from [[Data:Infobox hero.json]]; labels from [[Module:Lang]] and
-- [[Module:Dictionary]]. Styling lives in [[Template:Infobox hero/styles.css]]
-- and [[Template:Infobox hero/themes.css]].
--
--------------------------------------------------------------------------------
local p = {}
local hero_data_all = mw.loadJsonData('Data:HeroData.json')
local theme_data = mw.loadJsonData('Data:Infobox_hero.json')
local util = require('Module:Utilities')
local lang_module = require('Module:Lang')
local dict_module = require('Module:Dictionary')
local hero_module = require('Module:HeroData')
local get_stat, has_stat, has_override_or_stat, stat_number
--------------------------------------------------------------------------------
-- CONFIG
--------------------------------------------------------------------------------
local DEFAULT_HERO_KEY = 'hero_targetdummy' -- used when |key= is missing/unknown
local DEFAULT_SIG_FIGS = 3
local FALLBACK_CARD = 'Dummy card.png'
-- Row fields (all optional except a label):
-- stat Key in Data:HeroData.json. Resolved top-level, then .Weapon,
-- then .Weapon.AltFire. Omit for manual-input-only rows.
-- param |param= that overrides the value.
-- label Key in Data:Lang_<code>.json.
-- label_dict Key in Data:Dictionary. Used instead of `label`.
-- label_text Literal, untranslated label. Used instead of `label`.
-- link Target for the label, as 'Page' or 'Page#Anchor'. The language
-- suffix is inserted for us, so write the plain English title.
-- icon_link Target for the icon. Defaults to `link`.
-- icon File: name of the value icon. Omit for no icon.
-- sig_figs Significant figures. Defaults to 3.
-- suffix Appended directly after the value, e.g. '%', 's', 'm/s'.
-- scaling Set false to suppress the Boon/Spirit scaling span. Scaling is
-- shown by default wherever the data provides it.
-- optional Hide the row unless the hero has a non-zero value or scaling.
-- cell_class Extra class on the value cell.
-- show function(ctx) -> boolean. Row omitted when false.
local SECTIONS = {
{
class = 'infobox-h-dropdown-weapon',
header = 'CitadelHeroStats_Weapon',
link = 'Stats#Weapon',
color = 'Brown',
rows = {
{
stat = 'DPS',
param = 'dps',
label = 'DPS_label',
link = 'Damage per second',
icon = 'Damage.png',
-- DPS is derived from bullet damage and fire rate, both of which
-- show their own scaling; repeating it here would double-count.
scaling = false,
},
{
stat = 'BulletDamage',
param = 'bullet_damage',
label = 'StatDesc_BulletDamage',
link = 'Bullet Damage',
icon = 'Bullet damage.png',
cell_class = 'infobox-h-stat-value-padded',
},
{
stat = 'BulletsPerShot',
param = 'pellets_per_shot',
label = 'BonusPelletsPerShot_label',
link = 'Damage per second',
icon = 'Bullets per sec icon.png',
icon_link = 'Fire Rate',
show = function(ctx) return ctx.pellets > 1 end,
},
{
stat = 'BulletsPerBurst',
param = 'bullets_per_burst',
label_dict = 'BulletsPerBurst',
link = 'Damage per second',
icon = 'Bullets per sec icon.png',
icon_link = 'Fire Rate',
show = function(ctx) return ctx.burst > 1 end,
},
{
stat = 'ClipSize',
param = 'ammo',
label = 'StatDesc_ClipSizeBonus',
link = 'Ammo',
icon = 'Clip size icon.png',
},
{
stat = 'RoundsPerSecond',
param = 'bullets_per_sec',
label = 'StatDesc_RoundsPerSecond',
link = 'Fire Rate',
icon = 'Bullets per sec icon.png',
},
{
stat = 'ReloadTime',
param = 'reload_time',
label = 'StatDesc_ReloadTime',
link = 'Reload Time',
icon = 'AttributeIconReloadTime.png',
suffix = 's',
},
{
stat = 'BulletSpeed',
param = 'bullet_velocity',
label = 'StatDesc_BulletSpeed',
link = 'Bullet Velocity',
icon = 'AttributeIconBulletSpeed.png',
suffix = 'm/s',
},
{
stat = 'LightMeleeDamage',
param = 'light_melee',
label = 'StatDesc_LightMeleeDamage',
link = 'Melee Damage',
icon = 'Melee damage.png',
cell_class = 'infobox-h-stat-value-padded',
},
{
stat = 'HeavyMeleeDamage',
param = 'heavy_melee',
label = 'StatDesc_HeavyMeleeDamage',
link = 'Melee Damage',
icon = 'Melee damage.png',
cell_class = 'infobox-h-stat-value-padded',
},
{
param = 'max_range',
label_text = 'Max Range',
link = 'Falloff Range',
show = function(ctx) return ctx.args.max_range ~= '' end,
},
{
stat = 'CritDamageBonusPercent',
label = 'StatDesc_CritDamageBonusScale',
link = 'Weapon Damage#Crit_Multiplier',
icon = 'Crit reduction.png',
suffix = '%',
show = function(ctx)
return stat_number(ctx, 'CritDamageBonusPercent') ~= 0
end,
},
},
},
{
class = 'infobox-h-dropdown-vitality',
header = 'CitadelHeroStats_Vitality',
link = 'Stats#Vitality',
color = 'Green',
rows = {
{
stat = 'MaxHealth',
param = 'max_health',
label = 'StatDesc_HealthAttribute',
link = 'Health',
icon = 'Health.png',
},
{
stat = 'BaseHealthRegen',
param = 'health_regen',
label = 'StatDesc_HealthRegen',
link = 'Health Regen',
icon = 'Health regen.png',
sig_figs = 2,
},
{
stat = 'CritDamageReceivedPercent',
param = 'crit_reduction',
label = 'StatDesc_CritDamageReceivedScale',
link = 'Damage Resistance#Crit_Reduction',
icon = 'Crit reduction.png',
suffix = '%',
optional = true,
},
{
stat = 'BulletResist',
param = 'bullet_resist',
label = 'StatDesc_BulletArmorDamageReduction',
link = 'Damage Resistance',
icon = 'Bullet Armor.png',
icon_link = 'Bullet Resist',
suffix = '%',
optional = true,
},
{
stat = 'TechResist',
param = 'spirit_resist',
label = 'StatDesc_TechArmorDamageReduction',
link = 'Damage Resistance#Spirit_Resist',
icon = 'Spirit Armor.png',
suffix = '%',
optional = true,
},
{
stat = 'MeleeResist',
param = 'melee_resist',
label = 'StatDesc_MeleeResist',
link = 'Damage Resistance#Melee Resist',
icon = 'Melee damage.png',
suffix = '%',
optional = true,
},
{
stat = 'DebuffResist',
param = 'debuff_resist',
label = 'StatDesc_DebuffResist',
link = 'Debuff Resist',
icon = 'Spirit Armor.png',
suffix = '%',
optional = true,
},
{
stat = 'BulletLifesteal',
param = 'bullet_lifesteal',
label = 'StatDesc_BulletLifesteal',
link = 'Lifesteal#Bullet Lifesteal',
icon = 'AttributeIconBulletLifesteal.png',
suffix = '%',
optional = true,
},
{
stat = 'SpiritLifesteal',
param = 'spirit_lifesteal',
label = 'StatDesc_SpiritLifesteal',
link = 'Lifesteal#Spirit Lifesteal',
icon = 'AttributeIconBulletLifesteal.png',
suffix = '%',
optional = true,
},
{
stat = 'MaxMoveSpeed',
param = 'move_speed',
label = 'StatDesc_RunSpeed',
link = 'Move Speed',
icon = 'Move speed.png',
sig_figs = 2,
suffix = 'm/s',
},
{
stat = 'SprintSpeed',
param = 'sprint_speed',
label = 'StatDesc_SprintSpeed',
link = 'Move Speed#Sprint',
icon = 'Move speed sprint.png',
sig_figs = 2,
suffix = 'm/s',
},
{
stat = 'GroundDashSpeed',
label = 'DashSpeed_label',
link = 'Stamina#Dash_Speed',
icon = 'Springy boots icon.png',
suffix = 'm/s',
},
{
stat = 'Stamina',
param = 'stamina',
label = 'StatDesc_Stamina',
link = 'Stamina',
icon = 'Springy boots icon.png',
sig_figs = 2,
},
{
stat = 'StaminaCooldown',
param = 'stamina_cooldown',
label = 'StatDesc_StaminaCooldown',
link = 'Stamina#Cooldown',
icon = 'AttributeIconTechDuration.png',
sig_figs = 2,
suffix = 's',
},
},
},
{
class = 'infobox-h-dropdown-spirit',
header = 'CitadelHeroStats_Spirit',
link = 'Stats#Spirit',
color = 'Purple',
rows = {
{
stat = 'TechPower',
param = 'spirit_power',
label = 'SpiritPower_label',
link = 'Spirit Power',
icon = 'Spirit icon noborder.png',
sig_figs = 2,
},
},
},
}
-- The "Information" block. Manual input only; hidden entirely when all are empty.
local INFO_ROWS = {
{ param = 'release_date', label_dict = 'ReleaseDate', link = 'Heroes by release' },
{ param = 'pronouns', label_dict = 'Pronouns' },
{ param = 'voice_actor', label_dict = 'VoiceActor' },
{ param = 'codenames', label_dict = 'Codenames', tooltip = 'Codenames used for this hero in the game files' },
}
-- name: literal, or function(ctx) -> string. when: function(ctx) -> boolean.
local CATEGORIES = {
{ name = 'Heroes' },
{
name = function(ctx) return ctx.hero_en end,
-- Silver (Transformed) is documented on Silver's page and shares her category.
when = function(ctx) return ctx.hero_en ~= 'Silver (Transformed)' end,
},
{
name = 'Heroes with spreadshot weapons',
when = function(ctx) return ctx.pellets > 1 end,
},
{
name = 'Heroes with single damage spreadshot weapons',
when = function(ctx)
return ctx.pellets > 1 and get_stat(ctx.hero, 'HitOnceAcrossAllBullets') == true
end,
},
{
name = 'Heroes with burst weapons',
when = function(ctx) return ctx.burst > 1 end,
},
{
name = 'Heroes with automatic weapons',
when = function(ctx) return ctx.pellets <= 1 and ctx.burst <= 1 end,
},
{
name = 'Heroes with alternate fire weapons',
when = function(ctx)
return ctx.hero.Weapon ~= nil and ctx.hero.Weapon.AltFire ~= nil
end,
},
{
name = 'Heroes with single reload weapons',
when = function(ctx) return get_stat(ctx.hero, 'ReloadSingle') == true end,
},
{
name = 'Heroes with spin up weapons',
when = function(ctx) return has_stat(ctx.hero, 'SpinAcceleration') end,
},
{
name = 'Heroes with no weapon damage falloff',
when = function(ctx)
return (get_stat(ctx.hero, 'FalloffStartRange') or 0)
== (get_stat(ctx.hero, 'FalloffEndRange') or 0)
end,
},
{
name = "Heroes that don't deal critical damage",
when = function(ctx) return get_stat(ctx.hero, 'CanCrit') == false end,
},
{
name = 'Heroes with modified crit scale',
when = function(ctx) return stat_number(ctx, 'CritDamageBonusPercent') ~= 0 end,
},
{
name = 'Heroes with modified crit resistance',
when = function(ctx)
return has_override_or_stat(ctx, 'crit_reduction', 'CritDamageReceivedPercent')
end,
},
{
name = 'Heroes with modified bullet resist',
when = function(ctx)
return has_override_or_stat(ctx, 'bullet_resist', 'BulletResist')
end,
},
{
name = 'Heroes with modified spirit resist',
when = function(ctx)
return has_override_or_stat(ctx, 'spirit_resist', 'TechResist')
end,
},
{
name = 'Heroes with modified melee resist',
when = function(ctx)
return has_override_or_stat(ctx, 'melee_resist', 'MeleeResist')
end,
},
{
name = 'Heroes with modified debuff resist',
when = function(ctx)
return has_override_or_stat(ctx, 'debuff_resist', 'DebuffResist')
end,
},
{
name = 'Heroes with modified bullet lifesteal',
when = function(ctx)
return has_override_or_stat(ctx, 'bullet_lifesteal', 'BulletLifesteal')
end,
},
{
name = 'Heroes with modified spirit lifesteal',
when = function(ctx)
return has_override_or_stat(ctx, 'spirit_lifesteal', 'SpiritLifesteal')
end,
},
{
name = 'Heroes with modified gravity scale',
when = function(ctx) return has_stat(ctx.hero, 'GravityChange') end,
},
{
name = function(ctx) return 'Heroes with ' .. ctx.stamina .. ' stamina' end,
when = function(ctx) return ctx.stamina ~= '' end,
},
{
name = function(ctx)
if ctx.dash < 14.7 then return 'Heroes with slow dash speed' end
if ctx.dash > 14.7 then return 'Heroes with fast dash speed' end
return 'Heroes with medium dash speed'
end,
},
}
--------------------------------------------------------------------------------
-- Data access
--------------------------------------------------------------------------------
-- Stats live at the top level for vitality, and under .Weapon for weapon stats.
get_stat = function(hero, stat)
if hero == nil or stat == nil then return nil end
local value = hero[stat]
if value == nil and hero.Weapon then
value = hero.Weapon[stat]
if value == nil and hero.Weapon.AltFire then
value = hero.Weapon.AltFire[stat]
end
end
return value
end
-- The raw Boon (Level) and Spirit scaling values for a stat, if any.
local function get_scaling(hero, stat)
local out = {}
for _, scaling_type in ipairs({ 'Level', 'Spirit' }) do
local scaling = hero[scaling_type .. 'Scaling']
local value = scaling and stat and scaling[stat]
if value ~= nil and value ~= 0 then
out[#out + 1] = { type = scaling_type, value = value }
end
end
return out
end
-- True when the hero has a non-zero base value, or any scaling, for a stat.
has_stat = function(hero, stat)
local value = get_stat(hero, stat)
if value ~= nil and value ~= 0 then return true end
return #get_scaling(hero, stat) > 0
end
has_override_or_stat = function(ctx, param, stat)
if param and ctx.args[param] ~= '' then return true end
return has_stat(ctx.hero, stat)
end
-- Rounded numeric value of a stat, honouring a |param= override.
stat_number = function(ctx, stat, param)
if param and ctx.args[param] ~= '' then return tonumber(ctx.args[param]) or 0 end
return tonumber(util.round_to_sig_fig(get_stat(ctx.hero, stat) or 0, DEFAULT_SIG_FIGS)) or 0
end
local function format_value(raw, sig_figs)
if raw == nil then return '0' end
local rounded = util.round_to_sig_fig(raw, sig_figs or DEFAULT_SIG_FIGS)
return tostring(rounded ~= nil and rounded or raw)
end
--------------------------------------------------------------------------------
-- Rendering helpers
--------------------------------------------------------------------------------
local function theme_var(hero_key, name)
local hero_theme = theme_data[hero_key]
if hero_theme and hero_theme[name] ~= nil then return hero_theme[name] end
return theme_data['hero_default'][name]
end
local function label_of(row)
if row.label_text then return row.label_text end
if row.label_dict then return dict_module.translate(row.label_dict) end
return lang_module.get_string(row.label)
end
-- 'Damage Resistance#Spirit_Resist' -> 'Damage Resistance/ru#Spirit_Resist'
local function page_link(ctx, target)
if not target then return '' end
local page, anchor = target:match('^([^#]*)(#?.*)$')
return page .. ctx.if_lang .. anchor
end
local function icon_cell(ctx, row, color, value_html)
local icon = ''
if row.icon then
icon = string.format('[[File:%s|20px|link=%s]]', row.icon,
page_link(ctx, row.icon_link or row.link))
end
return ctx.frame:expandTemplate{ title = 'Icon/' .. color, args = { icon, value_html } }
end
-- Ammo and reload time are meaningless for heroes whose weapon never reloads.
local NO_AMMO_STATS = { ClipSize = true, ReloadTime = true }
-- Returns true when the row rendered.
local function render_stat_row(ctx, tbl, row, color, is_first)
if row.show and not row.show(ctx) then return false end
if row.optional and not has_override_or_stat(ctx, row.param, row.stat) then return false end
local value, scaling_inner
if NO_AMMO_STATS[row.stat] and ctx.args.no_ammo == 'true' then
value, scaling_inner = 'N/A', ''
else
local override = row.param and ctx.args[row.param] or ''
if override ~= '' then
value = override
else
value = format_value(get_stat(ctx.hero, row.stat), row.sig_figs)
end
value = value .. (row.suffix or '')
if row.stat and row.scaling ~= false then
local parts = {}
for _, scaling in ipairs(get_scaling(ctx.hero, row.stat)) do
parts[#parts + 1] = hero_module.write_scalar_str(scaling.value, scaling.type)
end
scaling_inner = table.concat(parts)
value = value .. '<span class="infobox-h-spiritboon">' .. scaling_inner .. '</span>'
else
scaling_inner = ''
end
end
local tr = tbl:tag('tr')
local label_cell = tr:tag('td')
-- The section's first row needs extra headroom when it carries a scaling icon.
if is_first and scaling_inner ~= '' then
label_cell:addClass('infobox-h-stat-label-scaled')
end
if row.link then
label_cell:wikitext(string.format('[[%s|<span>%s</span>]]:',
page_link(ctx, row.link), label_of(row)))
else
label_cell:wikitext(label_of(row) .. ':')
end
local value_cell = tr:tag('td')
if row.cell_class then value_cell:addClass(row.cell_class) end
value_cell:wikitext(icon_cell(ctx, row, color, value))
return true
end
local function render_section(ctx, parent, section)
local tbl = parent:tag('tr'):tag('th')
:attr('colspan', '2')
:addClass('infobox-h-subsection infobox-h-dropdown ' .. section.class)
:tag('table'):addClass('mw-collapsible infobox-h-subsection-child')
local caption = tbl:tag('caption'):addClass('infobox-h-dropdown-header')
if ctx.if_lang == '/ru' then
caption:addClass('infobox-h-dropdown-header-overflow')
end
caption:wikitext(string.format(
'[[%s|<span class="infotext-h-bold infobox-h-dropdown-title">%s</span>]]',
page_link(ctx, section.link), lang_module.get_string(section.header)))
local is_first = true
for _, row in ipairs(section.rows) do
if render_stat_row(ctx, tbl, row, section.color, is_first) then
is_first = false
end
end
end
local function render_information(ctx, parent)
local present = {}
for _, row in ipairs(INFO_ROWS) do
if ctx.args[row.param] ~= '' then present[#present + 1] = row end
end
if #present == 0 then return end
parent:tag('tr'):tag('th')
:attr('colspan', '2')
:addClass('infobox-h-text-stat-subtitle')
:wikitext('<span class="infotext-h-bold infotext-h-title infotext-h-big">'
.. dict_module.translate('Information') .. '</span>')
local tbl = parent:tag('tr'):tag('th')
:attr('colspan', '2')
:addClass('infobox-h-subsection')
:tag('table'):addClass('infobox-h-subsection-child')
for _, row in ipairs(present) do
local tr = tbl:tag('tr')
local label = dict_module.translate(row.label_dict)
if row.tooltip then
label = ctx.frame:expandTemplate{ title = 'Tooltip', args = { label, row.tooltip } }
end
if row.link then
tr:tag('td'):wikitext(string.format('[[%s|<span>%s</span>]]:',
page_link(ctx, row.link), label))
else
tr:tag('td'):wikitext(label .. ':')
end
tr:tag('td'):wikitext(ctx.args[row.param])
end
end
local function render_title(ctx, parent)
local th = parent:tag('tr'):tag('th'):attr('colspan', '2'):addClass('infobox-h-title')
if ctx.args.name_font_size ~= '' then
th:cssText('font-size: ' .. ctx.args.name_font_size .. ';')
end
local wrap = th:tag('div'):addClass('infobox-h-title-hero')
wrap:tag('div'):addClass('infobox-h-title-hero-left')
-- Name logo: <Name>_name.svg, or <Name>_name_<lang>.svg on translated pages.
-- Heroes without a logo fall back to their name as plain text.
local svg = ((ctx.args.name ~= '' and ctx.args.name) or ctx.hero_en) .. '_name'
if ctx.if_lang ~= '' then svg = svg .. '_' .. ctx.lang_code end
local middle = wrap:tag('div'):addClass('infobox-h-title-hero-middle')
if util.page_exists('File:' .. svg .. '.svg') then
middle:wikitext(ctx.frame:expandTemplate{ title = 'Icon/White', args = {
string.format('[[File:%s.svg|%s|class=notpageimage]]',
svg, theme_var(ctx.hero_key, 'logo-height')) } })
else
middle:wikitext(ctx.display_name)
end
local right = wrap:tag('div'):addClass('infobox-h-title-hero-right'):tag('div')
if ctx.show_icon then
local icon = ((ctx.args.icon ~= '' and ctx.args.icon) or ctx.hero_en) .. '.png'
if util.page_exists('File:' .. icon) then
right:tag('div'):addClass('infobox-h-title-hero-tornbox')
right:tag('div'):addClass('infobox-h-title-hero-icon')
:wikitext(string.format('[[File:%s|center|35px|class=notpageimage]]', icon))
end
end
end
local function render_card(ctx, parent)
local card = (ctx.args.card ~= '' and ctx.args.card) or (ctx.hero_en .. ' card.png')
if not util.page_exists('File:' .. card) then card = FALLBACK_CARD end
parent:tag('tr'):tag('th')
:attr('colspan', '2')
:addClass('infobox-h-image')
:tag('div')
:wikitext(string.format('[[File:%s|150px|alt=%s]]',
card, mw.title.getCurrentTitle().baseText))
end
local function render_tags(ctx, parent)
if not ctx.show_hero_tags then return end
local tags = {}
for i = 1, 3 do
local tag = hero_module.get_hero_tag({ args = { ctx.hero_en, tostring(i) } })
if tag and tag ~= '' and not tag:find('Error:', 1, true) then
tags[#tags + 1] = tag
end
end
if #tags == 0 then return end
local inner = parent:tag('tr'):tag('td')
:attr('colspan', '2')
:addClass('infobox-h-herotags-row')
:tag('div'):addClass('infobox-h-herotags-inner ' .. ctx.hero_key)
for _, tag in ipairs(tags) do
inner:tag('div'):addClass('infobox-h-herotag-pill'):wikitext(tag)
end
end
local function render_categories(ctx)
if mw.ustring.lower(ctx.args.nocat ~= '' and ctx.args.nocat or 'false') ~= 'false' then
return ''
end
if get_stat(ctx.hero, 'IsDisabled') ~= false then return '' end
local out = {}
for _, category in ipairs(CATEGORIES) do
if category.when == nil or category.when(ctx) then
local name = category.name
if type(name) == 'function' then name = name(ctx) end
out[#out + 1] = ctx.frame:expandTemplate{ title = 'lang cat', args = { name } }
end
end
return table.concat(out)
end
--------------------------------------------------------------------------------
-- Entry point
--------------------------------------------------------------------------------
-- |key= accepts a hero_* key or the hero's English name; unknown values fall back
-- to the target dummy, the default character in Sandbox mode.
local function resolve_hero_key(provided)
if provided == nil or provided == '' then return DEFAULT_HERO_KEY end
if hero_data_all[provided] ~= nil then return provided end
local wanted = mw.ustring.lower(provided)
for key, hero in pairs(hero_data_all) do
if hero.Name and mw.ustring.lower(hero.Name) == wanted then return key end
end
return DEFAULT_HERO_KEY
end
function p.render(frame)
local parent = frame:getParent()
local invoke_args = frame.args
local template_args = parent and parent.args or {}
-- Missing parameters read as '' so callers never have to nil-check.
local args = setmetatable({}, { __index = function(_, k)
local value = invoke_args[k]
if value == nil then value = template_args[k] end
return value or ''
end })
local hero_key = resolve_hero_key(args.key)
local hero = hero_data_all[hero_key]
local ctx = {
frame = frame,
args = args,
hero = hero,
hero_key = hero_key,
hero_en = hero.Name,
lang_code = lang_module.get_lang_code(),
if_lang = frame:expandTemplate{ title = 'If lang' },
}
ctx.display_name = (args.name ~= '' and args.name) or lang_module.get_string(hero_key)
ctx.show_hero_tags = args.show_hero_tags ~= 'false'
ctx.show_icon = args.show_icon ~= 'false'
-- Needed by both row visibility and the category rules.
ctx.pellets = stat_number(ctx, 'BulletsPerShot', 'pellets_per_shot')
ctx.burst = stat_number(ctx, 'BulletsPerBurst', 'bullets_per_burst')
ctx.dash = stat_number(ctx, 'GroundDashSpeed')
ctx.stamina = (args.stamina ~= '' and args.stamina)
or format_value(get_stat(hero, 'Stamina'), 2)
local root = mw.html.create('div'):addClass('infobox-h ' .. hero_key)
root:tag('div'):addClass('infobox-h-background')
:wikitext(string.format('[[File:%s|%s|right|link=|class=notpageimage]]',
theme_var(hero_key, 'bg-image'), theme_var(hero_key, 'bg-height')))
local tbl = root:tag('table'):addClass('infobox-h-content')
render_title(ctx, tbl)
render_card(ctx, tbl)
render_tags(ctx, tbl)
for _, section in ipairs(SECTIONS) do
render_section(ctx, tbl, section)
end
render_information(ctx, tbl)
if mw.title.getCurrentTitle().nsText ~= 'Template' then
frame:callParserFunction('DISPLAYTITLE', ctx.display_name)
end
return tostring(root) .. render_categories(ctx)
end
return p