Module:Sandbox/Vergir: Difference between revisionsGive feedback
Parity test for get_hero_scaling_data refactored onto get_scalings (with help from vergir-bot LLM) |
Testing InfoboxHero on shared HeroData helpers (with help from vergir-bot LLM) |
||
| Line 1: | Line 1: | ||
-- | -------------------------------------------------------------------------------- | ||
-- {{#invoke: | -- 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 function | local hero_data_all = mw.loadJsonData('Data:HeroData.json') | ||
local | local theme_data = mw.loadJsonData('Data:Infobox_hero.json') | ||
return | 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 | end | ||
function | -- Rounded numeric value of a stat, honouring a |param= override. | ||
local | stat_number = function(ctx, stat, param) | ||
for _, | 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 | |||
if (value | 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 | end | ||
end | end | ||
function | local function render_information(ctx, parent) | ||
local | local present = {} | ||
-- | for _, row in ipairs(INFO_ROWS) do | ||
if ctx.args[row.param] ~= '' then present[#present + 1] = row end | |||
local | end | ||
for | 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 | ||
end | end | ||
local function | local function render_title(ctx, parent) | ||
local th = parent:tag('tr'):tag('th'):attr('colspan', '2'):addClass('infobox-h-title') | |||
local | 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 | ||
end | end | ||
function | local function render_card(ctx, parent) | ||
local | 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 | ||
end | end | ||
if #tags == 0 then return end | |||
local | local inner = parent:tag('tr'):tag('td') | ||
local | :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 | |||
local | if type(name) == 'function' then name = name(ctx) end | ||
out[#out + 1] = ctx.frame:expandTemplate{ title = 'lang cat', args = { name } } | |||
end | end | ||
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 }) | |||
local | -- |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) | |||
table. | 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 | end | ||
if | |||
return table.concat( | 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 | end | ||
return p | return p | ||