Module:Abilities-test/card: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
Make default icon no icon
mNo edit summary
 
(8 intermediate revisions by the same user not shown)
Line 4: Line 4:


local p = {}
local p = {}
local data = mw.loadJsonData("User:Saag/AbilityCards.json")
local data = mw.loadJsonData("Data:AbilityCards.json")


-- Maps attr type to an appropriate image and page link
-- Maps attr type to an appropriate image and page link
Line 12: Line 12:
bullet_armor_down = {img='Bullet_Armor.png', link='Damage_Resistance', color = 'NoColor'},
bullet_armor_down = {img='Bullet_Armor.png', link='Damage_Resistance', color = 'NoColor'},
cast = {img='AttributeIconMaxChargesIncrease.png', link=''},
cast = {img='AttributeIconMaxChargesIncrease.png', link=''},
charges = {img='AttributeIconMaxChargesIncrease.png', link=''},
charges = {img='AttributeIconMaxChargesIncrease.png', link='', color = 'Purple'},
damage = {img='Damage_heart.png', link='', color = 'NoColor'}, -- 'NoColor' will apply no icon color and use the original image
damage = {img='Damage_heart.png', link='', color = 'NoColor'}, -- 'NoColor' will apply no icon color and use the original image
bullet_damage = {img='Bullet_damage.png', link='Bullet_Damage', color = 'NoColor'},
bullet_damage = {img='Bullet_damage.png', link='Bullet_Damage', color = 'NoColor'},
Line 19: Line 19:
health = {img='Health.png', link='Health', color = 'Green'},
health = {img='Health.png', link='Health', color = 'Green'},
move_speed = {img='Move speed.png', link='Move Speed', color = 'Green'},
move_speed = {img='Move speed.png', link='Move Speed', color = 'Green'},
range = {img='CastRange.png', link='Ability_Range'},
range = {img='CastRange.png', link='Ability_Range', color = 'Purple'},
radius = {img='CastRange.png', link='Ability_Range', color = 'Purple'},
tech_armor_up = {img='Spirit_Armor.png', link='Damage_Resistance', color = 'NoColor'},
tech_armor_up = {img='Spirit_Armor.png', link='Damage_Resistance', color = 'NoColor'},
tech_damage = {img='AttributeIconTechShieldHealth.png', link='Spirit_Damage', color = 'NoColor',size = '12px'},
tech_damage = {img='AttributeIconTechShieldHealth.png', link='Spirit_Damage', color = 'NoColor',size = '12px'},
distance = {img='AttributeIconTechRange.png', link='Ability_Range'},
distance = {img='AttributeIconTechRange.png', link='Ability_Range'},
duration = {img='AttributeIconTechDuration.png', link='Ability Duration'},
duration = {img='AttributeIconTechDuration.png', link='Ability Duration', color = 'Purple'},
slow = {img='MoveSlow.png', link=''},
slow = {img='MoveSlow.png', link='', color = 'Purple'},
melee_damage = {img='Melee damage.png', link='Melee Damage'},
melee_damage = {img='Melee damage.png', link='Melee Damage'},
cooldown = {img='AttributeIconTechDuration.png', link='Ability Cooldown'},
cooldown = {img='AttributeIconTechDuration.png', link='Ability Cooldown'},
combat_barrier = {img='Barrier.png', link='Barrier', color = 'Green'},
combat_barrier = {img='Barrier.png', link='Barrier', color = 'Green'},
time = {img='AttributeIconTechDuration.png', link='Ability Duration', color = 'Purple'},
}
}


-- TODO - replace this with utils file
-- TODO - replace this with utils file
function get_ability_card_data(hero_key, ability_num)
function utils.get_ability_card_data(hero_key, ability_num)
if(hero_key == nil) then return nil end
if(hero_key == nil) then return nil end
return data[hero_key][tonumber(ability_num)]
return data[hero_key][tonumber(ability_num)]
Line 38: Line 40:


function get_hero_key(hero_name)
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
for i, hero in pairs(data) do
for i, hero in pairs(data) do
if hero["Name"] == hero_name then
if hero["Name"] == hero_name then
Line 66: Line 73:
-- Pulls data from Data:AbilityCards.json to populate Template:Ability card v2
-- 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)
function build_ability_card(hero_key, ability_num, add_link, notes, notes_source_page)
local ability = get_ability_card_data(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then  
if(ability == nil) then  
return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num
return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num
Line 146: Line 153:
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.Cast and ability.Cast.AbilityChannelTime and ability.Cast.AbilityChannelTime.Value ~= 9999 and ability.Cast.AbilityChannelTime.Value,
channel_time_ss = ability.Cast and get_attr_ss(ability.Cast.AbilityChannelTime),
channel_time_ss = ability.Cast and get_attr_ss(ability.Cast.AbilityChannelTime),
radius = ability.Radius and ability.Radius.Value,
radius = ability.Radius and ability.Radius.Value,
Line 157: Line 164:
cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,
cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,
cooldown_ss = get_attr_ss(ability.AbilityCooldown),
cooldown_ss = get_attr_ss(ability.AbilityCooldown),
charge_cooldown = ability.AbilityCooldownBetweenCharge 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_ss = get_attr_ss(ability.AbilityCooldownBetweenCharge),
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value,
num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value,
Line 255: Line 262:
local info_section_num = frame.args[3]
local info_section_num = frame.args[3]
local ability = 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 270: Line 277:


function get_info_desc(hero_key, ability_num, info_section_num)
function get_info_desc(hero_key, ability_num, info_section_num)
local ability = 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 "Ability Not Found" end
local info_section = ability['Info'..info_section_num]
local info_section = ability['Info'..info_section_num]
Line 286: Line 293:


function get_main_boxes(hero_key, ability_num, info_section_num)
function get_main_boxes(hero_key, ability_num, info_section_num)
local ability = 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 "Ability Not Found" end
Line 296: Line 303:
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 = {}
if props == nil then
return ''
-- Start with the standard Props array
if main.Props then
for _, prop in ipairs(main.Props) do
table.insert(props, prop)
end
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
local value = prop.Value
local value = prop.Value
-- If it scales with melee damage, set the value using the hero's base melee to match in game visuals
-- If it scales with melee damage, set the value using the hero's base melee to match in game visuals
Line 332: Line 344:
scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
scale_type =  prop.Scale and prop.Scale.Type,
scale_type =  prop.Scale and prop.Scale.Type,
hide = (value == nil or value == 0) and "true" or "false",
-- 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",
}
}
}
}
Line 366: Line 379:


function get_alt_boxes(hero_key, ability_num, info_section_num)
function get_alt_boxes(hero_key, ability_num, info_section_num)
local ability = 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 "Ability Not Found" end
Line 411: Line 424:
local UPGRADE_COST_MAP = {1, 2, 5}
local UPGRADE_COST_MAP = {1, 2, 5}
function get_upgrade_boxes(hero_key, ability_num)
function get_upgrade_boxes(hero_key, ability_num)
local ability = 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 "Ability Not Found" end
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 pairs(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
Line 440: Line 466:
title = "User:Saag/Sandbox/Ability_card_v2/Card/UpgradeBox",
title = "User:Saag/Sandbox/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 = frame:preprocess(description),
  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,
fontsize = fontsize
}
}
}
}
Line 452: Line 479:
return upgrade_boxes
return upgrade_boxes
end
end


function create_description(prop, frame)
function create_description(prop, frame)
Line 459: Line 487:


     for k, v in pairs(prop) do
     for k, v in pairs(prop) do
    
     local value
         if type(v) ~= 'table' then
         if type(v) == 'table' then
            local formatted_value = utils.format_value_with_prepost(k, v, frame)
        value = v.Value
            local attr_name = lang.get_string(k..'_label')
        else
            local key = formatted_value .. '|' .. attr_name  -- Unique identifier
        value = v
 
    end
            -- Skip if this combination has already been added
   
            if not seen[key] then
        local formatted_value = utils.format_value_with_prepost(k, value, frame)
            -- Add line break if this is not the first item
        local attr_name = lang.get_string(k..'_label')
                if not first then
        local key = formatted_value .. '|' .. attr_name  -- Unique identifier
                  description = description .. '<br>'
                end
-- skip DescKey for the edge case where we are generating description instead of using localization files
 
if k ~= 'DescKey' then
            description = description .. string.format('%s %s', formatted_value, attr_name)
        -- Skip if this combination has already been added
            first = false
        if not seen[key] then
            seen[key] = true  -- Mark this combination as added
        -- Add line break if this is not the first item
           
            if not first then
            end
              description = description .. '<br>'
            end
        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
Line 492: Line 526:
function get_icon(attr_type)
function get_icon(attr_type)
local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type]
local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type]
local img = ''
local img = 'GenericProperty.png'
local link = ''
local link = ''
local size = ''
local size = ''
Line 559: Line 593:
end
end
local ability = get_ability_card_data(hero_key, ability_num)
local ability = utils.get_ability_card_data(hero_key, ability_num)
if(ability == nil) then  
if(ability == nil) then  
return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num
return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num

Latest revision as of 20:34, 26 February 2026

Documentation for this module may be created at Module:Abilities-test/card/doc

local lang = require "Module:Lang"
local commonutils = require "Module:Utilities"
local utils = require "Module:Abilities/utils"

local p = {}
local data = mw.loadJsonData("Data:AbilityCards.json")

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

-- TODO - replace this with utils file
function utils.get_ability_card_data(hero_key, ability_num)
	if(hero_key == nil) then return nil end
	return data[hero_key][tonumber(ability_num)]
end


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
	
	for i, hero in pairs(data) do
		if hero["Name"] == hero_name then
			return i
		end
	end
	return nil
end

--{{#invoke:Abilities/card|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}--
-- Args:
--   HERO_NAME (required) - Name of the hero that is found in Data:HeroData.json under "Name"
--   ABILITY_NUM (required) - Selects ability at index 1 to 4
--   ADD_LINK - Add a hyperlink to the title to the associated page
--   NOTES - User note data to include in the footer of the ability card
p.get_ability_card = function(frame)
	local hero_name = frame.args[1]
	local ability_num = frame.args[2]
	local add_link = frame.args[3]
	local notes = frame.args[4]
	
	local hero_key = get_hero_key(hero_name)
	if (hero_key == nil) then return 'Hero with name' .. hero_name .. 'not found' end
	
	return build_ability_card(hero_key, ability_num, add_link, notes)
end

-- Pulls data from Data:AbilityCards.json to populate Template:Ability card v2
function build_ability_card(hero_key, ability_num, add_link, notes, notes_source_page)
	local ability = utils.get_ability_card_data(hero_key, ability_num)
	if(ability == nil) then 
		return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num
	end
	
	local ability_name_localized = lang.get_string(ability.Key)
	local name_link = nil
	if add_link == 'true' then
		name_link = ability_name_localized
	end
	
	if notes_source_page ~= nil and notes ~= "" then
		-- Notes comes from a /Notes page, and the notes are not blank
		-- Confirm the notes source page exists, otherwise, don't display any notes
		local title = mw.title.new(notes_source_page)
		if not (title and title.exists) then
			notes = ""
		end
	end
	
	local frame = mw.getCurrentFrame()
	
	local info1_desc = get_info_desc(hero_key, ability_num, 1)
	local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1)
	
	-- Inject Hex Duration only for Rabbit Hex
	if ability.Key == "ability_magician_animalhexarea"
	   and ability.Other
	   and ability.Other.HexDuration
	   and ability.Other.HexDuration.Value then
	    local icon = get_icon("duration")
	    table.insert(info1_main_boxes, 1,
	        frame:expandTemplate{
	            title = "User:Saag/Sandbox/Ability_card_v2/Card/MainBox",
	            args = {
	                key = "HexDuration",
	                value = ability.Other.HexDuration.Value,
	                icon = icon.img,
	                icon_link = icon.link,
	                icon_color = icon.color,
	            }
	        })
	end

	local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1)
	
	if #info1_alt_boxes > 6 then
		error(#info1_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')	
	end
	
	local info2_desc = get_info_desc(hero_key, ability_num, 2)
	local info2_main_boxes = get_main_boxes(hero_key, ability_num, 2)
	local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2)
	
	if #info2_alt_boxes > 6 then
		error(#info2_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')	
	end
	
	local info3_desc = get_info_desc(hero_key, ability_num, 3)
	local info3_main_boxes = get_main_boxes(hero_key, ability_num, 3)
	local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3)
	
	if #info3_alt_boxes > 6 then
		error(#info3_alt_boxes .. ' alt boxes found, but only 6 are supported. Please update Module:Abilities/card and Template:Ability_card_v2')	
	end
			
	local upgrades = get_upgrade_boxes(hero_key, ability_num)
	
	return frame:expandTemplate{
		title = "User:Saag/Sandbox/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.Cast and ability.Cast.AbilityChannelTime and ability.Cast.AbilityChannelTime.Value ~= 9999 and ability.Cast.AbilityChannelTime.Value,
			channel_time_ss = ability.Cast and get_attr_ss(ability.Cast.AbilityChannelTime),
			radius = ability.Radius and ability.Radius.Value,
			radius_ss = get_attr_ss(ability.Radius),
			range = ability.AbilityCastRange and ability.AbilityCastRange.Value,
			range_ss = get_attr_ss(ability.AbilityCastRange),
			duration = ability.AbilityDuration and ability.AbilityDuration.Value,
			duration_ss = get_attr_ss(ability.AbilityDuration),
			-- ability_width = format_value_with_prepost(width_key, ability[width_key]),
			cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,
			cooldown_ss = get_attr_ss(ability.AbilityCooldown),
			charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value,
			charge_cooldown_ss = get_attr_ss(ability.AbilityCooldownBetweenCharge),
			num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value,
			
			-- Info section #1 defined in "Info1" attribute
			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],

			-- 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],
			
			-- 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],
			
			-- 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

-- Get spirit scaling of attribute, return nil if none is found
function get_attr_ss(attr)
	if not attr then 
		return nil 
	end
	
	local scale = attr.Scale
	if not scale then
		return nil
	end
	
	if scale.Type ~= 'spirit' then
		return nil	
	end
	
	if scale.Value == 0 then
		return nil	
	end
	
	return commonutils.round_to_sig_fig(scale.Value, 3)
end

-- Get the description for an ability's info section
--{{#invoke:AbilityData|get_info_desc|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}--
p.get_info_desc = function(frame)
	local hero_key = frame.args[1]
	local ability_num = frame.args[2]
	local info_section_num = frame.args[3]
	
	local ability = utils.get_ability_card_data(hero_key, ability_num)
	if(ability == nil) then return {} 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
		-- If it scales with melee damage, set the value using the hero's base melee to match in game visuals
		if prop.Scale and prop.Scale.Type == 'melee' then
			local hero = get_hero_data(hero_key)
			value = prop.Value + hero.LightMeleeDamage * prop.Scale.Value
		end
			
		local icon = get_icon(prop.Type)
		
		local main_box
		if prop.StatusEffect then
			main_box = frame:expandTemplate{
				title = "User:Saag/Sandbox/Ability_card_v2/Card/StatusBox",
				args = {
					title = prop.Title,
					key = prop.Key,
					value = value,
					status_effect = prop.StatusEffect,
					icon = icon.img,
					icon_link = icon.link,
					icon_color = icon.color,
					icon_size = icon.size,
					scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
					scale_type =  prop.Scale and prop.Scale.Type,
					-- 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 = "User:Saag/Sandbox/Ability_card_v2/Card/MainBox",
				args = {
					title = prop.Title,
					key = prop.Key,
					value = value,
					icon = icon.img,
					icon_link = icon.link,
					icon_color = icon.color,
					icon_size = icon.size,
					scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
					scale_type =  prop.Scale and prop.Scale.Type,
					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 pairs(props) do
		-- Some props don't have values, as those come from upgrades
		-- For now, we will ignore these and only show data for the base ability
		if prop.Value then
			local icon = get_icon(prop.Type)
			local alt_box = frame:expandTemplate{
				title = "User:Saag/Sandbox/Ability_card_v2/Card/AltBox",
				args = {
					key = prop.Key,
					value = prop.Value,
					icon = icon.img,
					icon_link = icon.link,
					icon_color = icon.color,
					icon_size = icon.size,
					scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3),
					scale_type =  prop.Scale and prop.Scale.Type,
					hide = prop.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}
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 pairs(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
		
		-- Vary the font size based on the number of characters to prevent overflow
		local fontsize = '1rem'
		if #description > 60 and #description < 71 then
			fontsize = '0.95rem'
		elseif #description > 70 and #description < 91 then
			fontsize = '0.875rem'
		elseif #description > 90 then
			fontsize = '0.8rem'
		end
		
		local upgrade_box = frame:expandTemplate{
			title = "User:Saag/Sandbox/Ability_card_v2/Card/UpgradeBox",
			args = {
				index = k,
				cost = UPGRADE_COST_MAP[k],	
				description = frame:preprocess(description),
				scale_value = upgrade.Scale and commonutils.round_to_sig_fig(upgrade.Scale.Value, 3),
				scale_type =  upgrade.Scale and upgrade.Scale.Type,
				fontsize = fontsize
			}
		}
		table.insert(upgrade_boxes, upgrade_box)
	end
	
	return upgrade_boxes
end


function create_description(prop, frame)
    local description = ''
    local first = true
    local seen = {}  -- Set to track added combinations

    for k, v in pairs(prop) do
    	local value
        if type(v) == 'table' then
        	value = v.Value
        else 
        	value = v
	    end
    
        local formatted_value = utils.format_value_with_prepost(k, value, frame)
        local attr_name = lang.get_string(k..'_label')
        local key = formatted_value .. '|' .. attr_name  -- Unique identifier
		
		-- skip DescKey for the edge case where we are generating description instead of using localization files
		if k ~= 'DescKey' then
	        -- Skip if this combination has already been added
	        if not seen[key] then
	        -- Add line break if this is not the first item
	            if not first then
	               description = description .. '<br>'
	            end
	        end
		
	        description = description .. string.format('%s %s', formatted_value, attr_name)
	        first = false
	        seen[key] = true  -- Mark this combination as added
        end
    end

    return description
end

p.get_attr_icon = function(frame)
	local attr_type = frame.args[1]
	local icon = get_icon(attr_type)

	return string.format('[[File:%s|%s|18px|link=%s]]', icon.img or '', icon.size or '', icon.link or '')
end

function get_icon(attr_type)
	local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type]
	local img = 'GenericProperty.png'
	local link = ''
	local size = ''
	local color = 'Grey'
	if mappedAttr then
		img = mappedAttr.img
		link = mappedAttr.link
		size = mappedAttr.size
		color = mappedAttr.color or color
	end
	
	return {img=img, link=link, color=color, size=size}
end

function find_width_key(ability)
	for key, value in pairs(ability) do
		if type(key) == "string" and key:sub(-5) == "Width" then
			return key
		end
	end
	return nil
end

--for use from an ability page (ability pages are WIP), not hero pages
function p.write_ability_card_from_ability_key(frame)
	local ability_key = frame.args[1]
	if ability_key == nil then return "ability_key '" .. ability_key "' not provided" end
	
	-- Determine the hero and ability number
	local found_hero_key
	local found_ability_num
	for hero_key, card_data in pairs(data) do
		if found_hero_key == nil or found_ability_num == nil then
			for ability_num, ability_data in pairs(card_data) do
				if ability_num ~= "Name" then
					if ability_data['Key'] == ability_key then 
						found_hero_key = hero_key
						found_ability_num = ability_num
						break 
					end -- hero_key and ability_num found
				end
			end
		end
	end
	
	if found_ability_num == nil or found_hero_key == nil then return "ability_key " .. ability_key .. " is not used by any heroes" end
	
	-- Get notes for this ability
	local notes_source_page_name = utils.get_notes_source_page_name(ability_key)
	local notes_str = frame:preprocess("{{"..notes_source_page_name.."}}")
	
	-- Create the ability card
	return p.get_ability_card_from_key(found_hero_key, found_ability_num, true, notes_str, notes_source_page_name)
end

--from hero key*
--notes parameter will eventually be removed, as notes_source_page would be sufficient or could even be determined from within this function
function p.get_ability_card_from_key(hero_key, ability_num, add_link, notes, notes_source_page)
	if type(hero_key) == 'table' and hero_key.args then
		local frame = hero_key
		hero_key = frame.args[1]
		ability_num = frame.args[2]
		add_link = frame.args[3]
		notes = frame.args[4]
		notes_source_page = frame.args[5]
	end
	
	local ability = utils.get_ability_card_data(hero_key, ability_num)
	if(ability == nil) then 
		return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num
	end
	
	local ability_name_localized = lang.get_string(ability.Key)
	local name_link = nil
	if add_link == 'true' then
		name_link = ability_name_localized
	end
	
	if notes_source_page ~= nil and notes ~= "" then
		--Notes comes from a /Notes page, and the notes are not blank
		-- Confirm the notes source page exists, otherwise, don't display any notes
		local title = mw.title.new(notes_source_page)
		if not (title and title.exists) then
			notes = ""
		end
	end
	
	return mw.getCurrentFrame():expandTemplate{
		title = "User:Saag/Sandbox/Ability card v2/Card",
		args = {
			hero_key = hero_key,
			ability_num = ability_num,
			name = ability_name_localized,
			name_link = name_link,
			icon = lang.get_string(ability.Key, 'en') .. '.png',
			description = mw.getCurrentFrame():preprocess(lang.get_string(ability.DescKey)),
			radius = ability.Radius and ability.Radius.Value,
			radius_ss = get_attr_ss(ability.Radius),
			range = ability.AbilityCastRange and ability.AbilityCastRange.Value,
			range_ss = get_attr_ss(ability.AbilityCastRange),
			duration = ability.AbilityDuration and ability.AbilityDuration.Value,
			duration_ss = get_attr_ss(ability.AbilityDuration),
			-- ability_width = format_value_with_prepost(width_key, ability[width_key]),
			cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,
			cooldown_ss = get_attr_ss(ability.AbilityCooldown),
			charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value,
			charge_cooldown_ss = get_attr_ss(ability.AbilityCooldownBetweenCharge),
			num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value,
			notes = notes,
			notes_source_page = notes_source_page,
		}
	}	
end

return p