Module:Test: Difference between revisions

From Elwiki
(Bot: Automated import of articles *** existing text overwritten ***)
No edit summary
 
(489 intermediate revisions by the same user not shown)
Line 1: Line 1:
require('Module:CommonFunctions');
-- pystart
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 7: 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 13: 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 29: Line 52:
         end
         end
         return newTable
         return newTable
    end
    function link(text)
        return '[[' .. text .. ']]'
     end
     end


     -- User requested options
     -- User requested options
     local OPTIONS = {
     local OPTIONS = {
         should_do_table = args[1] == 'true',
         do_table = args[1] == 'true',
         character = args[2] 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',
         is_append = args.append ~= nil,
         is_append = args.append ~= nil,
         append_index = args.append and split(args.append)[1],
         append_index = args.append and tonumber(split(args.append)[1]),
         append_name = args.append and split(args.append)[2]
         append_name = args.append and split(args.append)[2],
        combine_suffix = args.combine_suffix and (' ' .. args.combine_suffix) or '',
        combine = (function()
            local output = {}
            if not args.combine then
                return nil
            end
            for _, passive_key in ipairs(split(args.combine)) do
                table.insert(output, tonumber(passive_key))
            end
            if #output == 0 then
                return nil
            end
            return output
        end)(),
        perm_buff = {
            PvE = args.perm_buff or 1,
            PvP = args.pvp_perm_buff or args.perm_buff or 1
        },
        bug = args.bug == 'true',
        dump = args.dump == 'true',
        dump_table_data = args.dump_table_data == 'true',
        dump_parsed = args.dump_parsed == 'true',
        prefix = args.prefix,
        use_avg = args.use_avg == 'true',
        dmp = args.dmp == 'true' and 3 or args.dmp
     }
     }


Line 54: 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 ~= nil and (tonumber(args.useful_penalty) or 0.7)
             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 88: 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 split(args.combine or '')[2],
                 alias = args['alias' .. passive_index] or (passive_index == OPTIONS.append_index and OPTIONS.append_name) or display_title,
                 suffix = args['suffix' .. passive_index],
                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 105: 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(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
    -- Set basic hit count to 1 for all damage.
    for k, v in ipairs(args.dmg) do
        if not args.hits then
            args.hits = {}
        end
        if not args.hits[k] then
            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


     -- 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.
     local DAMAGE_CONFIG = {
    -- It will always take the first value if available. If not, fall back to the other (recursively).
     local BASE_DAMAGE_CONFIG = {
         total_damage = {
         total_damage = {
             damage_numbers = { 'dmg' },
             damage_numbers = { 'dmg' },
Line 131: Line 229:
             damage_numbers = { 'awk_dmg', 'dmg' },
             damage_numbers = { 'awk_dmg', 'dmg' },
             hit_counts = { 'avg_awk_hits', 'awk_hits', 'avg_hits', 'hits' },
             hit_counts = { 'avg_awk_hits', 'awk_hits', 'avg_hits', 'hits' },
             provided = { 'avg_awk_hits' }
             provided = { 'avg_awk_hits', args.awk_dmg and 'avg_hits' or nil }
         },
         },
         -- Store the logic for Useful traits
         -- Store the logic for Useful traits
Line 155: 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)
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
        for config_key, config_value in pairs(DAMAGE_CONFIG) do
            for k, v in pairs(config_value) do
                local output_value = {}


    function evaluateConfig(isPvP)
                -- When both min and max are found, we need to break from the loop.
        local mode = isPvP and 'PvP' or 'PvE'
                local isValueFound = { min = false, max = false }
        local prefix = isPvP and 'pvp_' or ''
 
        for config_key, config_value in pairs(DAMAGE_CONFIG) do
                for _, v2 in ipairs(v) do -- This array holds the argument names with fallbacks
            for k, v in pairs(DAMAGE_CONFIG[config_key]) do
                    forEachDamageType(function(damage_type)
                local output_value = v
                        -- If there already is a value for this damage type (min or max), do not continue.
                for _, v2 in ipairs(v) do
                        if isValueFound[damage_type] == true then
                    local arg_from_template = args[prefix .. v2] or args[v2]
                            return
                    if arg_from_template ~= nil then
                        end
                        output_value = arg_from_template
 
                         if k == 'provided' then
                        local arg_from_template =
                             output_value = true
                            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
                             else
                                output_value[damage_type] = {}
                            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 186: Line 408:
     end
     end


     evaluateConfig(false)
     forEach(parseConfig)
    evaluateConfig(true)


     -- 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 196: Line 418:
                     local new_value = table.deep_copy(damage_value)
                     local new_value = table.deep_copy(damage_value)


                     for k, v in ipairs(new_value.hit_counts) do
                     forEachDamageType(function(damage_type)
                        new_value.hit_counts[k] = v * args.count[1]
                        for k, hit_count in ipairs(new_value.hit_counts[damage_type]) do
                     end
                            hit_count = hit_count == '' and 1 or hit_count
                            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 214: 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, v in ipairs(damage_value.damage_numbers) do
                        -- Loop through damage numbers and multiply them with hits.
                        output = output + (v * damage_value.hit_counts[i])
                        for k, damage_number in ipairs(damage_value.damage_numbers[damage_type]) do
                        i = i + 1
                            local hit_count = damage_value.hit_counts[damage_type][i]
                            hit_count = hit_count == '' and 1 or hit_count
                            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 231: 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 244: 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 256: Line 515:


     local WITH_PASSIVES = createDamageDataTable()
     local WITH_PASSIVES = createDamageDataTable()
    local function generateCombinations(passives, n, combo, combos)
        if n == 0 then
            table.insert(combos, combo)
        else
            for i = 1, #passives do
                local newCombo = {}
                for j = 1, #combo do
                    table.insert(newCombo, combo[j])
                end
                table.insert(newCombo, passives[i])
                generateCombinations(passives, n - 1, newCombo, combos)
            end
        end
    end


     --[[
     --[[
Line 280: 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 309: 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])
         }
         }
     }
     }
    function formatDamage(number)
        local formattedDamage = formatnum(math.round(number, 2)) .. '%'
        return formattedDamage
    end


     local WITH_RANGE = createDamageDataTable()
     local WITH_RANGE = createDamageDataTable()
Line 332: 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;
                     WITH_RANGE[mode][damage_key][range] = not OPTIONS.format and final_damage_value or
                    -- If min count preset, use range_max for the multiplier.
                    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 342: Line 593:


     doDamageBuffRange()
     doDamageBuffRange()
    local FINAL_DAMAGE = WITH_RANGE


     -- Helper function to iterate over traits.
     -- Helper function to iterate over traits.
Line 354: Line 607:
         for trait_index, trait in ipairs(TRAITS) do
         for trait_index, trait in ipairs(TRAITS) do
             if trait.value ~= false and trait_index ~= 1 then
             if trait.value ~= false and trait_index ~= 1 then
                 if type(settings.action) == 'function' then
                 if settings and type(settings.action) == 'function' then
                     settings.action(trait, output, settings)
                     settings.action(trait, output, settings)
                 else
                 else
Line 367: Line 620:
     function checkPassives(settings)
     function checkPassives(settings)
         local output = settings.output or {}
         local output = settings.output or {}
         for passive_index, passive in ipairs(PASSIVES) do
        local PASSIVES_WITH_COMBINED = table.deep_copy(PASSIVES)
             if (not OPTIONS.is_append or (OPTIONS.is_append and tonumber(OPTIONS.append_index) ~= passive_index)) then
 
        -- Handle combined passives properly.
        if OPTIONS.combine then
            table.insert(PASSIVES_WITH_COMBINED, {
                is_combined = true
            })
        end
 
         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 {}) 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 381: Line 643:
     -- Generate the table
     -- Generate the table
     local TABLE = mw.html.create('table'):attr({
     local TABLE = mw.html.create('table'):attr({
         ['cellpadding'] = 5,
         cellpadding = 5,
         ['border'] = 1,
         border = 1,
         ['style'] = 'border-collapse: collapse; text-align: center',
         style = 'border-collapse: collapse; text-align: center',
         ['class'] = 'colortable-' .. OPTIONS.character
         class = 'colortable-' .. OPTIONS.character
     })
     })


Line 391: Line 653:
         {
         {
             type = 'extra',
             type = 'extra',
             text = { 'Average' },
             text = { translate('Average') },
             is_visible = OPTIONS.no_max
             is_visible = OPTIONS.no_max,
            no_damage = true
         },
         },
         {
         {
             type = 'passives',
             type = 'passives',
             text = checkPassives({
             text = checkPassives({
                 output = { 'Base' },
                 output = { translate('Base') },
                 action = function (passive, output)
                 action = function(passive, output)
                     table.insert(output, link(passive.name))
                     if passive.is_combined then
                        -- Handling combined passive header name.
                        local combo = {}
                        for _, passive_key in ipairs(OPTIONS.combine) do
                            passive = PASSIVES[passive_key]
                            table.insert(combo,
                                link(passive.name, passive.alias, passive.prefix, passive.suffix, passive.exist))
                        end
                        table.insert(output, table.concat(combo, '/') .. OPTIONS.combine_suffix)
                    else
                        table.insert(output,
                            link(passive.name, passive.alias, passive.prefix, passive.suffix, passive.exist))
                    end
                 end
                 end
             }),
             }),
             keywords = checkPassives({
             keywords = checkPassives({
                 action = function (passive, output, passive_index)
                 action = function(passive, output, passive_index)
                     table.insert(output, 'passive' .. passive_index)
                     if passive.is_combined then
                        -- Handling combined passive damage cells.
                        table.insert(output, sortPassives('passive' .. table.concat(OPTIONS.combine, '_passive')))
                    else
                        table.insert(output, 'passive' .. passive_index)
                    end
                 end
                 end
             }),
             }),
             combined = args.combine,
             is_visible = not OPTIONS.no_max or #PASSIVES > 0
            is_visible = true
         },
         },
         {
         {
             type = 'passives_appended',
             type = 'passive_appended',
             text = { 'Normal', OPTIONS.is_append and link(OPTIONS.append_name or PASSIVES[OPTIONS.append_index].name) },
             text = {
             keywords = { OPTIONS.is_append and 'passive' .. OPTIONS.append_index },
                translate('Normal'),
                OPTIONS.is_append and
                link(PASSIVES[OPTIONS.append_index].name,
                    PASSIVES[OPTIONS.append_index].alias or OPTIONS.append_name 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 437: 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')
         },
         },
         {
         {
             type = 'hit_count',
             type = 'hit_count',
             text = {
             text = {
                 (inArgs('count') and not inArgs('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()
                 if inArgs('avg_hits') or inArgs('count') then
                 if inArgs('avg_hits') or inArgs('count') then
                     return { 'avg', 'total' }
                     return { (inArgs('count') and not OPTIONS.use_avg) and 'each' or 'avg', 'total' }
                 end
                 end
                 return { 'total' }
                 return { 'total' }
             end)(),
             end)(),
             is_visible = inArgs('avg_hits') or inArgs('count') or false
             is_visible = ((inArgs('avg_hits') or inArgs('count')) and not OPTIONS.no_max) or false
         }
         }
     }
     }
Line 457: Line 765:
     function TABLE:new()
     function TABLE:new()
         return self:tag('tr')
         return self:tag('tr')
    end
    function sortPassives(s)
        local passives = {}
        for passive in s:gmatch("_passive%d+") do
            table.insert(passives, passive)
        end
        if #passives == 0 then
            return s
        end
        table.sort(passives)
        local base = s:gsub("_passive%d+", "")
        return base .. table.concat(passives)
     end
     end


Line 479: Line 774:
         local current_list = { main_key }
         local current_list = { main_key }


         for i = #TABLE_CONTENT, 2, -1 do
         for i = #TABLE_CONTENT, 1, -1 do
             local current_row = TABLE_CONTENT[i]
             local current_row = TABLE_CONTENT[i]
             local new_list = {}
             local new_list = {}


             -- Check if it's the first iteration. If so, append phrases.
             -- Check if it's the first iteration. If so, append phrases.
             if i == #TABLE_CONTENT then
             if not current_row.no_damage then
                for _, keyword in ipairs(current_row.keywords) do
                if i == #TABLE_CONTENT then
                    local new_key = keyword .. '_' .. main_key
                    for _, keyword in ipairs(current_row.keywords) do
                    table.insert(new_list, new_key)
                        if not OPTIONS.no_max or (OPTIONS.no_max and keyword ~= 'total') then
                end
                            local new_key = keyword .. '_' .. main_key
            elseif current_row.is_visible then
                            table.insert(new_list, new_key)
                -- Iterate through previous keys
                        end
                for _, prev_key in ipairs(all_list) do
                    end
                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
                         local new_key = prev_key .. '_' .. keyword
                         -- Iterate through previous keys
 
                        for _, prev_key in ipairs(all_list) do
                        -- If needed, move the suffix to the rightmost of main_key.
                            local new_key = prev_key .. '_' .. keyword
                        if current_row.keyword_next_to_main_key then
                            -- If needed, move the suffix to the rightmost of main_key.
                            new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
                            if current_row.keyword_next_to_main_key then
                                new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
                            elseif current_row.keyword_first then
                                new_key = keyword .. '_' .. prev_key
                            end
                            table.insert(new_list, new_key)
                         end
                         end
                        table.insert(new_list, new_key)
                     end
                     end
                 end
                 end
            end


            -- Append new_list to all_list
                -- Append new_list to all_list
            for _, new_key in ipairs(new_list) do
                for _, new_key in ipairs(new_list) do
                table.insert(all_list, sortPassives(new_key))
                    table.insert(all_list, sortPassives(new_key))
                end
             end
             end
         end
         end


         return all_list
         -- Sort the list once more, in order to swap the order of cancel & full.
    end
        if inArgs('cancel_dmg') then
 
            local new_list = {}
    function doContentByMode(mode)
            local cancel_counter = 1
        local mode_row = TABLE:new()
            local full_counter = 2
        mode_row:tag('td'):wikitext(frame:expandTemplate { title = mode })
            for i, damage_key in ipairs(all_list) do
        local damage_entries = returnDamageInOrder()
                local regex = "^(%w+_)"
 
                local prefix = 'cancel_'
        for _, damage_key in ipairs(damage_entries) do
                 local match = string.match(damage_key, regex)
            if args.dump_names ~= 'true' then
                 if (match == prefix) then
                 local damage_number = WITH_RANGE[mode][damage_key]
                     new_list[i] = damage_key:gsub(prefix, "")
 
                 else
                -- Display ranges.
                     new_list[i] = prefix .. damage_key
                 if damage_number and damage_number.min == damage_number.max then
                     damage_number = damage_number.min
                 elseif damage_number then
                     damage_number = damage_number.min ..
                        '<span style="white-space: nowrap;"> ~</span> ' .. damage_number.max
                 end
                 end
                mode_row:tag('td'):wikitext(damage_number
                    -- Error out if it doesn't exist
                    or frame:expandTemplate {
                        title = 'color',
                        args = { 'red', '&#35;ERROR' }
                    })
            else
                mode_row:tag('td'):wikitext(damage_key)
             end
             end
            all_list = new_list
         end
         end
        return all_list
     end
     end


     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 554: Line 842:


         for row_index, row in ipairs(TABLE_CONTENT) do
         for row_index, row in ipairs(TABLE_CONTENT) do
            local new_row = TABLE:new()
            if iterations == 0 and not initial_header_cell then
                initial_header_cell = doInitialCell(new_row)
            end
             if row.is_visible then
             if row.is_visible then
                local new_row = TABLE:new()
                 local next_multiplier = 0
                 local next_multiplier = 0


                -- Only spawn the initial cell in the first generated row.
                if iterations == 0 and not initial_header_cell then
                    initial_header_cell = doInitialCell(new_row)
                end


                 --[[
                 --[[
Line 577: Line 865:
                 -- Now we can spawn our header cells depending on what is known.
                 -- Now we can spawn our header cells depending on what is known.
                 for i = 1, (current_multiplier == 0 and 1 or current_multiplier), 1 do
                 for i = 1, (current_multiplier == 0 and 1 or current_multiplier), 1 do
                     if row_index > 0 then
                     for _, text in ipairs(row.text) do
                        for _, text in ipairs(row.text) do
                        local new_cell = new_row:tag('th')
                            local new_cell = new_row:tag('th')
                        new_cell:attr('colspan', colspan_value):wikitext(text)
                            new_cell:attr('colspan', colspan_value):wikitext(text)
                        next_multiplier = next_multiplier + 1
                            next_multiplier = next_multiplier + 1
                        end
                     end
                     end
                 end
                 end
Line 589: Line 875:
             end
             end
         end
         end
         initial_header_cell:attr('rowspan', iterations > 1 and iterations + 1 or iterations)
        -- Apply rowspan of the same value as iteration count.
         initial_header_cell:attr('rowspan', iterations)
    end
 
    -- Helper function to display ranges.
    function doRangeText(damage_number)
        if damage_number and damage_number.min == damage_number.max then
            damage_number = damage_number.min
        elseif damage_number then
            damage_number = damage_number.min ..
                '<span style="white-space: nowrap;"> ~</span> ' .. damage_number.max
        end
        return damage_number
    end
 
    function doContentByMode(mode)
        local mode_row = TABLE:new()
        mode_row:tag('td'):wikitext(frame:expandTemplate { title = translate(mode) })
        local damage_entries = returnDamageInOrder()
        local last_number
        local last_unique_cell
 
        for _, damage_key in ipairs(damage_entries) do
            if args.dump_names ~= 'true' then
                local damage_number = FINAL_DAMAGE[mode][damage_key]
                damage_number = doRangeText(damage_number)
 
                if last_number ~= damage_number then
                    -- Display ranges.
                    local new_cell = mode_row:tag('td'):wikitext(damage_number
                        -- Error out if it doesn't exist
                        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
                mode_row:tag('td'):wikitext(damage_key)
            end
        end
     end
     end


     function doTable()
     function doTable()
         doHeaders()
         doHeaders()
         doContentByMode('PvE')
         forEach(doContentByMode)
        doContentByMode('PvP')
     end
     end


     doTable()
     if OPTIONS.do_table then
        doTable()
    end


     -- Dump all values if wanted.
     -- Dump all values if wanted.
     if args.dump == 'true' then
     if OPTIONS.dump_table_data then
         return inspect_dump(TABLE_CONTENT, frame)
        return inspect_dump(frame, TABLE_CONTENT)
    elseif OPTIONS.dump then
        return inspect_dump(frame, FINAL_DAMAGE)
    elseif OPTIONS.dump_parsed then
        return inspect_dump(frame, DAMAGE_PARSED)
    end
 
    local bug = ''
    if OPTIONS.bug then
        bug = frame:expandTemplate {
            title = translate('SkillText'),
            args = { 'FreeTraining' }
        }
    end
 
    -- Transform into variables
    local variables = doVariables(frame, FINAL_DAMAGE, OPTIONS.prefix)
 
    if out ~= nil then
         return inspect_dump(frame, out)
     end
     end


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


return p
return p
-- pyend

Latest revision as of 16:55, 18 March 2024

Documentation for this module may be created at Module:Test/doc

-- pystart
require('Module:CommonFunctions')
local i18n = require('Module:I18n')
local getArgs = require('Module:Arguments').getArgs
local inspect = require('Module:Inspect').inspect
local getTranslations = i18n.getTranslations
local p = {}

-- Main process
function p.main(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)
        if args[key] ~= nil then
            return true
        end
    end

    local modes = { 'PvE', 'PvP' }

    -- Define the schema for the table
    local tableSchema = {}
    for _, mode in ipairs(modes) do
        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 createDamageDataTable()
        local newTable = {}
        for key, value in pairs(tableSchema) do
            if type(value) == "table" then
                newTable[key] = {}
            end
        end
        return newTable
    end

    -- User requested options
    local OPTIONS = {
        do_table = args[1] == 'true',
        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',
        no_max = args.no_max == 'true',
        is_append = args.append ~= nil,
        append_index = args.append and tonumber(split(args.append)[1]),
        append_name = args.append and split(args.append)[2],
        combine_suffix = args.combine_suffix and (' ' .. args.combine_suffix) or '',
        combine = (function()
            local output = {}
            if not args.combine then
                return nil
            end
            for _, passive_key in ipairs(split(args.combine)) do
                table.insert(output, tonumber(passive_key))
            end
            if #output == 0 then
                return nil
            end
            return output
        end)(),
        perm_buff = {
            PvE = args.perm_buff or 1,
            PvP = args.pvp_perm_buff or args.perm_buff or 1
        },
        bug = args.bug == 'true',
        dump = args.dump == 'true',
        dump_table_data = args.dump_table_data == 'true',
        dump_parsed = args.dump_parsed == 'true',
        prefix = args.prefix,
        use_avg = args.use_avg == 'true',
        dmp = args.dmp == 'true' and 3 or args.dmp
    }

    -- Define a table with parsed damage information of all kind.
    local BASIC_DAMAGE = createDamageDataTable()

    -- Define a table with trait names and their values to apply.
    local TRAITS = {
        -- An empty trait so we keep the original values there.
        {
            key = '',
            name = translate('Normal'),
            value = 1
        },
        {
            key = 'enhanced',
            name = translate('Enhanced (Trait)'),
            value = args.enhanced ~= nil and 0.8
        },
        {
            key = 'empowered',
            name = translate('Empowered'),
            value = args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false
        },
        {
            key = 'useful',
            name = translate('Useful'),
            value = (args.hits_useful or args.avg_hits_useful) and (args.useful_penalty or args.useful or 0.8) or false
        },
        {
            key = 'heavy',
            name = translate('Heavy'),
            value = args.heavy ~= nil and 1.44
        }
    }

    function eval(s)
        return frame:preprocess('{{#expr:' .. s .. '}}')
    end

    -- A table with user-requested passive skills (empty by default).
    local PASSIVES = {}
    -- A table with non-numeric arguments to split.
    local TO_SPLIT = { 'append', 'awk_alias' }

    for k, v in pairs(args) do
        if string.find(k, 'passive') then
            --[[
            Fix up the passives and put them into a separate table.
            |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_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)] = {
                name = passive_name,
                value = passive_values[1],
                value_pvp = passive_values[2],
                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 '',
                prefix = args['prefix' .. passive_index] and (args['prefix' .. passive_index] .. ' ') or '',
                exist = frame:preprocess('{{#ifexist:' .. passive_name .. '|true|false}}') == 'true'
            }
        elseif string.match(v, '^[()+%-*/%d%s,.i]+$') then
            --[[
            Change how args are received.
            dmg = 500, 700, 800 (string) -> dmg = { 500, 700, 800 } (table)
            --]]
            local split_values = split(v)
            -- Perform automatic math on each value.
            for k2, v2 in pairs(split_values) do
                if not string.find(v, '[a-zA-Z]+') then
                    split_values[k2] = eval(v2)
                end
            end
            args[k] = split_values
        elseif inArrayHasValue(k, TO_SPLIT) then
            args[k] = split(v)
        end
    end

    -- Set basic hit count to 1 for all damage.
    for k, v in ipairs(args.dmg) do
        if not args.hits then
            args.hits = {}
        end
        if not args.hits[k] then
            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

    -- 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).
    local BASE_DAMAGE_CONFIG = {
        total_damage = {
            damage_numbers = { 'dmg' },
            hit_counts = { 'hits' },
            provided = { 'dmg' }
        },
        total_damage_awk = {
            damage_numbers = { 'awk_dmg', 'dmg' },
            hit_counts = { 'awk_hits', 'hits' },
            provided = { 'awk_dmg', 'awk_hits' }
        },
        avg_damage = {
            damage_numbers = { 'dmg' },
            hit_counts = { 'avg_hits', 'hits' },
            provided = { 'avg_hits' }
        },
        avg_damage_awk = {
            damage_numbers = { 'awk_dmg', 'dmg' },
            hit_counts = { 'avg_awk_hits', 'awk_hits', 'avg_hits', 'hits' },
            provided = { 'avg_awk_hits', args.awk_dmg and 'avg_hits' or nil }
        },
        -- Store the logic for Useful traits
        total_damage_useful = {
            damage_numbers = { 'dmg' },
            hit_counts = { 'hits_useful', 'hits' },
            provided = { 'hits_useful' }
        },
        total_damage_awk_useful = {
            damage_numbers = { 'awk_dmg', 'dmg' },
            hit_counts = { 'awk_hits_useful', 'awk_hits', 'hits_useful', 'hits' },
            provided = { 'awk_hits_useful' }
        },
        avg_damage_useful = {
            damage_numbers = { 'dmg' },
            hit_counts = { 'avg_hits_useful', 'hits_useful', 'avg_hits', 'hits' },
            provided = { 'avg_hits_useful' }
        },
        avg_damage_awk_useful = {
            damage_numbers = { 'awk_dmg', 'dmg' },
            hit_counts = { 'avg_awk_hits_useful', 'avg_awk_hits', 'hits_useful', 'hits' },
            provided = { 'avg_awk_hits_useful' }
        },
    }

    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()
    function parseConfig(mode)
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
        for config_key, config_value in pairs(DAMAGE_CONFIG) do
            for k, v in pairs(config_value) do
                local output_value = {}

                -- When both min and max are found, we need to break from the loop.
                local isValueFound = { min = false, max = false }

                for _, v2 in ipairs(v) do -- This array holds the argument names with fallbacks
                    forEachDamageType(function(damage_type)
                        -- If there already is a value for this damage type (min or max), do not continue.
                        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
                            else
                                output_value[damage_type] = {}
                            end
                        end
                    end)

                    -- Both values found, we can now break the loop.
                    if isValueFound.min and isValueFound.max then
                        break
                    end
                end
                if DAMAGE_PARSED[mode][config_key] == nil then
                    DAMAGE_PARSED[mode][config_key] = {}
                end
                DAMAGE_PARSED[mode][config_key][k] = output_value
            end
        end
    end

    forEach(parseConfig)

    -- Detected "count", for skills like Clementine, Enough Mineral, etc.
    function doEachDamage()
        local WITH_EACH = table.deep_copy(DAMAGE_PARSED)
        for mode, mode_content in pairs(DAMAGE_PARSED) do
            for damage_key, damage_value in pairs(mode_content) do
                if string.find(damage_key, 'total_') then
                    local new_value = table.deep_copy(damage_value)

                    forEachDamageType(function(damage_type)
                        for k, hit_count in ipairs(new_value.hit_counts[damage_type]) do
                            hit_count = hit_count == '' and 1 or hit_count
                            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)

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

    if args.count then
        DAMAGE_PARSED = doEachDamage()
    end

    function doBasicDamage()
        for mode, mode_content in pairs(DAMAGE_PARSED) do
            for damage_key, damage_value in pairs(mode_content) do
                forEachDamageType(function(damage_type)
                    local i = 1
                    local output = 0
                    -- Check if to even generate the damage.
                    if damage_value.provided then
                        -- Loop through damage numbers and multiply them with hits.
                        for k, damage_number in ipairs(damage_value.damage_numbers[damage_type]) do
                            local hit_count = damage_value.hit_counts[damage_type][i]
                            hit_count = hit_count == '' and 1 or hit_count
                            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)
            end
        end
    end

    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()
    function doTraits()
        -- Handle traits here
        for mode, mode_content in pairs(BASIC_DAMAGE) do
            for damage_key, damage_value in pairs(mode_content) do
                for _, trait in pairs(TRAITS) do
                    --[[
                    Suffix all damage values with existing traits.
                    Useful already has the prefix, so only multiply with its value.
                    Also, we don't want other traits to multiply with Useful,
                    so we skip those situations, as impossible in-game.
                    --]]
                    if (trait.value and trait.key ~= 'useful') or (string.find(damage_key, 'useful') and trait.key == 'useful') then
                        forEachDamageType(function(damage_type)
                            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
    end

    doTraits()

    local WITH_PASSIVES = createDamageDataTable()

    --[[
    Generates passives with every possible combinations of all subsets.
    For example: 3 passives are given, so it will generate the following:
    (1), (2), (3), (1, 2), (1, 3), (1, 2, 3), (2, 3)
    ]]
    function doPassives()
        for mode, mode_content in pairs(WITH_TRAITS) do
            for damage_key, damage_value in pairs(mode_content) do
                forEachDamageType(function(damage_type)
                    local combinations = { {} }
                    for passive_key, passive in pairs(PASSIVES) do
                        local count = #combinations
                        for i = 1, count do
                            local new_combination = { unpack(combinations[i]) }
                            table.insert(new_combination, passive_key)
                            table.insert(combinations, new_combination)
                        end
                    end
                    for _, combination in pairs(combinations) do
                        local passive_multiplier = 1
                        local name_suffix = ''
                        if #combination > 0 then
                            table.sort(combination)
                            for _, passive_key in pairs(combination) do
                                passive_multiplier = passive_multiplier *
                                    tonumber(PASSIVES[passive_key][mode == 'PvE' and 'value' or 'value_pvp'])
                                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
                        WITH_PASSIVES[mode][new_damage_key][damage_type] = damage_value[damage_type] * passive_multiplier
                    end
                end)
            end
        end
    end

    doPassives()

    local RANGE = {
        min_count = args.range_min_count and args.range_min_count[1],
        max_count = args.range_max_count and args.range_max_count[1],
        PvE = {
            min = args.range_min and args.range_min[1],
            max = args.range_max and args.range_max[1]
        },
        PvP = {
            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])
        }
    }

    local WITH_RANGE = createDamageDataTable()
    function doDamageBuffRange()
        -- Handle damage range here
        for mode, mode_content in pairs(WITH_PASSIVES) do
            for damage_key, damage_value in pairs(mode_content) do
                WITH_RANGE[mode][damage_key] = { min = 0, max = 0 }
                forEachDamageType(function(damage_type)
                    local range_count = RANGE[damage_type .. '_count'] or 1;
                    -- If min count preset, use range_max for the multiplier.
                    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)
                end)
            end
        end
    end

    doDamageBuffRange()

    local FINAL_DAMAGE = WITH_RANGE

    -- Helper function to iterate over traits.
    function checkTraits(settings)
        local output
        if not settings then
            output = false
        else
            output = settings.output or {}
        end

        for trait_index, trait in ipairs(TRAITS) do
            if trait.value ~= false and trait_index ~= 1 then
                if settings and type(settings.action) == 'function' then
                    settings.action(trait, output, settings)
                else
                    return true
                end
            end
        end
        return output
    end

    -- Helper function to iterate over passives.
    function checkPassives(settings)
        local output = settings.output or {}
        local PASSIVES_WITH_COMBINED = table.deep_copy(PASSIVES)

        -- Handle combined passives properly.
        if OPTIONS.combine then
            table.insert(PASSIVES_WITH_COMBINED, {
                is_combined = true
            })
        end

        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 {}) or inArrayHasValue(passive_index, args.display_separated or {})) then
                if type(settings.action) == 'function' then
                    settings.action(passive, output, passive_index)
                else
                    return true
                end
            end
        end
        return output
    end

    -- Generate the table
    local TABLE = mw.html.create('table'):attr({
        cellpadding = 5,
        border = 1,
        style = 'border-collapse: collapse; text-align: center',
        class = 'colortable-' .. OPTIONS.character
    })

    -- Our table structure
    local TABLE_CONTENT = {
        {
            type = 'extra',
            text = { translate('Average') },
            is_visible = OPTIONS.no_max,
            no_damage = true
        },
        {
            type = 'passives',
            text = checkPassives({
                output = { translate('Base') },
                action = function(passive, output)
                    if passive.is_combined then
                        -- Handling combined passive header name.
                        local combo = {}
                        for _, passive_key in ipairs(OPTIONS.combine) do
                            passive = PASSIVES[passive_key]
                            table.insert(combo,
                                link(passive.name, passive.alias, passive.prefix, passive.suffix, passive.exist))
                        end
                        table.insert(output, table.concat(combo, '/') .. OPTIONS.combine_suffix)
                    else
                        table.insert(output,
                            link(passive.name, passive.alias, passive.prefix, passive.suffix, passive.exist))
                    end
                end
            }),
            keywords = checkPassives({
                action = function(passive, output, passive_index)
                    if passive.is_combined then
                        -- Handling combined passive damage cells.
                        table.insert(output, sortPassives('passive' .. table.concat(OPTIONS.combine, '_passive')))
                    else
                        table.insert(output, 'passive' .. passive_index)
                    end
                end
            }),
            is_visible = not OPTIONS.no_max or #PASSIVES > 0
        },
        {
            type = 'passive_appended',
            text = {
                translate('Normal'),
                OPTIONS.is_append and
                link(PASSIVES[OPTIONS.append_index].name,
                    PASSIVES[OPTIONS.append_index].alias or OPTIONS.append_name 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
        },
        {
            type = 'awakening',
            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' },
            keyword_next_to_main_key = true,
            is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or inArgs('avg_awk_hits') or false
        },
        {
            type = 'traits',
            text = checkTraits({
                output = { translate('Normal') },
                action = function(trait, output)
                    table.insert(output, trait.name)
                end
            }),
            keywords = checkTraits({
                action = function(trait, output)
                    table.insert(output, trait.key)
                end
            }),
            is_visible = checkTraits()
        },
        {
            type = 'cancel',
            text = {
                translate('Cancel'),
                translate('Full'),
            },
            keywords = { 'cancel' },
            keyword_first = true,
            is_visible = inArgs('cancel_dmg')
        },
        {
            type = 'hit_count',
            text = {
                (inArgs('count') and not OPTIONS.use_avg) and
                (fillTemplate(translate('Per {1}'), { args.count_name or translate('Group') })) or
                translate('Average'),
                translate('Max')
            },
            keywords = (function()
                if inArgs('avg_hits') or inArgs('count') then
                    return { (inArgs('count') and not OPTIONS.use_avg) and 'each' or 'avg', 'total' }
                end
                return { 'total' }
            end)(),
            is_visible = ((inArgs('avg_hits') or inArgs('count')) and not OPTIONS.no_max) or false
        }
    }

    function TABLE:new()
        return self:tag('tr')
    end

    function returnDamageInOrder()
        local main_key = 'damage'
        local all_list = {}

        -- Initialize current list with main key
        local current_list = { main_key }

        for i = #TABLE_CONTENT, 1, -1 do
            local current_row = TABLE_CONTENT[i]
            local new_list = {}

            -- Check if it's the first iteration. If so, append phrases.
            if not current_row.no_damage then
                if i == #TABLE_CONTENT then
                    for _, keyword in ipairs(current_row.keywords) do
                        if not OPTIONS.no_max or (OPTIONS.no_max and keyword ~= 'total') then
                            local new_key = keyword .. '_' .. main_key
                            table.insert(new_list, new_key)
                        end
                    end
                elseif current_row.is_visible then
                    -- Append suffix for each keyword in current row
                    for _, keyword in ipairs(current_row.keywords) do
                        -- Iterate through previous keys
                        for _, prev_key in ipairs(all_list) do
                            local new_key = prev_key .. '_' .. keyword
                            -- If needed, move the suffix to the rightmost of main_key.
                            if current_row.keyword_next_to_main_key then
                                new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
                            elseif current_row.keyword_first then
                                new_key = keyword .. '_' .. prev_key
                            end
                            table.insert(new_list, new_key)
                        end
                    end
                end

                -- Append new_list to all_list
                for _, new_key in ipairs(new_list) do
                    table.insert(all_list, sortPassives(new_key))
                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

        return all_list
    end

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

    function doHeaders()
        local current_multiplier = 0 -- Keeps track of the number of cells to spawn
        local initial_header_cell    -- The leftmost cell that says "Mode"
        local iterations = 0         -- Keeps track of iterations that successfully rendered something. Required to tell the initial cell how many columns to span.

        for row_index, row in ipairs(TABLE_CONTENT) do
            if row.is_visible then
                local new_row = TABLE:new()
                local next_multiplier = 0

                -- Only spawn the initial cell in the first generated row.
                if iterations == 0 and not initial_header_cell then
                    initial_header_cell = doInitialCell(new_row)
                end

                --[[
                We need to know how the colspan will look like.
                So the solution is to loop through the table again and check how many cells will be spawned.
                And also multiply everything, because it is exponential.
                ]]
                local colspan_value = 1
                for k, v in ipairs(TABLE_CONTENT) do
                    if k > row_index and v.is_visible then
                        colspan_value = colspan_value * #v.text
                    end
                end

                -- Now we can spawn our header cells depending on what is known.
                for i = 1, (current_multiplier == 0 and 1 or current_multiplier), 1 do
                    for _, text in ipairs(row.text) do
                        local new_cell = new_row:tag('th')
                        new_cell:attr('colspan', colspan_value):wikitext(text)
                        next_multiplier = next_multiplier + 1
                    end
                end
                current_multiplier = next_multiplier
                iterations = iterations + 1
            end
        end
        -- Apply rowspan of the same value as iteration count.
        initial_header_cell:attr('rowspan', iterations)
    end

    -- Helper function to display ranges.
    function doRangeText(damage_number)
        if damage_number and damage_number.min == damage_number.max then
            damage_number = damage_number.min
        elseif damage_number then
            damage_number = damage_number.min ..
                '<span style="white-space: nowrap;"> ~</span> ' .. damage_number.max
        end
        return damage_number
    end

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

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

                if last_number ~= damage_number then
                    -- Display ranges.
                    local new_cell = mode_row:tag('td'):wikitext(damage_number
                        -- Error out if it doesn't exist
                        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
                mode_row:tag('td'):wikitext(damage_key)
            end
        end
    end

    function doTable()
        doHeaders()
        forEach(doContentByMode)
    end

    if OPTIONS.do_table then
        doTable()
    end

    -- Dump all values if wanted.
    if OPTIONS.dump_table_data then
        return inspect_dump(frame, TABLE_CONTENT)
    elseif OPTIONS.dump then
        return inspect_dump(frame, FINAL_DAMAGE)
    elseif OPTIONS.dump_parsed then
        return inspect_dump(frame, DAMAGE_PARSED)
    end

    local bug = ''
    if OPTIONS.bug then
        bug = frame:expandTemplate {
            title = translate('SkillText'),
            args = { 'FreeTraining' }
        }
    end

    -- Transform into variables
    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 '')
end

return p
-- pyend