Home
Random
Log in
Settings
About the Deadlock Wiki
Search
Editing
Module
:
Abilities/card
Give feedback
Warning:
You are not logged in. Once you make an edit, a temporary account will be created for you.
Learn more
.
Log in
or
create an account
to continue receiving notifications after this account expires, and to access other features.
Anti-spam check. Do
not
fill this in!
local lang = require "Module:Lang" local commonutils = require "Module:Utilities" local utils = require "Module:Abilities/utils" local Icon = require"Module:Abilities/icon" local textfit = require "Module:TextFit" local p = {} local data = mw.loadJsonData("Data:AbilityCards.json") function get_hero_key(hero_name) if hero_name == nil then return nil end -- if name starts with "hero_", use it directly as the key if string.sub(hero_name, 1, 5) == "hero_" then return hero_name end local resource_lookup = mw.loadJsonData("Data:ResourceLookup.json") return resource_lookup[hero_name:lower()].key end --{{#invoke:Abilities/card|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}-- -- Args: -- HERO_NAME (required) - Name of the hero that is found in Data:HeroData.json under "Name" -- ABILITY_NUM (required) - Selects ability at index 1 to 4 -- ADD_LINK - Add a hyperlink to the title to the associated page -- NOTES - User note data to include in the footer of the ability card p.get_ability_card = function(frame) local hero_name = frame.args[1] local ability_num = frame.args[2] local add_link = frame.args[3] local notes = frame.args[4] local hero_key = get_hero_key(hero_name) if (hero_key == nil) then return 'Hero with name' .. hero_name .. 'not found' end return build_ability_card(hero_key, ability_num, add_link, notes) end -- Pulls data from Data:AbilityCards.json to populate Template:Ability card v2 function build_ability_card(hero_key, ability_num, add_link, notes, notes_source_page) local ability = utils.get_ability_card_data(hero_key, ability_num) if(ability == nil) then return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num end local ability_name_localized = lang.get_string(ability.Key) local name_link = nil if add_link == 'true' then name_link = ability_name_localized end if notes_source_page ~= nil and notes ~= "" then -- Notes comes from a /Notes page, and the notes are not blank -- Confirm the notes source page exists, otherwise, don't display any notes local title = mw.title.new(notes_source_page) if not (title and title.exists) then notes = "" end end local frame = mw.getCurrentFrame() local info1_desc = get_info_desc(hero_key, ability_num, 1) local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1) local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1) if #info1_alt_boxes > 7 then error(#info1_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2') end local info2_desc = get_info_desc(hero_key, ability_num, 2) local info2_main_boxes = get_main_boxes(hero_key, ability_num, 2) local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2) if #info2_alt_boxes > 7 then error(#info2_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2') end local info3_desc = get_info_desc(hero_key, ability_num, 3) local info3_main_boxes = get_main_boxes(hero_key, ability_num, 3) local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3) if #info3_alt_boxes > 7 then error(#info3_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2') end local upgrades = get_upgrade_boxes(hero_key, ability_num) return frame:expandTemplate{ title = "Ability_card_v2/Card", args = { id = hero_key .. ':' .. ability.Key, hero_key = hero_key, ability_num = ability_num, -- Header info defined in various attributes name = ability_name_localized, name_link = name_link, icon = lang.get_string(ability.Key, 'en') .. '.png', channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value, channel_time_scale = get_attr_scale(ability.AbilityChannelTime), channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime), channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime), radius = ability.Radius and ability.Radius.Value, radius_scale = get_attr_scale(ability.Radius), radius_scale_type = get_attr_scale_type(ability.Radius), radius_scale_multiply = get_attr_scale_multiply(ability.Radius), range = ability.AbilityCastRange and ability.AbilityCastRange.Value, range_scale = get_attr_scale(ability.AbilityCastRange), range_scale_type = get_attr_scale_type(ability.AbilityCastRange), range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange), duration = ability.AbilityDuration and ability.AbilityDuration.Value, duration_scale = get_attr_scale(ability.AbilityDuration), duration_scale_type = get_attr_scale_type(ability.AbilityDuration), duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration), -- ability_width = format_value_with_prepost(width_key, ability[width_key]), cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value, cooldown_scale = get_attr_scale(ability.AbilityCooldown), cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown), cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown), charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value, charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge), charge_cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldownBetweenCharge), charge_cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldownBetweenCharge), num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value or 0, -- Info section #1 defined in "Info1" attribute hide_info1 = infobox_is_hidden(ability.Info1) and "true" or "false", info1_desc = info1_desc, info1_mainbox1 = info1_main_boxes[1], info1_mainbox2 = info1_main_boxes[2], info1_mainbox3 = info1_main_boxes[3], info1_mainbox4 = info1_main_boxes[4], info1_mainbox5 = info1_main_boxes[5], info1_altbox1 = info1_alt_boxes[1], info1_altbox2 = info1_alt_boxes[2], info1_altbox3 = info1_alt_boxes[3], info1_altbox4 = info1_alt_boxes[4], info1_altbox5 = info1_alt_boxes[5], info1_altbox6 = info1_alt_boxes[6], info1_altbox7 = info1_alt_boxes[7], -- Info section #2 defined in "Info2" attribute hide_info2 = infobox_is_hidden(ability.Info2) and "true" or "false", info2_desc = info2_desc, info2_mainbox1 = info2_main_boxes[1], info2_mainbox2 = info2_main_boxes[2], info2_mainbox3 = info2_main_boxes[3], info2_mainbox4 = info2_main_boxes[4], info2_mainbox5 = info2_main_boxes[5], info2_altbox1 = info2_alt_boxes[1], info2_altbox2 = info2_alt_boxes[2], info2_altbox3 = info2_alt_boxes[3], info2_altbox4 = info2_alt_boxes[4], info2_altbox5 = info2_alt_boxes[5], info2_altbox6 = info2_alt_boxes[6], info2_altbox7 = info2_alt_boxes[7], -- Info section #3 defined in "Info3" attribute hide_info3 = infobox_is_hidden(ability.Info3) and "true" or "false", info3_desc = info3_desc, info3_mainbox1 = info3_main_boxes[1], info3_mainbox2 = info3_main_boxes[2], info3_mainbox3 = info3_main_boxes[3], info3_mainbox4 = info3_main_boxes[4], info3_mainbox5 = info3_main_boxes[5], info3_altbox1 = info3_alt_boxes[1], info3_altbox2 = info3_alt_boxes[2], info3_altbox3 = info3_alt_boxes[3], info3_altbox4 = info3_alt_boxes[4], info3_altbox5 = info3_alt_boxes[5], info3_altbox6 = info3_alt_boxes[6], info3_altbox7 = info3_alt_boxes[7], -- Ability upgrades defined in "Upgrades" attribute upgrade1 = upgrades[1], upgrade2 = upgrades[2], upgrade3 = upgrades[3], -- User-created ability notes notes = notes, notes_source_page = notes_source_page } } end function infobox_is_hidden(infobox) if infobox == nil then return false end return infobox.RequiresUpgradeIndex ~= nil end -- An attribute may carry either a single scaling or a list of them. -- Extra entries are stat-category markers (duration, cooldown, range, radius, -- healing, charges) that always hold a value of 1 and are never displayed, so -- return the first entry that actually renders a badge. -- A zero-valued scaling is still returned when nothing else is displayable. The -- card renders it as an x0 placeholder, and the ability upgrade gadget fills -- that element in when an upgrade grants real scaling (Doorman's Doorway). local function pick_scale(attr) if not attr then return nil end local scale = attr.Scale if type(scale) ~= 'table' then return nil end -- A single scaling is stored directly as an object if scale[1] == nil then return scale end local placeholder = nil for _, entry in ipairs(scale) do if type(entry) == 'table' and Icon.isDisplayedScale(entry.Type) then if entry.Value ~= 0 then return entry end placeholder = placeholder or entry end end return placeholder end -- Get scaling of attribute, return nil if none is found function get_attr_scale(attr) local scale = pick_scale(attr) if not scale then return nil end if scale.Value == 0 then return nil end return commonutils.round_to_sig_fig(scale.Value, 3) end -- Get scaling type of attribute, return nil if none is found function get_attr_scale_type(attr) local scale = pick_scale(attr) if not scale then return nil end if scale.Value == 0 then return nil end return scale.Type end -- Check if scaling should be multiplied instead of added function get_attr_scale_multiply(attr) local scale = pick_scale(attr) if not scale then return nil end return scale.Multiply end -- Get the description for an ability's info section --{{#invoke:AbilityData|get_info_desc|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}-- p.get_info_desc = function(frame) local hero_key = frame.args[1] local ability_num = frame.args[2] local info_section_num = frame.args[3] local ability = utils.get_ability_card_data(hero_key, ability_num) if(ability == nil) then return {} end -- return empty table, not string local info_section = ability['Info'..info_section_num] -- some abilities have no info sections if info_section == nil then return '' end if info_section.DescKey == nil then return '' end return frame:preprocess(lang.get_string(info_section.DescKey)) end function get_info_desc(hero_key, ability_num, info_section_num) local ability = utils.get_ability_card_data(hero_key, ability_num) if(ability == nil) then return "Ability Not Found" end local info_section = ability['Info'..info_section_num] -- some abilities have no info sections if info_section == nil then return '' end if info_section.DescKey == nil then return '' end local frame = mw.getCurrentFrame() return frame:preprocess(lang.get_string(info_section.DescKey)) end function get_main_boxes(hero_key, ability_num, info_section_num) local ability = utils.get_ability_card_data(hero_key, ability_num) if(ability == nil) then return "Ability Not Found" end local info_section = ability['Info'..info_section_num] -- some abilities have no info sections if info_section == nil then return '' end local main = info_section.Main if main == nil then return {} end local frame = mw.getCurrentFrame() local props = {} -- Start with the standard Props array if main.Props then for _, prop in ipairs(main.Props) do table.insert(props, prop) end end local main_boxes = {} for k, prop in ipairs(props) do -- use ipairs for ordered iteration local value = prop.Value local scale = pick_scale(prop) -- If it scales with melee damage, set the value using the hero's base melee to match in game visuals if scale and scale.Type == 'melee' then local hero = get_hero_data(hero_key) value = prop.Value + hero.LightMeleeDamage * scale.Value elseif scale and scale.Type == 'heavy_melee' then local hero = get_hero_data(hero_key) value = prop.Value + hero.HeavyMeleeDamage * scale.Value elseif scale and scale.Type == 'weapon_power' then local hero = get_hero_data(hero_key) value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100 end local icon = Icon.getAttrIcon(prop.Type) local main_box if prop.StatusEffect then main_box = frame:expandTemplate{ title = "Ability_card_v2/Card/StatusBox", args = { title = prop.Title, key = prop.Key, value = value, status_effect = prop.StatusEffect, icon = icon and icon.img, icon_link = icon and icon.link, icon_color = icon and icon.color, icon_size = icon and icon.size, scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3), scale_type = scale and scale.Type, -- allow nil value here as some status effects don't have a value but should not be hidden hide = value == 0 and "true" or "false", } } else main_box = frame:expandTemplate{ title = "Ability_card_v2/Card/MainBox", args = { title = prop.Title, key = prop.Key, value = value, icon = icon and icon.img, icon_link = icon and icon.link, icon_color = icon and icon.color, icon_size = icon and icon.size, scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3), scale_type = scale and scale.Type, hide = (value == nil or value == 0) and "true" or "false", } } end table.insert(main_boxes, main_box) end return main_boxes end function get_hero_data(hero_key) local hero_data = mw.loadJsonData("Data:HeroData.json") local hero = hero_data[hero_key] if (hero == nil) then return "Hero Not Found" end return hero end function get_alt_boxes(hero_key, ability_num, info_section_num) local ability = utils.get_ability_card_data(hero_key, ability_num) if(ability == nil) then return "Ability Not Found" end local info_section = ability['Info'..info_section_num] -- some abilities have no info sections if info_section == nil then return '' end local props = info_section.Alt if props == nil then return '' end local frame = mw.getCurrentFrame() local alt_boxes = {} for k, prop in ipairs(props) do -- Some props don't have values, as those come from upgrades -- For now, we will ignore these and only show data for the base ability if prop.Value then local icon = Icon.getAttrIcon(prop.Type) local value = prop.Value local scale = pick_scale(prop) if scale and scale.Type == 'weapon_power' then local hero = get_hero_data(hero_key) value = hero.Weapon.BulletDamage * hero.Weapon.BulletsPerBurst * (scale.Value + 100) / 100 end local alt_box = frame:expandTemplate{ title = "Ability_card_v2/Card/AltBox", args = { key = prop.Key, value = value, icon = icon and icon.img, icon_link = icon and icon.link, icon_color = icon and icon.color, icon_size = icon and icon.size, scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3), scale_type = scale and scale.Type, hide = value == 0 and "true" or "false", } } table.insert(alt_boxes, alt_box) end end return alt_boxes end local UPGRADE_COST_MAP = {1, 2, 5} -- Upgrade description sizing. These are design proportions, not copies of any -- CSS measurement: the browser supplies the box width through the container -- query on .ac-upgrade-descwrap, so nothing here needs updating if the card's -- widths, margins or padding change. local UG_DESC_ASPECT = 0.357 -- max height of the text block, as a multiple of its width local UG_LINE_HEIGHT = 1.4 -- emitted alongside the size so the two cannot drift local UG_MAX_CQW = 11.9 -- ceiling, as % of the text width (~16px on a full-width card) local UG_MIN_CQW = 6.7 -- floor (~9px) function get_upgrade_boxes(hero_key, ability_num) local ability = utils.get_ability_card_data(hero_key, ability_num) if(ability == nil) then return "Ability Not Found" end local frame = mw.getCurrentFrame() local upgrades = ability.Upgrades local upgrade_boxes = {} for k, upgrade in ipairs(upgrades) do local description = lang.get_string(upgrade.DescKey) -- bypass some keys if they are mistakenly left in game files local IGNORE_DESC_KEYS = {'citadel_ability_hornet_snipe_t3_desc', 'ability_fencer_ultimate_t3_desc'} -- Generate a description if there is no localized string if (description == nil or description == '' or commonutils.contains(IGNORE_DESC_KEYS, upgrade.DescKey)) then description = create_description(upgrade, frame) else -- Replace embedded value placeholders with just the base value -- Pattern: {'Value': X, 'Scale': {...}} -> X description = description:gsub("{'Value':%s*([%d%.%-]+),%s*'Scale':%s*{[^}]+}}", function(value) return commonutils.round_to_sig_fig(tonumber(value), 3) end) -- Pattern: {'Value': X} -> X description = description:gsub("{'Value':%s*([%d%.%-]+)}", function(value) return commonutils.round_to_sig_fig(tonumber(value), 3) end) end -- Expand once, then size from the text the reader actually sees, rather -- than from a character count of the unexpanded string. local rendered = frame:preprocess(description) local cqw = textfit.fit(rendered, { aspect = UG_DESC_ASPECT, lineHeight = UG_LINE_HEIGHT, max = UG_MAX_CQW, min = UG_MIN_CQW, }) local upgrade_scale = pick_scale(upgrade) local upgrade_box = frame:expandTemplate{ title = "Ability_card_v2/Card/UpgradeBox", args = { index = k, cost = UPGRADE_COST_MAP[k], description = rendered, scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3), scale_type = upgrade_scale and upgrade_scale.Type, -- Measured against the box's own width, so the size stays correct -- when the card renders below its 500px max-width. fontsize = string.format('%.2fcqw', cqw), lineheight = UG_LINE_HEIGHT } } table.insert(upgrade_boxes, upgrade_box) end return upgrade_boxes end function create_description(prop, frame) local value_lines = {} -- normal stat changes local scale_lines = {} -- "Increased X scaling" notes local seen = {} for k, v in pairs(prop) do if k == 'DescKey' then -- skip elseif type(v) == 'table' then local has_value = v.Value ~= nil local has_scale = v.Scale and v.Scale.Value and v.Scale.Value ~= 0 -- Value is 0 and we have a scaling change β scaling note if has_value and has_scale and v.Value == 0 then if not seen["scale_" .. k] then local scale_type = v.Scale.Type local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2) table.insert(scale_lines, "Increased " .. capitalized .. " scaling") seen["scale_" .. k] = true end -- Only a scaling change, no value β scaling note (future bot correction) elseif not has_value and has_scale then if not seen["scale_" .. k] then local scale_type = v.Scale.Type local capitalized = scale_type:sub(1,1):upper() .. scale_type:sub(2) table.insert(scale_lines, "Increased " .. capitalized .. " scaling") seen["scale_" .. k] = true end -- Normal value (0 without scale, or nonβzero with/without scale) elseif has_value then local formatted_value = utils.format_value_with_prepost(k, v.Value, frame) local attr_name = lang.get_string(k .. '_label') local key = formatted_value .. '|' .. attr_name if not seen[key] then table.insert(value_lines, string.format('%s %s', formatted_value, attr_name)) seen[key] = true end end else -- Plain number/string (e.g. OutgoingDamagePenaltyPercent = -15) local formatted_value = utils.format_value_with_prepost(k, v, frame) local attr_name = lang.get_string(k .. '_label') local key = formatted_value .. '|' .. attr_name if not seen[key] then table.insert(value_lines, string.format('%s %s', formatted_value, attr_name)) seen[key] = true end end end -- Combine: value lines first, then scaling lines local all_lines = {} for _, line in ipairs(value_lines) do table.insert(all_lines, line) end for _, line in ipairs(scale_lines) do table.insert(all_lines, line) end return table.concat(all_lines, '<br>') end function find_width_key(ability) for key, value in pairs(ability) do if type(key) == "string" and key:sub(-5) == "Width" then return key end end return nil end --for use from an ability page (ability pages are WIP), not hero pages function p.write_ability_card_from_ability_key(frame) local ability_key = frame.args[1] if ability_key == nil then return "ability_key '" .. ability_key "' not provided" end -- Determine the hero and ability number local found_hero_key local found_ability_num for hero_key, card_data in pairs(data) do if found_hero_key == nil or found_ability_num == nil then for ability_num, ability_data in pairs(card_data) do if ability_num ~= "Name" then if ability_data['Key'] == ability_key then found_hero_key = hero_key found_ability_num = ability_num break end -- hero_key and ability_num found end end end end if found_ability_num == nil or found_hero_key == nil then return "ability_key " .. ability_key .. " is not used by any heroes" end -- Get notes for this ability local notes_source_page_name = utils.get_notes_source_page_name(ability_key) local notes_str = frame:preprocess("{{"..notes_source_page_name.."}}") -- Create the ability card return p.get_ability_card_from_key(found_hero_key, found_ability_num, true, notes_str, notes_source_page_name) end --from hero key* --notes parameter will eventually be removed, as notes_source_page would be sufficient or could even be determined from within this function function p.get_ability_card_from_key(hero_key, ability_num, add_link, notes, notes_source_page) if type(hero_key) == 'table' and hero_key.args then local frame = hero_key hero_key = frame.args[1] ability_num = frame.args[2] add_link = frame.args[3] notes = frame.args[4] notes_source_page = frame.args[5] end local ability = utils.get_ability_card_data(hero_key, ability_num) if(ability == nil) then return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num end local ability_name_localized = lang.get_string(ability.Key) local name_link = nil if add_link == 'true' then name_link = ability_name_localized end if notes_source_page ~= nil and notes ~= "" then --Notes comes from a /Notes page, and the notes are not blank -- Confirm the notes source page exists, otherwise, don't display any notes local title = mw.title.new(notes_source_page) if not (title and title.exists) then notes = "" end end return mw.getCurrentFrame():expandTemplate{ title = "Ability card v2/Card", args = { hero_key = hero_key, ability_num = ability_num, name = ability_name_localized, name_link = name_link, icon = lang.get_string(ability.Key, 'en') .. '.png', description = mw.getCurrentFrame():preprocess(lang.get_string(ability.DescKey)), channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value, channel_time_scale = get_attr_scale(ability.AbilityChannelTime), channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime), channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime), radius = ability.Radius and ability.Radius.Value, radius_scale = get_attr_scale(ability.Radius), radius_scale_type = get_attr_scale_type(ability.Radius), radius_scale_multiply = get_attr_scale_multiply(ability.Radius), range = ability.AbilityCastRange and ability.AbilityCastRange.Value, range_scale = get_attr_scale(ability.AbilityCastRange), range_scale_type = get_attr_scale_type(ability.AbilityCastRange), range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange), duration = ability.AbilityDuration and ability.AbilityDuration.Value, duration_scale = get_attr_scale(ability.AbilityDuration), duration_scale_type = get_attr_scale_type(ability.AbilityDuration), duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration), -- ability_width = format_value_with_prepost(width_key, ability[width_key]), cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value, cooldown_scale = get_attr_scale(ability.AbilityCooldown), cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown), cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown), charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value, charge_cooldown_scale = get_attr_scale(ability.AbilityCooldownBetweenCharge), charge_cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldownBetweenCharge), charge_cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldownBetweenCharge), num_of_charges = ability.AbilityCharges and ability.AbilityCharges.Value or 0, } } end return p
Summary:
Please note that all contributions to The Deadlock Wiki are considered to be released under the Creative Commons Attribution-NonCommercial-ShareAlike (see
Deadlock:Copyrights
for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)
Preview page with this template
Pages included on this page:
Module:Abilities/card/doc
(
edit
)
Module:Abilities/doc/submodules
(
edit
)