Module:Damage: Difference between revisions

no edit summary
No edit summary
No edit summary
 
(119 intermediate revisions by 3 users not shown)
Line 1: Line 1:
-- pystart
-- pystart
require('Module:CommonFunctions');
require('Module:CommonFunctions')
local i18n = require('Module:I18n')
local getArgs = require('Module:Arguments').getArgs
local getArgs = require('Module:Arguments').getArgs
local inspect = require('Module:Inspect').inspect
local inspect = require('Module:Inspect').inspect
local getTranslations = i18n.getTranslations
local p = {}
local p = {}


Line 8: Line 10:
function p.main(frame)
function p.main(frame)
     local args = getArgs(frame)
     local args = getArgs(frame)
    local tr = getTranslations(frame, 'Template:Damage', args.lang, true)
    local out
    function translate(key)
        return i18n.translate(tr, key)
    end


     function inArgs(key)
     function inArgs(key)
Line 14: Line 22:
         end
         end
     end
     end
    local modes = { 'PvE', 'PvP' }


     -- Define the schema for the table
     -- Define the schema for the table
     local tableSchema = {
     local tableSchema = {}
         PvE = {},
    for _, mode in ipairs(modes) do
         PvP = {}
         tableSchema[mode] = {}
     }
    end
 
    function forEach(func)
        for _, mode in ipairs(modes) do
            func(mode)
        end
    end
 
    function forEachDamageType(func)
         for _, damage_type in ipairs({ 'min', 'max' }) do
            func(damage_type)
        end
     end


     -- Function to create a new table with the desired schema
     -- Function to create a new table with the desired schema
Line 36: Line 58:
         do_table = args[1] == 'true',
         do_table = args[1] == 'true',
         character = args[2] or args.char or 'Elsword',
         character = args[2] or args.char or 'Elsword',
        lang_suffix = args.lang and ('/' .. args.lang) or '',
        lang_append = args.lang ~= nil and args.lang ~= '',
         format = args.format ~= 'false',
         format = args.format ~= 'false',
         no_max = args.no_max == 'true',
         no_max = args.no_max == 'true',
Line 62: Line 86:
         dump = args.dump == 'true',
         dump = args.dump == 'true',
         dump_table_data = args.dump_table_data == 'true',
         dump_table_data = args.dump_table_data == 'true',
        dump_parsed = args.dump_parsed == 'true',
         prefix = args.prefix,
         prefix = args.prefix,
         use_avg = args.use_avg == 'true'
         use_avg = args.use_avg == 'true',
        dmp = args.dmp == 'true' and 3 or args.dmp
     }
     }


Line 74: Line 100:
         {
         {
             key = '',
             key = '',
             name = 'Normal',
             name = translate('Normal'),
             value = 1
             value = 1
         },
         },
         {
         {
             key = 'enhanced',
             key = 'enhanced',
             name = 'Enhanced',
             name = translate('Enhanced (Trait)'),
             value = args.enhanced ~= nil and 0.8
             value = args.enhanced ~= nil and 0.8
         },
         },
         {
         {
             key = 'empowered',
             key = 'empowered',
             name = 'Empowered',
             name = translate('Empowered'),
             value = args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false
             value = args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false
         },
         },
         {
         {
             key = 'useful',
             key = 'useful',
             name = 'Useful',
             name = translate('Useful'),
             value = args.hits_useful and (args.useful_penalty or 0.7) or false
             value = (args.hits_useful or args.avg_hits_useful) and (args.useful_penalty or args.useful or 0.8) or false
         },
         },
         {
         {
             key = 'heavy',
             key = 'heavy',
             name = 'Heavy',
             name = translate('Heavy'),
             value = args.heavy ~= nil and 1.44
             value = args.heavy ~= nil and 1.44
         }
         }
     }
     }


     -- Define a table with user-requested passive skills (empty by default).
    function eval(s)
        return frame:preprocess('{{#expr:' .. s .. '}}')
    end
 
     -- A table with user-requested passive skills (empty by default).
     local PASSIVES = {}
     local PASSIVES = {}
    -- A table with non-numeric arguments to split.
    local TO_SPLIT = { 'append', 'awk_alias' }


     for k, v in pairs(args) do
     for k, v in pairs(args) do
Line 108: Line 140:
             |passive1=... |passive2=... -> { passive1, passive2 }
             |passive1=... |passive2=... -> { passive1, passive2 }
             --]]
             --]]
            local passive_name = v
            local passive_title = v .. OPTIONS.lang_suffix
            local is_custom = string.find(k, '_define') ~= nil
             local passive_index = string.match(k, "%d")
             local passive_index = string.match(k, "%d")
             local passive_values = split(frame:preprocess('{{:' .. v .. '}}{{#arrayprint:' .. v .. '}}'));
             local passive_values = split(is_custom and v or
                frame:preprocess('{{:' .. passive_name .. '}}{{#arrayprint:' .. passive_name .. '}}'));
            local display_title
 
            if is_custom then
                passive_name = passive_values[#passive_values]
                passive_values[#passive_values] = nil
            elseif OPTIONS.lang_append then
                --[[
                Translate page's display title to passive name.
                Customized will override this name, thus no need to perform the translation
                --]]
                display_title = i18n.getTranslatedTitle(passive_title)
            end
 
             PASSIVES[tonumber(passive_index)] = {
             PASSIVES[tonumber(passive_index)] = {
                 name = v,
                 name = passive_name,
                 value = passive_values[1],
                 value = passive_values[1],
                 value_pvp = passive_values[2],
                 value_pvp = passive_values[2],
                 alias = args['alias' .. passive_index] or (passive_index == OPTIONS.append_index and OPTIONS.append_name),
                 alias = args['alias' .. passive_index] or (passive_index == OPTIONS.append_index and OPTIONS.append_name) or display_title,
                 suffix = args['suffix' .. passive_index] and (' ' .. args['suffix' .. passive_index]) or '',
                 suffix = args['suffix' .. passive_index] and (' ' .. args['suffix' .. passive_index]) or '',
                prefix = args['prefix' .. passive_index] and (args['prefix' .. passive_index] .. ' ') or '',
                exist = frame:preprocess('{{#ifexist:' .. passive_name .. '|true|false}}') == 'true'
             }
             }
         elseif string.find(k, 'append') or not string.find(v, '[a-zA-Z]+') then
         elseif string.match(v, '^[()+%-*/%d%s,.i]+$') then
             --[[
             --[[
             Change how args are received.
             Change how args are received.
Line 125: Line 176:
             -- Perform automatic math on each value.
             -- Perform automatic math on each value.
             for k2, v2 in pairs(split_values) do
             for k2, v2 in pairs(split_values) do
                 split_values[k2] = frame:preprocess('{{#expr:' .. v2 .. '}}')
                 if not string.find(v, '[a-zA-Z]+') then
                    split_values[k2] = eval(v2)
                end
             end
             end
             args[k] = split_values
             args[k] = split_values
        elseif inArrayHasValue(k, TO_SPLIT) then
            args[k] = split(v)
         end
         end
     end
     end
Line 138: Line 193:
         if not args.hits[k] then
         if not args.hits[k] then
             args.hits[k] = 1
             args.hits[k] = 1
        end
    end
    -- Set basic hit count to 1 for all cancel damage.
    if args.cancel_dmg then
        for k, v in ipairs(args.cancel_dmg) do
            if not args.cancel_hits then
                args.cancel_hits = {}
            end
            if not args.cancel_hits[k] then
                args.cancel_hits[k] = 1
            end
         end
         end
     end
     end
Line 143: Line 210:
     -- Store a configuration that will tell the main function how to behave given different inputs.
     -- Store a configuration that will tell the main function how to behave given different inputs.
     -- It will always take the first value if available. If not, fall back to the other (recursively).
     -- It will always take the first value if available. If not, fall back to the other (recursively).
     local DAMAGE_CONFIG = {
     local BASE_DAMAGE_CONFIG = {
         total_damage = {
         total_damage = {
             damage_numbers = { 'dmg' },
             damage_numbers = { 'dmg' },
Line 186: Line 253:
         },
         },
     }
     }
    local DAMAGE_CONFIG = {}
    function handleCancel()
        local processed_keys = {}
        for config_key, config_value in pairs(BASE_DAMAGE_CONFIG) do
            if not config_key:match('cancel_') then
                local new_config_value = {}
                for arg_table_key, arg_table in pairs(config_value) do
                    local new_arg_table = {}
                    for _, arg in ipairs(arg_table) do
                        table.insert(new_arg_table, 'cancel_' .. arg)
                    end
                    new_config_value[arg_table_key] = new_arg_table
                end
                local new_key = 'cancel_' .. config_key
                DAMAGE_CONFIG[new_key] = new_config_value
                processed_keys[new_key] = true
            end
        end
        return processed_keys
    end
    if args.cancel_dmg then
        handleCancel()
        DAMAGE_CONFIG = table.fuse(BASE_DAMAGE_CONFIG, DAMAGE_CONFIG)
    else
        DAMAGE_CONFIG = BASE_DAMAGE_CONFIG
    end
    -- Helper function to check if a table is not empty
    local function isTableNotEmpty(tbl)
        return next(tbl) ~= nil
    end
    -- Function to apply inheritance for a specific damage type and argument
    local function applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritValue)
        if mainArgValue == '' then
            return inheritValue
        elseif mainArgValue and string.find(mainArgValue, 'i') and inheritValue then
            return eval(mainArgValue:gsub('i', inheritValue))
        end
        return mainArgValue
    end
    -- Function to apply inheritance for a specific argument key
    local function applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType)
        local mainKey = argTable[1] .. damageType
        local mainKeyPrefixed = prefix .. mainKey
        local mainArgValues = args[mainKeyPrefixed]
        if mainArgValues then
            local i = 1
            local cancelDmgLen = args.cancel_dmg and #args.cancel_dmg or 0
            while i <= (#args.dmg + cancelDmgLen) do
                local mainArgValue = mainArgValues[i]
                for ix, inheritKey in ipairs(argTable) do
                    local inheritArg = args[prefix .. inheritKey .. damageType] or args[inheritKey .. damageType]
                    -- Basic damage/hits inheritance request detected. Ignore min/max.
                    if damageType and mainKey:gsub(damageType, "") == argTable[#argTable] then
                        inheritArg = args[prefix .. inheritKey] or args[inheritKey]
                    end
                    if inheritArg and inheritArg[i] and
                        (damageTypeIndex == 1 and ix ~= 1 or damageTypeIndex ~= 1) and tonumber(inheritArg[i])
                    then
                        mainArgValues[i] = applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritArg[i])
                        break
                    end
                end
                i = i + 1
            end
        end
    end
    -- Inherits values from args if not provided, but usage suggests that they're meant to be generated.
    function inherit(mode)
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
        for configKey, configValue in pairs(DAMAGE_CONFIG) do
            for argTableKey, argTable in pairs(configValue) do
                if argTableKey ~= 'provided' and isTableNotEmpty(argTable) then
                    for damageTypeIndex, damageType in ipairs({ '', '_min', '_max' }) do
                        applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType)
                    end
                end
            end
        end
    end
    forEach(inherit)


     local DAMAGE_PARSED = createDamageDataTable()
     local DAMAGE_PARSED = createDamageDataTable()
     function parseConfig(mode)
     function parseConfig(mode)
         local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
         local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
         for config_key, config_value in pairs(DAMAGE_CONFIG) do
         for config_key, config_value in pairs(DAMAGE_CONFIG) do
             for k, v in pairs(DAMAGE_CONFIG[config_key]) do
             for k, v in pairs(config_value) do
                 local output_value = v
                 local output_value = {}
                 for _, v2 in ipairs(v) do
 
                     local arg_from_template = args[prefix .. v2] or args[v2]
                -- When both min and max are found, we need to break from the loop.
                    if arg_from_template ~= nil then
                local isValueFound = { min = false, max = false }
                        output_value = arg_from_template
 
                        if k == 'provided' then
                 for _, v2 in ipairs(v) do -- This array holds the argument names with fallbacks
                            output_value = true
                     forEachDamageType(function(damage_type)
                            -- Do not generate total_damage values at all if the skill can't reach them.
                        -- If there already is a value for this damage type (min or max), do not continue.
                            if string.find(config_key, 'total_') and OPTIONS.no_max then
                        if isValueFound[damage_type] == true then
                            return
                        end
 
                        local arg_from_template =
                            args[prefix .. v2 .. '_' .. damage_type]
                            or args[v2 .. '_' .. damage_type]
                            or args[prefix .. v2]
                            or args[v2];
 
                        if arg_from_template ~= nil then
                            if k == 'provided' then
                                output_value = true
                                -- Do not generate total_damage values at all if the skill can't reach them.
                                if string.find(config_key, 'total_') and OPTIONS.no_max then
                                    output_value = false
                                end
                            else
                                if type(output_value) ~= "table" then
                                    output_value = {}
                                end
                                output_value[damage_type] = arg_from_template
                            end
                            -- Mark the value as found.
                            isValueFound[damage_type] = true
                        else
                            if k == 'provided' then
                                 output_value = false
                                 output_value = false
                            else
                                output_value[damage_type] = {}
                             end
                             end
                         end
                         end
                    end)
                    -- Both values found, we can now break the loop.
                    if isValueFound.min and isValueFound.max then
                         break
                         break
                    else
                        if k == 'provided' then
                            output_value = false
                        end
                     end
                     end
                 end
                 end
Line 220: Line 408:
     end
     end


     parseConfig('PvE')
     forEach(parseConfig)
    parseConfig('PvP')


     -- Detected "count", for skills like Clementine, Enough Mineral, etc.
     -- Detected "count", for skills like Clementine, Enough Mineral, etc.
     function doEachDamage()
     function doEachDamage()
        local WITH_EACH = table.deep_copy(DAMAGE_PARSED)
         for mode, mode_content in pairs(DAMAGE_PARSED) do
         for mode, mode_content in pairs(DAMAGE_PARSED) do
             for damage_key, damage_value in pairs(mode_content) do
             for damage_key, damage_value in pairs(mode_content) do
Line 230: Line 418:
                     local new_value = table.deep_copy(damage_value)
                     local new_value = table.deep_copy(damage_value)


                     for k, hit_count in ipairs(new_value.hit_counts) do
                     forEachDamageType(function(damage_type)
                        hit_count = hit_count == '' and 1 or hit_count
                        for k, hit_count in ipairs(new_value.hit_counts[damage_type]) do
                        new_value.hit_counts[k] = hit_count * args.count[1]
                            hit_count = hit_count == '' and 1 or hit_count
                     end
                            new_value.hit_counts[damage_type][k] = hit_count *
                                ((string.find(damage_key, 'awk') and args.awk_count) and args.awk_count[1] or args.count[1])
                        end
                     end)


                     DAMAGE_PARSED[mode][damage_key:gsub("total_", "each_")] = damage_value
                     WITH_EACH[mode][damage_key:gsub("total_", "each_")] = damage_value
                     DAMAGE_PARSED[mode][damage_key] = new_value
                     WITH_EACH[mode][damage_key] = new_value
                 end
                 end
             end
             end
         end
         end
        return WITH_EACH
     end
     end


     if inArgs('count') then
     if args.count then
         doEachDamage()
         DAMAGE_PARSED = doEachDamage()
     end
     end


Line 249: Line 441:
         for mode, mode_content in pairs(DAMAGE_PARSED) do
         for mode, mode_content in pairs(DAMAGE_PARSED) do
             for damage_key, damage_value in pairs(mode_content) do
             for damage_key, damage_value in pairs(mode_content) do
                 local i = 1
                 forEachDamageType(function(damage_type)
                local output = 0
                    local i = 1
                -- Check if to even generate the damage.
                    local output = 0
                if damage_value.provided then
                    -- Check if to even generate the damage.
                    -- Loop through damage numbers and multiply them with hits.
                    if damage_value.provided then
                    for k, damage_number in ipairs(damage_value.damage_numbers) do
                        -- Loop through damage numbers and multiply them with hits.
                        local hit_count = damage_value.hit_counts[i]
                        for k, damage_number in ipairs(damage_value.damage_numbers[damage_type]) do
                        hit_count = hit_count == '' and 1 or hit_count
                            local hit_count = damage_value.hit_counts[damage_type][i]
                        output = output + (damage_number * hit_count)
                            hit_count = hit_count == '' and 1 or hit_count
                        i = i + 1
                            output = output + (damage_number * hit_count)
                            i = i + 1
                        end
                        -- Write the result to a separate object.
                        if not BASIC_DAMAGE[mode][damage_key] then
                            BASIC_DAMAGE[mode][damage_key] = {}
                        end
                        BASIC_DAMAGE[mode][damage_key][damage_type] = output
                     end
                     end
                    -- Write the result to a separate object.
                 end)
                    BASIC_DAMAGE[mode][damage_key] = output
                 end
             end
             end
         end
         end
Line 268: Line 465:


     doBasicDamage()
     doBasicDamage()
    -- Adding missing cancel part damage to full, so that repetition wouldn't be a problem.
    function addCancelDamage()
        for mode, mode_content in pairs(BASIC_DAMAGE) do
            for damage_key, damage_value in pairs(mode_content) do
                local cancel_candidate = BASIC_DAMAGE[mode]['cancel_' .. damage_key]
                forEachDamageType(function(damage_type)
                    if not string.find(damage_key, 'cancel_') and cancel_candidate then
                        BASIC_DAMAGE[mode][damage_key][damage_type] = damage_value[damage_type] +
                            cancel_candidate[damage_type]
                    end
                end)
            end
        end
    end
    if args.cancel_dmg then
        addCancelDamage()
    end


     local WITH_TRAITS = createDamageDataTable()
     local WITH_TRAITS = createDamageDataTable()
Line 281: Line 497:
                     so we skip those situations, as impossible in-game.
                     so we skip those situations, as impossible in-game.
                     --]]
                     --]]
                     if trait.value and (not string.match(damage_key, '_useful') or trait.key == '') then
                     if (trait.value and trait.key ~= 'useful') or (string.find(damage_key, 'useful') and trait.key == 'useful') then
                         WITH_TRAITS[mode][damage_key .. ((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key))] =
                         forEachDamageType(function(damage_type)
                             damage_value * trait.value
                            local new_key = damage_key ..
                                ((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key));
                            if not WITH_TRAITS[mode][new_key] then
                                WITH_TRAITS[mode][new_key] = {}
                             end
                            WITH_TRAITS[mode][new_key][damage_type] = damage_value[damage_type] * trait.value
                        end)
                     end
                     end
                 end
                 end
Line 302: Line 524:
         for mode, mode_content in pairs(WITH_TRAITS) do
         for mode, mode_content in pairs(WITH_TRAITS) do
             for damage_key, damage_value in pairs(mode_content) do
             for damage_key, damage_value in pairs(mode_content) do
                 local combinations = { {} }
                 forEachDamageType(function(damage_type)
                for passive_key, passive in pairs(PASSIVES) do
                    local combinations = { {} }
                    local count = #combinations
                    for passive_key, passive in pairs(PASSIVES) do
                    for i = 1, count do
                        local count = #combinations
                        local new_combination = { unpack(combinations[i]) }
                        for i = 1, count do
                        table.insert(new_combination, passive_key)
                            local new_combination = { unpack(combinations[i]) }
                        table.insert(combinations, new_combination)
                            table.insert(new_combination, passive_key)
                            table.insert(combinations, new_combination)
                        end
                     end
                     end
                end
                    for _, combination in pairs(combinations) do
                for _, combination in pairs(combinations) do
                        local passive_multiplier = 1
                    local passive_multiplier = 1
                        local name_suffix = ''
                    local name_suffix = ''
                        if #combination > 0 then
                    if #combination > 0 then
                            table.sort(combination)
                        table.sort(combination)
                            for _, passive_key in pairs(combination) do
                        for _, passive_key in pairs(combination) do
                                passive_multiplier = passive_multiplier *
                            passive_multiplier = passive_multiplier *
                                    tonumber(PASSIVES[passive_key][mode == 'PvE' and 'value' or 'value_pvp'])
                                tonumber(PASSIVES[passive_key][mode == 'PvE' and 'value' or 'value_pvp'])
                                name_suffix = name_suffix .. '_passive' .. passive_key
                            name_suffix = name_suffix .. '_passive' .. passive_key
                            end
                        end
                        local new_damage_key = damage_key .. name_suffix;
                        if not WITH_PASSIVES[mode][new_damage_key] then
                            WITH_PASSIVES[mode][new_damage_key] = {}
                         end
                         end
                        WITH_PASSIVES[mode][new_damage_key][damage_type] = damage_value[damage_type] * passive_multiplier
                     end
                     end
                    WITH_PASSIVES[mode][damage_key .. name_suffix] = damage_value * passive_multiplier
                 end)
                 end
             end
             end
         end
         end
Line 331: Line 559:


     local RANGE = {
     local RANGE = {
         min_count = args.range_min_count and args.range_min_count[1] or 1,
         min_count = args.range_min_count and args.range_min_count[1],
         max_count = args.range_max_count and args.range_max_count[1] or 1,
         max_count = args.range_max_count and args.range_max_count[1],
         PvE = {
         PvE = {
             min = args.range_min and args.range_min[1] or 1,
             min = args.range_min and args.range_min[1],
             max = args.range_max and args.range_max[1] or 1
             max = args.range_max and args.range_max[1]
         },
         },
         PvP = {
         PvP = {
             min = args.range_min and (args.range_min[2] or args.range_min[1]) or 1,
             min = args.range_min and (args.range_min[2] or args.range_min[1]),
             max = args.range_max and (args.range_max[2] or args.range_max[1]) or 1
             max = args.range_max and (args.range_max[2] or args.range_max[1])
         }
         }
     }
     }
Line 349: Line 577:
             for damage_key, damage_value in pairs(mode_content) do
             for damage_key, damage_value in pairs(mode_content) do
                 WITH_RANGE[mode][damage_key] = { min = 0, max = 0 }
                 WITH_RANGE[mode][damage_key] = { min = 0, max = 0 }
                 for _, range in ipairs({ 'min', 'max' }) do
                 forEachDamageType(function(damage_type)
                     local final_damage_value = damage_value * (1 + ((RANGE[mode][range] - 1) * RANGE[range .. '_count'])) *
                    local range_count = RANGE[damage_type .. '_count'] or 1;
                        OPTIONS.perm_buff[mode]
                    -- If min count preset, use range_max for the multiplier.
                     WITH_RANGE[mode][damage_key][range] = not OPTIONS.format and final_damage_value or
                    local range_multiplier = RANGE[mode][damage_type] or (damage_type == 'min' and RANGE.min_count and RANGE[mode].max) or 1;
                     local final_range_multiplier = (1 + ((range_multiplier - 1) * range_count));
                    local perm_buff = OPTIONS.perm_buff[mode];
 
                    local final_damage_value = damage_value[damage_type] * final_range_multiplier * perm_buff;
                     WITH_RANGE[mode][damage_key][damage_type] = not OPTIONS.format and final_damage_value or
                         formatDamage(final_damage_value)
                         formatDamage(final_damage_value)
                 end
                 end)
             end
             end
         end
         end
Line 382: Line 615:
         end
         end
         return output
         return output
    end
    -- Helper function to detect combined passives.
    function isCombined(index)
        for k, v in ipairs(OPTIONS.combine) do
            if index == v then
                return true
            end
        end
        return false
     end
     end


Line 407: Line 630:


         for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do
         for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do
             if (not OPTIONS.is_append or (OPTIONS.is_append and OPTIONS.append_index ~= passive_index)) and not inArrayHasValue(passive_index, OPTIONS.combine or {}) then
             if (not OPTIONS.is_append or (OPTIONS.is_append and OPTIONS.append_index ~= passive_index)) and (not inArrayHasValue(passive_index, OPTIONS.combine or {}) or inArrayHasValue(passive_index, args.display_separated or {})) then
                 if type(settings.action) == 'function' then
                 if type(settings.action) == 'function' then
                     settings.action(passive, output, passive_index)
                     settings.action(passive, output, passive_index)
Line 430: Line 653:
         {
         {
             type = 'extra',
             type = 'extra',
             text = { 'Average' },
             text = { translate('Average') },
             is_visible = OPTIONS.no_max,
             is_visible = OPTIONS.no_max,
             no_damage = true
             no_damage = true
Line 437: Line 660:
             type = 'passives',
             type = 'passives',
             text = checkPassives({
             text = checkPassives({
                 output = { 'Base' },
                 output = { translate('Base') },
                 action = function(passive, output)
                 action = function(passive, output)
                     if passive.is_combined then
                     if passive.is_combined then
Line 444: Line 667:
                         for _, passive_key in ipairs(OPTIONS.combine) do
                         for _, passive_key in ipairs(OPTIONS.combine) do
                             passive = PASSIVES[passive_key]
                             passive = PASSIVES[passive_key]
                             table.insert(combo, link(passive.name, passive.alias) .. passive.suffix)
                             table.insert(combo,
                                link(passive.name, passive.alias, passive.prefix, passive.suffix, passive.exist))
                         end
                         end
                         table.insert(output, table.concat(combo, '/') .. OPTIONS.combine_suffix)
                         table.insert(output, table.concat(combo, '/') .. OPTIONS.combine_suffix)
                     else
                     else
                         table.insert(output, link(passive.name, passive.alias) .. passive.suffix)
                         table.insert(output,
                            link(passive.name, passive.alias, passive.prefix, passive.suffix, passive.exist))
                     end
                     end
                 end
                 end
Line 462: Line 687:
                 end
                 end
             }),
             }),
             is_visible = not OPTIONS.no_max or #PASSIVES > 0,
             is_visible = not OPTIONS.no_max or #PASSIVES > 0
            is_special = true
         },
         },
         {
         {
             type = 'passive_appended',
             type = 'passive_appended',
             text = { 'Normal',
             text = {
                translate('Normal'),
                 OPTIONS.is_append and
                 OPTIONS.is_append and
                 link(PASSIVES[OPTIONS.append_index].name, PASSIVES[OPTIONS.append_index].alias or OPTIONS.append_name) or
                 link(PASSIVES[OPTIONS.append_index].name,
                 nil },
                    PASSIVES[OPTIONS.append_index].alias or OPTIONS.append_name or nil,
             keywords = { OPTIONS.is_append and 'passive' .. OPTIONS.append_index or nil },
                    PASSIVES[OPTIONS.append_index].prefix,
                    PASSIVES[OPTIONS.append_index].suffix,
                    PASSIVES[OPTIONS.append_index].exist
                 )
            },
             keywords = { OPTIONS.is_append and ('passive' .. OPTIONS.append_index) or nil },
             is_visible = OPTIONS.is_append or false
             is_visible = OPTIONS.is_append or false
         },
         },
         {
         {
             type = 'awakening',
             type = 'awakening',
             text = { 'Regular', link('Awakening Mode') },
             text = { translate('Regular'), (function()
                if OPTIONS.dmp then
                    return link('Dynamo Point System' .. OPTIONS.lang_suffix, 'Dynamo Configuration', args.awk_prefix,
                    OPTIONS.dmp ~= 'false' and (fillTemplate('({1} DMP)', { OPTIONS.dmp })) .. (args.awk_suffix and (' ' .. args.awk_suffix) or ''))
                elseif args.awk_alias then
                    return link(args.awk_alias[1], args.awk_alias[2], args.awk_prefix, args.awk_suffix)
                end
                return link('Awakening Mode' .. OPTIONS.lang_suffix, translate('Awakening Mode'), args.awk_prefix, args.awk_suffix)
            end)()
            },
             keywords = { 'awk' },
             keywords = { 'awk' },
             keyword_next_to_main_key = true,
             keyword_next_to_main_key = true,
             is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or false
             is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or inArgs('avg_awk_hits') or false
         },
         },
         {
         {
             type = 'traits',
             type = 'traits',
             text = checkTraits({
             text = checkTraits({
                 output = { 'Normal' },
                 output = { translate('Normal') },
                 action = function(trait, output)
                 action = function(trait, output)
                     table.insert(output, trait.name)
                     table.insert(output, trait.name)
Line 495: Line 734:
             }),
             }),
             is_visible = checkTraits()
             is_visible = checkTraits()
        },
        {
            type = 'cancel',
            text = {
                translate('Cancel'),
                translate('Full'),
            },
            keywords = { 'cancel' },
            keyword_first = true,
            is_visible = inArgs('cancel_dmg')
         },
         },
         {
         {
Line 500: Line 749:
             text = {
             text = {
                 (inArgs('count') and not OPTIONS.use_avg) and
                 (inArgs('count') and not OPTIONS.use_avg) and
                 (table.concat({ 'Per', args.count_name or 'Instance' }, ' ')) or 'Average',
                 (fillTemplate(translate('Per {1}'), { args.count_name or translate('Group') })) or
                 'Max'
                translate('Average'),
                 translate('Max')
             },
             },
             keywords = (function()
             keywords = (function()
Line 537: Line 787:
                         end
                         end
                     end
                     end
                 elseif current_row.is_special and current_row.is_visible then
                 elseif current_row.is_visible then
                     -- Append suffix for each keyword in current row
                     -- Append suffix for each keyword in current row
                     for _, keyword in ipairs(current_row.keywords) do
                     for _, keyword in ipairs(current_row.keywords) do
Line 543: Line 793:
                         for _, prev_key in ipairs(all_list) do
                         for _, prev_key in ipairs(all_list) do
                             local new_key = prev_key .. '_' .. keyword
                             local new_key = prev_key .. '_' .. keyword
                            table.insert(new_list, new_key)
                        end
                    end
                elseif current_row.is_visible then
                    -- Iterate through previous keys
                    for _, prev_key in ipairs(all_list) do
                        -- Append suffix for each keyword in current row
                        for _, keyword in ipairs(current_row.keywords) do
                            local new_key = prev_key .. '_' .. keyword
                             -- If needed, move the suffix to the rightmost of main_key.
                             -- If needed, move the suffix to the rightmost of main_key.
                             if current_row.keyword_next_to_main_key then
                             if current_row.keyword_next_to_main_key then
                                 new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
                                 new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
                            elseif current_row.keyword_first then
                                new_key = keyword .. '_' .. prev_key
                             end
                             end
                             table.insert(new_list, new_key)
                             table.insert(new_list, new_key)
                         end
                         end
Line 568: Line 809:
                 end
                 end
             end
             end
        end
        -- Sort the list once more, in order to swap the order of cancel & full.
        if inArgs('cancel_dmg') then
            local new_list = {}
            local cancel_counter = 1
            local full_counter = 2
            for i, damage_key in ipairs(all_list) do
                local regex = "^(%w+_)"
                local prefix = 'cancel_'
                local match = string.match(damage_key, regex)
                if (match == prefix) then
                    new_list[i] = damage_key:gsub(prefix, "")
                else
                    new_list[i] = prefix .. damage_key
                end
            end
            all_list = new_list
         end
         end


Line 574: Line 833:


     function doInitialCell(new_row)
     function doInitialCell(new_row)
         return new_row:tag('th'):wikitext('Mode')
         return new_row:tag('th'):wikitext(translate('Mode'))
     end
     end


Line 633: Line 892:
     function doContentByMode(mode)
     function doContentByMode(mode)
         local mode_row = TABLE:new()
         local mode_row = TABLE:new()
         mode_row:tag('td'):wikitext(frame:expandTemplate { title = mode })
         mode_row:tag('td'):wikitext(frame:expandTemplate { title = translate(mode) })
         local damage_entries = returnDamageInOrder()
         local damage_entries = returnDamageInOrder()
        local last_number
        local last_unique_cell


         for _, damage_key in ipairs(damage_entries) do
         for _, damage_key in ipairs(damage_entries) do
             if args.dump_names ~= 'true' then
             if args.dump_names ~= 'true' then
                 local damage_number = FINAL_DAMAGE[mode][damage_key]
                 local damage_number = FINAL_DAMAGE[mode][damage_key]
                -- Display ranges.
                 damage_number = doRangeText(damage_number)
                 damage_number = doRangeText(damage_number)


                 mode_row:tag('td'):wikitext(damage_number
                 if last_number ~= damage_number then
                    -- Error out if it doesn't exist
                    -- Display ranges.
                    or frame:expandTemplate {
                    local new_cell = mode_row:tag('td'):wikitext(damage_number
                        title = 'color',
                        -- Error out if it doesn't exist
                        args = { 'red', '&#35;ERROR' }
                        or frame:expandTemplate {
                     })
                            title = 'color',
                            args = { 'red', '&#35;ERROR' }
                        })
                    last_unique_cell = new_cell
                else
                     last_unique_cell:attr('colspan', (last_unique_cell:getAttr('colspan') or 1) + 1)
                end
                last_number = damage_number
             else
             else
                 mode_row:tag('td'):wikitext(damage_key)
                 mode_row:tag('td'):wikitext(damage_key)
Line 657: Line 923:
     function doTable()
     function doTable()
         doHeaders()
         doHeaders()
         doContentByMode('PvE')
         forEach(doContentByMode)
         doContentByMode('PvP')
    end
 
    if OPTIONS.do_table then
         doTable()
     end
     end
    doTable()


     -- Dump all values if wanted.
     -- Dump all values if wanted.
Line 667: Line 935:
     elseif OPTIONS.dump then
     elseif OPTIONS.dump then
         return inspect_dump(frame, FINAL_DAMAGE)
         return inspect_dump(frame, FINAL_DAMAGE)
    elseif OPTIONS.dump_parsed then
        return inspect_dump(frame, DAMAGE_PARSED)
     end
     end


Line 672: Line 942:
     if OPTIONS.bug then
     if OPTIONS.bug then
         bug = frame:expandTemplate {
         bug = frame:expandTemplate {
             title = 'SkillText',
             title = translate('SkillText'),
             args = { 'FreeTraining' }
             args = { 'FreeTraining' }
         }
         }
Line 679: Line 949:
     -- Transform into variables
     -- Transform into variables
     local variables = doVariables(frame, FINAL_DAMAGE, OPTIONS.prefix)
     local variables = doVariables(frame, FINAL_DAMAGE, OPTIONS.prefix)
    if out ~= nil then
        return inspect_dump(frame, out)
    end


     return variables .. bug .. (OPTIONS.do_table and tostring(TABLE) or '')
     return variables .. bug .. (OPTIONS.do_table and tostring(TABLE) or '')