Module:HeroData: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
LVL (talk | contribs)
Now bucket 3 fixed
Vergir (talk | contribs)
Consolidate stat/scaling lookups into shared helpers, add resolve_key and has_tags, drop unused dependencies and dead code (with help from vergir-bot LLM)
Line 1: Line 1:
local p = {};
local p = {};
local heroes_data = mw.loadJsonData("Data:HeroData.json")
local heroes_data = mw.loadJsonData("Data:HeroData.json")
local attributes_data = mw.loadJsonData("Data:AttributeData.json")
local attribute_orders = mw.loadJsonData("Data:StatInfoboxOrder.json")
local util_module = require('Module:Utilities')
local util_module = require('Module:Utilities')
local lang_module = require('Module:Lang')
local lang_module = require('Module:Lang')
local dictionary_module = require('Module:Dictionary')
 
local attribute_module = require('Module:AttributeData')
--------------------------------------------------------------------------------
-- Internal helpers
--------------------------------------------------------------------------------
 
-- Localized string for a key, falling back to a readable version of `fallback`
-- plus a "missing translation" tooltip.
local function localize(key, fallback)
local result = lang_module.get_string(key)
if (result == "") or (result == nil) then
result = util_module.add_space_before_cap(fallback) .. mw.getCurrentFrame():expandTemplate{title="MissingValveTranslationTooltip"}
end
return result
end
 
-- Raw scaling value of a stat under LevelScaling / SpiritScaling, or nil when
-- the hero has no scaling of that type for that stat.
local function raw_scaling(hero_data, scaling_type, stat)
local scaling = hero_data and hero_data[scaling_type .. "Scaling"]
if (scaling == nil or stat == nil) then return nil end
return scaling[stat]
end
 
-- Mapping from hero name to internal tag name
local TAG_NAME_MAP = {
["Abrams"] = "Abrams",
["Bebop"] = "Bebop",
["Billy"] = "Punkgoat",
["Calico"] = "Nano",
["The Doorman"] = "Doorman",
["Drifter"] = "Drifter",
["Dynamo"] = "Dynamo",
["Apollo"] = "Fencer",
["Graves"] = "Necro",
["Grey Talon"] = "Orion",
["Haze"] = "Haze",
["Holliday"] = "Astro",
["Infernus"] = "Inferno",
["Ivy"] = "Tengu",
["Kelvin"] = "Kelvin",
["Lady Geist"] = "Geist",
["Lash"] = "Lash",
["McGinnis"] = "Engineer",
["Mina"] = "VampireBat",
["Mirage"] = "Mirage",
["Mo & Krill"] = "Digger",
["Paige"] = "Bookworm",
["Paradox"] = "Chrono",
["Celeste"] = "Unicorn",
["Pocket"] = "Synth",
["Rem"] = "Familiar",
["Seven"] = "Gigawatt",
["Shiv"] = "Shiv",
["Silver"] = "Werewolf",
["Silver (Transformed)"] = "Werewolf",
["Sinclair"] = "Magician",
["Venator"] = "Priest",
["Victor"] = "Frank",
["Vindicta"] = "Vindicta",
["Viscous"] = "Viscous",
["Vyper"] = "Viper",
["Warden"] = "Warden",
["Wraith"] = "Wraith",
["Yamato"] = "Yamato",
}
 
--------------------------------------------------------------------------------
-- Data access, for use by other modules
--------------------------------------------------------------------------------


-- returns the table of a specific item, used by external modules
-- returns the table of a specific item, used by external modules
Line 18: Line 83:
end
end


-- Returns an array of item tables that have the same properties
-- Hero table for a hero_* key or an exact English name, or nil.
-- @function   get_similar_items
local function hero_by_key_or_name(input)
-- @param      {string}
local hero = heroes_data[input]
-- @return    {array of tables}
if (hero == nil) then hero = p.get_json_item(input) end
local function get_similar_items(property)
return hero
local similarItems = {}
end
for _, v in pairs(heroes_data) do
 
if (v[property] ~= nil) then
-- Value of a stat, checking the hero's top level, then Weapon, then AltFire.
table.insert(similarItems, v)
-- Returns nil when the hero has no such stat.
function p.get_stat(hero_data, stat)
if (hero_data == nil or stat == nil) then return nil end
local value = hero_data[stat]
if (value == nil and hero_data.Weapon) then
value = hero_data.Weapon[stat]
end
if (value == nil and hero_data.Weapon and hero_data.Weapon.AltFire) then
value = hero_data.Weapon.AltFire[stat]
end
return value
end
 
-- Ordered list of a hero's scalings for a stat, Level first then Spirit:
--   { { type = "Level", value = 1.5 }, { type = "Spirit", value = 0.04 } }
-- Absent scalings are omitted, so an empty list means "no scaling". Zero
-- scalings are omitted too unless include_zero is set, which only the
-- deprecated get_hero_scaling_data below needs.
function p.get_scalings(hero_data, stat, include_zero)
local scalings = {}
for _, scaling_type in ipairs({"Level", "Spirit"}) do
local value = raw_scaling(hero_data, scaling_type, stat)
if (value ~= nil and (include_zero or value ~= 0)) then
table.insert(scalings, { type = scaling_type, value = value })
end
end
end
end
return similarItems
return scalings
end
end
-- DEPRECATED, kept for [[Module:HeroComparisonTable]]. Use p.get_scalings.
-- Keyed by scaling value, so it silently drops an entry when Level and Spirit
-- scale by the same amount, and it keeps zero scalings.
function p.get_hero_scaling_data(hero_data, hero_stat_key)
local scaling_data_returned = {}
-- Walked backwards so Level is written last and wins a value collision,
-- matching the original Spirit-then-Level assignment order.
local scalings = p.get_scalings(hero_data, hero_stat_key, true)
for i = #scalings, 1, -1 do
scaling_data_returned[scalings[i].value] = scalings[i].type
end
return scaling_data_returned
end
-- Whether a hero has a stat at all: a non-zero base value, or any scaling.
function p.has_stat(hero_data, stat)
local base = p.get_stat(hero_data, stat)
if (base ~= nil and base ~= 0) then return true end
return #p.get_scalings(hero_data, stat) > 0
end
-- Resolves a hero_* key or an English hero name to a hero_* key.
-- Returns nil when the input matches no hero. Name matching is case-insensitive.
function p.find_hero_key(input)
if (input == nil or input == '') then return nil end
if (heroes_data[input] ~= nil) then return input end
local wanted = mw.ustring.lower(input)
for hero_key, hero_data in pairs(heroes_data) do
if (hero_data["Name"] and mw.ustring.lower(hero_data["Name"]) == wanted) then
return hero_key
end
end
return nil
end
-- A hero's Nth roster tag (1-3) by English name, or '' when there is no such tag.
function p.hero_tag(hero_en, index)
local tagname = hero_en and TAG_NAME_MAP[hero_en]
if (tagname == nil) then return '' end
if (index ~= 1 and index ~= 2 and index ~= 3) then return '' end
return lang_module.get_string(string.format("Citadel_%s_HeroTag_%d", tagname, index)) or ''
end
--------------------------------------------------------------------------------
-- Wikitext entry points
--------------------------------------------------------------------------------


-- returns the key of the specified hero's english name
-- returns the key of the specified hero's english name
function p.get_hero_key(frame)
function p.get_hero_key(frame)
    local hero_name_input = frame.args[1] -- Get the hero name from the template argument
local hero_name_input = frame.args[1]
    if not hero_name_input then
if not hero_name_input then
        return "Error: Hero name not specified."
return "Error: Hero name not specified."
    end
end
 
return p.find_hero_key(hero_name_input) or "Hero not found."
    hero_name_input = hero_name_input:lower() -- For case-independent search
end
 
    -- Go through all the characters and look for a name match.
    for hero_key, hero_data in pairs(heroes_data) do
        if hero_data["Name"] then
            local current_hero_name = hero_data["Name"]:lower()
            if current_hero_name == hero_name_input then
                return hero_key -- Return the key if the name matches
            end
        end
    end


    return "Hero not found." -- If nothing is found
--{{#invoke:HeroData|resolve_key|HERO_KEY_OR_NAME}}--
-- The matching hero_* key, or '' when the input matches no hero.
function p.resolve_key(frame)
return p.find_hero_key(frame.args[1]) or ''
end
end


Line 61: Line 189:
     local sig_figs_or_localize = frame.args[3]
     local sig_figs_or_localize = frame.args[3]
      
      
     local hero = heroes_data[hero_name]
     local hero = hero_by_key_or_name(hero_name)
    if(hero == nil) then hero = p.get_json_item(hero_name) end
     if(hero == nil) then return "Hero Not Found" end
     if(hero == nil) then return "Hero Not Found" end
      
      
    -- Check top-level first, then Weapon, then AltFire
     local var_value = p.get_stat(hero, hero_stat_key)
     local var_value = hero[hero_stat_key]
    if var_value == nil and hero.Weapon then
        var_value = hero.Weapon[hero_stat_key]
    end
    if var_value == nil and hero.Weapon and hero.Weapon.AltFire then
        var_value = hero.Weapon.AltFire[hero_stat_key]
    end
   
     if(var_value == nil) then return 0 end
     if(var_value == nil) then return 0 end
      
      
Line 96: Line 215:
     local sig_figs_or_localize = frame.args[3]
     local sig_figs_or_localize = frame.args[3]
      
      
     local hero = heroes_data[hero_name]
     local hero = hero_by_key_or_name(hero_name)
    if(hero == nil) then hero = p.get_json_item(hero_name) end
     if(hero == nil) then return "Hero Not Found" end
     if(hero == nil) then return "Hero Not Found" end
      
      
Line 124: Line 242:
local hero_name = frame.args[1]
local hero_name = frame.args[1]
local var = frame.args[2]
local var = frame.args[2]
local number_str = frame.args[3]
local number_int = tonumber(frame.args[3])
local number_int = tonumber(number_str)
local localize_arg = frame.args[4]
local localize = frame.args[4]
local hero = heroes_data[hero_name]
local hero = hero_by_key_or_name(hero_name)
if(hero == nil) then hero = p.get_json_item(hero_name) end
if(hero == nil) then return "Hero Not Found" end
if(hero == nil) then return "Hero Not Found" end
-- Check top-level first, then Weapon, then AltFire
local list = p.get_stat(hero, var)
local list = hero[var]
if list == nil and hero.Weapon then
    list = hero.Weapon[var]
end
if list == nil and hero.Weapon and hero.Weapon.AltFire then
    list = hero.Weapon.AltFire[var]
end
if (list == nil) then return "" end
if (list == nil) then return "" end
local element = list[number_int]
local element = list[number_int]
if (element == nil) then return "" end
if (element == nil) then return "" end
if localize=="true" then  
if localize_arg=="true" then  
element = lang_module.get_string(element)
element = lang_module.get_string(element)
end
end
Line 185: Line 293:
if (hero == nil) then return "Hero not found, must be unlocalized" end
if (hero == nil) then return "Hero not found, must be unlocalized" end
local template_title = 'PageRef'
local template_title = 'PageRef'
for i, item_key in ipairs(hero["RecommendedItems"]) do
for i, item_key in ipairs(hero["RecommendedItems"] or {}) do
template_args = {}
local template_args = {}
template_args[1] = lang_module.get_string(item_key, 'en')
template_args[1] = lang_module.get_string(item_key, 'en')
template_args['alt_name'] = localize(item_key, item_key)
template_args['alt_name'] = localize(item_key, item_key)
Line 194: Line 302:
return str
return str
end
end


--If the hero scales with the stat, it returns {{Ss|value}} or {{Ls|value}}, else blank string
--If the hero scales with the stat, it returns {{Ss|value}} or {{Ls|value}}, else blank string
Line 205: Line 312:
local no_template = frame.args["no_template"]
local no_template = frame.args["no_template"]
local hero_data = heroes_data[hero_key]
local hero_data = hero_by_key_or_name(hero_key)
if(hero_data == nil) then hero_data = p.get_json_item(hero_key) end
if(hero_data == nil) then return "Hero not found." end
if(hero_data == nil) then return "Hero not found." end
local scaling_data = hero_data[scaling_type .. "Scaling"]
local scaling_value = raw_scaling(hero_data, scaling_type, hero_stat_key)
if scaling_data == nil then return "" end
local scaling_value = scaling_data[hero_stat_key]
if scaling_value == nil then return "" end
if scaling_value == nil then return "" end


Line 226: Line 329:
end
end
local scaling_str = p.write_scalar_str(scaling_value, scaling_type)
if no_template == "true" then
   
return scaling_value
    if no_template == "true" then
end
    return scaling_value
    else
return p.write_scalar_str(scaling_value, scaling_type)
    return scaling_str
    end
end
end


Line 238: Line 339:
-- Returns "true" if found, nil if not (for #if templates)
-- Returns "true" if found, nil if not (for #if templates)
p.hero_has_stat = function(frame)
p.hero_has_stat = function(frame)
    local hero_key = frame.args[1]
local hero_key = frame.args[1]
    local stat_key = frame.args[2]
local stat_key = frame.args[2]
if (hero_key == nil) then return nil end
local hero_data = hero_by_key_or_name(hero_key)
if (hero_data == nil) then return nil end
if p.has_stat(hero_data, stat_key) then return "true" end
return nil
end
 
-- Function to call hero tags based on name
p.get_hero_tag = function (frame)
local name = frame.args[1]
local number = tonumber(frame.args[2])


    if (hero_key == nil) then return nil end
-- Validate input
   
if not name then
    local hero_data = heroes_data[hero_key]
return "Error: Missing name input."
    if (hero_data == nil) then hero_data = p.get_json_item(hero_key) end
end
    if (hero_data == nil) then return nil end


    -- 1. Check Base Value
if number ~= 1 and number ~= 2 and number ~= 3 then
    -- We allow 0 if it's explicitly set, but usually we filter 0 for hiding.  
return string.format("Error: Invalid number '%s'. Must be 1, 2, or 3.", tostring(frame.args[2]))
    -- However, for resists, 0 usually means "hide", so we check ~= 0.
end
    local val = hero_data[stat_key]
    if (val ~= nil and val ~= 0) then return "true" end


    -- 2. Check Level Scaling (Boons)
if TAG_NAME_MAP[name] == nil then
    if (hero_data["LevelScaling"] ~= nil and hero_data["LevelScaling"][stat_key] ~= nil and hero_data["LevelScaling"][stat_key] ~= 0) then  
return string.format("Error: Unknown name '%s'.", name)
        return "true"  
end
    end


    -- 3. Check Spirit Scaling
return p.hero_tag(name, number)
    if (hero_data["SpiritScaling"] ~= nil and hero_data["SpiritScaling"][stat_key] ~= nil and hero_data["SpiritScaling"][stat_key] ~= 0) then
end
        return "true"
    end


    return nil
--{{#invoke:HeroData|has_tags|HERO_ENGLISH_NAME}}--
-- 'true' when the hero has roster tags, '' otherwise.
function p.has_tags(frame)
if p.hero_tag(frame.args[1], 1) ~= '' then return 'true' end
return ''
end
end


Line 289: Line 401:
      
      
return template_call:gsub("\n", ""):gsub("\r", "")
return template_call:gsub("\n", ""):gsub("\r", "")
end
-- Retrieve scaling value and type of a hero's given stat, if it has scaling, else return 0
function p.get_hero_scaling_data(hero_data, hero_stat_key)
local scaling_type_full
local scaling_data
local scaling_value
local scaling_types = {"Spirit", "Level"}
local scaling_data_returned = {}
for index, scaling_type in ipairs(scaling_types) do
scaling_type_full = scaling_type .. "Scaling"
scaling_data = hero_data[scaling_type_full]
--If the scaling data exists
if (scaling_data ~= nil) then
scaling_value = scaling_data[hero_stat_key]
--If the stat scales
if (scaling_value ~= nil) then
scaling_data_returned[scaling_value] = scaling_type
end
end
end
return scaling_data_returned
end
function localize(key, fallback)
local result = lang_module.get_string(key)
if (result == "") or (result == nil) then
result = util_module.add_space_before_cap(fallback) .. mw.getCurrentFrame():expandTemplate{title="MissingValveTranslationTooltip"}
end
return result
end
-- Mapping from hero name to internal tag name
local TAG_NAME_MAP = {
["Abrams"] = "Abrams",
["Bebop"] = "Bebop",
["Billy"] = "Punkgoat",
["Calico"] = "Nano",
["The Doorman"] = "Doorman",
["Drifter"] = "Drifter",
["Dynamo"] = "Dynamo",
["Apollo"] = "Fencer",
["Graves"] = "Necro",
["Grey Talon"] = "Orion",
["Haze"] = "Haze",
    ["Holliday"] = "Astro",
    ["Infernus"] = "Inferno",
    ["Ivy"] = "Tengu",
    ["Kelvin"] = "Kelvin",
    ["Lady Geist"] = "Geist",
    ["Lash"] = "Lash",
    ["McGinnis"] = "Engineer",
    ["Mina"] = "VampireBat",
    ["Mirage"] = "Mirage",
    ["Mo & Krill"] = "Digger",
    ["Paige"] = "Bookworm",
    ["Paradox"] = "Chrono",
    ["Celeste"] = "Unicorn",
    ["Pocket"] = "Synth",
    ["Rem"] = "Familiar",
    ["Seven"] = "Gigawatt",
    ["Shiv"] = "Shiv",
    ["Silver"] = "Werewolf",
    ["Silver (Transformed)"] = "Werewolf",
    ["Sinclair"] = "Magician",
    ["Venator"] = "Priest",
    ["Victor"] = "Frank",
    ["Vindicta"] = "Vindicta",
    ["Viscous"] = "Viscous",
    ["Vyper"] = "Viper",
    ["Warden"] = "Warden",
    ["Wraith"] = "Wraith",
    ["Yamato"] = "Yamato",
}
-- Function to call hero tags based on name
p.get_hero_tag = function (frame)
    local name = frame.args[1]
    local number = tonumber(frame.args[2])
    -- Validate input
    if not name then
        return "Error: Missing name input."
    end
    if number ~= 1 and number ~= 2 and number ~= 3 then
        return string.format("Error: Invalid number '%s'. Must be 1, 2, or 3.", tostring(frame.args[2]))
    end
    local tagname = TAG_NAME_MAP[name]
    if not tagname then
        return string.format("Error: Unknown name '%s'.", name)
    end
    -- Build localization key
    local key = string.format("Citadel_%s_HeroTag_%d", tagname, number)
    -- Call Lang.get_string
    local result = lang_module.get_string(key)
    return result or ("[Missing string for key: " .. key .. "]")
end
end


Line 404: Line 411:
         [0.72] = {},
         [0.72] = {},
     }
     }
    local get_dash = function(hero)
        return hero.GroundDashDuration
            or (hero.Weapon and hero.Weapon.GroundDashDuration)
            or (hero.Weapon and hero.Weapon.AltFire and hero.Weapon.AltFire.GroundDashDuration)
    end


     -- Collect heroes
     -- Collect heroes
     for hero_key, hero_data in pairs(heroes_data) do
     for hero_key, hero_data in pairs(heroes_data) do
         if not hero_data.InDevelopment and not hero_data.IsDisabled then
         if not hero_data.InDevelopment and not hero_data.IsDisabled then
             local dash = get_dash(hero_data)
             local dash = p.get_stat(hero_data, "GroundDashDuration")
             if dash and buckets[dash] then
             if dash and buckets[dash] then
                 table.insert(buckets[dash], hero_data.Name or hero_key)
                 table.insert(buckets[dash], hero_data.Name or hero_key)

Revision as of 13:33, 11 August 2026

Also see:

Overview

This module provides functions to create hero infoboxes, statboxes, or retrieve hero information using the data uploaded to Data:HeroData.json.

Both will automatically be translated depending on the language selected. If called on

  • Page - translates to english
  • Page/en - translates to english
  • Page/es - translates to spanish

See Template:Lang for more

Functions

get_hero_var

Retrieve a hero variable's value, such as Abram's MaxHealth

Parameters

  • hero_name - Name of the hero in english, or key of the hero. Preferred and recommended to use hero key, as its much more efficient. Search for the key in Data:HeroData.json
  • hero_var - Key of the hero's variable, see Data:HeroData.json
  • sig_figs_or_localize - OPTIONAL, # of sig figs to round to if retrieving a float, or "true" if its a string that should be localized. See Localizable values section.

Note: Recommended to use hero key where possible, i.e. hero_atlas as its O(1) time complexity instead of hero name (Abrams) which is O(N).

Examples

From wikitext:
Using hero name in english

{{#invoke:HeroData|get_hero_var|Abrams|MaxHealth}}

800


Using hero key

{{#invoke:HeroData|get_hero_var|hero_atlas|MaxHealth}}

800


{{#invoke:HeroData|get_hero_var|Abrams|FalloffStartRange}}

17


Round to 2 sig figs

{{#invoke:HeroData|get_hero_var|Abrams|FalloffStartRange|2}}

17


{{#invoke:HeroData|get_hero_var|Abrams|WeaponName}}

0


Localize

{{#invoke:HeroData|get_hero_var|Abrams|WeaponName|true}}

0

Notes

Only usable on variables that are integers, strings, or floats (meaning not dictionaries/hashes or arrays/lists).

If using sig_figs parameter, ensure the value is a float.

If using localize parameter, ensure the value is a string. See Data:HeroData.json, you will notice that hero_astro's "Lore" variable has the value "hero_astro_lore". This key is then sent to Module:Lang which checks for it in Data:Lang_en.json (or a different language).

This function will automatically search for the variable in the hero's top-level data, then the 'Weapon' block, then the 'AltFire' block.

get_alt_fire_var

Retrieve a hero's variable's value from "AltFire" table. Works the same as get_hero_var

Examples

{{#invoke:HeroData|get_alt_fire_var|Viscous|BulletDamage}}

42

get_list_elem

Retrieve a specified element from a list

Parameters

  • hero_name - Name of the hero in english, or hero key. Preferred and recommended to use hero key, as its much more efficient. Search for the key in Data:HeroData.json
  • hero_var - Key of the hero's variable, see Data:HeroData.json
  • number - Index to retrieve from the list. 1 for 1st element, 2 for 2nd element, etc.
  • localize - OPTIONAL - "true" if its a string that should be localized. See Localizable values section.

Example

From wikitext:
Using hero name in english

{{#invoke:HeroData|get_list_elem|Abrams|WeaponTypes|2}}

Attribute_EWeaponAttribute_CloseRange


Using hero key

{{#invoke:HeroData|get_list_elem|hero_atlas|WeaponTypes|2}}

Attribute_EWeaponAttribute_CloseRange


{{#invoke:HeroData|get_list_elem|Abrams|WeaponTypes|2|true}}

Close Range


get_hero_scalar

Retrieve a hero variable's scalar value that is inside either "Spirit Scaling" or "Level Scaling", such as Abram's BulletDamage (scaling).

Parameters

  • hero_key - Key of the hero. Search for the key of a given hero in Data:HeroData.json
  • scaling_type - "Spirit" or "Level"
  • hero_var - Key of the hero's variable that is inside either "Spirit Scaling" or "Level Scaling", see Data:HeroData.json
  • sig_figs_or_localize - OPTIONAL, # of sig figs to round to.
  • no_template - OPTIONAL, return numerical value instead of scaling template. Has to be defined as "true"

Examples

{{#invoke:HeroData|get_hero_scalar|hero_atlas|Level|BulletDamage}}

+0.10

Round to 2 sig figs

{{#invoke:HeroData|get_hero_scalar|hero_atlas|Level|BulletDamage|2}}

+0.10

No template

{{#invoke:HeroData|get_hero_scalar|hero_atlas|Level|BulletDamage|2|no_template=true}}

0.1

Notes

If the scaling type or the stat key is not found in the hero's data, this function returns an empty string instead of an error message.

hero_has_stat

Checks if a hero possesses a specific stat in their Base data, Level Scaling (Boons), or Spirit Scaling. Used primarily for conditionally hiding rows in infoboxes.

The base value is looked up in the hero's top-level data, then the 'Weapon' block, then the 'AltFire' block, the same as get_hero_var. A stat counts as present when its base value is non-zero, or when it has any non-zero scaling.

Parameters

  • hero_key - Key of the hero.
  • stat_key - Key of the stat to check (e.g., BulletResist, TechResist).

Example

{{#invoke:HeroData|hero_has_stat|hero_kelvin|TechResist}}

true

get_hero_key

Returns the key of the specified hero's english name. A hero key may also be passed, in which case it is returned unchanged. Matching is case-insensitive. Returns "Hero not found." when the input matches no hero.

Parameters

  • hero_name - Name of the hero in english, or a hero key

Example

{{#invoke:HeroData|get_hero_key|Abrams}}

resolve_key

The same lookup as get_hero_key, but returns an empty string instead of "Hero not found." when the input matches no hero. Intended for templates that need to branch on whether the input names a hero at all.

Parameters

  • hero_name_or_key - Name of the hero in english, or a hero key

Example

{{#invoke:HeroData|resolve_key|Abrams}}

hero_atlas

get_ability_key

Gets the ability key from a hero's bound ability slot

Parameters

  • hero_key - Key of the hero
  • slot_number - Slot number (1-based index)

Example

{{#invoke:HeroData|get_ability_key|hero_atlas|1}}

get_hero_tag

Retrieve a hero's description tags with Module:Lang, to be used in Template:HeroRosterCard and Template:Infobox hero

Parameters

  • name - Name of the hero in english. This is normally referenced from the template parameters
  • number - Which tag to call (1, 2 or 3)

Example

{{#invoke:HeroData|get_hero_tag|Abrams|1}}

has_tags

Returns "true" when the hero has description tags, or an empty string otherwise. Intended for templates that need to hide a tag block entirely for heroes without tags.

Parameters

  • name - Name of the hero in english

Example

{{#invoke:HeroData|has_tags|Abrams}}

true

write_role_playstyle_quote

Writes a formatted role and playstyle quotation for a hero

Parameters

  • hero_key - Key of the hero

Example

{{#invoke:HeroData|write_role_playstyle_quote|hero_atlas}}

write_default_items

Writes the recommended items list for a hero

Parameters

  • hero_key - Key of the hero (unlocalized)

Example

{{#invoke:HeroData|write_default_items|hero_atlas}}

write_ground_dash_buckets

Outputs a wikitable of heroes grouped by their GroundDashDuration using Template:HeroIcon

Example

{{#invoke:HeroData|write_ground_dash_buckets}}
Hero Dash Buckets
Bucket 1 Bucket 2 Bucket 3
Calico Apollo Abrams
Celeste Billy Bebop
Grey Talon Drifter Dynamo
Haze Graves Kelvin
Holliday Infernus Lady Geist
Ivy Lash Mo & Krill
Mina McGinnis Paige
Paradox Mirage Rem
The Doorman Pocket Shiv
Seven Venator
Silver Victor
Silver (Transformed)
Sinclair
Vindicta
Viscous
Vyper
Warden
Wraith
Yamato

Localizable values

Localizable values as of writing this:

  • Hero names (via hero keys)
  • Lore
  • Playstyle
  • Role
  • WeaponDescription
  • WeaponName
  • elements in WeaponTypes
  • elements in RecommendedItems
  • All stat labels and postfixes
  • Ability descriptions and names
  • Hero tags (1, 2, 3)

Internal Utility Functions

The following functions are called directly from Lua by other modules, not from wikitext:

get_json_item

Returns the table of a specific hero by name

Parameters

  • name - Name of the hero in english

get_stat

Returns the value of a stat, checking the hero's top-level data, then the 'Weapon' block, then the 'AltFire' block. Returns nil when the hero has no such stat. This is the shared lookup behind get_hero_var, get_list_elem and hero_has_stat.

Parameters

  • hero_data - Hero table, such as the one returned by get_json_item
  • stat - Key of the stat

get_scalings

Returns an ordered list of a hero's scalings for a stat, Level first then Spirit:

{ { type = "Level", value = 1.5 }, { type = "Spirit", value = 0.04 } }

Absent and zero scalings are omitted, so an empty list means the stat does not scale.

Parameters

  • hero_data - Hero table
  • stat - Key of the stat
  • include_zero - OPTIONAL, keep scalings whose value is 0. Only get_hero_scaling_data needs this.

has_stat

Returns true when a hero has a non-zero base value for a stat, or any scaling for it. Backs hero_has_stat.

Parameters

  • hero_data - Hero table
  • stat - Key of the stat

find_hero_key

Resolves a hero key or an english hero name to a hero key, case-insensitively. Returns nil when the input matches no hero. Backs get_hero_key and resolve_key.

Parameters

  • input - Hero key or english hero name

hero_tag

Returns a hero's Nth description tag, or an empty string when there is no such tag. Backs get_hero_tag and has_tags.

Parameters

  • hero_en - Name of the hero in english
  • index - Which tag to return (1, 2 or 3)

write_scalar_str

Returns the expanded Ss or PI template for a scaling value, or an empty string when the value is 0.

Parameters

  • scaling_value - The scaling amount
  • scaling_type - "Spirit" or "Level"
  • compact - OPTIONAL, render the compact form with the value shown

get_hero_scaling_data

Deprecated, use get_scalings instead. Returns a table keyed by scaling value, so an entry is silently dropped when Level and Spirit scale by the same amount, and zero scalings are kept. Retained only for Module:HeroComparisonTable.

Parameters

  • hero_data - Hero table
  • stat - Key of the stat

Scaling System

The module supports two types of scaling:

  • Spirit Scaling - Affects stats based on Spirit Power
  • Level Scaling - Affects stats based on Power Increases/Levels/Boons

local p = {};
local heroes_data = mw.loadJsonData("Data:HeroData.json")
local util_module = require('Module:Utilities')
local lang_module = require('Module:Lang')

--------------------------------------------------------------------------------
-- Internal helpers
--------------------------------------------------------------------------------

-- Localized string for a key, falling back to a readable version of `fallback`
-- plus a "missing translation" tooltip.
local function localize(key, fallback)
	local result = lang_module.get_string(key)
	if (result == "") or (result == nil) then
		result = util_module.add_space_before_cap(fallback) .. mw.getCurrentFrame():expandTemplate{title="MissingValveTranslationTooltip"}
	end
	return result
end

-- Raw scaling value of a stat under LevelScaling / SpiritScaling, or nil when
-- the hero has no scaling of that type for that stat.
local function raw_scaling(hero_data, scaling_type, stat)
	local scaling = hero_data and hero_data[scaling_type .. "Scaling"]
	if (scaling == nil or stat == nil) then return nil end
	return scaling[stat]
end

-- Mapping from hero name to internal tag name
local TAG_NAME_MAP = {
	["Abrams"] = "Abrams",
	["Bebop"] = "Bebop",
	["Billy"] = "Punkgoat",
	["Calico"] = "Nano",
	["The Doorman"] = "Doorman",
	["Drifter"] = "Drifter",
	["Dynamo"] = "Dynamo",
	["Apollo"] = "Fencer",
	["Graves"] = "Necro",
	["Grey Talon"] = "Orion",
	["Haze"] = "Haze",
	["Holliday"] = "Astro",
	["Infernus"] = "Inferno",
	["Ivy"] = "Tengu",
	["Kelvin"] = "Kelvin",
	["Lady Geist"] = "Geist",
	["Lash"] = "Lash",
	["McGinnis"] = "Engineer",
	["Mina"] = "VampireBat",
	["Mirage"] = "Mirage",
	["Mo & Krill"] = "Digger",
	["Paige"] = "Bookworm",
	["Paradox"] = "Chrono",
	["Celeste"] = "Unicorn",
	["Pocket"] = "Synth",
	["Rem"] = "Familiar",
	["Seven"] = "Gigawatt",
	["Shiv"] = "Shiv",
	["Silver"] = "Werewolf",
	["Silver (Transformed)"] = "Werewolf",
	["Sinclair"] = "Magician",
	["Venator"] = "Priest",
	["Victor"] = "Frank",
	["Vindicta"] = "Vindicta",
	["Viscous"] = "Viscous",
	["Vyper"] = "Viper",
	["Warden"] = "Warden",
	["Wraith"] = "Wraith",
	["Yamato"] = "Yamato",
}

--------------------------------------------------------------------------------
-- Data access, for use by other modules
--------------------------------------------------------------------------------

-- returns the table of a specific item, used by external modules
function p.get_json_item(name)
	for i,v in pairs(heroes_data) do
		if (v["Name"] == name) then
			return v
		end
	end
	return nil
end

-- Hero table for a hero_* key or an exact English name, or nil.
local function hero_by_key_or_name(input)
	local hero = heroes_data[input]
	if (hero == nil) then hero = p.get_json_item(input) end
	return hero
end

-- Value of a stat, checking the hero's top level, then Weapon, then AltFire.
-- Returns nil when the hero has no such stat.
function p.get_stat(hero_data, stat)
	if (hero_data == nil or stat == nil) then return nil end
	local value = hero_data[stat]
	if (value == nil and hero_data.Weapon) then
		value = hero_data.Weapon[stat]
	end
	if (value == nil and hero_data.Weapon and hero_data.Weapon.AltFire) then
		value = hero_data.Weapon.AltFire[stat]
	end
	return value
end

-- Ordered list of a hero's scalings for a stat, Level first then Spirit:
--   { { type = "Level", value = 1.5 }, { type = "Spirit", value = 0.04 } }
-- Absent scalings are omitted, so an empty list means "no scaling". Zero
-- scalings are omitted too unless include_zero is set, which only the
-- deprecated get_hero_scaling_data below needs.
function p.get_scalings(hero_data, stat, include_zero)
	local scalings = {}
	for _, scaling_type in ipairs({"Level", "Spirit"}) do
		local value = raw_scaling(hero_data, scaling_type, stat)
		if (value ~= nil and (include_zero or value ~= 0)) then
			table.insert(scalings, { type = scaling_type, value = value })
		end
	end
	return scalings
end

-- DEPRECATED, kept for [[Module:HeroComparisonTable]]. Use p.get_scalings.
-- Keyed by scaling value, so it silently drops an entry when Level and Spirit
-- scale by the same amount, and it keeps zero scalings.
function p.get_hero_scaling_data(hero_data, hero_stat_key)
	local scaling_data_returned = {}
	-- Walked backwards so Level is written last and wins a value collision,
	-- matching the original Spirit-then-Level assignment order.
	local scalings = p.get_scalings(hero_data, hero_stat_key, true)
	for i = #scalings, 1, -1 do
		scaling_data_returned[scalings[i].value] = scalings[i].type
	end
	return scaling_data_returned
end

-- Whether a hero has a stat at all: a non-zero base value, or any scaling.
function p.has_stat(hero_data, stat)
	local base = p.get_stat(hero_data, stat)
	if (base ~= nil and base ~= 0) then return true end
	return #p.get_scalings(hero_data, stat) > 0
end

-- Resolves a hero_* key or an English hero name to a hero_* key.
-- Returns nil when the input matches no hero. Name matching is case-insensitive.
function p.find_hero_key(input)
	if (input == nil or input == '') then return nil end
	if (heroes_data[input] ~= nil) then return input end
	local wanted = mw.ustring.lower(input)
	for hero_key, hero_data in pairs(heroes_data) do
		if (hero_data["Name"] and mw.ustring.lower(hero_data["Name"]) == wanted) then
			return hero_key
		end
	end
	return nil
end

-- A hero's Nth roster tag (1-3) by English name, or '' when there is no such tag.
function p.hero_tag(hero_en, index)
	local tagname = hero_en and TAG_NAME_MAP[hero_en]
	if (tagname == nil) then return '' end
	if (index ~= 1 and index ~= 2 and index ~= 3) then return '' end
	return lang_module.get_string(string.format("Citadel_%s_HeroTag_%d", tagname, index)) or ''
end

--------------------------------------------------------------------------------
-- Wikitext entry points
--------------------------------------------------------------------------------

-- returns the key of the specified hero's english name
function p.get_hero_key(frame)
	local hero_name_input = frame.args[1]
	if not hero_name_input then
		return "Error: Hero name not specified."
	end
	return p.find_hero_key(hero_name_input) or "Hero not found."
end

--{{#invoke:HeroData|resolve_key|HERO_KEY_OR_NAME}}--
-- The matching hero_* key, or '' when the input matches no hero.
function p.resolve_key(frame)
	return p.find_hero_key(frame.args[1]) or ''
end

--{{#invoke:HeroData|get_hero_var|HERO_NAME|STAT_NAME|sig_figs_or_localize}}--
--sig_figs optional for rounding floats
p.get_hero_var = function(frame)
    local hero_name = frame.args[1]
    local hero_stat_key = frame.args[2]
    local sig_figs_or_localize = frame.args[3]
    
    local hero = hero_by_key_or_name(hero_name)
    if(hero == nil) then return "Hero Not Found" end
    
    local var_value = p.get_stat(hero, hero_stat_key)
    if(var_value == nil) then return 0 end
    
    --round
    if (sig_figs_or_localize ~= nil and tonumber(sig_figs_or_localize) ~= nil) then
        var_value = util_module.round_to_sig_fig(var_value, sig_figs_or_localize)
        if (var_value == nil) then return "get_hero_var() error with rounding" end
    end
    
    --localize
    if (sig_figs_or_localize == "true") then
        return lang_module.get_string(var_value)
    end

    return var_value
end

--{{#invoke:HeroData|get_alt_fire_var|HERO_NAME|STAT_NAME|sig_figs_or_localize}}--
p.get_alt_fire_var = function(frame)
    local hero_name = frame.args[1]
    local stat_key = frame.args[2]
    local sig_figs_or_localize = frame.args[3]
    
    local hero = hero_by_key_or_name(hero_name)
    if(hero == nil) then return "Hero Not Found" end
    
    if not (hero.Weapon and hero.Weapon.AltFire) then
        return "No Alt Fire"
    end
    
    local var_value = hero.Weapon.AltFire[stat_key]
    if(var_value == nil) then return 0 end
    
    --round
    if (sig_figs_or_localize ~= nil and tonumber(sig_figs_or_localize) ~= nil) then
        var_value = util_module.round_to_sig_fig(var_value, sig_figs_or_localize)
    end
    
    --localize
    if (sig_figs_or_localize == "true") then
        return lang_module.get_string(var_value)
    end

    return var_value
end

--{{#invoke:HeroData|get_list_elem|HERO_NAME|VAR|NUMBER|LOCALIZE}}
p.get_list_elem = function(frame)
	local hero_name = frame.args[1]
	local var = frame.args[2]
	local number_int = tonumber(frame.args[3])
	local localize_arg = frame.args[4]
	
	local hero = hero_by_key_or_name(hero_name)
	if(hero == nil) then return "Hero Not Found" end
	
	local list = p.get_stat(hero, var)
	if (list == nil) then return "" end
	local element = list[number_int]
	if (element == nil) then return "" end
	
	if localize_arg=="true" then 
		element = lang_module.get_string(element)
	end
	
	return element
end

p.get_ability_key = function(frame)
	local hero_key = frame.args[1]
	local bound_slot_number = frame.args[2]
	local hero_data = heroes_data[hero_key]
	if (hero_data == nil) then return "Hero key "..hero_key.. " not found" end
	local bound_abilities_data = hero_data["BoundAbilities"]
	if (bound_abilities_data == nil) then return "Hero key " .. hero_key.. " has no BoundAbilities" end
	return bound_abilities_data[tonumber(bound_slot_number)]["Key"]
end

p.write_role_playstyle_quote = function(frame)
	local hero_key = frame.args[1]
	local hero_data = heroes_data[hero_key]
	if (hero_data == nil) then return hero_key.." not found" end
	local role_key = hero_data["Role"]
	local role_localized = lang_module.get_string(role_key, nil, 'en')
	local playstyle_key = hero_data["Playstyle"]
	local playstyle_localized = lang_module.get_string(playstyle_key, nil, 'en')
	
	local str = "<b>" .. role_localized .. '</b><br>' .. playstyle_localized
	local template_args = {}
	template_args[1] = ""
	template_args[2] = str
	return frame:expandTemplate{title = 'Quotation', args = template_args}
end

p.write_default_items = function(frame)
	local hero_key = frame.args[1] --unlocalized
	if (hero_key == nil) then return "No hero key provided" end
	local str = ""
	local hero = heroes_data[hero_key]
	if (hero == nil) then return "Hero not found, must be unlocalized" end
	local template_title = 'PageRef'
	for i, item_key in ipairs(hero["RecommendedItems"] or {}) do
		local template_args = {}
		template_args[1] = lang_module.get_string(item_key, 'en')
		template_args['alt_name'] = localize(item_key, item_key)
		local expanded_template = mw.getCurrentFrame():expandTemplate{ title = template_title, args = template_args }
		str = str .. "* " .. expanded_template .. "\n"
	end
	return str
end

--If the hero scales with the stat, it returns {{Ss|value}} or {{Ls|value}}, else blank string
--{{#invoke:HeroData|get_hero_scalar|HERO_NAME|SCALING_TYPE|STAT_NAME|sig_figs_or_localize}}--
p.get_hero_scalar = function(frame)
	local hero_key = frame.args[1]
	local scaling_type = frame.args[2]
	local hero_stat_key = frame.args[3]
	local sig_figs_or_localize = frame.args[4]
	local no_template = frame.args["no_template"]
	
	local hero_data = hero_by_key_or_name(hero_key)
	if(hero_data == nil) then return "Hero not found." end
	
	local scaling_value = raw_scaling(hero_data, scaling_type, hero_stat_key)
	if scaling_value == nil then return "" end

	--round
	if (sig_figs_or_localize ~= nil and tonumber(sig_figs_or_localize) ~= nil) then
		scaling_value = util_module.round_to_sig_fig(scaling_value, sig_figs_or_localize)
		if (scaling_value == nil) then return "get_hero_scalar() error with rounding" end
	end
	
	--localize
	if (sig_figs_or_localize == "true") then
		return lang_module.get_string(scaling_value)
	end
	
	if no_template == "true" then
		return scaling_value
	end
	
	return p.write_scalar_str(scaling_value, scaling_type)
end

-- Check if a hero has a stat (Base, Boons, or Spirit Scaling)
-- Returns "true" if found, nil if not (for #if templates)
p.hero_has_stat = function(frame)
	local hero_key = frame.args[1]
	local stat_key = frame.args[2]
	if (hero_key == nil) then return nil end
	
	local hero_data = hero_by_key_or_name(hero_key)
	if (hero_data == nil) then return nil end
	
	if p.has_stat(hero_data, stat_key) then return "true" end
	return nil
end

-- Function to call hero tags based on name
p.get_hero_tag = function (frame)
	local name = frame.args[1]
	local number = tonumber(frame.args[2])

	-- Validate input
	if not name then
		return "Error: Missing name input."
	end

	if number ~= 1 and number ~= 2 and number ~= 3 then
		return string.format("Error: Invalid number '%s'. Must be 1, 2, or 3.", tostring(frame.args[2]))
	end

	if TAG_NAME_MAP[name] == nil then
		return string.format("Error: Unknown name '%s'.", name)
	end

	return p.hero_tag(name, number)
end

--{{#invoke:HeroData|has_tags|HERO_ENGLISH_NAME}}--
-- 'true' when the hero has roster tags, '' otherwise.
function p.has_tags(frame)
	if p.hero_tag(frame.args[1], 1) ~= '' then return 'true' end
	return ''
end

-- Retrieve scaling string of a hero's given stat, if it has scaling, else return blank
-- Scaling string meaning the expanded template {{Ss|scalar}} or {{Ls|scalar}}
function p.write_scalar_str(scaling_value, scaling_type, compact)
	local scaling_abbrevs = {Spirit = "Ss", Level = "PI"}
	
	-- Return blank if it doesnt scale	
	if (scaling_value == 0) then return "" end
	
	-- Round it
	scaling_value = util_module.round_to_sig_fig(scaling_value, 3)
	
	--The hero has a scaling value with this stat	
	local template_title = "Template:" .. scaling_abbrevs[scaling_type]
	local template_args = {}
	template_args["1"] = scaling_value
	if compact then 
		template_args["compact"] = "yes"
		template_args["show_value"] = "yes"
	end
	
	local template_call = mw.getCurrentFrame():expandTemplate{ title = template_title, args = template_args }
    
	return template_call:gsub("\n", ""):gsub("\r", "")
end

-- Outputs a wikitable of heroes grouped by GroundDashDuration using {{HeroIcon|hero_name}}
-- {{#invoke:HeroData|write_ground_dash_buckets}}
p.write_ground_dash_buckets = function(frame)
    local buckets = {
        [0.62] = {},
        [0.68] = {},
        [0.72] = {},
    }

    -- Collect heroes
    for hero_key, hero_data in pairs(heroes_data) do
        if not hero_data.InDevelopment and not hero_data.IsDisabled then
            local dash = p.get_stat(hero_data, "GroundDashDuration")
            if dash and buckets[dash] then
                table.insert(buckets[dash], hero_data.Name or hero_key)
            end
        end
    end

    -- Sort alphabetically
    for _, list in pairs(buckets) do
        table.sort(list)
    end

    -- Determine max rows dynamically
    local max_rows = 0
    for _, list in pairs(buckets) do
        if #list > max_rows then max_rows = #list end
    end

    local output = {"{| class='wikitable mw-collapsible'",
    	"|+Hero Dash Buckets", "! Bucket 1 !! Bucket 2 !! Bucket 3"}
    local current_frame = frame or mw.getCurrentFrame()

    for i = 1, max_rows do
        local row = {}
        for _, bucket in ipairs({0.62, 0.68, 0.72}) do
            local hero_name = buckets[bucket][i]
            table.insert(row, hero_name and current_frame:expandTemplate{title = "HeroIcon", args = {hero_name}} or "")
        end
        table.insert(output, "|-\n| " .. table.concat(row, " || "))
    end

    table.insert(output, "|}")
    return table.concat(output, "\n")
end

return p