<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://deadlock.wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=~2026-DynamoMoCamps780</id>
	<title>The Deadlock Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://deadlock.wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=~2026-DynamoMoCamps780"/>
	<link rel="alternate" type="text/html" href="https://deadlock.wiki/Special:Contributions/~2026-DynamoMoCamps780"/>
	<updated>2026-09-15T11:47:46Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://deadlock.wiki/Module:Abilities/card?diff=103794</id>
		<title>Module:Abilities/card</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Module:Abilities/card?diff=103794"/>
		<updated>2026-07-27T01:03:30Z</updated>

		<summary type="html">&lt;p&gt;~2026-DynamoMoCamps780: Keep zero-valued scalings from multi-scaling attributes so the placeholder badge still renders and upgrades can populate it, fixing Doorman&amp;#039;s Doorway distance scaling (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;local lang = require &amp;quot;Module:Lang&amp;quot;&lt;br /&gt;
local commonutils = require &amp;quot;Module:Utilities&amp;quot;&lt;br /&gt;
local utils = require &amp;quot;Module:Abilities/utils&amp;quot;&lt;br /&gt;
local Icon = require&amp;quot;Module:Abilities/icon&amp;quot;&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
local data = mw.loadJsonData(&amp;quot;Data:AbilityCards.json&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
function get_hero_key(hero_name)&lt;br /&gt;
	if hero_name == nil then return nil end&lt;br /&gt;
		&lt;br /&gt;
	-- if name starts with &amp;quot;hero_&amp;quot;, use it directly as the key&lt;br /&gt;
	if string.sub(hero_name, 1, 5) == &amp;quot;hero_&amp;quot; then return hero_name end&lt;br /&gt;
	&lt;br /&gt;
	local resource_lookup = mw.loadJsonData(&amp;quot;Data:ResourceLookup.json&amp;quot;)&lt;br /&gt;
	return resource_lookup[hero_name:lower()].key&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--{{#invoke:Abilities/card|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}--&lt;br /&gt;
-- Args:&lt;br /&gt;
--   HERO_NAME (required) - Name of the hero that is found in Data:HeroData.json under &amp;quot;Name&amp;quot;&lt;br /&gt;
--   ABILITY_NUM (required) - Selects ability at index 1 to 4&lt;br /&gt;
--   ADD_LINK - Add a hyperlink to the title to the associated page&lt;br /&gt;
--   NOTES - User note data to include in the footer of the ability card&lt;br /&gt;
p.get_ability_card = function(frame)&lt;br /&gt;
	local hero_name = frame.args[1]&lt;br /&gt;
	local ability_num = frame.args[2]&lt;br /&gt;
	local add_link = frame.args[3]&lt;br /&gt;
	local notes = frame.args[4]&lt;br /&gt;
	&lt;br /&gt;
	local hero_key = get_hero_key(hero_name)&lt;br /&gt;
	if (hero_key == nil) then return &#039;Hero with name&#039; .. hero_name .. &#039;not found&#039; end&lt;br /&gt;
	&lt;br /&gt;
	return build_ability_card(hero_key, ability_num, add_link, notes)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Pulls data from Data:AbilityCards.json to populate Template:Ability card v2&lt;br /&gt;
function build_ability_card(hero_key, ability_num, add_link, notes, notes_source_page)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then &lt;br /&gt;
		return &#039;Ability data not found for hero &#039; ..hero_key.. &#039; and num &#039; .. ability_num&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local ability_name_localized = lang.get_string(ability.Key)&lt;br /&gt;
	local name_link = nil&lt;br /&gt;
	if add_link == &#039;true&#039; then&lt;br /&gt;
		name_link = ability_name_localized&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if notes_source_page ~= nil and notes ~= &amp;quot;&amp;quot; then&lt;br /&gt;
		-- Notes comes from a /Notes page, and the notes are not blank&lt;br /&gt;
		-- Confirm the notes source page exists, otherwise, don&#039;t display any notes&lt;br /&gt;
		local title = mw.title.new(notes_source_page)&lt;br /&gt;
		if not (title and title.exists) then&lt;br /&gt;
			notes = &amp;quot;&amp;quot;&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
	&lt;br /&gt;
	local info1_desc = get_info_desc(hero_key, ability_num, 1)&lt;br /&gt;
	local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1)&lt;br /&gt;
&lt;br /&gt;
	local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1)&lt;br /&gt;
	&lt;br /&gt;
	if #info1_alt_boxes &amp;gt; 7 then&lt;br /&gt;
		error(#info1_alt_boxes .. &#039; alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2&#039;)	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local info2_desc = get_info_desc(hero_key, ability_num, 2)&lt;br /&gt;
	local info2_main_boxes = get_main_boxes(hero_key, ability_num, 2)&lt;br /&gt;
	local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2)&lt;br /&gt;
	&lt;br /&gt;
	if #info2_alt_boxes &amp;gt; 7 then&lt;br /&gt;
		error(#info2_alt_boxes .. &#039; alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2&#039;)	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local info3_desc = get_info_desc(hero_key, ability_num, 3)&lt;br /&gt;
	local info3_main_boxes = get_main_boxes(hero_key, ability_num, 3)&lt;br /&gt;
	local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3)&lt;br /&gt;
	&lt;br /&gt;
	if #info3_alt_boxes &amp;gt; 7 then&lt;br /&gt;
		error(#info3_alt_boxes .. &#039; alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2&#039;)	&lt;br /&gt;
	end&lt;br /&gt;
			&lt;br /&gt;
	local upgrades = get_upgrade_boxes(hero_key, ability_num)&lt;br /&gt;
	&lt;br /&gt;
	return frame:expandTemplate{&lt;br /&gt;
		title = &amp;quot;Ability_card_v2/Card&amp;quot;,&lt;br /&gt;
		args = {&lt;br /&gt;
			id = hero_key .. &#039;:&#039; .. ability.Key,&lt;br /&gt;
			hero_key = hero_key,&lt;br /&gt;
			ability_num = ability_num,&lt;br /&gt;
			&lt;br /&gt;
			-- Header info defined in various attributes	&lt;br /&gt;
			name = ability_name_localized,&lt;br /&gt;
			name_link = name_link,&lt;br /&gt;
			icon = lang.get_string(ability.Key, &#039;en&#039;) .. &#039;.png&#039;,&lt;br /&gt;
			&lt;br /&gt;
			channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value,&lt;br /&gt;
			channel_time_scale = get_attr_scale(ability.AbilityChannelTime),&lt;br /&gt;
			channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime),&lt;br /&gt;
			channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime),&lt;br /&gt;
			radius = ability.Radius and ability.Radius.Value,&lt;br /&gt;
			radius_scale = get_attr_scale(ability.Radius),&lt;br /&gt;
			radius_scale_type = get_attr_scale_type(ability.Radius),&lt;br /&gt;
			radius_scale_multiply = get_attr_scale_multiply(ability.Radius),&lt;br /&gt;
			range = ability.AbilityCastRange and ability.AbilityCastRange.Value,&lt;br /&gt;
			range_scale = get_attr_scale(ability.AbilityCastRange),&lt;br /&gt;
			range_scale_type = get_attr_scale_type(ability.AbilityCastRange),&lt;br /&gt;
			range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange),&lt;br /&gt;
			duration = ability.AbilityDuration and ability.AbilityDuration.Value,&lt;br /&gt;
			duration_scale = get_attr_scale(ability.AbilityDuration),&lt;br /&gt;
			duration_scale_type = get_attr_scale_type(ability.AbilityDuration),&lt;br /&gt;
			duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration),&lt;br /&gt;
			-- ability_width = format_value_with_prepost(width_key, ability[width_key]),&lt;br /&gt;
			cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,&lt;br /&gt;
			cooldown_scale = get_attr_scale(ability.AbilityCooldown),&lt;br /&gt;
			cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown),&lt;br /&gt;
			cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown),&lt;br /&gt;
			charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value,&lt;br /&gt;
			charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			charge_cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			charge_cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value or 0,&lt;br /&gt;
			&lt;br /&gt;
			-- Info section #1 defined in &amp;quot;Info1&amp;quot; attribute&lt;br /&gt;
			hide_info1 = infobox_is_hidden(ability.Info1) and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
			info1_desc = info1_desc,&lt;br /&gt;
			info1_mainbox1 = info1_main_boxes[1],&lt;br /&gt;
			info1_mainbox2 = info1_main_boxes[2],&lt;br /&gt;
			info1_mainbox3 = info1_main_boxes[3],&lt;br /&gt;
			info1_mainbox4 = info1_main_boxes[4],&lt;br /&gt;
			info1_mainbox5 = info1_main_boxes[5],&lt;br /&gt;
			info1_altbox1 =  info1_alt_boxes[1],&lt;br /&gt;
			info1_altbox2 =  info1_alt_boxes[2],&lt;br /&gt;
			info1_altbox3 =  info1_alt_boxes[3],&lt;br /&gt;
			info1_altbox4 =  info1_alt_boxes[4],&lt;br /&gt;
			info1_altbox5 =  info1_alt_boxes[5],&lt;br /&gt;
			info1_altbox6 =  info1_alt_boxes[6],&lt;br /&gt;
			info1_altbox7 =  info1_alt_boxes[7],&lt;br /&gt;
&lt;br /&gt;
			-- Info section #2 defined in &amp;quot;Info2&amp;quot; attribute&lt;br /&gt;
			hide_info2 = infobox_is_hidden(ability.Info2) and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
			info2_desc = info2_desc,&lt;br /&gt;
			info2_mainbox1 = info2_main_boxes[1],&lt;br /&gt;
			info2_mainbox2 = info2_main_boxes[2],&lt;br /&gt;
			info2_mainbox3 = info2_main_boxes[3],&lt;br /&gt;
			info2_mainbox4 = info2_main_boxes[4],&lt;br /&gt;
			info2_mainbox5 = info2_main_boxes[5],&lt;br /&gt;
			info2_altbox1 =  info2_alt_boxes[1],&lt;br /&gt;
			info2_altbox2 =  info2_alt_boxes[2],&lt;br /&gt;
			info2_altbox3 =  info2_alt_boxes[3],&lt;br /&gt;
			info2_altbox4 =  info2_alt_boxes[4],&lt;br /&gt;
			info2_altbox5 =  info2_alt_boxes[5],&lt;br /&gt;
			info2_altbox6 =  info2_alt_boxes[6],&lt;br /&gt;
			info2_altbox7 =  info2_alt_boxes[7],&lt;br /&gt;
&lt;br /&gt;
			-- Info section #3 defined in &amp;quot;Info3&amp;quot; attribute&lt;br /&gt;
			hide_info3 = infobox_is_hidden(ability.Info3) and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
			info3_desc = info3_desc,&lt;br /&gt;
			info3_mainbox1 = info3_main_boxes[1],&lt;br /&gt;
			info3_mainbox2 = info3_main_boxes[2],&lt;br /&gt;
			info3_mainbox3 = info3_main_boxes[3],&lt;br /&gt;
			info3_mainbox4 = info3_main_boxes[4],&lt;br /&gt;
			info3_mainbox5 = info3_main_boxes[5],&lt;br /&gt;
			info3_altbox1 =  info3_alt_boxes[1],&lt;br /&gt;
			info3_altbox2 =  info3_alt_boxes[2],&lt;br /&gt;
			info3_altbox3 =  info3_alt_boxes[3],&lt;br /&gt;
			info3_altbox4 =  info3_alt_boxes[4],&lt;br /&gt;
			info3_altbox5 =  info3_alt_boxes[5],&lt;br /&gt;
			info3_altbox6 =  info3_alt_boxes[6],&lt;br /&gt;
			info3_altbox7 =  info3_alt_boxes[7],&lt;br /&gt;
&lt;br /&gt;
			-- Ability upgrades defined in &amp;quot;Upgrades&amp;quot; attribute&lt;br /&gt;
			upgrade1 = upgrades[1],&lt;br /&gt;
			upgrade2 = upgrades[2],&lt;br /&gt;
			upgrade3 = upgrades[3],&lt;br /&gt;
			&lt;br /&gt;
			-- User-created ability notes&lt;br /&gt;
			notes = notes,&lt;br /&gt;
			notes_source_page = notes_source_page&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function infobox_is_hidden(infobox)&lt;br /&gt;
	if infobox == nil then &lt;br /&gt;
		return false&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return infobox.RequiresUpgradeIndex ~= nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- An attribute may carry either a single scaling or a list of them.&lt;br /&gt;
-- Extra entries are stat-category markers (duration, cooldown, range, radius,&lt;br /&gt;
-- healing, charges) that always hold a value of 1 and are never displayed, so&lt;br /&gt;
-- return the first entry that actually renders a badge.&lt;br /&gt;
-- A zero-valued scaling is still returned when nothing else is displayable. The&lt;br /&gt;
-- card renders it as an x0 placeholder, and the ability upgrade gadget fills&lt;br /&gt;
-- that element in when an upgrade grants real scaling (Doorman&#039;s Doorway).&lt;br /&gt;
local function pick_scale(attr)&lt;br /&gt;
	if not attr then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local scale = attr.Scale&lt;br /&gt;
	if type(scale) ~= &#039;table&#039; then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	-- A single scaling is stored directly as an object&lt;br /&gt;
	if scale[1] == nil then&lt;br /&gt;
		return scale&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local placeholder = nil&lt;br /&gt;
	for _, entry in ipairs(scale) do&lt;br /&gt;
		if type(entry) == &#039;table&#039; and Icon.isDisplayedScale(entry.Type) then&lt;br /&gt;
			if entry.Value ~= 0 then&lt;br /&gt;
				return entry&lt;br /&gt;
			end&lt;br /&gt;
			placeholder = placeholder or entry&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return placeholder&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Get scaling of attribute, return nil if none is found&lt;br /&gt;
function get_attr_scale(attr)&lt;br /&gt;
	local scale = pick_scale(attr)&lt;br /&gt;
	if not scale then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if scale.Value == 0 then&lt;br /&gt;
		return nil	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return commonutils.round_to_sig_fig(scale.Value, 3)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Get scaling type of attribute, return nil if none is found&lt;br /&gt;
function get_attr_scale_type(attr)&lt;br /&gt;
	local scale = pick_scale(attr)&lt;br /&gt;
	if not scale then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if scale.Value == 0 then&lt;br /&gt;
		return nil	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return scale.Type&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Check if scaling should be multiplied instead of added&lt;br /&gt;
function get_attr_scale_multiply(attr)&lt;br /&gt;
	local scale = pick_scale(attr)&lt;br /&gt;
	if not scale then&lt;br /&gt;
		return nil&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return scale.Multiply&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Get the description for an ability&#039;s info section&lt;br /&gt;
--{{#invoke:AbilityData|get_info_desc|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}--&lt;br /&gt;
p.get_info_desc = function(frame)&lt;br /&gt;
	local hero_key = frame.args[1]&lt;br /&gt;
	local ability_num = frame.args[2]&lt;br /&gt;
	local info_section_num = frame.args[3]&lt;br /&gt;
	&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return {} end -- return empty table, not string&lt;br /&gt;
	local info_section = ability[&#039;Info&#039;..info_section_num]&lt;br /&gt;
	&lt;br /&gt;
	-- some abilities have no info sections&lt;br /&gt;
	if info_section == nil then return &#039;&#039; end&lt;br /&gt;
	&lt;br /&gt;
	if info_section.DescKey == nil then&lt;br /&gt;
		return &#039;&#039;	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return frame:preprocess(lang.get_string(info_section.DescKey))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function get_info_desc(hero_key, ability_num, info_section_num)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return &amp;quot;Ability Not Found&amp;quot; end&lt;br /&gt;
	local info_section = ability[&#039;Info&#039;..info_section_num]&lt;br /&gt;
	&lt;br /&gt;
	-- some abilities have no info sections&lt;br /&gt;
	if info_section == nil then return &#039;&#039; end&lt;br /&gt;
	&lt;br /&gt;
	if info_section.DescKey == nil then&lt;br /&gt;
		return &#039;&#039;	&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
	return frame:preprocess(lang.get_string(info_section.DescKey))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function get_main_boxes(hero_key, ability_num, info_section_num)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return &amp;quot;Ability Not Found&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	local info_section = ability[&#039;Info&#039;..info_section_num]&lt;br /&gt;
	&lt;br /&gt;
	-- some abilities have no info sections&lt;br /&gt;
	if info_section == nil then return &#039;&#039; end&lt;br /&gt;
	&lt;br /&gt;
	local main = info_section.Main&lt;br /&gt;
	if main == nil then&lt;br /&gt;
		return {}&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
	&lt;br /&gt;
	local props = {}&lt;br /&gt;
	&lt;br /&gt;
	-- Start with the standard Props array&lt;br /&gt;
	if main.Props then&lt;br /&gt;
		for _, prop in ipairs(main.Props) do&lt;br /&gt;
			table.insert(props, prop)&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	&lt;br /&gt;
	local main_boxes = {}&lt;br /&gt;
	for k, prop in ipairs(props) do  -- use ipairs for ordered iteration&lt;br /&gt;
		local value = prop.Value&lt;br /&gt;
		local scale = pick_scale(prop)&lt;br /&gt;
		-- If it scales with melee damage, set the value using the hero&#039;s base melee to match in game visuals&lt;br /&gt;
		if scale and scale.Type == &#039;melee&#039; then&lt;br /&gt;
			local hero = get_hero_data(hero_key)&lt;br /&gt;
			value = prop.Value + hero.LightMeleeDamage * scale.Value&lt;br /&gt;
			elseif scale and scale.Type == &#039;heavy_melee&#039; then&lt;br /&gt;
			local hero = get_hero_data(hero_key)&lt;br /&gt;
			value = prop.Value + hero.HeavyMeleeDamage * scale.Value&lt;br /&gt;
			elseif scale and scale.Type == &#039;weapon_power&#039; then&lt;br /&gt;
			local hero = get_hero_data(hero_key)&lt;br /&gt;
			value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100&lt;br /&gt;
		end&lt;br /&gt;
			&lt;br /&gt;
		local icon = Icon.getAttrIcon(prop.Type)&lt;br /&gt;
		&lt;br /&gt;
		local main_box&lt;br /&gt;
		if prop.StatusEffect then&lt;br /&gt;
			main_box = frame:expandTemplate{&lt;br /&gt;
				title = &amp;quot;Ability_card_v2/Card/StatusBox&amp;quot;,&lt;br /&gt;
				args = {&lt;br /&gt;
					title = prop.Title,&lt;br /&gt;
					key = prop.Key,&lt;br /&gt;
					value = value,&lt;br /&gt;
					status_effect = prop.StatusEffect,&lt;br /&gt;
					icon = icon and icon.img,&lt;br /&gt;
					icon_link = icon and icon.link,&lt;br /&gt;
					icon_color = icon and icon.color,&lt;br /&gt;
					icon_size = icon and icon.size,&lt;br /&gt;
					scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),&lt;br /&gt;
					scale_type =  scale and scale.Type,&lt;br /&gt;
					-- allow nil value here as some status effects don&#039;t have a value but should not be hidden&lt;br /&gt;
					hide = value == 0 and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		else &lt;br /&gt;
			main_box = frame:expandTemplate{&lt;br /&gt;
				title = &amp;quot;Ability_card_v2/Card/MainBox&amp;quot;,&lt;br /&gt;
				args = {&lt;br /&gt;
					title = prop.Title,&lt;br /&gt;
					key = prop.Key,&lt;br /&gt;
					value = value,&lt;br /&gt;
					icon = icon and icon.img,&lt;br /&gt;
					icon_link = icon and icon.link,&lt;br /&gt;
					icon_color = icon and icon.color,&lt;br /&gt;
					icon_size = icon and icon.size,&lt;br /&gt;
					scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),&lt;br /&gt;
					scale_type =  scale and scale.Type,&lt;br /&gt;
					hide = (value == nil or value == 0) and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
				}&lt;br /&gt;
			}&lt;br /&gt;
		end&lt;br /&gt;
		table.insert(main_boxes, main_box)&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return main_boxes&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function get_hero_data(hero_key)&lt;br /&gt;
	local hero_data = mw.loadJsonData(&amp;quot;Data:HeroData.json&amp;quot;)&lt;br /&gt;
	local hero = hero_data[hero_key]&lt;br /&gt;
	if (hero == nil) then return &amp;quot;Hero Not Found&amp;quot; end&lt;br /&gt;
	return hero&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function get_alt_boxes(hero_key, ability_num, info_section_num)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return &amp;quot;Ability Not Found&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	local info_section = ability[&#039;Info&#039;..info_section_num]&lt;br /&gt;
	&lt;br /&gt;
	-- some abilities have no info sections&lt;br /&gt;
	if info_section == nil then return &#039;&#039; end&lt;br /&gt;
	&lt;br /&gt;
	local props = info_section.Alt&lt;br /&gt;
	if props == nil then&lt;br /&gt;
		return &#039;&#039;&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
	&lt;br /&gt;
	local alt_boxes = {}&lt;br /&gt;
	&lt;br /&gt;
    for k, prop in pairs(props) do&lt;br /&gt;
    	-- Some props don&#039;t have values, as those come from upgrades&lt;br /&gt;
    	-- For now, we will ignore these and only show data for the base ability&lt;br /&gt;
    	if prop.Value then&lt;br /&gt;
    		local icon = Icon.getAttrIcon(prop.Type)&lt;br /&gt;
&lt;br /&gt;
    		local value = prop.Value&lt;br /&gt;
    		local scale = pick_scale(prop)&lt;br /&gt;
&lt;br /&gt;
    		if scale and scale.Type == &#039;weapon_power&#039; then&lt;br /&gt;
    			local hero = get_hero_data(hero_key)&lt;br /&gt;
    			value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100&lt;br /&gt;
    		end&lt;br /&gt;
&lt;br /&gt;
    		local alt_box = frame:expandTemplate{&lt;br /&gt;
    			title = &amp;quot;Ability_card_v2/Card/AltBox&amp;quot;,&lt;br /&gt;
    			args = {&lt;br /&gt;
    				key = prop.Key,&lt;br /&gt;
    				value = value,&lt;br /&gt;
    				icon = icon and icon.img,&lt;br /&gt;
    				icon_link = icon and icon.link,&lt;br /&gt;
    				icon_color = icon and icon.color,&lt;br /&gt;
    				icon_size = icon and icon.size,&lt;br /&gt;
    				scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),&lt;br /&gt;
    				scale_type = scale and scale.Type,&lt;br /&gt;
    				hide = value == 0 and &amp;quot;true&amp;quot; or &amp;quot;false&amp;quot;,&lt;br /&gt;
    			}&lt;br /&gt;
    		}&lt;br /&gt;
    		table.insert(alt_boxes, alt_box)&lt;br /&gt;
    	end&lt;br /&gt;
    end&lt;br /&gt;
	&lt;br /&gt;
	return alt_boxes&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local UPGRADE_COST_MAP = {1, 2, 5}&lt;br /&gt;
function get_upgrade_boxes(hero_key, ability_num)&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then return &amp;quot;Ability Not Found&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	local frame = mw.getCurrentFrame()&lt;br /&gt;
&lt;br /&gt;
	local upgrades = ability.Upgrades&lt;br /&gt;
	&lt;br /&gt;
	local upgrade_boxes = {}&lt;br /&gt;
	for k, upgrade in pairs(upgrades) do&lt;br /&gt;
		local description = lang.get_string(upgrade.DescKey)&lt;br /&gt;
		local desc_length = description and #description or 0&lt;br /&gt;
&lt;br /&gt;
		-- bypass some keys if they are mistakenly left in game files&lt;br /&gt;
		local IGNORE_DESC_KEYS = {&#039;citadel_ability_hornet_snipe_t3_desc&#039;, &#039;ability_fencer_ultimate_t3_desc&#039;}&lt;br /&gt;
		&lt;br /&gt;
		-- Generate a description if there is no localized string&lt;br /&gt;
		if (description == nil or description == &#039;&#039; or commonutils.contains(IGNORE_DESC_KEYS, upgrade.DescKey)) then&lt;br /&gt;
			description = create_description(upgrade, frame)&lt;br /&gt;
		else&lt;br /&gt;
			-- Replace embedded value placeholders with just the base value&lt;br /&gt;
			-- Pattern: {&#039;Value&#039;: X, &#039;Scale&#039;: {...}} -&amp;gt; X&lt;br /&gt;
			description = description:gsub(&amp;quot;{&#039;Value&#039;:%s*([%d%.%-]+),%s*&#039;Scale&#039;:%s*{[^}]+}}&amp;quot;, function(value)&lt;br /&gt;
				return commonutils.round_to_sig_fig(tonumber(value), 3)&lt;br /&gt;
			end)&lt;br /&gt;
			-- Pattern: {&#039;Value&#039;: X} -&amp;gt; X&lt;br /&gt;
			description = description:gsub(&amp;quot;{&#039;Value&#039;:%s*([%d%.%-]+)}&amp;quot;, function(value)&lt;br /&gt;
				return commonutils.round_to_sig_fig(tonumber(value), 3)&lt;br /&gt;
			end)&lt;br /&gt;
		end&lt;br /&gt;
		&lt;br /&gt;
		-- Vary the font size based on the number of characters to prevent overflow&lt;br /&gt;
		local fontsize = &#039;1rem&#039;&lt;br /&gt;
		if desc_length &amp;gt; 30 and desc_length &amp;lt; 51 then&lt;br /&gt;
			fontsize = &#039;0.9rem&#039;&lt;br /&gt;
		elseif desc_length &amp;gt; 50 and desc_length &amp;lt; 71 then&lt;br /&gt;
			fontsize = &#039;0.8rem&#039;&lt;br /&gt;
		elseif desc_length &amp;gt; 70 then&lt;br /&gt;
			fontsize = &#039;0.7rem&#039;&lt;br /&gt;
		end&lt;br /&gt;
		&lt;br /&gt;
		local upgrade_scale = pick_scale(upgrade)&lt;br /&gt;
		local upgrade_box = frame:expandTemplate{&lt;br /&gt;
			title = &amp;quot;Ability_card_v2/Card/UpgradeBox&amp;quot;,&lt;br /&gt;
			args = {&lt;br /&gt;
				index = k,&lt;br /&gt;
				cost = UPGRADE_COST_MAP[k],	&lt;br /&gt;
				description = frame:preprocess(description),&lt;br /&gt;
				scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3),&lt;br /&gt;
				scale_type =  upgrade_scale and upgrade_scale.Type,&lt;br /&gt;
				fontsize = fontsize&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
		table.insert(upgrade_boxes, upgrade_box)&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return upgrade_boxes&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
function create_description(prop, frame)&lt;br /&gt;
    local value_lines = {}   -- normal stat changes&lt;br /&gt;
    local scale_lines = {}   -- &amp;quot;Increased X scaling&amp;quot; notes&lt;br /&gt;
    local seen = {}&lt;br /&gt;
&lt;br /&gt;
    for k, v in pairs(prop) do&lt;br /&gt;
        if k == &#039;DescKey&#039; then&lt;br /&gt;
            -- skip&lt;br /&gt;
        elseif type(v) == &#039;table&#039; then&lt;br /&gt;
            local has_value = v.Value ~= nil&lt;br /&gt;
            local has_scale = v.Scale and v.Scale.Value and v.Scale.Value ~= 0&lt;br /&gt;
&lt;br /&gt;
            -- Value is 0 and we have a scaling change → scaling note&lt;br /&gt;
            if has_value and has_scale and v.Value == 0 then&lt;br /&gt;
                if not seen[&amp;quot;scale_&amp;quot; .. k] then&lt;br /&gt;
                    local scale_type = v.Scale.Type&lt;br /&gt;
                    local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2)&lt;br /&gt;
                    table.insert(scale_lines, &amp;quot;Increased &amp;quot; .. capitalized .. &amp;quot; scaling&amp;quot;)&lt;br /&gt;
                    seen[&amp;quot;scale_&amp;quot; .. k] = true&lt;br /&gt;
                end&lt;br /&gt;
            -- Only a scaling change, no value → scaling note (future bot correction)&lt;br /&gt;
            elseif not has_value and has_scale then&lt;br /&gt;
                if not seen[&amp;quot;scale_&amp;quot; .. k] then&lt;br /&gt;
                    local scale_type = v.Scale.Type&lt;br /&gt;
                    local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2)&lt;br /&gt;
                    table.insert(scale_lines, &amp;quot;Increased &amp;quot; .. capitalized .. &amp;quot; scaling&amp;quot;)&lt;br /&gt;
                    seen[&amp;quot;scale_&amp;quot; .. k] = true&lt;br /&gt;
                end&lt;br /&gt;
            -- Normal value (0 without scale, or non‑zero with/without scale)&lt;br /&gt;
            elseif has_value then&lt;br /&gt;
                local formatted_value = utils.format_value_with_prepost(k, v.Value, frame)&lt;br /&gt;
                local attr_name = lang.get_string(k .. &#039;_label&#039;)&lt;br /&gt;
                local key = formatted_value .. &#039;|&#039; .. attr_name&lt;br /&gt;
                if not seen[key] then&lt;br /&gt;
                    table.insert(value_lines, string.format(&#039;%s %s&#039;, formatted_value, attr_name))&lt;br /&gt;
                    seen[key] = true&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        else&lt;br /&gt;
            -- Plain number/string (e.g. OutgoingDamagePenaltyPercent = -15)&lt;br /&gt;
            local formatted_value = utils.format_value_with_prepost(k, v, frame)&lt;br /&gt;
            local attr_name = lang.get_string(k .. &#039;_label&#039;)&lt;br /&gt;
            local key = formatted_value .. &#039;|&#039; .. attr_name&lt;br /&gt;
            if not seen[key] then&lt;br /&gt;
                table.insert(value_lines, string.format(&#039;%s %s&#039;, formatted_value, attr_name))&lt;br /&gt;
                seen[key] = true&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Combine: value lines first, then scaling lines&lt;br /&gt;
    local all_lines = {}&lt;br /&gt;
    for _, line in ipairs(value_lines) do&lt;br /&gt;
        table.insert(all_lines, line)&lt;br /&gt;
    end&lt;br /&gt;
    for _, line in ipairs(scale_lines) do&lt;br /&gt;
        table.insert(all_lines, line)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return table.concat(all_lines, &#039;&amp;lt;br&amp;gt;&#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function find_width_key(ability)&lt;br /&gt;
	for key, value in pairs(ability) do&lt;br /&gt;
		if type(key) == &amp;quot;string&amp;quot; and key:sub(-5) == &amp;quot;Width&amp;quot; then&lt;br /&gt;
			return key&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--for use from an ability page (ability pages are WIP), not hero pages&lt;br /&gt;
function p.write_ability_card_from_ability_key(frame)&lt;br /&gt;
	local ability_key = frame.args[1]&lt;br /&gt;
	if ability_key == nil then return &amp;quot;ability_key &#039;&amp;quot; .. ability_key &amp;quot;&#039; not provided&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	-- Determine the hero and ability number&lt;br /&gt;
	local found_hero_key&lt;br /&gt;
	local found_ability_num&lt;br /&gt;
	for hero_key, card_data in pairs(data) do&lt;br /&gt;
		if found_hero_key == nil or found_ability_num == nil then&lt;br /&gt;
			for ability_num, ability_data in pairs(card_data) do&lt;br /&gt;
				if ability_num ~= &amp;quot;Name&amp;quot; then&lt;br /&gt;
					if ability_data[&#039;Key&#039;] == ability_key then &lt;br /&gt;
						found_hero_key = hero_key&lt;br /&gt;
						found_ability_num = ability_num&lt;br /&gt;
						break &lt;br /&gt;
					end -- hero_key and ability_num found&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if found_ability_num == nil or found_hero_key == nil then return &amp;quot;ability_key &amp;quot; .. ability_key .. &amp;quot; is not used by any heroes&amp;quot; end&lt;br /&gt;
	&lt;br /&gt;
	-- Get notes for this ability&lt;br /&gt;
	local notes_source_page_name = utils.get_notes_source_page_name(ability_key)&lt;br /&gt;
	local notes_str = frame:preprocess(&amp;quot;{{&amp;quot;..notes_source_page_name..&amp;quot;}}&amp;quot;)&lt;br /&gt;
	&lt;br /&gt;
	-- Create the ability card&lt;br /&gt;
	return p.get_ability_card_from_key(found_hero_key, found_ability_num, true, notes_str, notes_source_page_name)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--from hero key*&lt;br /&gt;
--notes parameter will eventually be removed, as notes_source_page would be sufficient or could even be determined from within this function&lt;br /&gt;
function p.get_ability_card_from_key(hero_key, ability_num, add_link, notes, notes_source_page)&lt;br /&gt;
	if type(hero_key) == &#039;table&#039; and hero_key.args then&lt;br /&gt;
		local frame = hero_key&lt;br /&gt;
		hero_key = frame.args[1]&lt;br /&gt;
		ability_num = frame.args[2]&lt;br /&gt;
		add_link = frame.args[3]&lt;br /&gt;
		notes = frame.args[4]&lt;br /&gt;
		notes_source_page = frame.args[5]&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local ability = utils.get_ability_card_data(hero_key, ability_num)&lt;br /&gt;
	if(ability == nil) then &lt;br /&gt;
		return &#039;Ability data not found for hero &#039; ..hero_key.. &#039; and num &#039; .. ability_num&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	local ability_name_localized = lang.get_string(ability.Key)&lt;br /&gt;
	local name_link = nil&lt;br /&gt;
	if add_link == &#039;true&#039; then&lt;br /&gt;
		name_link = ability_name_localized&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	if notes_source_page ~= nil and notes ~= &amp;quot;&amp;quot; then&lt;br /&gt;
		--Notes comes from a /Notes page, and the notes are not blank&lt;br /&gt;
		-- Confirm the notes source page exists, otherwise, don&#039;t display any notes&lt;br /&gt;
		local title = mw.title.new(notes_source_page)&lt;br /&gt;
		if not (title and title.exists) then&lt;br /&gt;
			notes = &amp;quot;&amp;quot;&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	&lt;br /&gt;
	return mw.getCurrentFrame():expandTemplate{&lt;br /&gt;
		title = &amp;quot;Ability card v2/Card&amp;quot;,&lt;br /&gt;
		args = {&lt;br /&gt;
			hero_key = hero_key,&lt;br /&gt;
			ability_num = ability_num,&lt;br /&gt;
			name = ability_name_localized,&lt;br /&gt;
			name_link = name_link,&lt;br /&gt;
			icon = lang.get_string(ability.Key, &#039;en&#039;) .. &#039;.png&#039;,&lt;br /&gt;
			description = mw.getCurrentFrame():preprocess(lang.get_string(ability.DescKey)),&lt;br /&gt;
			channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value,&lt;br /&gt;
			channel_time_scale = get_attr_scale(ability.AbilityChannelTime),&lt;br /&gt;
			channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime),&lt;br /&gt;
			channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime),&lt;br /&gt;
			radius = ability.Radius and ability.Radius.Value,&lt;br /&gt;
			radius_scale = get_attr_scale(ability.Radius),&lt;br /&gt;
			radius_scale_type = get_attr_scale_type(ability.Radius),&lt;br /&gt;
			radius_scale_multiply = get_attr_scale_multiply(ability.Radius),&lt;br /&gt;
			range = ability.AbilityCastRange and ability.AbilityCastRange.Value,&lt;br /&gt;
			range_scale = get_attr_scale(ability.AbilityCastRange),&lt;br /&gt;
			range_scale_type = get_attr_scale_type(ability.AbilityCastRange),&lt;br /&gt;
			range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange),&lt;br /&gt;
			duration = ability.AbilityDuration and ability.AbilityDuration.Value,&lt;br /&gt;
			duration_scale = get_attr_scale(ability.AbilityDuration),&lt;br /&gt;
			duration_scale_type = get_attr_scale_type(ability.AbilityDuration),&lt;br /&gt;
			duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration),&lt;br /&gt;
			-- ability_width = format_value_with_prepost(width_key, ability[width_key]),&lt;br /&gt;
			cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value,&lt;br /&gt;
			cooldown_scale = get_attr_scale(ability.AbilityCooldown),&lt;br /&gt;
			cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown),&lt;br /&gt;
			cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown),&lt;br /&gt;
			charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value,&lt;br /&gt;
			charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			charge_cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			charge_cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldownBetweenCharge),&lt;br /&gt;
			num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value or 0,&lt;br /&gt;
		}&lt;br /&gt;
	}	&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>~2026-DynamoMoCamps780</name></author>
	</entry>
</feed>