Module:Abilities/card: Difference between revisions

No edit summary
Vergir (talk | contribs)
m undo the undo, it works now
Tag: Undo
 
(66 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")


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


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


Line 80: 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 > 6 then
if #info1_alt_boxes > 7 then
error(#info1_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
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 90: 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 > 6 then
if #info2_alt_boxes > 7 then
error(#info2_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
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 98: 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 > 6 then
if #info3_alt_boxes > 7 then
error(#info3_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')
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 107: 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 115: Line 100:
icon = lang.get_string(ability.Key, 'en') .. '.png',
icon = lang.get_string(ability.Key, 'en') .. '.png',
channel_time = ability.Cast and ability.Cast.AbilityChannelTime and ability.Cast.AbilityChannelTime.Value,
channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value,
channel_time_ss = ability.Cast and get_attr_ss(ability.Cast.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),
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,
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 141: 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 153: 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 165: 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 178: Line 187:
end
end


-- Get spirit scaling of attribute, return nil if none is found
function infobox_is_hidden(infobox)
function get_attr_ss(attr)
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.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 197: 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 208: 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 "Ability Not Found" end
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 248: Line 316:
local main = info_section.Main
local main = info_section.Main
if main == nil then
if main == nil then
return ''
return {}
end
end
local frame = mw.getCurrentFrame()
local frame = mw.getCurrentFrame()
local props = info_section.Main.Props
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 = {}
local main_boxes = {}
for k, prop in pairs(props) do
for k, prop in ipairs(props) do -- use ipairs for ordered iteration
-- Exclude 0 values
local value = prop.Value
if prop.Value and prop.Value ~= 0 then
local scale = pick_scale(prop)
local icon = get_icon(prop.Type)
-- If it scales with melee damage, set the value using the hero's base melee to match in game visuals
local main_box = frame:expandTemplate{
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",
title = "Ability_card_v2/Card/MainBox",
args = {
args = {
title = prop.Title,
title = prop.Title,
key = prop.Key,
key = prop.Key,
value = prop.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",
}
}
}
}
table.insert(main_boxes, main_box)
end
end
table.insert(main_boxes, main_box)
end
end
return main_boxes
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
end


Line 299: 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 and prop.Value ~= 0 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",
}
    args = {
}
    key = prop.Key,
table.insert(alt_boxes, alt_box)
    value = value,
end
    icon = icon and icon.img,
end
    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 325: 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 331: Line 468:
local frame = mw.getCurrentFrame()
local frame = mw.getCurrentFrame()


local props = ability.Upgrades
local upgrades = ability.Upgrades
local upgrade_boxes = {}
local upgrade_boxes = {}
for k, prop in pairs(props) do
for k, upgrade in ipairs(upgrades) do
local description = lang.get_string(prop.DescKey)
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(prop, frame)
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
-- 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 = '1rem'
-- than from a character count of the unexpanded string.
if #description > 60 and #description < 71 then
local rendered = frame:preprocess(description)
fontsize = '0.95rem'
local cqw = textfit.fit(rendered, {
elseif #description > 70 and #description < 91 then
aspect = UG_DESC_ASPECT,
fontsize = '0.875rem'
lineHeight = UG_LINE_HEIGHT,
elseif #description > 90 then
max = UG_MAX_CQW,
fontsize = '0.8rem'
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",
args = {
args = {
  cost = UPGRADE_COST_MAP[k],
index = k,
  description = frame:preprocess(description),
cost = UPGRADE_COST_MAP[k],
  scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
description = rendered,
  scale_type =  prop.Scale and prop.Scale.Type,
scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3),
  fontsize = fontsize
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 367: Line 522:
return upgrade_boxes
return upgrade_boxes
end
end


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
   
        if k == 'DescKey' then
         if type(v) ~= 'table' 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 -- Unique identifier
             local key = formatted_value .. '|' .. attr_name
 
            -- Skip if this combination has already been added
             if not seen[key] then
             if not seen[key] then
            -- Add line break if this is not the first item
                 table.insert(value_lines, string.format('%s %s', formatted_value, attr_name))
                 if not first then
                seen[key] = true
                  description = description .. '<br>'
                end
 
            description = description .. string.format('%s %s', formatted_value, attr_name)
            first = false
            seen[key] = true -- Mark this combination as added
           
             end
             end
         end
         end
     end
     end


     return description
     -- 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


p.get_attr_icon = function(frame)
    return table.concat(all_lines, '<br>')
local attr_type = frame.args[1]
local icon = get_icon(attr_type)
 
return string.format('[[File:%s|%s|18px|link=%s]]', icon.img or '', icon.size or '', icon.link or '')
end
 
function get_icon(attr_type)
local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type]
local img = 'GenericProperty.png'
local link = ''
local size = ''
local color = 'Grey'
if mappedAttr then
img = mappedAttr.img
link = mappedAttr.link
size = mappedAttr.size
color = mappedAttr.color or color
end
return {img=img, link=link, color=color, size=size}
end
end


Line 495: Line 660:
return mw.getCurrentFrame():expandTemplate{
return mw.getCurrentFrame():expandTemplate{
title = "Template:Ability card v2/Card",
title = "Ability card v2/Card",
args = {
args = {
hero_key = hero_key,
hero_key = hero_key,
Line 503: 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,
}
}
}
}