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) |
m undo the undo, it works now Tag: Undo |
||
| (49 intermediate revisions by 9 users not shown) | |||
| 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") | ||
function get_hero_key(hero_name) | function get_hero_key(hero_name) | ||
if hero_name == nil then return nil end | |||
if | |||
-- if name starts with "hero_", use it directly as the key | |||
if string.sub(hero_name, 1, 5) == "hero_" then return hero_name end | |||
return | local resource_lookup = mw.loadJsonData("Data:ResourceLookup.json") | ||
return resource_lookup[hero_name:lower()].key | |||
end | end | ||
| Line 84: | Line 63: | ||
local info1_desc = get_info_desc(hero_key, ability_num, 1) | local info1_desc = get_info_desc(hero_key, ability_num, 1) | ||
local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1) | local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1) | ||
local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1) | local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1) | ||
if #info1_alt_boxes > | if #info1_alt_boxes > 7 then | ||
error(#info1_alt_boxes .. ' alt boxes found, but only | error(#info1_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2') | ||
end | end | ||
| Line 114: | Line 74: | ||
local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2) | local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2) | ||
if #info2_alt_boxes > | if #info2_alt_boxes > 7 then | ||
error(#info2_alt_boxes .. ' alt boxes found, but only | error(#info2_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2') | ||
end | end | ||
| Line 122: | Line 82: | ||
local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3) | local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3) | ||
if #info3_alt_boxes > | if #info3_alt_boxes > 7 then | ||
error(#info3_alt_boxes .. ' alt boxes found, but only | error(#info3_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2') | ||
end | end | ||
| Line 131: | Line 91: | ||
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, | ||
| Line 139: | Line 100: | ||
icon = lang.get_string(ability.Key, 'en') .. '.png', | icon = lang.get_string(ability.Key, 'en') .. '.png', | ||
channel_time = ability. | 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_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_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_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), | |||
charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value, | cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown), | ||
cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown), | |||
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value, | 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, | |||
-- Info section #1 defined in "Info1" attribute | -- Info section #1 defined in "Info1" attribute | ||
hide_info1 = infobox_is_hidden(ability.Info1) and "true" or "false", | |||
info1_desc = info1_desc, | info1_desc = info1_desc, | ||
info1_mainbox1 = info1_main_boxes[1], | info1_mainbox1 = info1_main_boxes[1], | ||
info1_mainbox2 = info1_main_boxes[2], | info1_mainbox2 = info1_main_boxes[2], | ||
info1_mainbox3 = info1_main_boxes[3], | 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_altbox1 = info1_alt_boxes[1], | ||
info1_altbox2 = info1_alt_boxes[2], | info1_altbox2 = info1_alt_boxes[2], | ||
| Line 165: | Line 141: | ||
info1_altbox5 = info1_alt_boxes[5], | info1_altbox5 = info1_alt_boxes[5], | ||
info1_altbox6 = info1_alt_boxes[6], | info1_altbox6 = info1_alt_boxes[6], | ||
info1_altbox7 = info1_alt_boxes[7], | |||
-- Info section #2 defined in "Info2" attribute | -- Info section #2 defined in "Info2" attribute | ||
hide_info2 = infobox_is_hidden(ability.Info2) and "true" or "false", | |||
info2_desc = info2_desc, | info2_desc = info2_desc, | ||
info2_mainbox1 = info2_main_boxes[1], | info2_mainbox1 = info2_main_boxes[1], | ||
info2_mainbox2 = info2_main_boxes[2], | info2_mainbox2 = info2_main_boxes[2], | ||
info2_mainbox3 = info2_main_boxes[3], | 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_altbox1 = info2_alt_boxes[1], | ||
info2_altbox2 = info2_alt_boxes[2], | info2_altbox2 = info2_alt_boxes[2], | ||
| Line 177: | Line 157: | ||
info2_altbox5 = info2_alt_boxes[5], | info2_altbox5 = info2_alt_boxes[5], | ||
info2_altbox6 = info2_alt_boxes[6], | info2_altbox6 = info2_alt_boxes[6], | ||
info2_altbox7 = info2_alt_boxes[7], | |||
-- Info section #3 defined in "Info3" attribute | -- Info section #3 defined in "Info3" attribute | ||
hide_info3 = infobox_is_hidden(ability.Info3) and "true" or "false", | |||
info3_desc = info3_desc, | info3_desc = info3_desc, | ||
info3_mainbox1 = info3_main_boxes[1], | info3_mainbox1 = info3_main_boxes[1], | ||
info3_mainbox2 = info3_main_boxes[2], | info3_mainbox2 = info3_main_boxes[2], | ||
info3_mainbox3 = info3_main_boxes[3], | 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_altbox1 = info3_alt_boxes[1], | ||
info3_altbox2 = info3_alt_boxes[2], | info3_altbox2 = info3_alt_boxes[2], | ||
| Line 189: | Line 173: | ||
info3_altbox5 = info3_alt_boxes[5], | info3_altbox5 = info3_alt_boxes[5], | ||
info3_altbox6 = info3_alt_boxes[6], | info3_altbox6 = info3_alt_boxes[6], | ||
info3_altbox7 = info3_alt_boxes[7], | |||
-- Ability upgrades defined in "Upgrades" attribute | -- Ability upgrades defined in "Upgrades" attribute | ||
upgrade1 = upgrades[1], | upgrade1 = upgrades[1], | ||
| Line 202: | Line 187: | ||
end | end | ||
-- | function infobox_is_hidden(infobox) | ||
function | if infobox == nil then | ||
if not attr then | return false | ||
return nil | end | ||
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 | |||
return nil | |||
end | |||
-- A single scaling is stored directly as an object | |||
if scale[1] == nil then | |||
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 | if not scale then | ||
return nil | return nil | ||
end | end | ||
if scale. | if scale.Value == 0 then | ||
return nil | return nil | ||
end | |||
return commonutils.round_to_sig_fig(scale.Value, 3) | |||
end | |||
-- Get scaling type of attribute, return nil if none is found | |||
function get_attr_scale_type(attr) | |||
local scale = pick_scale(attr) | |||
if not scale then | |||
return nil | |||
end | end | ||
| Line 221: | Line 255: | ||
end | end | ||
return | return scale.Type | ||
end | |||
-- Check if scaling should be multiplied instead of added | |||
function get_attr_scale_multiply(attr) | |||
local scale = pick_scale(attr) | |||
if not scale then | |||
return nil | |||
end | |||
return scale.Multiply | |||
end | end | ||
| Line 232: | Line 276: | ||
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 | if(ability == nil) then return {} end -- return empty table, not string | ||
local info_section = ability['Info'..info_section_num] | local info_section = ability['Info'..info_section_num] | ||
| Line 263: | Line 307: | ||
function get_main_boxes(hero_key, ability_num, info_section_num) | 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 | 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] | ||
-- some abilities have no info sections | -- some abilities have no info sections | ||
if info_section == nil then return | if info_section == nil then return '' end | ||
local main = info_section.Main | local main = info_section.Main | ||
| Line 286: | Line 330: | ||
end | end | ||
local main_boxes = {} | local main_boxes = {} | ||
for k, prop in ipairs(props) do -- use ipairs for ordered iteration | for k, prop in ipairs(props) do -- use ipairs for ordered iteration | ||
local value = prop.Value | local value = prop.Value | ||
local scale = pick_scale(prop) | |||
-- If it scales with melee damage, set the value using the hero's base melee to match in game visuals | -- If it scales with melee damage, set the value using the hero's base melee to match in game visuals | ||
if | if scale and scale.Type == 'melee' then | ||
local hero = get_hero_data(hero_key) | local hero = get_hero_data(hero_key) | ||
value = prop.Value + hero.LightMeleeDamage * prop. | 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 | end | ||
local icon = Icon.getAttrIcon(prop.Type) | |||
if value and | |||
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, | ||
icon = icon.img, | icon = icon and icon.img, | ||
icon_link = icon.link, | icon_link = icon and icon.link, | ||
icon_color = icon.color, | icon_color = icon and icon.color, | ||
icon_size = icon.size, | icon_size = icon and icon.size, | ||
scale_value = | scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3), | ||
scale_type = | scale_type = scale and scale.Type, | ||
hide = (value == nil or value == 0) and "true" or "false", | |||
} | } | ||
} | } | ||
end | end | ||
table.insert(main_boxes, main_box) | |||
end | end | ||
| Line 365: | Line 416: | ||
local alt_boxes = {} | local alt_boxes = {} | ||
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 | return alt_boxes | ||
| Line 391: | Line 452: | ||
local UPGRADE_COST_MAP = {1, 2, 5} | local UPGRADE_COST_MAP = {1, 2, 5} | ||
-- Upgrade description sizing. These are design proportions, not copies of any | |||
-- CSS measurement: the browser supplies the box width through the container | |||
-- query on .ac-upgrade-descwrap, so nothing here needs updating if the card's | |||
-- 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) | 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) | ||
| Line 397: | Line 468: | ||
local frame = mw.getCurrentFrame() | local frame = mw.getCurrentFrame() | ||
local | local upgrades = ability.Upgrades | ||
local upgrade_boxes = {} | local upgrade_boxes = {} | ||
for k, | for k, upgrade in ipairs(upgrades) do | ||
local description = lang.get_string( | local description = lang.get_string(upgrade.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 == '') then | if (description == nil or description == '' or commonutils.contains(IGNORE_DESC_KEYS, upgrade.DescKey)) then | ||
description = create_description( | description = create_description(upgrade, 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 | ||
local | -- than from a character count of the unexpanded string. | ||
local rendered = frame:preprocess(description) | |||
local cqw = textfit.fit(rendered, { | |||
aspect = UG_DESC_ASPECT, | |||
lineHeight = UG_LINE_HEIGHT, | |||
max = UG_MAX_CQW, | |||
min = UG_MIN_CQW, | |||
}) | |||
local upgrade_scale = pick_scale(upgrade) | |||
local 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], | |||
description = rendered, | |||
scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3), | |||
scale_type = upgrade_scale and upgrade_scale.Type, | |||
-- 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 | |||
} | } | ||
} | } | ||
| Line 433: | Line 522: | ||
return upgrade_boxes | return upgrade_boxes | ||
end | end | ||
function create_description(prop, frame) | function create_description(prop, frame) | ||
local | local value_lines = {} -- normal stat changes | ||
local | local scale_lines = {} -- "Increased X scaling" notes | ||
local seen = {} | local seen = {} | ||
for k, v in pairs(prop) do | for k, v in pairs(prop) do | ||
if k == 'DescKey' then | |||
-- skip | |||
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 | |||
if has_value and has_scale and v.Value == 0 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 | |||
-- 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 formatted_value = utils.format_value_with_prepost(k, v, frame) | ||
local attr_name = lang.get_string(k..'_label') | local attr_name = lang.get_string(k .. '_label') | ||
local key = formatted_value .. '|' .. attr_name | local key = formatted_value .. '|' .. attr_name | ||
if not seen[key] then | if not seen[key] then | ||
table.insert(value_lines, string.format('%s %s', formatted_value, attr_name)) | |||
seen[key] = true | |||
end | end | ||
end | end | ||
end | end | ||
-- Combine: value lines first, then scaling lines | |||
end | local all_lines = {} | ||
for _, line in ipairs(value_lines) do | |||
table.insert(all_lines, line) | |||
end | |||
for _, line in ipairs(scale_lines) do | |||
table.insert(all_lines, line) | |||
end | |||
return table.concat(all_lines, '<br>') | |||
end | end | ||
| Line 561: | Line 660: | ||
return mw.getCurrentFrame():expandTemplate{ | return mw.getCurrentFrame():expandTemplate{ | ||
title = " | title = "Ability card v2/Card", | ||
args = { | args = { | ||
hero_key = hero_key, | hero_key = hero_key, | ||
| Line 569: | Line 668: | ||
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)), | 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_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_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_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), | |||
charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value, | cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown), | ||
cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown), | |||
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value, | 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, | |||
} | } | ||
} | } | ||
Latest revision as of 12:59, 31 July 2026
Overview
[edit source]Generates hero ability cards
Usage
[edit source]Submodules
[edit source]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 Icon = require"Module:Abilities/icon"
local textfit = require "Module:TextFit"
local p = {}
local data = mw.loadJsonData("Data:AbilityCards.json")
function get_hero_key(hero_name)
if hero_name == nil then return nil end
-- if name starts with "hero_", use it directly as the key
if string.sub(hero_name, 1, 5) == "hero_" then return hero_name end
local resource_lookup = mw.loadJsonData("Data:ResourceLookup.json")
return resource_lookup[hero_name:lower()].key
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)
local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1)
if #info1_alt_boxes > 7 then
error(#info1_alt_boxes .. ' alt boxes found, but only 7 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 > 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
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 > 7 then
error(#info3_alt_boxes .. ' alt boxes found, but only 7 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 = {
id = hero_key .. ':' .. ability.Key,
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.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,
-- 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_source_page = notes_source_page
}
}
end
function infobox_is_hidden(infobox)
if infobox == nil then
return false
end
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
local scale = attr.Scale
if type(scale) ~= 'table' then
return nil
end
-- A single scaling is stored directly as an object
if scale[1] == nil then
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
if scale.Value == 0 then
return nil
end
return commonutils.round_to_sig_fig(scale.Value, 3)
end
-- Get scaling type of attribute, return nil if none is found
function get_attr_scale_type(attr)
local scale = pick_scale(attr)
if not scale then
return nil
end
if scale.Value == 0 then
return nil
end
return scale.Type
end
-- Check if scaling should be multiplied instead of added
function get_attr_scale_multiply(attr)
local scale = pick_scale(attr)
if not scale then
return nil
end
return scale.Multiply
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 {} 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
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 "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 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
local main_boxes = {}
for k, prop in ipairs(props) do -- use ipairs for ordered iteration
local value = prop.Value
local scale = pick_scale(prop)
-- If it scales with melee damage, set the value using the hero's base melee to match in game visuals
if scale and scale.Type == 'melee' then
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)
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",
args = {
title = prop.Title,
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 == nil or value == 0) and "true" or "false",
}
}
end
table.insert(main_boxes, main_box)
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 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
local UPGRADE_COST_MAP = {1, 2, 5}
-- Upgrade description sizing. These are design proportions, not copies of any
-- CSS measurement: the browser supplies the box width through the container
-- query on .ac-upgrade-descwrap, so nothing here needs updating if the card's
-- 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)
if(ability == nil) then return "Ability Not Found" end
local frame = mw.getCurrentFrame()
local upgrades = ability.Upgrades
local upgrade_boxes = {}
for k, upgrade in ipairs(upgrades) do
local description = lang.get_string(upgrade.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
if (description == nil or description == '' or commonutils.contains(IGNORE_DESC_KEYS, upgrade.DescKey)) then
description = create_description(upgrade, 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
-- Expand once, then size from the text the reader actually sees, rather
-- than from a character count of the unexpanded string.
local rendered = frame:preprocess(description)
local cqw = textfit.fit(rendered, {
aspect = UG_DESC_ASPECT,
lineHeight = UG_LINE_HEIGHT,
max = UG_MAX_CQW,
min = UG_MIN_CQW,
})
local upgrade_scale = pick_scale(upgrade)
local upgrade_box = frame:expandTemplate{
title = "Ability_card_v2/Card/UpgradeBox",
args = {
index = k,
cost = UPGRADE_COST_MAP[k],
description = rendered,
scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3),
scale_type = upgrade_scale and upgrade_scale.Type,
-- 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)
end
return upgrade_boxes
end
function create_description(prop, frame)
local value_lines = {} -- normal stat changes
local scale_lines = {} -- "Increased X scaling" notes
local seen = {}
for k, v in pairs(prop) do
if k == 'DescKey' then
-- skip
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
if has_value and has_scale and v.Value == 0 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
-- 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
local all_lines = {}
for _, line in ipairs(value_lines) do
table.insert(all_lines, line)
end
for _, line in ipairs(scale_lines) do
table.insert(all_lines, line)
end
return table.concat(all_lines, '<br>')
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 = "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
return p