Module:Abilities/card: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Removed generic property (square) icons for attributes |
m undo the undo, it works now Tag: Undo |
||
| (26 intermediate revisions by 8 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") | ||
| Line 65: | 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) | ||
| Line 122: | Line 101: | ||
channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value, | 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), | |||
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 = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value, | ||
charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge), | |||
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value, | 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 | ||
| Line 204: | Line 195: | ||
end | end | ||
-- | -- An attribute may carry either a single scaling or a list of them. | ||
function | -- Extra entries are stat-category markers (duration, cooldown, range, radius, | ||
if not attr then | -- healing, charges) that always hold a value of 1 and are never displayed, so | ||
return nil | -- 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 223: | 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 292: | Line 334: | ||
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 prop. | 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) | local hero = get_hero_data(hero_key) | ||
value = | value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100 | ||
end | end | ||
local icon = | local icon = Icon.getAttrIcon(prop.Type) | ||
local main_box | local main_box | ||
| Line 313: | Line 362: | ||
icon_color = icon and icon.color, | icon_color = icon and icon.color, | ||
icon_size = icon and 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, | ||
-- allow nil value here as some status effects don't have a value but should not be hidden | -- 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", | hide = value == 0 and "true" or "false", | ||
| Line 330: | Line 379: | ||
icon_color = icon and icon.color, | icon_color = icon and icon.color, | ||
icon_size = icon and 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", | hide = (value == nil or value == 0) and "true" or "false", | ||
} | } | ||
| Line 367: | 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 394: | 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 403: | Line 471: | ||
local upgrade_boxes = {} | local upgrade_boxes = {} | ||
for k, upgrade in | for k, upgrade in ipairs(upgrades) do | ||
local description = lang.get_string(upgrade.DescKey) | local description = lang.get_string(upgrade.DescKey) | ||
-- bypass some keys if they are mistakenly left in game files | -- bypass some keys if they are mistakenly left in game files | ||
| Line 425: | Line 492: | ||
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", | ||
| Line 440: | Line 508: | ||
index = k, | index = k, | ||
cost = UPGRADE_COST_MAP[k], | cost = UPGRADE_COST_MAP[k], | ||
description = | description = rendered, | ||
scale_value = | scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3), | ||
scale_type = | scale_type = upgrade_scale and upgrade_scale.Type, | ||
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 | |||
} | } | ||
} | } | ||
| Line 454: | Line 525: | ||
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 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 | ||
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 603: | 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