Editing Module:Abilities/card

Warning: You are not logged in. Once you make an edit, a temporary account will be created for you. Learn more. Log in or create an account to continue receiving notifications after this account expires, and to access other features.
The edit can be undone. Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
Latest revision Your text
Line 2: Line 2:
local commonutils = require "Module:Utilities"
local commonutils = require "Module:Utilities"
local utils = require "Module:Abilities/utils"
local utils = require "Module:Abilities/utils"
local Icon = require"Module:Abilities/icon"
local textfit = require "Module:TextFit"


local p = {}
local p = {}
local data = mw.loadJsonData("Data:AbilityCards.json")
local data = mw.loadJsonData("Data:AbilityCards.json")


-- Maps attr type to an appropriate image and page link
-- Optional icon size, will be defaulted on the template
local ATTR_TYPE_ICON_MAP = {
bullet_armor_up = {img='Bullet_Armor.png', link='Damage_Resistance'},
bullet_armor_down = {img='Bullet_armor_down.png', link='Damage_Resistance', size='23px'},
cast = {img='AttributeIconMaxChargesIncrease.png', link=''},
charges = {img='AttributeIconMaxChargesIncrease.png', link=''},
damage = {img='Damage_heart.png', link='', color = 'NoColor'}, -- 'NoColor' will apply no icon color and use the original image
bullet_damage = {img='Damage.png', link='Bullet_Damage', color = 'Brown'},
fire_rate = {img='Fire Rate.png', link='Fire_Rate'},
healing = {img='Health regen.png', link='Health_Regen'},
health = {img='Extra Health.png', link='Health'},
move_speed = {img='Move speed.png', link='Move Speed'},
range = {img='CastRange.png', link='Ability_Range'},
tech_armor_up = {img='Spirit_Armor.png', link='Damage_Resistance'},
tech_damage = {img='AttributeIconTechShieldHealth.png', link='Spirit_Damage', color = 'Purple',size = '12px'},
distance = {img='AttributeIconTechRange.png', link='Ability_Range'},
duration = {img='AttributeIconTechDuration.png', link='Ability Duration'},
slow = {img='MoveSlow.png', link=''}
}


function get_hero_key(hero_name)
function get_hero_key(hero_name)
if hero_name == nil then return nil end
for i, hero in pairs(data) do
if hero["Name"] == hero_name then
-- if name starts with "hero_", use it directly as the key
return i
if string.sub(hero_name, 1, 5) == "hero_" then return hero_name end
end
end
local resource_lookup = mw.loadJsonData("Data:ResourceLookup.json")
return nil
return resource_lookup[hero_name:lower()].key
end
end


--{{#invoke:Abilities/card|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}--
--{{#invoke:AbilityData|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}--
-- Args:
--Simply calls get_ability_card_from_key with the same parameters, but with key
--   HERO_NAME (required) - Name of the hero that is found in Data:HeroData.json under "Name"
--get_ability_card and invoke_get_ability_card_key will eventually be removed, and
--   ABILITY_NUM (required) - Selects ability at index 1 to 4
--get_ability_card_from_key will be renamed to get_ability_card
--   ADD_LINK - Add a hyperlink to the title to the associated page
--both routes exist temporarily as current hero pages use get_ability_card via hero name
--   NOTES - User note data to include in the footer of the ability card
--and im currently testing via hero_key on [[User:Sur/Abrams]]
--on the final version with fully automated pages, hero_key will be used.
--for now, hero_name can suffice
p.get_ability_card = function(frame)
p.get_ability_card = function(frame)
local hero_name = frame.args[1]
local hero_name = frame.args[1]
Line 32: Line 51:
local hero_key = get_hero_key(hero_name)
local hero_key = get_hero_key(hero_name)
if (hero_key == nil) then return 'Hero with name' .. hero_name .. 'not found' end
return build_ability_card(hero_key, ability_num, add_link, notes)
return p.get_ability_card_from_key(hero_key, ability_num, add_link, notes)
end
end


-- Pulls data from Data:AbilityCards.json to populate Template:Ability card v2
--from hero key*
function build_ability_card(hero_key, ability_num, add_link, notes, notes_source_page)
--notes parameter will eventually be removed, as notes_source_page would be sufficient or could even be determined from within this function
function p.get_ability_card_from_key(hero_key, ability_num, add_link, notes, notes_source_page)
if type(hero_key) == 'table' and hero_key.args then
local frame = hero_key
hero_key = frame.args[1]
ability_num = frame.args[2]
add_link = frame.args[3]
notes = frame.args[4]
notes_source_page = frame.args[5]
end
local ability = utils.get_ability_card_data(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then  
if(ability == nil) then  
Line 51: Line 79:
if notes_source_page ~= nil and notes ~= "" then
if notes_source_page ~= nil and notes ~= "" then
-- Notes comes from a /Notes page, and the notes are not blank
--Notes comes from a /Notes page, and the notes are not blank
-- Confirm the notes source page exists, otherwise, don't display any notes
-- Confirm the notes source page exists, otherwise, don't display any notes
local title = mw.title.new(notes_source_page)
local title = mw.title.new(notes_source_page)
Line 59: Line 87:
end
end
local frame = mw.getCurrentFrame()
return mw.getCurrentFrame():expandTemplate{
title = "Ability card v2/Card",
local info1_desc = get_info_desc(hero_key, ability_num, 1)
args = {
local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1)
hero_key = hero_key,
ability_num = ability_num,
name = ability_name_localized,
name_link = name_link,
icon = lang.get_string(ability.Key, 'en') .. '.png',
description = mw.getCurrentFrame():preprocess(lang.get_string(ability.DescKey)),
radius = ability.Radius and ability.Radius.Value,
radius_ss = get_attr_ss(ability.Radius),
range = ability.AbilityCastRange and ability.AbilityCastRange.Value,
range_ss = get_attr_ss(ability.AbilityCastRange),
duration = ability.AbilityDuration and ability.AbilityDuration.Value,
duration_ss = get_attr_ss(ability.AbilityDuration),
-- ability_width = format_value_with_prepost(width_key, ability[width_key]),
cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,
cooldown_ss = get_attr_ss(ability.AbilityCooldown),
charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value,
charge_cooldown_ss = get_attr_ss(ability.AbilityCooldownBetweenCharge),
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value,
notes = notes,
notes_source_page = notes_source_page
}
}
end


local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1)
-- Function for sandboxing workshopping potential update of entire ability card generation
function p.get_ability_card_test(hero_key, ability_num, add_link, notes, notes_source_page)
if type(hero_key) == 'table' and hero_key.args then
local frame = hero_key
hero_key = frame.args[1]
ability_num = frame.args[2]
add_link = frame.args[3]
notes = frame.args[4]
notes_source_page = frame.args[5]
end
if #info1_alt_boxes > 7 then
local ability = utils.get_ability_card_data(hero_key, ability_num)
error(#info1_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
if(ability == nil) then  
return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num
end
end
local info2_desc = get_info_desc(hero_key, ability_num, 2)
local ability_name_localized = lang.get_string(ability.Key)
local info2_main_boxes = get_main_boxes(hero_key, ability_num, 2)
local name_link = nil
local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2)
if add_link == 'true' then
name_link = ability_name_localized
if #info2_alt_boxes > 7 then
error(#info2_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
end
end
local info3_desc = get_info_desc(hero_key, ability_num, 3)
if notes_source_page ~= nil and notes ~= "" then
local info3_main_boxes = get_main_boxes(hero_key, ability_num, 3)
--Notes comes from a /Notes page, and the notes are not blank
local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3)
-- Confirm the notes source page exists, otherwise, don't display any notes
local title = mw.title.new(notes_source_page)
if #info3_alt_boxes > 7 then
if not (title and title.exists) then
error(#info3_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
notes = ""
end
end
end
local upgrades = get_upgrade_boxes(hero_key, ability_num)
return frame:expandTemplate{
return mw.getCurrentFrame():expandTemplate{
title = "Ability_card_v2/Card",
title = "Ability card v2/Card",
args = {
args = {
id = hero_key .. ':' .. ability.Key,
hero_key = hero_key,
hero_key = hero_key,
ability_num = ability_num,
ability_num = ability_num,
-- Header info defined in various attributes
name = ability_name_localized,
name = ability_name_localized,
name_link = name_link,
name_link = name_link,
icon = lang.get_string(ability.Key, 'en') .. '.png',
icon = lang.get_string(ability.Key, 'en') .. '.png',
description = mw.getCurrentFrame():preprocess(lang.get_string(ability.DescKey)),
channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value,
channel_time_scale = get_attr_scale(ability.AbilityChannelTime),
channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime),
channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime),
radius = ability.Radius and ability.Radius.Value,
radius = ability.Radius and ability.Radius.Value,
radius_scale = get_attr_scale(ability.Radius),
radius_ss = get_attr_ss(ability.Radius),
radius_scale_type = get_attr_scale_type(ability.Radius),
radius_scale_multiply = get_attr_scale_multiply(ability.Radius),
range = ability.AbilityCastRange and ability.AbilityCastRange.Value,
range = ability.AbilityCastRange and ability.AbilityCastRange.Value,
range_scale = get_attr_scale(ability.AbilityCastRange),
range_ss = get_attr_ss(ability.AbilityCastRange),
range_scale_type = get_attr_scale_type(ability.AbilityCastRange),
range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange),
duration = ability.AbilityDuration and ability.AbilityDuration.Value,
duration = ability.AbilityDuration and ability.AbilityDuration.Value,
duration_scale = get_attr_scale(ability.AbilityDuration),
duration_ss = get_attr_ss(ability.AbilityDuration),
duration_scale_type = get_attr_scale_type(ability.AbilityDuration),
duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration),
-- ability_width = format_value_with_prepost(width_key, ability[width_key]),
-- ability_width = format_value_with_prepost(width_key, ability[width_key]),
cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,
cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,
cooldown_scale = get_attr_scale(ability.AbilityCooldown),
cooldown_ss = get_attr_ss(ability.AbilityCooldown),
cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown),
charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value,
cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown),
charge_cooldown_ss = get_attr_ss(ability.AbilityCooldownBetweenCharge),
charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value,
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value,
charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge),
charge_cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldownBetweenCharge),
charge_cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldownBetweenCharge),
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value or 0,
-- Info section #1 defined in "Info1" attribute
hide_info1 = infobox_is_hidden(ability.Info1) and "true" or "false",
info1_desc = info1_desc,
info1_mainbox1 = info1_main_boxes[1],
info1_mainbox2 = info1_main_boxes[2],
info1_mainbox3 = info1_main_boxes[3],
info1_mainbox4 = info1_main_boxes[4],
info1_mainbox5 = info1_main_boxes[5],
info1_altbox1 =  info1_alt_boxes[1],
info1_altbox2 =  info1_alt_boxes[2],
info1_altbox3 =  info1_alt_boxes[3],
info1_altbox4 =  info1_alt_boxes[4],
info1_altbox5 =  info1_alt_boxes[5],
info1_altbox6 =  info1_alt_boxes[6],
info1_altbox7 =  info1_alt_boxes[7],
 
-- Info section #2 defined in "Info2" attribute
hide_info2 = infobox_is_hidden(ability.Info2) and "true" or "false",
info2_desc = info2_desc,
info2_mainbox1 = info2_main_boxes[1],
info2_mainbox2 = info2_main_boxes[2],
info2_mainbox3 = info2_main_boxes[3],
info2_mainbox4 = info2_main_boxes[4],
info2_mainbox5 = info2_main_boxes[5],
info2_altbox1 =  info2_alt_boxes[1],
info2_altbox2 =  info2_alt_boxes[2],
info2_altbox3 =  info2_alt_boxes[3],
info2_altbox4 =  info2_alt_boxes[4],
info2_altbox5 =  info2_alt_boxes[5],
info2_altbox6 =  info2_alt_boxes[6],
info2_altbox7 =  info2_alt_boxes[7],
 
-- Info section #3 defined in "Info3" attribute
hide_info3 = infobox_is_hidden(ability.Info3) and "true" or "false",
info3_desc = info3_desc,
info3_mainbox1 = info3_main_boxes[1],
info3_mainbox2 = info3_main_boxes[2],
info3_mainbox3 = info3_main_boxes[3],
info3_mainbox4 = info3_main_boxes[4],
info3_mainbox5 = info3_main_boxes[5],
info3_altbox1 =  info3_alt_boxes[1],
info3_altbox2 =  info3_alt_boxes[2],
info3_altbox3 =  info3_alt_boxes[3],
info3_altbox4 =  info3_alt_boxes[4],
info3_altbox5 =  info3_alt_boxes[5],
info3_altbox6 =  info3_alt_boxes[6],
info3_altbox7 =  info3_alt_boxes[7],
 
-- Ability upgrades defined in "Upgrades" attribute
upgrade1 = upgrades[1],
upgrade2 = upgrades[2],
upgrade3 = upgrades[3],
-- User-created ability notes
notes = notes,
notes = notes,
notes_source_page = notes_source_page
notes_source_page = notes_source_page
}
}
}
}
end
end


function infobox_is_hidden(infobox)
-- Get spirit scaling of attribute, return nil if none is found
if infobox == nil then
function get_attr_ss(attr)
return false
if not attr then  
end
return nil  
return infobox.RequiresUpgradeIndex ~= nil
end
 
-- An attribute may carry either a single scaling or a list of them.
-- Extra entries are stat-category markers (duration, cooldown, range, radius,
-- healing, charges) that always hold a value of 1 and are never displayed, so
-- return the first entry that actually renders a badge.
-- A zero-valued scaling is still returned when nothing else is displayable. The
-- card renders it as an x0 placeholder, and the ability upgrade gadget fills
-- that element in when an upgrade grants real scaling (Doorman's Doorway).
local function pick_scale(attr)
if not attr then
return nil
end
end
local scale = attr.Scale
local scale = attr.Scale
if type(scale) ~= 'table' then
if not scale then
return nil
return nil
end
end
-- A single scaling is stored directly as an object
if scale.Type ~= 'spirit' then
if scale[1] == nil then
return nil
return scale
end
local placeholder = nil
for _, entry in ipairs(scale) do
if type(entry) == 'table' and Icon.isDisplayedScale(entry.Type) then
if entry.Value ~= 0 then
return entry
end
placeholder = placeholder or entry
end
end
return placeholder
end
 
-- Get scaling of attribute, return nil if none is found
function get_attr_scale(attr)
local scale = pick_scale(attr)
if not scale then
return nil
end
end
Line 243: Line 193:
return commonutils.round_to_sig_fig(scale.Value, 3)
return commonutils.round_to_sig_fig(scale.Value, 3)
end
end
 
-- Get scaling type of attribute, return nil if none is found
--for use from an ability page (ability pages are WIP), not hero pages
function get_attr_scale_type(attr)
function p.write_ability_card_from_ability_key(frame)
local scale = pick_scale(attr)
local ability_key = frame.args[1]
if not scale then
if ability_key == nil then return "ability_key '" .. ability_key "' not provided" end
return nil
-- Determine the hero and ability number
local found_hero_key
local found_ability_num
for hero_key, card_data in pairs(data) do
if found_hero_key == nil or found_ability_num == nil then
for ability_num, ability_data in pairs(card_data) do
if ability_num ~= "Name" then
if ability_data['Key'] == ability_key then  
found_hero_key = hero_key
found_ability_num = ability_num
break
end -- hero_key and ability_num found
end
end
end
end
end
if scale.Value == 0 then
if found_ability_num == nil or found_hero_key == nil then return "ability_key " .. ability_key .. " is not used by any heroes" end
return nil
end
-- Get notes for this ability
local notes_source_page_name = utils.get_notes_source_page_name(ability_key)
local notes_str = frame:preprocess("{{"..notes_source_page_name.."}}")
return scale.Type
-- Create the ability card
return p.get_ability_card_from_key(found_hero_key, found_ability_num, true, notes_str, notes_source_page_name)
end
end


-- Check if scaling should be multiplied instead of added
-- Get all info sections for specified ability
function get_attr_scale_multiply(attr)
--{{#invoke:AbilityData|get_ability_card|HERO_KEY|ABILITY_NUM}}--
local scale = pick_scale(attr)
p.get_all_info_sections = function(frame)
if not scale then
local hero_key = frame.args[1]
return nil
local ability_num = frame.args[2]
 
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then return "Ability Not Found" end
local output_template = ''
for info_section_num=1, 10 do
local info_section = ability['Info'..info_section_num]
-- some abilities have no info sections
if info_section == nil then break end
local info_template = frame:expandTemplate{
title = "Ability_card_v2/Card/Info section",
args = {
hero_key = hero_key,
ability_num = ability_num,
info_section_num = info_section_num,
}
}
output_template = output_template .. info_template
end
end
return scale.Multiply
return output_template
end
end


Line 276: Line 262:
local ability = utils.get_ability_card_data(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then return {} end -- return empty table, not string
if(ability == nil) then return "Ability Not Found" end
local info_section = ability['Info'..info_section_num]
local info_section = ability['Info'..info_section_num]
Line 288: Line 274:
return frame:preprocess(lang.get_string(info_section.DescKey))
return frame:preprocess(lang.get_string(info_section.DescKey))
end
end
function get_info_desc(hero_key, ability_num, info_section_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then return "Ability Not Found" end
local info_section = ability['Info'..info_section_num]
-- some abilities have no info sections
--{{#invoke:AbilityData|get_info_main|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}--
if info_section == nil then return '' end
p.get_info_main = function(frame)
local hero_key = frame.args[1]
if info_section.DescKey == nil then
local ability_num = frame.args[2]
return ''
local info_section_num = frame.args[3]
end
local frame = mw.getCurrentFrame()
return frame:preprocess(lang.get_string(info_section.DescKey))
end
 
function get_main_boxes(hero_key, ability_num, info_section_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then return "Ability Not Found" end
if(ability == nil) then return "Ability Not Found" end
Line 316: Line 291:
local main = info_section.Main
local main = info_section.Main
if main == nil then
if main == nil then
return {}
return ''
end
local frame = mw.getCurrentFrame()
local props = {}
-- Start with the standard Props array
if main.Props then
for _, prop in ipairs(main.Props) do
table.insert(props, prop)
end
end
end
local props = info_section.Main.Props
local main_boxes = {}
-- Concatenate multiple section boxes into a single output template
for k, prop in ipairs(props) do -- use ipairs for ordered iteration
local info_box_template = ''
local value = prop.Value
for k, prop in pairs(props) do
local scale = pick_scale(prop)
-- Exclude 0 values
-- If it scales with melee damage, set the value using the hero's base melee to match in game visuals
if prop.Value and prop.Value ~= 0 then
if scale and scale.Type == 'melee' then
local icon = get_icon(prop.Type)
local hero = get_hero_data(hero_key)
value = prop.Value + hero.LightMeleeDamage * scale.Value
elseif scale and scale.Type == 'heavy_melee' then
local hero = get_hero_data(hero_key)
value = prop.Value + hero.HeavyMeleeDamage * scale.Value
elseif scale and scale.Type == 'weapon_power' then
local hero = get_hero_data(hero_key)
value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100
end
local icon = Icon.getAttrIcon(prop.Type)
section_box = frame:expandTemplate{
local main_box
if prop.StatusEffect then
main_box = frame:expandTemplate{
title = "Ability_card_v2/Card/StatusBox",
args = {
title = prop.Title,
key = prop.Key,
value = value,
status_effect = prop.StatusEffect,
icon = icon and icon.img,
icon_link = icon and icon.link,
icon_color = icon and icon.color,
icon_size = icon and icon.size,
scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),
scale_type =  scale and scale.Type,
-- allow nil value here as some status effects don't have a value but should not be hidden
hide = value == 0 and "true" or "false",
}
}
else
main_box = frame:expandTemplate{
title = "Ability_card_v2/Card/MainBox",
title = "Ability_card_v2/Card/MainBox",
args = {
args = {
title = prop.Title,
title = prop.Title,
key = prop.Key,
key = prop.Key,
value = value,
value = prop.Value,
icon = icon and icon.img,
icon = icon.img,
icon_link = icon and icon.link,
icon_link = icon.link,
icon_color = icon and icon.color,
icon_color = icon.color,
icon_size = icon and icon.size,
icon_size = icon.size,
scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),
scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
scale_type =  scale and scale.Type,
scale_type =  prop.Scale and prop.Scale.Type
hide = (value == nil or value == 0) and "true" or "false",
}
}
}
}
info_box_template = info_box_template .. section_box .. '\n'
end
end
table.insert(main_boxes, main_box)
end
end
return main_boxes
return info_box_template
end
end


function get_hero_data(hero_key)
--{{#invoke:AbilityData|get_info_alt|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}--
local hero_data = mw.loadJsonData("Data:HeroData.json")
p.get_info_alt = function(frame)
local hero = hero_data[hero_key]
local hero_key = frame.args[1]
if (hero == nil) then return "Hero Not Found" end
local ability_num = frame.args[2]
return hero
local info_section_num = frame.args[3]
end
 
function get_alt_boxes(hero_key, ability_num, info_section_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then return "Ability Not Found" end
if(ability == nil) then return "Ability Not Found" end
Line 412: Line 342:
end
end
local frame = mw.getCurrentFrame()
-- Concatenate multiple section boxes into a single output template
local info_box_template = ''
for k, prop in pairs(props) do
-- Some props don't have values, as those come from upgrades
-- For now, we will ignore these and only show data for the base ability
if prop.Value and prop.Value ~= 0 then
local icon = get_icon(prop.Type)
section_box = frame:expandTemplate{
title = "Ability_card_v2/Card/AltBox",
args = {
  key = prop.Key,
  value = prop.Value,
  icon = icon.img,
  icon_link = icon.link,
  icon_color = icon.color,
    icon_size = icon.size,
  scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
  scale_type =  prop.Scale and prop.Scale.Type
}
}
info_box_template = info_box_template .. section_box .. '\n'
end
end
local alt_boxes = {}
return info_box_template
    for k, prop in ipairs(props) do
    -- Some props don't have values, as those come from upgrades
    -- For now, we will ignore these and only show data for the base ability
    if prop.Value then
    local icon = Icon.getAttrIcon(prop.Type)
 
    local value = prop.Value
    local scale = pick_scale(prop)
 
    if scale and scale.Type == 'weapon_power' then
    local hero = get_hero_data(hero_key)
    value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100
    end
 
    local alt_box = frame:expandTemplate{
    title = "Ability_card_v2/Card/AltBox",
    args = {
    key = prop.Key,
    value = value,
    icon = icon and icon.img,
    icon_link = icon and icon.link,
    icon_color = icon and icon.color,
    icon_size = icon and icon.size,
    scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),
    scale_type = scale and scale.Type,
    hide = value == 0 and "true" or "false",
    }
    }
    table.insert(alt_boxes, alt_box)
    end
    end
return alt_boxes
end
end


local UPGRADE_COST_MAP = {1, 2, 5}
local UPGRADE_COST_MAP = {1, 2, 5}
 
--{{#invoke:AbilityData|get_upgrades|HERO_KEY|ABILITY_NUM}}--
-- Upgrade description sizing. These are design proportions, not copies of any
p.get_upgrades = function(frame)
-- CSS measurement: the browser supplies the box width through the container
local hero_key = frame.args[1]
-- query on .ac-upgrade-descwrap, so nothing here needs updating if the card's
local ability_num = frame.args[2]
-- widths, margins or padding change.
local UG_DESC_ASPECT = 0.357  -- max height of the text block, as a multiple of its width
local UG_LINE_HEIGHT = 1.4    -- emitted alongside the size so the two cannot drift
local UG_MAX_CQW    = 11.9  -- ceiling, as % of the text width (~16px on a full-width card)
local UG_MIN_CQW    = 6.7    -- floor (~9px)
 
function get_upgrade_boxes(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then return "Ability Not Found" end
if(ability == nil) then return "Ability Not Found" end
local frame = mw.getCurrentFrame()
local props = ability.Upgrades
 
local upgrades = ability.Upgrades
local upgrade_boxes = {}
-- Concatenate multiple section boxes into a single output template
for k, upgrade in ipairs(upgrades) do
local upgrades_template = ''
local description = lang.get_string(upgrade.DescKey)
for k, prop in pairs(props) do
 
local description = lang.get_string(prop.DescKey)
-- bypass some keys if they are mistakenly left in game files
local IGNORE_DESC_KEYS = {'citadel_ability_hornet_snipe_t3_desc', 'ability_fencer_ultimate_t3_desc'}
-- Generate a description if there is no localized string
-- Generate a description if there is no localized string
if (description == nil or description == '' or commonutils.contains(IGNORE_DESC_KEYS, upgrade.DescKey)) then
if (description == nil or description == '') then
description = create_description(upgrade, frame)
description = create_description(prop, frame)
else
-- Replace embedded value placeholders with just the base value
-- Pattern: {'Value': X, 'Scale': {...}} -> X
description = description:gsub("{'Value':%s*([%d%.%-]+),%s*'Scale':%s*{[^}]+}}", function(value)
return commonutils.round_to_sig_fig(tonumber(value), 3)
end)
-- Pattern: {'Value': X} -> X
description = description:gsub("{'Value':%s*([%d%.%-]+)}", function(value)
return commonutils.round_to_sig_fig(tonumber(value), 3)
end)
end
end
-- Expand once, then size from the text the reader actually sees, rather
-- Vary the font size based on the number of characters to prevent overflow
-- than from a character count of the unexpanded string.
local fontsize = '1rem'
local rendered = frame:preprocess(description)
if #description > 60 and #description < 71 then
local cqw = textfit.fit(rendered, {
fontsize = '0.95rem'
aspect = UG_DESC_ASPECT,
elseif #description > 70 and #description < 91 then
lineHeight = UG_LINE_HEIGHT,
fontsize = '0.875rem'
max = UG_MAX_CQW,
elseif #description > 90 then
min = UG_MIN_CQW,
fontsize = '0.8rem'
})
end
local upgrade_scale = pick_scale(upgrade)
box = frame:expandTemplate{
local upgrade_box = frame:expandTemplate{
title = "Ability_card_v2/Card/UpgradeBox",
title = "Ability_card_v2/Card/UpgradeBox",
args = {
args = {
index = k,
  cost = UPGRADE_COST_MAP[k],
cost = UPGRADE_COST_MAP[k],
  description = frame:preprocess(description),
description = rendered,
  scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3),
  scale_type =  prop.Scale and prop.Scale.Type,
scale_type =  upgrade_scale and upgrade_scale.Type,
  fontsize = fontsize
-- Measured against the box's own width, so the size stays correct
-- when the card renders below its 500px max-width.
fontsize = string.format('%.2fcqw', cqw),
lineheight = UG_LINE_HEIGHT
}
}
}
}
table.insert(upgrade_boxes, upgrade_box)
upgrades_template = upgrades_template .. box .. '\n'
end
end
return upgrade_boxes
return upgrades_template
end
end


function create_description(prop, frame)
function create_description(prop, frame)
    local value_lines = {}  -- normal stat changes
local description = ''
    local scale_lines = {}  -- "Increased X scaling" notes
for k, v in pairs(prop) do
    local seen = {}
if type(v) ~= 'table' then
local formatted_value = utils.format_value_with_prepost(k, v, frame)
local attr_name = lang.get_string(k..'_label')
description = description .. string.format('%s %s', formatted_value, attr_name)
end
end
return description
end


    for k, v in pairs(prop) do
p.get_attr_icon = function(frame)
        if k == 'DescKey' then
local attr_type = frame.args[1]
            -- skip
local icon = get_icon(attr_type)
        elseif type(v) == 'table' then
            local has_value = v.Value ~= nil
            local has_scale = v.Scale and v.Scale.Value and v.Scale.Value ~= 0


            -- Value is 0 and we have a scaling change → scaling note
return string.format('[[File:%s|%s|18px|link=%s]]', icon.img or '', icon.size or '', icon.link or '')
            if has_value and has_scale and v.Value == 0 then
end
                if not seen["scale_" .. k] then
                    local scale_type = v.Scale.Type
                    local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2)
                    table.insert(scale_lines, "Increased " .. capitalized .. " scaling")
                    seen["scale_" .. k] = true
                end
            -- Only a scaling change, no value → scaling note (future bot correction)
            elseif not has_value and has_scale then
                if not seen["scale_" .. k] then
                    local scale_type = v.Scale.Type
                    local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2)
                    table.insert(scale_lines, "Increased " .. capitalized .. " scaling")
                    seen["scale_" .. k] = true
                end
            -- Normal value (0 without scale, or non‑zero with/without scale)
            elseif has_value then
                local formatted_value = utils.format_value_with_prepost(k, v.Value, frame)
                local attr_name = lang.get_string(k .. '_label')
                local key = formatted_value .. '|' .. attr_name
                if not seen[key] then
                    table.insert(value_lines, string.format('%s %s', formatted_value, attr_name))
                    seen[key] = true
                end
            end
        else
            -- Plain number/string (e.g. OutgoingDamagePenaltyPercent = -15)
            local formatted_value = utils.format_value_with_prepost(k, v, frame)
            local attr_name = lang.get_string(k .. '_label')
            local key = formatted_value .. '|' .. attr_name
            if not seen[key] then
                table.insert(value_lines, string.format('%s %s', formatted_value, attr_name))
                seen[key] = true
            end
        end
    end


    -- Combine: value lines first, then scaling lines
function get_icon(attr_type)
    local all_lines = {}
local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type]
    for _, line in ipairs(value_lines) do
local img = 'GenericProperty.png'
        table.insert(all_lines, line)
local link = ''
    end
local size = ''
    for _, line in ipairs(scale_lines) do
local color = 'Grey'
        table.insert(all_lines, line)
if mappedAttr then
    end
img = mappedAttr.img
 
link = mappedAttr.link
    return table.concat(all_lines, '<br>')
size = mappedAttr.size
color = mappedAttr.color or color
end
return {img=img, link=link, color=color, size=size}
end
end


Line 593: Line 458:
end
end
return nil
return nil
end
--for use from an ability page (ability pages are WIP), not hero pages
function p.write_ability_card_from_ability_key(frame)
local ability_key = frame.args[1]
if ability_key == nil then return "ability_key '" .. ability_key "' not provided" end
-- Determine the hero and ability number
local found_hero_key
local found_ability_num
for hero_key, card_data in pairs(data) do
if found_hero_key == nil or found_ability_num == nil then
for ability_num, ability_data in pairs(card_data) do
if ability_num ~= "Name" then
if ability_data['Key'] == ability_key then
found_hero_key = hero_key
found_ability_num = ability_num
break
end -- hero_key and ability_num found
end
end
end
end
if found_ability_num == nil or found_hero_key == nil then return "ability_key " .. ability_key .. " is not used by any heroes" end
-- Get notes for this ability
local notes_source_page_name = utils.get_notes_source_page_name(ability_key)
local notes_str = frame:preprocess("{{"..notes_source_page_name.."}}")
-- Create the ability card
return p.get_ability_card_from_key(found_hero_key, found_ability_num, true, notes_str, notes_source_page_name)
end
--from hero key*
--notes parameter will eventually be removed, as notes_source_page would be sufficient or could even be determined from within this function
function p.get_ability_card_from_key(hero_key, ability_num, add_link, notes, notes_source_page)
if type(hero_key) == 'table' and hero_key.args then
local frame = hero_key
hero_key = frame.args[1]
ability_num = frame.args[2]
add_link = frame.args[3]
notes = frame.args[4]
notes_source_page = frame.args[5]
end
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then
return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num
end
local ability_name_localized = lang.get_string(ability.Key)
local name_link = nil
if add_link == 'true' then
name_link = ability_name_localized
end
if notes_source_page ~= nil and notes ~= "" then
--Notes comes from a /Notes page, and the notes are not blank
-- Confirm the notes source page exists, otherwise, don't display any notes
local title = mw.title.new(notes_source_page)
if not (title and title.exists) then
notes = ""
end
end
return mw.getCurrentFrame():expandTemplate{
title = "Ability card v2/Card",
args = {
hero_key = hero_key,
ability_num = ability_num,
name = ability_name_localized,
name_link = name_link,
icon = lang.get_string(ability.Key, 'en') .. '.png',
description = mw.getCurrentFrame():preprocess(lang.get_string(ability.DescKey)),
channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value,
channel_time_scale = get_attr_scale(ability.AbilityChannelTime),
channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime),
channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime),
radius = ability.Radius and ability.Radius.Value,
radius_scale = get_attr_scale(ability.Radius),
radius_scale_type = get_attr_scale_type(ability.Radius),
radius_scale_multiply = get_attr_scale_multiply(ability.Radius),
range = ability.AbilityCastRange and ability.AbilityCastRange.Value,
range_scale = get_attr_scale(ability.AbilityCastRange),
range_scale_type = get_attr_scale_type(ability.AbilityCastRange),
range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange),
duration = ability.AbilityDuration and ability.AbilityDuration.Value,
duration_scale = get_attr_scale(ability.AbilityDuration),
duration_scale_type = get_attr_scale_type(ability.AbilityDuration),
duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration),
-- ability_width = format_value_with_prepost(width_key, ability[width_key]),
cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,
cooldown_scale = get_attr_scale(ability.AbilityCooldown),
cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown),
cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown),
charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value,
charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge),
charge_cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldownBetweenCharge),
charge_cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldownBetweenCharge),
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value or 0,
}
}
end
end


return p
return p
Please note that all contributions to The Deadlock Wiki are considered to be released under the Creative Commons Attribution-NonCommercial-ShareAlike (see Deadlock:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!
Cancel Editing help (opens in new window)
Preview page with this template