Editing Module:Sandbox/VergirGive feedback
The edit can be undone.
Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
| Latest revision | Your text | ||
| Line 1: | Line 1: | ||
-------------------------------------------------------------------------------- | |||
-- 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 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 sb_module = require('Module:StreetBrawlAbilities') | |||
-- Shared hero-data lookups, see [[Module:HeroData]]. | |||
local get_stat = hero_module.get_stat | |||
local get_scalings = hero_module.get_scalings | |||
local has_stat = hero_module.has_stat | |||
local 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 = 'pronouns', label_dict = 'Pronouns' }, | |||
{ param = 'voice_actor', label_dict = 'VoiceActor', link = 'Voice actors' }, | |||
{ param = 'release_date', label_dict = 'ReleaseDate', link = 'Heroes by release' }, | |||
{ 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, | |||
}, | |||
{ | |||
name = 'Heroes with Street Brawl changes', | |||
when = function(ctx) return sb_module.hero_has_changes(ctx.hero_key) end, | |||
}, | |||
} | |||
-------------------------------------------------------------------------------- | |||
-- Data access | |||
-------------------------------------------------------------------------------- | |||
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_scalings(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 class="infobox-h-stat-label">%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 class="infobox-h-stat-label">%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.hero_tag(ctx.hero_en, i) | |||
if tag ~= '' 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 | |||
-------------------------------------------------------------------------------- | |||
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 }) | |||
-- |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 hero_key = hero_module.find_hero_key(args.key) or DEFAULT_HERO_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 | |||
-------------------------------------------------------------------------------- | |||
-- TEMPORARY render-diff harness - NOT part of the live module. | |||
-- {{#invoke:Sandbox/Vergir|diff}} or |diff|hero_atlas,hero_viscous | |||
-------------------------------------------------------------------------------- | |||
-- Kept small: each render makes several expensive page_exists calls, and the | |||
-- parser allows only 100 per page. | |||
local SAMPLE_HEROES = { | |||
'hero_atlas', -- burst-ish baseline, has scaling on many stats | |||
'hero_viscous', -- has an AltFire block | |||
'hero_haze', -- spreadshot | |||
'hero_werewolf', -- Silver, has a transformed variant | |||
'hero_targetdummy',-- the fallback hero | |||
} | |||
function p.diff(frame) | |||
local other = require('Module:InfoboxHero') | |||
local keys = {} | |||
if frame.args[1] and frame.args[1] ~= '' then | |||
for key in string.gmatch(frame.args[1], '[^,%s]+') do | |||
table.insert(keys, key) | |||
end | |||
else | |||
keys = SAMPLE_HEROES | |||
end | |||
local checked, mismatches = 0, {} | |||
for _, hero_key in ipairs(keys) do | |||
local child = frame:newChild{ args = { key = hero_key } } | |||
local ok_a, a = pcall(other.render, child) | |||
local ok_b, b = pcall(p.render, child) | |||
if not ok_a then a = 'ERR:' .. tostring(a) end | |||
if not ok_b then b = 'ERR:' .. tostring(b) end | |||
checked = checked + 1 | |||
if a ~= b then | |||
table.insert(mismatches, hero_key .. ' (len ' .. #tostring(a) .. ' vs ' .. #tostring(b) .. ')') | |||
end | |||
end | |||
return 'Heroes rendered through both modules: ' .. checked .. | |||
'\n\nMismatches: ' .. #mismatches .. | |||
(#mismatches > 0 and ('\n* ' .. table.concat(mismatches, '\n* ')) or '\n\nIdentical output.') | |||
end | |||
return p | return p | ||