| Latest revision |
Your text |
| Line 2: |
Line 2: |
| local commonutils = require "Module:Utilities" | | local commonutils = require "Module:Utilities" |
| local utils = require "Module:Abilities/utils" | | local utils = require "Module:Abilities/utils" |
| local Icon = require"Module:Abilities/icon"
| |
| local textfit = require "Module:TextFit"
| |
|
| |
|
| local p = {} | | local p = {} |
| local data = mw.loadJsonData("Data:AbilityCards.json") | | local data = mw.loadJsonData("Data:AbilityCards.json") |
|
| |
|
| | -- Maps attr type to an appropriate image and page link |
| | -- Optional icon size, will be defaulted on the template |
| | local ATTR_TYPE_ICON_MAP = { |
| | bullet_armor_up = {img='Bullet_Armor.png', link='Damage_Resistance'}, |
| | bullet_armor_down = {img='Bullet_armor_down.png', link='Damage_Resistance', size='23px'}, |
| | cast = {img='AttributeIconMaxChargesIncrease.png', link=''}, |
| | charges = {img='AttributeIconMaxChargesIncrease.png', link=''}, |
| | damage = {img='Damage_heart.png', link='', color = 'NoColor'}, -- 'NoColor' will apply no icon color and use the original image |
| | bullet_damage = {img='Damage.png', link='Bullet_Damage', color = 'Brown'}, |
| | fire_rate = {img='Fire Rate.png', link='Fire_Rate'}, |
| | healing = {img='Health regen.png', link='Health_Regen'}, |
| | health = {img='Extra Health.png', link='Health'}, |
| | move_speed = {img='Move speed.png', link='Move Speed'}, |
| | range = {img='CastRange.png', link='Ability_Range'}, |
| | tech_armor_up = {img='Spirit_Armor.png', link='Damage_Resistance'}, |
| | tech_damage = {img='AttributeIconTechShieldHealth.png', link='Spirit_Damage', color = 'Purple',size = '12px'}, |
| | distance = {img='AttributeIconTechRange.png', link='Ability_Range'}, |
| | duration = {img='AttributeIconTechDuration.png', link='Ability Duration'}, |
| | slow = {img='MoveSlow.png', link=''} |
| | } |
|
| |
|
| function get_hero_key(hero_name) | | function get_hero_key(hero_name) |
| if hero_name == nil then return nil end | | for i, hero in pairs(data) do |
| | | if hero["Name"] == hero_name then |
| -- if name starts with "hero_", use it directly as the key
| | return i |
| if string.sub(hero_name, 1, 5) == "hero_" then return hero_name end
| | end |
| | | end |
| local resource_lookup = mw.loadJsonData("Data:ResourceLookup.json")
| | return nil |
| return resource_lookup[hero_name:lower()].key | |
| end | | end |
|
| |
|
| --{{#invoke:Abilities/card|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}-- | | --{{#invoke:AbilityData|get_ability_card|HERO_NAME|ABILITY_NUM|ADD_LINK|NOTES}}-- |
| -- Args: | | --Simply calls get_ability_card_from_key with the same parameters, but with key |
| -- HERO_NAME (required) - Name of the hero that is found in Data:HeroData.json under "Name" | | --get_ability_card and invoke_get_ability_card_key will eventually be removed, and |
| -- ABILITY_NUM (required) - Selects ability at index 1 to 4 | | --get_ability_card_from_key will be renamed to get_ability_card |
| -- ADD_LINK - Add a hyperlink to the title to the associated page | | --both routes exist temporarily as current hero pages use get_ability_card via hero name |
| -- NOTES - User note data to include in the footer of the ability card | | --and im currently testing via hero_key on [[User:Sur/Abrams]] |
| | --on the final version with fully automated pages, hero_key will be used. |
| | --for now, hero_name can suffice |
| p.get_ability_card = function(frame) | | p.get_ability_card = function(frame) |
| local hero_name = frame.args[1] | | local hero_name = frame.args[1] |
| Line 32: |
Line 51: |
| | | |
| local hero_key = get_hero_key(hero_name) | | 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) | | return p.get_ability_card_from_key(hero_key, ability_num, add_link, notes) |
| end | | end |
|
| |
|
| -- Pulls data from Data:AbilityCards.json to populate Template:Ability card v2 | | --from hero key* |
| function build_ability_card(hero_key, ability_num, add_link, notes, notes_source_page) | | --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) | | local ability = utils.get_ability_card_data(hero_key, ability_num) |
| if(ability == nil) then | | if(ability == nil) then |
| Line 51: |
Line 79: |
| | | |
| if notes_source_page ~= nil and notes ~= "" then | | if notes_source_page ~= nil and notes ~= "" then |
| -- Notes comes from a /Notes page, and the notes are not blank | | --Notes comes from a /Notes page, and the notes are not blank |
| -- Confirm the notes source page exists, otherwise, don't display any notes | | -- Confirm the notes source page exists, otherwise, don't display any notes |
| local title = mw.title.new(notes_source_page) | | local title = mw.title.new(notes_source_page) |
| Line 59: |
Line 87: |
| end | | end |
| | | |
| local frame = mw.getCurrentFrame() | | 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)), |
| | 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 |
| | |
| | p.get_ability_card_test = 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) |
| | | |
| local info1_desc = get_info_desc(hero_key, ability_num, 1) | | return p.get_ability_card_test(hero_key, ability_num, add_link, notes) |
| local info1_main_boxes = get_main_boxes(hero_key, ability_num, 1)
| | end |
|
| |
|
| local info1_alt_boxes = get_alt_boxes(hero_key, ability_num, 1) | | -- Function for sandboxing workshopping potential update of entire ability card generation |
|
| | function get_ability_card_test(hero_key, ability_num, add_link, notes, notes_source_page) |
| if #info1_alt_boxes > 7 then | | local ability = utils.get_ability_card_data(hero_key, ability_num) |
| error(#info1_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2') | | if(ability == nil) then |
| | return 'Ability data not found for hero ' ..hero_key.. ' and num ' .. ability_num |
| end | | end |
| | | |
| local info2_desc = get_info_desc(hero_key, ability_num, 2) | | local ability_name_localized = lang.get_string(ability.Key) |
| local info2_main_boxes = get_main_boxes(hero_key, ability_num, 2) | | local name_link = nil |
| local info2_alt_boxes = get_alt_boxes(hero_key, ability_num, 2) | | if add_link == 'true' then |
|
| | name_link = ability_name_localized |
| 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 | | end |
| | | |
| local info3_desc = get_info_desc(hero_key, ability_num, 3) | | if notes_source_page ~= nil and notes ~= "" then |
| local info3_main_boxes = get_main_boxes(hero_key, ability_num, 3)
| | --Notes comes from a /Notes page, and the notes are not blank |
| local info3_alt_boxes = get_alt_boxes(hero_key, ability_num, 3)
| | -- Confirm the notes source page exists, otherwise, don't display any notes |
|
| | local title = mw.title.new(notes_source_page) |
| if #info3_alt_boxes > 7 then
| | if not (title and title.exists) then |
| error(#info3_alt_boxes .. ' alt boxes found, but only 7 are supported. Please update Module:Abilities/card and Template:Ability_card_v2') | | notes = "" |
| | end |
| end | | end |
|
| |
| local upgrades = get_upgrade_boxes(hero_key, ability_num)
| |
| | | |
| return frame:expandTemplate{ | | return mw.getCurrentFrame():expandTemplate{ |
| title = "Ability_card_v2/Card", | | title = "Ability card v2/Card", |
| args = { | | args = { |
| id = hero_key .. ':' .. ability.Key,
| |
| hero_key = hero_key, | | hero_key = hero_key, |
| ability_num = ability_num, | | ability_num = ability_num, |
|
| |
| -- Header info defined in various attributes
| |
| name = ability_name_localized, | | name = ability_name_localized, |
| name_link = name_link, | | name_link = name_link, |
| icon = lang.get_string(ability.Key, 'en') .. '.png', | | icon = lang.get_string(ability.Key, 'en') .. '.png', |
| | | description = mw.getCurrentFrame():preprocess(lang.get_string(ability.DescKey)), |
| channel_time = ability.AbilityChannelTime and ability.AbilityChannelTime.Value ~= 9999 and ability.AbilityChannelTime.Value,
| |
| channel_time_scale = get_attr_scale(ability.AbilityChannelTime),
| |
| channel_time_scale_type = get_attr_scale_type(ability.AbilityChannelTime),
| |
| channel_time_scale_multiply = get_attr_scale_multiply(ability.AbilityChannelTime),
| |
| radius = ability.Radius and ability.Radius.Value, | | radius = ability.Radius and ability.Radius.Value, |
| radius_scale = get_attr_scale(ability.Radius), | | radius_ss = get_attr_ss(ability.Radius), |
| radius_scale_type = get_attr_scale_type(ability.Radius),
| |
| radius_scale_multiply = get_attr_scale_multiply(ability.Radius),
| |
| range = ability.AbilityCastRange and ability.AbilityCastRange.Value, | | range = ability.AbilityCastRange and ability.AbilityCastRange.Value, |
| range_scale = get_attr_scale(ability.AbilityCastRange), | | range_ss = get_attr_ss(ability.AbilityCastRange), |
| range_scale_type = get_attr_scale_type(ability.AbilityCastRange),
| |
| range_scale_multiply = get_attr_scale_multiply(ability.AbilityCastRange),
| |
| duration = ability.AbilityDuration and ability.AbilityDuration.Value, | | duration = ability.AbilityDuration and ability.AbilityDuration.Value, |
| duration_scale = get_attr_scale(ability.AbilityDuration), | | duration_ss = get_attr_ss(ability.AbilityDuration), |
| duration_scale_type = get_attr_scale_type(ability.AbilityDuration),
| |
| duration_scale_multiply = get_attr_scale_multiply(ability.AbilityDuration),
| |
| -- ability_width = format_value_with_prepost(width_key, ability[width_key]), | | -- ability_width = format_value_with_prepost(width_key, ability[width_key]), |
| cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value, | | cooldown =ability.AbilityCooldown and ability.AbilityCooldown.Value, |
| cooldown_scale = get_attr_scale(ability.AbilityCooldown), | | cooldown_ss = get_attr_ss(ability.AbilityCooldown), |
| cooldown_scale_type = get_attr_scale_type(ability.AbilityCooldown),
| | charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value, |
| cooldown_scale_multiply = get_attr_scale_multiply(ability.AbilityCooldown),
| | charge_cooldown_ss = get_attr_ss(ability.AbilityCooldownBetweenCharge), |
| charge_cooldown = ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value ~= -1 and ability.AbilityCooldownBetweenCharge.Value, | | num_of_charges = ability.AbilityCharges and ability.AbilityCharges.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 = notes, |
| notes_source_page = notes_source_page | | notes_source_page = notes_source_page |
| } | | } |
| } | | } |
| end | | end |
|
| |
|
| function infobox_is_hidden(infobox)
| | -- Get spirit scaling of attribute, return nil if none is found |
| if infobox == nil then
| | function get_attr_ss(attr) |
| return false
| | if not attr then |
| end
| | return nil |
|
| |
| return infobox.RequiresUpgradeIndex ~= nil
| |
| end
| |
| | |
| -- An attribute may carry either a single scaling or a list of them. | |
| -- Extra entries are stat-category markers (duration, cooldown, range, radius,
| |
| -- healing, charges) that always hold a value of 1 and are never displayed, so
| |
| -- return the first entry that actually renders a badge.
| |
| -- A zero-valued scaling is still returned when nothing else is displayable. The
| |
| -- card renders it as an x0 placeholder, and the ability upgrade gadget fills
| |
| -- that element in when an upgrade grants real scaling (Doorman's Doorway).
| |
| local function pick_scale(attr)
| |
| if not attr then | |
| return nil | |
| end | | end |
| | | |
| local scale = attr.Scale | | local scale = attr.Scale |
| if type(scale) ~= 'table' then | | if not scale then |
| return nil | | return nil |
| end | | end |
| | | |
| -- A single scaling is stored directly as an object
| | if scale.Type ~= 'spirit' then |
| if scale[1] == nil then | | return nil |
| 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 | | end |
| | | |
| Line 243: |
Line 195: |
| return commonutils.round_to_sig_fig(scale.Value, 3) | | return commonutils.round_to_sig_fig(scale.Value, 3) |
| end | | end |
| | | |
| -- Get scaling type of attribute, return nil if none is found | | --for use from an ability page (ability pages are WIP), not hero pages |
| function get_attr_scale_type(attr) | | function p.write_ability_card_from_ability_key(frame) |
| local scale = pick_scale(attr) | | local ability_key = frame.args[1] |
| if not scale then
| | if ability_key == nil then return "ability_key '" .. ability_key "' not provided" end |
| return nil | | |
| | -- 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 | | end |
| | | |
| if scale.Value == 0 then | | if found_ability_num == nil or found_hero_key == nil then return "ability_key " .. ability_key .. " is not used by any heroes" end |
| return nil
| | |
| 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.."}}") |
| | | |
| return scale.Type | | -- 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 | | end |
|
| |
|
| -- Check if scaling should be multiplied instead of added | | -- Get all info sections for specified ability |
| function get_attr_scale_multiply(attr) | | --{{#invoke:AbilityData|get_ability_card|HERO_KEY|ABILITY_NUM}}-- |
| local scale = pick_scale(attr) | | p.get_all_info_sections = function(frame) |
| if not scale then | | local hero_key = frame.args[1] |
| return nil | | local ability_num = frame.args[2] |
| | |
| | local ability = utils.get_ability_card_data(hero_key, ability_num) |
| | if(ability == nil) then return "Ability Not Found" end |
| | |
| | local output_template = '' |
| | for info_section_num=1, 10 do |
| | local info_section = ability['Info'..info_section_num] |
| | -- some abilities have no info sections |
| | if info_section == nil then break end |
| | local info_template = frame:expandTemplate{ |
| | title = "Ability_card_v2/Card/Info section", |
| | args = { |
| | hero_key = hero_key, |
| | ability_num = ability_num, |
| | info_section_num = info_section_num, |
| | } |
| | } |
| | output_template = output_template .. info_template |
| end | | end |
| | | |
| return scale.Multiply | | return output_template |
| end | | end |
|
| |
|
| Line 276: |
Line 264: |
| | | |
| local ability = utils.get_ability_card_data(hero_key, ability_num) | | local ability = utils.get_ability_card_data(hero_key, ability_num) |
| if(ability == nil) then return {} end -- return empty table, not string | | 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 288: |
Line 276: |
| return frame:preprocess(lang.get_string(info_section.DescKey)) | | return frame:preprocess(lang.get_string(info_section.DescKey)) |
| end | | 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
| | --{{#invoke:AbilityData|get_info_main|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}-- |
| if info_section == nil then return '' end
| | p.get_info_main = function(frame) |
| | | local hero_key = frame.args[1] |
| if info_section.DescKey == nil then
| | local ability_num = frame.args[2] |
| return ''
| | local info_section_num = frame.args[3] |
| 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) | | 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 316: |
Line 293: |
| local main = info_section.Main | | local main = info_section.Main |
| if main == nil then | | if main == nil then |
| return {} | | 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 | | end |
| | | |
| | | local props = info_section.Main.Props |
| local main_boxes = {} | | -- Concatenate multiple section boxes into a single output template |
| for k, prop in ipairs(props) do -- use ipairs for ordered iteration | | local info_box_template = '' |
| local value = prop.Value
| | for k, prop in pairs(props) do |
| local scale = pick_scale(prop)
| | -- Exclude 0 values |
| -- If it scales with melee damage, set the value using the hero's base melee to match in game visuals | | if prop.Value and prop.Value ~= 0 then |
| if scale and scale.Type == 'melee' then | | local icon = get_icon(prop.Type) |
| 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)
| | section_box = frame:expandTemplate{ |
|
| |
| local main_box
| |
| if prop.StatusEffect then
| |
| main_box = frame:expandTemplate{ | |
| title = "Ability_card_v2/Card/StatusBox",
| |
| args = {
| |
| title = prop.Title,
| |
| key = prop.Key,
| |
| value = value,
| |
| status_effect = prop.StatusEffect,
| |
| icon = icon and icon.img,
| |
| icon_link = icon and icon.link,
| |
| icon_color = icon and icon.color,
| |
| icon_size = icon and icon.size,
| |
| scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3),
| |
| scale_type = scale and scale.Type,
| |
| -- allow nil value here as some status effects don't have a value but should not be hidden
| |
| hide = value == 0 and "true" or "false",
| |
| }
| |
| }
| |
| else
| |
| main_box = frame:expandTemplate{
| |
| title = "Ability_card_v2/Card/MainBox", | | title = "Ability_card_v2/Card/MainBox", |
| args = { | | args = { |
| title = prop.Title, | | title = prop.Title, |
| key = prop.Key, | | key = prop.Key, |
| value = value, | | value = prop.Value, |
| icon = icon and icon.img, | | icon = icon.img, |
| icon_link = icon and icon.link, | | icon_link = icon.link, |
| icon_color = icon and icon.color, | | icon_color = icon.color, |
| icon_size = icon and icon.size, | | icon_size = icon.size, |
| scale_value = scale and commonutils.round_to_sig_fig(scale.Value, 3), | | scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3), |
| scale_type = scale and scale.Type, | | scale_type = prop.Scale and prop.Scale.Type |
| hide = (value == nil or value == 0) and "true" or "false",
| |
| } | | } |
| } | | } |
| | info_box_template = info_box_template .. section_box .. '\n' |
| end | | end |
| table.insert(main_boxes, main_box)
| |
| end | | end |
| | | |
| return main_boxes | | return info_box_template |
| end | | end |
|
| |
|
| function get_hero_data(hero_key) | | --{{#invoke:AbilityData|get_info_alt|HERO_KEY|ABILITY_NUM|INFO_SECTION_INDEX}}-- |
| local hero_data = mw.loadJsonData("Data:HeroData.json") | | p.get_info_alt = function(frame) |
| local hero = hero_data[hero_key] | | local hero_key = frame.args[1] |
| if (hero == nil) then return "Hero Not Found" end | | local ability_num = frame.args[2] |
| return hero
| | local info_section_num = frame.args[3] |
| end
| | |
| | |
| function get_alt_boxes(hero_key, ability_num, info_section_num)
| |
| local ability = utils.get_ability_card_data(hero_key, ability_num) | | local ability = utils.get_ability_card_data(hero_key, ability_num) |
| if(ability == nil) then return "Ability Not Found" end | | if(ability == nil) then return "Ability Not Found" end |
| Line 412: |
Line 344: |
| end | | end |
| | | |
| local frame = mw.getCurrentFrame() | | -- Concatenate multiple section boxes into a single output template |
| | local info_box_template = '' |
| | 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 and prop.Value ~= 0 then |
| | local icon = get_icon(prop.Type) |
| | section_box = frame:expandTemplate{ |
| | title = "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 |
| | } |
| | } |
| | info_box_template = info_box_template .. section_box .. '\n' |
| | end |
| | end |
| | | |
| local alt_boxes = {}
| | return info_box_template |
|
| |
| 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 | | end |
|
| |
|
| local UPGRADE_COST_MAP = {1, 2, 5} | | local UPGRADE_COST_MAP = {1, 2, 5} |
| | | --{{#invoke:AbilityData|get_upgrades|HERO_KEY|ABILITY_NUM}}-- |
| -- Upgrade description sizing. These are design proportions, not copies of any
| | p.get_upgrades = function(frame) |
| -- CSS measurement: the browser supplies the box width through the container | | local hero_key = frame.args[1] |
| -- query on .ac-upgrade-descwrap, so nothing here needs updating if the card's
| | local ability_num = frame.args[2] |
| -- 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) | | 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 props = ability.Upgrades |
| | |
| local upgrades = ability.Upgrades
| |
| | | |
| local upgrade_boxes = {} | | -- Concatenate multiple section boxes into a single output template |
| for k, upgrade in ipairs(upgrades) do | | local upgrades_template = '' |
| local description = lang.get_string(upgrade.DescKey) | | for k, prop in pairs(props) do |
| | | local description = lang.get_string(prop.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 == '' or commonutils.contains(IGNORE_DESC_KEYS, upgrade.DescKey)) then | | if (description == nil or description == '') then |
| description = create_description(upgrade, frame) | | description = create_description(prop, 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 |
| | | |
| -- Expand once, then size from the text the reader actually sees, rather | | -- Vary the font size based on the number of characters to prevent overflow |
| -- than from a character count of the unexpanded string.
| | local fontsize = '1rem' |
| local rendered = frame:preprocess(description) | | if #description > 60 and #description < 71 then |
| local cqw = textfit.fit(rendered, {
| | fontsize = '0.95rem' |
| aspect = UG_DESC_ASPECT,
| | elseif #description > 70 and #description < 91 then |
| lineHeight = UG_LINE_HEIGHT, | | fontsize = '0.875rem' |
| max = UG_MAX_CQW,
| | elseif #description > 90 then |
| min = UG_MIN_CQW, | | fontsize = '0.8rem' |
| }) | | end |
| | | |
| local upgrade_scale = pick_scale(upgrade) | | box = frame:expandTemplate{ |
| local upgrade_box = frame:expandTemplate{
| |
| title = "Ability_card_v2/Card/UpgradeBox", | | title = "Ability_card_v2/Card/UpgradeBox", |
| args = { | | args = { |
| index = k,
| | cost = UPGRADE_COST_MAP[k], |
| cost = UPGRADE_COST_MAP[k],
| | description = frame:preprocess(description), |
| description = rendered,
| | scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3), |
| scale_value = upgrade_scale and commonutils.round_to_sig_fig(upgrade_scale.Value, 3),
| | scale_type = prop.Scale and prop.Scale.Type, |
| scale_type = upgrade_scale and upgrade_scale.Type,
| | fontsize = fontsize |
| -- Measured against the box's own width, so the size stays correct
| |
| -- when the card renders below its 500px max-width.
| |
| fontsize = string.format('%.2fcqw', cqw),
| |
| lineheight = UG_LINE_HEIGHT
| |
| } | | } |
| } | | } |
| table.insert(upgrade_boxes, upgrade_box) | | upgrades_template = upgrades_template .. box .. '\n' |
| end | | end |
| | | |
| return upgrade_boxes | | return upgrades_template |
| end | | end |
|
| |
|
| |
|
| function create_description(prop, frame) | | function create_description(prop, frame) |
| local value_lines = {} -- normal stat changes
| | local description = '' |
| local scale_lines = {} -- "Increased X scaling" notes
| | for k, v in pairs(prop) do |
| local seen = {}
| | if type(v) ~= 'table' then |
| | local formatted_value = utils.format_value_with_prepost(k, v, frame) |
| | local attr_name = lang.get_string(k..'_label') |
| | description = description .. string.format('%s %s', formatted_value, attr_name) |
| | end |
| | end |
| | |
| | return description |
| | end |
|
| |
|
| for k, v in pairs(prop) do
| | p.get_attr_icon = function(frame) |
| if k == 'DescKey' then
| | local attr_type = frame.args[1] |
| -- skip
| | local icon = get_icon(attr_type) |
| 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
| | return string.format('[[File:%s|%s|18px|link=%s]]', icon.img or '', icon.size or '', icon.link or '') |
| if has_value and has_scale and v.Value == 0 then
| | end |
| 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
| | function get_icon(attr_type) |
| local all_lines = {}
| | local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type] |
| for _, line in ipairs(value_lines) do
| | local img = 'GenericProperty.png' |
| table.insert(all_lines, line)
| | local link = '' |
| end
| | local size = '' |
| for _, line in ipairs(scale_lines) do
| | local color = 'Grey' |
| table.insert(all_lines, line)
| | if mappedAttr then |
| end
| | img = mappedAttr.img |
| | | link = mappedAttr.link |
| return table.concat(all_lines, '<br>')
| | size = mappedAttr.size |
| | color = mappedAttr.color or color |
| | end |
| | |
| | return {img=img, link=link, color=color, size=size} |
| end | | end |
|
| |
|
| Line 593: |
Line 460: |
| end | | end |
| return nil | | 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 | | end |
|
| |
|
| return p | | return p |