Module:Abilities/card: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Have channel time not appear if its 9999 (fixes Grey Talon charged shot ability card to look like it does in game) |
Added nil check for channel time |
||
| Line 139: | Line 139: | ||
icon = lang.get_string(ability.Key, 'en') .. '.png', | icon = lang.get_string(ability.Key, 'en') .. '.png', | ||
channel_time = ability.Cast and ability.Cast.AbilityChannelTime.Value ~= 9999 and ability.Cast.AbilityChannelTime.Value, | channel_time = ability.Cast and ability.Cast.AbilityChannelTime and ability.Cast.AbilityChannelTime.Value ~= 9999 and ability.Cast.AbilityChannelTime.Value, | ||
channel_time_ss = ability.Cast and get_attr_ss(ability.Cast.AbilityChannelTime), | channel_time_ss = ability.Cast and get_attr_ss(ability.Cast.AbilityChannelTime), | ||
radius = ability.Radius and ability.Radius.Value, | radius = ability.Radius and ability.Radius.Value, | ||
Revision as of 12:55, 1 November 2025
Overview
Generates hero ability cards
Usage
Submodules
Abilities - Simple functions. Eg. getting ability name
Abilities/utils - Common internal functions that are shared amongst any Abilities/ modules
Abilities/card - Generates hero ability cards
Abilities/icon - Functions for retrieving ability and attribute icon data
Abilities/details table (WIP) - Generates details table to show raw ability data
local lang = require "Module:Lang"
local commonutils = require "Module:Utilities"
local utils = require "Module:Abilities/utils"
local p = {}
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', color = 'NoColor'},
bullet_armor_down = {img='Bullet_Armor.png', link='Damage_Resistance', color = 'NoColor'},
cast = {img='AttributeIconMaxChargesIncrease.png', link=''},
charges = {img='AttributeIconMaxChargesIncrease.png', link='', color = 'Purple'},
damage = {img='Damage_heart.png', link='', color = 'NoColor'}, -- 'NoColor' will apply no icon color and use the original image
bullet_damage = {img='Bullet_damage.png', link='Bullet_Damage', color = 'NoColor'},
fire_rate = {img='Fire Rate.png', link='Fire_Rate', color = 'Brown'},
healing = {img='Healing.png', link='Healing', color = 'Green'},
health = {img='Health.png', link='Health', color = 'Green'},
move_speed = {img='Move speed.png', link='Move Speed', color = 'Green'},
range = {img='CastRange.png', link='Ability_Range', color = 'Purple'},
tech_armor_up = {img='Spirit_Armor.png', link='Damage_Resistance', color = 'NoColor'},
tech_damage = {img='AttributeIconTechShieldHealth.png', link='Spirit_Damage', color = 'NoColor',size = '12px'},
distance = {img='AttributeIconTechRange.png', link='Ability_Range', color = 'Purple'},
duration = {img='AttributeIconTechDuration.png', link='Ability Duration', color = 'Purple'},
slow = {img='MoveSlow.png', link='', color = 'Purple'},
melee_damage = {img='Melee damage.png', link='Melee Damage'},
cooldown = {img='Cooldown Icon.png', link='Ability Cooldown', color = 'Purple'},
combat_barrier = {img='Barrier.png', link='Barrier', color = 'Green'},
time = {img='AttributeIconTechDuration.png', link='Ability Duration', color = 'Purple'},
}
function get_hero_key(hero_name)
for i, hero in pairs(data) do
if hero["Name"] == hero_name then
return i
end
end
return nil
end
--{{#invoke:Abilities/card|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}--
-- Args:
-- HERO_NAME (required) - Name of the hero that is found in Data:HeroData.json under "Name"
-- ABILITY_NUM (required) - Selects ability at index 1 to 4
-- ADD_LINK - Add a hyperlink to the title to the associated page
-- NOTES - User note data to include in the footer of the ability card
p.get_ability_card = function(frame)
local hero_name = frame.args[1]
local ability_num = frame.args[2]
local add_link = frame.args[3]
local notes = frame.args[4]
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)
end
-- Pulls data from Data:AbilityCards.json to populate Template:Ability card v2
function build_ability_card(hero_key, ability_num, add_link, notes, notes_source_page)
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
local frame = mw.getCurrentFrame()
local info1_desc = get_info_desc(hero_key, ability_num, 1)
local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1)
-- Inject Hex Duration only for Rabbit Hex
if ability.Key == "ability_magician_animalhexarea"
and ability.Other
and ability.Other.HexDuration
and ability.Other.HexDuration.Value then
local icon = get_icon("duration")
table.insert(info1_main_boxes, 1,
frame:expandTemplate{
title = "Ability_card_v2/Card/MainBox",
args = {
key = "HexDuration",
value = ability.Other.HexDuration.Value,
icon = icon.img,
icon_link = icon.link,
icon_color = icon.color,
}
})
end
local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1)
if #info1_alt_boxes > 6 then
error(#info1_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
end
local info2_desc = get_info_desc(hero_key, ability_num, 2)
local info2_main_boxes = get_main_boxes(hero_key, ability_num, 2)
local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2)
if #info2_alt_boxes > 6 then
error(#info2_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
end
local info3_desc = get_info_desc(hero_key, ability_num, 3)
local info3_main_boxes = get_main_boxes(hero_key, ability_num, 3)
local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3)
if #info3_alt_boxes > 6 then
error(#info3_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
end
local upgrades = get_upgrade_boxes(hero_key, ability_num)
return frame:expandTemplate{
title = "Ability_card_v2/Card",
args = {
hero_key = hero_key,
ability_num = ability_num,
-- Header info defined in various attributes
name = ability_name_localized,
name_link = name_link,
icon = lang.get_string(ability.Key, 'en') .. '.png',
channel_time = ability.Cast and ability.Cast.AbilityChannelTime and ability.Cast.AbilityChannelTime.Value ~= 9999 and ability.Cast.AbilityChannelTime.Value,
channel_time_ss = ability.Cast and get_attr_ss(ability.Cast.AbilityChannelTime),
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,
-- Info section #1 defined in "Info1" attribute
info1_desc = info1_desc,
info1_mainbox1 = info1_main_boxes[1],
info1_mainbox2 = info1_main_boxes[2],
info1_mainbox3 = info1_main_boxes[3],
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],
-- Info section #2 defined in "Info2" attribute
info2_desc = info2_desc,
info2_mainbox1 = info2_main_boxes[1],
info2_mainbox2 = info2_main_boxes[2],
info2_mainbox3 = info2_main_boxes[3],
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],
-- Info section #3 defined in "Info3" attribute
info3_desc = info3_desc,
info3_mainbox1 = info3_main_boxes[1],
info3_mainbox2 = info3_main_boxes[2],
info3_mainbox3 = info3_main_boxes[3],
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],
-- Ability upgrades defined in "Upgrades" attribute
upgrade1 = upgrades[1],
upgrade2 = upgrades[2],
upgrade3 = upgrades[3],
-- User-created ability notes
notes = notes,
notes_source_page = notes_source_page
}
}
end
-- Get spirit scaling of attribute, return nil if none is found
function get_attr_ss(attr)
if not attr then
return nil
end
local scale = attr.Scale
if not scale then
return nil
end
if scale.Type ~= 'spirit' then
return nil
end
if scale.Value == 0 then
return nil
end
return commonutils.round_to_sig_fig(scale.Value, 3)
end
-- Get the description for an ability's info section
--{{#invoke:AbilityData|get_info_desc|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}--
p.get_info_desc = function(frame)
local hero_key = frame.args[1]
local ability_num = frame.args[2]
local info_section_num = frame.args[3]
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
if info_section == nil then return '' end
if info_section.DescKey == nil then
return ''
end
return frame:preprocess(lang.get_string(info_section.DescKey))
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
if info_section == nil then return '' end
if info_section.DescKey == nil then
return ''
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)
if(ability == nil) then return {} end -- return empty table, not string
local info_section = ability['Info'..info_section_num]
-- some abilities have no info sections
if info_section == nil then return {} end
local main = info_section.Main
if main == nil then
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
--- A set of stat types that represent absolute values and should not have a '+' prefix.
local ABSOLUTE_VALUE_TYPES = {
damage = true,
bullet_damage = true,
tech_damage = true,
melee_damage = true,
health = true,
healing = true,
range = true,
distance = true,
duration = true,
cooldown = true,
time = true,
charges = true,
cast = true,
combat_barrier = true
}
local main_boxes = {}
for k, prop in ipairs(props) do -- use ipairs for ordered iteration
local value = prop.Value
-- If it scales with melee damage, set the value using the hero's base melee to match in game visuals
if prop.Scale and prop.Scale.Type == 'melee' then
local hero = get_hero_data(hero_key)
value = prop.Value + hero.LightMeleeDamage * prop.Scale.Value
end
-- Exclude 0 values
if value and value ~= 0 then
--- Determine if the '+' sign should be excluded for this stat type by checking against the set.
local exclude_plus_flag = ABSOLUTE_VALUE_TYPES[prop.Type] or false
local icon = get_icon(prop.Type)
local main_box = frame:expandTemplate{
title = "Ability_card_v2/Card/MainBox",
args = {
title = prop.Title,
key = prop.Key,
value = utils.format_value_with_prepost(prop.Key, value, frame, exclude_plus_flag),
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
}
}
table.insert(main_boxes, main_box)
end
end
return main_boxes
end
function get_hero_data(hero_key)
local hero_data = mw.loadJsonData("Data:HeroData.json")
local hero = hero_data[hero_key]
if (hero == nil) then return "Hero Not Found" end
return hero
end
function get_alt_boxes(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
if info_section == nil then return '' end
local props = info_section.Alt
if props == nil then
return ''
end
local frame = mw.getCurrentFrame()
local alt_boxes = {}
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)
local alt_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
}
}
table.insert(alt_boxes, alt_box)
end
end
return alt_boxes
end
local UPGRADE_COST_MAP = {1, 2, 5}
function get_upgrade_boxes(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then return "Ability Not Found" end
local frame = mw.getCurrentFrame()
local props = ability.Upgrades
local upgrade_boxes = {}
for k, prop in pairs(props) do
local description = lang.get_string(prop.DescKey)
-- Generate a description if there is no localized string
if (description == nil or description == '') then
description = create_description(prop, frame)
end
-- Vary the font size based on the number of characters to prevent overflow
local fontsize = '1rem'
if #description > 60 and #description < 71 then
fontsize = '0.95rem'
elseif #description > 70 and #description < 91 then
fontsize = '0.875rem'
elseif #description > 90 then
fontsize = '0.8rem'
end
local upgrade_box = frame:expandTemplate{
title = "Ability_card_v2/Card/UpgradeBox",
args = {
cost = UPGRADE_COST_MAP[k],
description = frame:preprocess(description),
scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
scale_type = prop.Scale and prop.Scale.Type,
fontsize = fontsize
}
}
table.insert(upgrade_boxes, upgrade_box)
end
return upgrade_boxes
end
function create_description(prop, frame)
local description = ''
local first = true
local seen = {} -- Set to track added combinations
for k, v in pairs(prop) do
if type(v) ~= 'table' then
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 -- Unique identifier
-- Skip if this combination has already been added
if not seen[key] then
-- Add line break if this is not the first item
if not first then
description = description .. '<br>'
end
description = description .. string.format('%s %s', formatted_value, attr_name)
first = false
seen[key] = true -- Mark this combination as added
end
end
end
return description
end
p.get_attr_icon = function(frame)
local attr_type = frame.args[1]
local icon = get_icon(attr_type)
return string.format('[[File:%s|%s|18px|link=%s]]', icon.img or '', icon.size or '', icon.link or '')
end
function get_icon(attr_type)
local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type]
local img = 'GenericProperty.png'
local link = ''
local size = ''
local color = 'Grey'
if mappedAttr then
img = mappedAttr.img
link = mappedAttr.link
size = mappedAttr.size
color = mappedAttr.color or color
end
return {img=img, link=link, color=color, size=size}
end
function find_width_key(ability)
for key, value in pairs(ability) do
if type(key) == "string" and key:sub(-5) == "Width" then
return key
end
end
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 = "Template: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)),
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
return p