Module:Abilities/card: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
Vergir (talk | contribs)
m make upgradebox fonts smaller so they always fit into the box
Vergir (talk | contribs)
m undo the undo, it works now
Tag: Undo
 
(30 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")
-- Maps attr type to an appropriate image and page link
-- Optional icon size, will be defaulted on the template
local ATTR_TYPE_ICON_MAP = require("Module:Icon/attr")




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)
-- 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)
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_ss = get_attr_ss(ability.AbilityChannelTime),
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_ss = get_attr_ss(ability.Radius),
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_ss = get_attr_ss(ability.AbilityCastRange),
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_ss = get_attr_ss(ability.AbilityDuration),
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_ss = get_attr_ss(ability.AbilityCooldown),
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_ss = get_attr_ss(ability.AbilityCooldownBetweenCharge),
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


-- Get spirit scaling of attribute, return nil if none is found
-- An attribute may carry either a single scaling or a list of them.
function get_attr_ss(attr)
-- 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.Type ~= 'spirit' then
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 commonutils.round_to_sig_fig(scale.Value, 3)
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.Scale and prop.Scale.Type == 'melee' then
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.Scale.Value
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 = get_icon(prop.Type)
local icon = Icon.getAttrIcon(prop.Type)
local main_box
local main_box
Line 309: Line 358:
value = value,
value = value,
status_effect = prop.StatusEffect,
status_effect = prop.StatusEffect,
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 = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),
scale_type =  prop.Scale and prop.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 326: Line 375:
key = prop.Key,
key = prop.Key,
value = 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 = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),
scale_type =  prop.Scale and prop.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 pairs(props) do
    for k, prop in ipairs(props) do
-- Some props don't have values, as those come from upgrades
    -- 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
    -- For now, we will ignore these and only show data for the base ability
if prop.Value then
    if prop.Value then
local icon = get_icon(prop.Type)
    local icon = Icon.getAttrIcon(prop.Type)
local alt_box = frame:expandTemplate{
 
title = "Ability_card_v2/Card/AltBox",
    local value = prop.Value
args = {
    local scale = pick_scale(prop)
key = prop.Key,
 
value = prop.Value,
    if scale and scale.Type == 'weapon_power' then
icon = icon.img,
    local hero = get_hero_data(hero_key)
icon_link = icon.link,
    value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100
icon_color = icon.color,
    end
icon_size = icon.size,
 
scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
    local alt_box = frame:expandTemplate{
scale_type = prop.Scale and prop.Scale.Type,
    title = "Ability_card_v2/Card/AltBox",
hide = prop.Value == 0 and "true" or "false",
    args = {
}
    key = prop.Key,
}
    value = value,
table.insert(alt_boxes, alt_box)
    icon = icon and icon.img,
end
    icon_link = icon and icon.link,
end
    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 pairs(upgrades) do
for k, upgrade in ipairs(upgrades) do
local description = lang.get_string(upgrade.DescKey)
local description = lang.get_string(upgrade.DescKey)


Line 424: Line 492:
end
end
-- Vary the font size based on the number of characters to prevent overflow
-- Expand once, then size from the text the reader actually sees, rather
local fontsize = '0.85rem'
-- than from a character count of the unexpanded string.
if #description > 60 and #description < 71 then
local rendered = frame:preprocess(description)
fontsize = '0.8rem'
local cqw = textfit.fit(rendered, {
elseif #description > 70 and #description < 91 then
aspect = UG_DESC_ASPECT,
fontsize = '0.75rem'
lineHeight = UG_LINE_HEIGHT,
elseif #description > 90 then
max = UG_MAX_CQW,
fontsize = '0.7rem'
min = UG_MIN_CQW,
end
})
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 439: Line 508:
index = k,
index = k,
cost = UPGRADE_COST_MAP[k],
cost = UPGRADE_COST_MAP[k],
description = frame:preprocess(description),
description = rendered,
scale_value = upgrade.Scale and commonutils.round_to_sig_fig(upgrade.Scale.Value, 3),
scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3),
scale_type =  upgrade.Scale and upgrade.Scale.Type,
scale_type =  upgrade_scale and upgrade_scale.Type,
fontsize = fontsize
-- Measured against the box's own width, so the size stays correct
-- when the card renders below its 500px max-width.
fontsize = string.format('%.2fcqw', cqw),
lineheight = UG_LINE_HEIGHT
}
}
}
}
Line 453: Line 525:


function create_description(prop, frame)
function create_description(prop, frame)
     local description = ''
     local value_lines = {}  -- normal stat changes
     local first = true
     local scale_lines = {}  -- "Increased X scaling" notes
     local seen = {} -- Set to track added combinations
     local seen = {}


     for k, v in pairs(prop) do
     for k, v in pairs(prop) do
    local value
        if k == 'DescKey' then
         if type(v) == 'table' then
            -- skip
        value = v.Value
         elseif type(v) == 'table' then
        else
            local has_value = v.Value ~= nil
        value = v
            local has_scale = v.Scale and v.Scale.Value and v.Scale.Value ~= 0
    end
 
   
            -- Value is 0 and we have a scaling change → scaling note
        local formatted_value = utils.format_value_with_prepost(k, value, frame)
            if has_value and has_scale and v.Value == 0 then
        local attr_name = lang.get_string(k..'_label')
                if not seen["scale_" .. k] then
        local key = formatted_value .. '|' .. attr_name -- Unique identifier
                    local scale_type = v.Scale.Type
                    local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2)
-- skip DescKey for the edge case where we are generating description instead of using localization files
                    table.insert(scale_lines, "Increased " .. capitalized .. " scaling")
if k ~= 'DescKey' then
                    seen["scale_" .. k] = true
        -- Skip if this combination has already been added
                end
        if not seen[key] then
            -- Only a scaling change, no value → scaling note (future bot correction)
        -- Add line break if this is not the first item
            elseif not has_value and has_scale then
            if not first then
                if not seen["scale_" .. k] then
              description = description .. '<br>'
                    local scale_type = v.Scale.Type
            end
                    local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2)
        end
                    table.insert(scale_lines, "Increased " .. capitalized .. " scaling")
                    seen["scale_" .. k] = true
        description = description .. string.format('%s %s', formatted_value, attr_name)
                end
        first = false
            -- Normal value (0 without scale, or non‑zero with/without scale)
        seen[key] = true -- Mark this combination as added
            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


     return description
     -- Combine: value lines first, then scaling lines
end
    local all_lines = {}
 
    for _, line in ipairs(value_lines) do
p.get_attr_icon = function(frame)
        table.insert(all_lines, line)
local attr_type = frame.args[1]
    end
local icon = get_icon(attr_type)
    for _, line in ipairs(scale_lines) do
 
        table.insert(all_lines, line)
return string.format('[[File:%s|%s|18px|link=%s]]', icon.img or '', icon.size or '', icon.link or '')
    end
end


function get_icon(attr_type)
    return table.concat(all_lines, '<br>')
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
end


Line 593: 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_ss = get_attr_ss(ability.Radius),
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_ss = get_attr_ss(ability.AbilityCastRange),
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_ss = get_attr_ss(ability.AbilityDuration),
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_ss = get_attr_ss(ability.AbilityCooldown),
cooldown_scale = get_attr_scale(ability.AbilityCooldown),
charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value,
cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown),
charge_cooldown_ss = get_attr_ss(ability.AbilityCooldownBetweenCharge),
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,
notes = notes,
charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge),
notes_source_page = notes_source_page,
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

Template:Ability card v2

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