|
|
| (12 intermediate revisions by 2 users not shown) |
| 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 |