Module:Damage: Difference between revisions

From Elwiki
No edit summary
No edit summary
Tag: Reverted
Line 2: Line 2:
require('Module:CommonFunctions');
require('Module:CommonFunctions');
local getArgs = require('Module:Arguments').getArgs
local getArgs = require('Module:Arguments').getArgs
local inspect = require('Module:Inspect').inspect
local p = {}
local p = {}


Line 8: Line 7:
function p.main(frame)
function p.main(frame)
     local args = getArgs(frame)
     local args = getArgs(frame)
    local out


     function inArgs(key)
     function inArgs(key)
Line 16: Line 14:
     end
     end


     local modes = { 'PvE', 'PvP' }
    -- Collect data from the input
     local data = {}
    local data_types = {
        'dmg',
        'awk_dmg',
        'hits',
        'avg_hits',
        'awk_hits',
        'avg_awk_hits',
        'hits_useful',
        'avg_hits_useful',
        'awk_hits_useful',
        'avg_awk_hits_useful',
        'perm_buff'
    }


     -- Define the schema for the table
     -- Handle the PvP split values
    local tableSchema = {}
     for k, v in spairs(data_types) do
     for _, mode in ipairs(modes) do
         table.insert(data_types, 'pvp_' .. v)
         tableSchema[mode] = {}
     end
     end


     function forEach(func)
     for k, v in spairs(data_types) do
         for _, mode in ipairs(modes) do
        local i = 1
            func(mode)
         if inArgs(v) then
            for k2, v2 in spairs(split(args[v])) do
                -- Check for operators. If detected, evaluate.
                if string.find(v2, '*') or string.find(v2, '+') then
                    v2 = frame:preprocess('{{#expr:' .. v2 .. '}}')
                end
 
                -- Check if proper hit count values provided. If empty string detected, inherit from 'hits'.
                if string.find(v, 'avg_') and string.find(v, '_hits') and v2 == '' then
                    data[v .. i] = data['avg_hits' .. i]
                elseif string.find(v, 'hits') and v2 == '' then
                    data[v .. i] = data['hits' .. i]
                elseif string.find(v, 'awk_dmg') and v2 == '' then
                    if string.find(v, 'pvp') then
                        data[v .. i] = data['pvp_dmg' .. i]
                    else
                        data[v .. i] = data['dmg' .. i]
                    end
                else
                    data[v .. i] = v2
                end
                i = i + 1
            end
         end
         end
     end
     end


     function forEachDamageType(func)
    -- For weird skills
         for _, damage_type in ipairs({ 'min', 'max' }) do
     function inheritMissing(keyTable, inheritTable)
             func(damage_type)
        local n = 1; -- counter for the func. argument loop
        local i;
         for k_key, v_key in spairs(keyTable) do
             if inArgs(inheritTable[n]) and not inArgs(v_key) then
                i = 1
                for k, v in spairs(split(args.dmg)) do
                    data[v_key .. i] = data[inheritTable[n] .. i]
                    i = i + 1
                end
            end
            n = n + 1
         end
         end
     end
     end


     -- Function to create a new table with the desired schema
     inheritMissing({'awk_dmg', 'pvp_awk_dmg', 'awk_hits', 'avg_awk_hits'}, {'dmg', 'pvp_dmg', 'hits', 'avg_hits'})
    function createDamageDataTable()
 
        local newTable = {}
    -- Laziness
        for key, value in pairs(tableSchema) do
    if args.hits and args.awk_dmg and not args.awk_hits then
            if type(value) == "table" then
        data.awk_hits = args.hits
                newTable[key] = {}
    end
            end
 
        end
    if args.awk_dmg and args.avg_hits and not args.avg_awk_hits then
         return newTable
         data.avg_awk_hits = args.avg_hits
     end
     end


     -- User requested options
     if args.awk_dmg and args.avg_hits_useful and not args.avg_awk_hits_useful then
    local OPTIONS = {
         data.avg_awk_hits_useful = args.avg_hits_useful
        do_table = args[1] == 'true',
     end
        character = args[2] or args.char or 'Elsword',
        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.
     -- Handle trait table
     local BASIC_DAMAGE = createDamageDataTable()
     local traits = {}


     -- Define a table with trait names and their values to apply.
     if inArgs('heavy') then
    local TRAITS = {
         traits.heavy = 1.44
        -- An empty trait so we keep the original values there.
     end
        {
            key = '',
            name = 'Normal',
            value = 1
        },
        {
            key = 'enhanced',
            name = 'Enhanced (Trait)',
            value = args.enhanced ~= nil and 0.8
        },
        {
            key = 'empowered',
            name = 'Empowered',
            value = args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false
         },
        {
            key = 'useful',
            name = '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 = 'Heavy',
            value = args.heavy ~= nil and 1.44
        }
     }


     function eval(s)
     if inArgs('enhanced') then
         return frame:preprocess('{{#expr:' .. s .. '}}')
         traits.enhanced = 0.8
     end
     end


     -- A table with user-requested passive skills (empty by default).
     -- Customizable for empowered, it had to be special lol.
    local PASSIVES = {}
    if inArgs('empowered') then
    -- A table with non-numeric arguments to split.
        if (args.empowered == 'true') then
     local TO_SPLIT = { 'append', 'awk_alias' }
            traits.empowered = 1.2
        else
            traits.empowered = args.empowered
        end
     end


     for k, v in pairs(args) do
     if args.useful == 'true' then
        if string.find(k, 'passive') then
        args.useful = 0.7
            --[[
    end
            Fix up the passives and put them into a separate table.
            |passive1=... |passive2=... -> { passive1, passive2 }
            --]]
            local passive_name = v
            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 .. '}}'));


            if is_custom then
    if args.useful_penalty == 'true' then
                passive_name = passive_values[#passive_values]
        args.useful_penalty = 0.7
                passive_values[#passive_values] = nil
    end
            end


            PASSIVES[tonumber(passive_index)] = {
    -- Output passives if provided
                name = passive_name,
    local passives = {}
                value = passive_values[1],
    for i = 1, 3 do
                value_pvp = passive_values[2],
        if inArgs('passive' .. i) then
                alias = args['alias' .. passive_index] or (passive_index == OPTIONS.append_index and OPTIONS.append_name),
            passives[i] = args['passive' .. i]
                suffix = args['suffix' .. passive_index] and (' ' .. args['suffix' .. passive_index]) or '',
            passives[i] = split(frame:preprocess('{{:' .. passives[i] .. '}}{{#arrayprint:' .. passives[i] .. '}}'))
                prefix = args['prefix' .. passive_index] and (args['prefix' .. passive_index] .. ' ') or '',
                exist = frame:preprocess('{{#ifexist:' .. passive_name .. '|true|false}}') == 'true'
            }
        elseif not string.find(v, '[a-hj-zA-HJ-Z]+') 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
     end
     end


     -- Set basic hit count to 1 for all damage.
     function list(ispvp)
    for k, v in ipairs(args.dmg) do
 
         if not args.hits then
        -- Define tables that hold the subsequent damage values.
             args.hits = {}
        -- I know this isn't the best, but I don't want to work with nested tables in this language.
        local fvals = {}
        local tvals = {}
        local pvals = {
            [1] = {},
            [2] = {},
            [3] = {},
            [12] = {},
            [13] = {},
            [23] = {},
            [123] = {}
        }
 
        -- Check the specified mode and define the prefixes/suffixes first.
        local pr = ''
        local su = ''
        local p_index = 1
         if ispvp then
             p_index = 2
         end
         end
         if not args.hits[k] then
 
             args.hits[k] = 1
         if (ispvp) then
             pr = 'pvp_'
            su = '_pvp'
         end
         end
    end


    -- Set basic hit count to 1 for all cancel damage.
        -- Define total/average damage calculation based on damage per hit and hit amount.
    if args.cancel_dmg then
        function getTotal(dmg, hits, fval, count)
        for k, v in ipairs(args.cancel_dmg) do
            -- Handle PvP prefixes/suffixes
             if not args.cancel_hits then
            if inArgs(pr .. dmg) then
                 args.cancel_hits = {}
                dmg = pr .. dmg
            end
             if inArgs(pr .. hits) then
                 hits = pr .. hits
             end
             end
             if not args.cancel_hits[k] then
 
                 args.cancel_hits[k] = 1
             if dmg == 'awk_dmg' and ispvp and not inArgs(pr .. 'awk_dmg') then
                 dmg = pr .. 'dmg'
             end
             end
        end
    end


    -- Store a configuration that will tell the main function how to behave given different inputs.
             fval = fval .. su
    -- 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 = {}
            local i = 1
    function handleCancel()
            fvals[fval] = 0
        local processed_keys = {}
            for k, v in spairs(split(args.dmg)) do
        for config_key, config_value in pairs(BASE_DAMAGE_CONFIG) do
                if -- If 'hits' defined, but 'avg_hits' not defined, inherit from 'hits'.
            if not config_key:match('cancel_') then
                (data[hits .. i] == nil and data['hits' .. i] ~= nil and hits == 'avg_hits') then
                local new_config_value = {}
                    data[hits .. i] = data['hits' .. i]
                 for arg_table_key, arg_table in pairs(config_value) do
                 elseif -- If 'hits' undefined, assume they're equal to 1.
                     local new_arg_table = {}
                (data[hits .. i] == nil) then
                    for _, arg in ipairs(arg_table) do
                     data[hits .. i] = 1
                         table.insert(new_arg_table, 'cancel_' .. arg)
                end
                -- Proceed to combine
                fvals[fval] = fvals[fval] + data[dmg .. i] * data[hits .. i] * (data[pr .. 'perm_buff' .. i] or data['perm_buff' .. i] or 1)
                i = i + 1
            end
            -- For skills with multiple same parts, ex. Clementine, Enough Mineral
            if count == true then
                fvals[fval] = fvals[fval] * args.count
                if inArgs('count_extra' .. su) then
                    if args.count_extra_hits == nil then
                         args.count_extra_hits = 1
                    end
                    if not string.find(fval, "each_") then
                        fvals[fval] = fvals[fval] + (args['count_extra' .. su] * args['count_extra_hits'])
                     end
                     end
                    new_config_value[arg_table_key] = new_arg_table
                 end
                 end
                local new_key = 'cancel_' .. config_key
            end
                 DAMAGE_CONFIG[new_key] = new_config_value
            -- Apply Useful modifier.
                processed_keys[new_key] = true
            if string.find(fval, 'useful') then
                 fvals[fval] = fvals[fval] * (args.useful_penalty or args.useful)
             end
             end
         end
         end
        return processed_keys
    end


    if args.cancel_dmg then
        -- Actually generate the values depending on arguments provided.
        handleCancel()
        if inArgs(pr .. 'dmg') then
        DAMAGE_CONFIG = table.fuse(BASE_DAMAGE_CONFIG, DAMAGE_CONFIG)
            if (inArgs('count')) then
    else
                getTotal('dmg', 'hits', 'each_damage')
         DAMAGE_CONFIG = BASE_DAMAGE_CONFIG
                getTotal('dmg', 'hits', 'total_damage', true)
    end
            else
                getTotal(pr .. 'dmg', 'hits', 'total_damage')
            end
 
            if inArgs('avg_hits') then
                getTotal('dmg', 'avg_hits', 'avg_damage')
            end
         end


    -- Helper function to check if a table is not empty
        if inArgs(pr .. 'awk_dmg') or inArrayStarts(pr .. 'awk_dmg', data) then
    local function isTableNotEmpty(tbl)
            getTotal('awk_dmg', 'awk_hits', 'total_damage_awk')
        return next(tbl) ~= nil
    end


    -- Function to apply inheritance for a specific damage type and argument
            if (inArgs('avg_hits') and (inArgs('awk_dmg') or inArgs('awk_hits'))) or inArgs('avg_awk_hits') then
    local function applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritValue)
                getTotal('awk_dmg', 'avg_awk_hits', 'avg_damage_awk')
        if mainArgValue == '' then
            end
            return inheritValue
        elseif mainArgValue and string.find(mainArgValue, 'i') and inheritValue then
            return eval(mainArgValue:gsub('i', inheritValue))
         end
         end
        return mainArgValue
    end


    -- Function to apply inheritance for a specific argument key
        -- Handling traits
    local function applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType)
        -- Useful handled separately
        local mainKey = argTable[1] .. damageType
        if inArgs('useful_penalty') or inArgs('useful') then
        local mainKeyPrefixed = prefix .. mainKey
            getTotal(pr .. 'dmg', 'hits_useful', 'total_damage_useful')
        local mainArgValues = args[mainKeyPrefixed]


        if mainArgValues then
            if inArgs('avg_hits_useful') then
            local i = 1
                getTotal('dmg', 'avg_hits_useful', 'avg_damage_useful')
             local cancelDmgLen = args.cancel_dmg and #args.cancel_dmg or 0
             end


             while i <= (#args.dmg + cancelDmgLen) do
             if inArgs(pr .. 'awk_dmg') and inArgs('awk_hits_useful') then
                 local mainArgValue = mainArgValues[i]
                 getTotal('awk_dmg', 'awk_hits_useful', 'total_damage_awk_useful')
            end


                 for ix, inheritKey in ipairs(argTable) do
            if inArgs(pr .. 'avg_awk_hits') and inArgs('avg_awk_hits_useful') then
                    local inheritArg = args[prefix .. inheritKey .. damageType] or args[inheritKey .. damageType]
                 getTotal('awk_dmg', 'avg_awk_hits_useful', 'avg_damage_awk_useful')
            end
        end


                    -- Basic damage/hits inheritance request detected. Ignore min/max.
        -- Multiply all values with traits and store them in another table.
                     if damageType and mainKey:gsub(damageType, "") == argTable[#argTable] then
        for k, v in spairs(fvals) do
                         inheritArg = args[prefix .. inheritKey] or args[inheritKey]
            if not string.find(k, 'useful') then
                for kt, vt in spairs(traits) do
                     if inArgs(kt) then
                        local dmg_name = k .. '_' .. kt
                        if ispvp then
                            dmg_name = dmg_name:gsub(su, '') .. su
                        end
                        local dmg_formula = v * vt
                         tvals[dmg_name] = dmg_formula
                     end
                     end
                end
            end
        end
        -- Get a table of merged base & trait values
        local ftvals = fvals
        tableMerge(ftvals, tvals)


                    if inheritArg and inheritArg[i] and
        function addPassive(num, loop_table)
                        (damageTypeIndex == 1 and ix ~= 1 or damageTypeIndex ~= 1) and tonumber(inheritArg[i])
            local pval_index
                    then
            if loop_table == nil then
                        mainArgValues[i] = applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritArg[i])
                pval_index = num
                        break
                loop_table = ftvals
                    end
            else
                pval_index = tonumber(loop_table .. num)
                loop_table = pvals[loop_table]
            end
            for k, v in spairs(loop_table) do
                local dmg_name = k .. '_passive' .. num
                if ispvp then
                    dmg_name = dmg_name:gsub(su, '') .. su
                 end
                 end
                local dmg_formula = v * passives[num][p_index]
                pvals[pval_index][dmg_name] = dmg_formula
            end
        end


                 i = i + 1
        -- Add passives and combine them.
        if inArgs('passive2') then
            addPassive(2)
            if inArgs('passive3') then
                 addPassive(3, 2)
             end
             end
         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
         if inArgs('passive1') then
             for argTableKey, argTable in pairs(configValue) do
             addPassive(1)
                if argTableKey ~= 'provided' and isTableNotEmpty(argTable) then
            if inArgs('passive2') then
                    for damageTypeIndex, damageType in ipairs({ '', '_min', '_max' }) do
                addPassive(2, 1)
                        applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType)
                if inArgs('passive3') then
                    end
                    addPassive(3, 12)
                 end
                 end
            end
            if inArgs('passive3') then
                addPassive(3, 1)
             end
             end
         end
         end
    end


    forEach(inherit)
        if inArgs('passive3') then
            addPassive(3)
        end


    local DAMAGE_PARSED = createDamageDataTable()
        -- Merge all tables into one.
    function parseConfig(mode)
         tableMerge(fvals, tvals)
         local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
         for k, v in spairs(pvals) do
         for config_key, config_value in pairs(DAMAGE_CONFIG) do
             tableMerge(fvals, v)
             for k, v in pairs(config_value) do
        end
                local output_value = {}


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


                for _, v2 in ipairs(v) do -- This array holds the argument names with fallbacks
    local out = list(false)
                    forEachDamageType(function(damage_type)
    local out_pvp = list(true)
                        -- 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 =
    -- Merge the output to a unified table.
                            args[prefix .. v2 .. '_' .. damage_type]
    tableMerge(out, out_pvp)
                            or args[v2 .. '_' .. damage_type]
                            or args[prefix .. v2]
                            or args[v2];


                        if arg_from_template ~= nil then
    -- Function wrapper for vardefine syntax in MW.
                            if k == 'provided' then
    function var(name, dmg, prefix)
                                output_value = true
        if prefix == nil then
                                -- Do not generate total_damage values at all if the skill can't reach them.
            prefix = ''
                                if string.find(config_key, 'total_') and OPTIONS.no_max then
        else
                                    output_value = false
            prefix = prefix .. '_'
                                end
        end
                            else
        if dmg == 0 then
                                if type(output_value) ~= "table" then
            dmg = 'N/A'
                                    output_value = {}
        else
                                end
            dmg = round(dmg)
                                output_value[damage_type] = arg_from_template
        end
                            end
        if (args.format == 'false' or dmg == 'N/A') then
                            -- Mark the value as found.
            return '{{#vardefine:' .. prefix .. name .. '|' .. dmg .. '}}'
                            isValueFound[damage_type] = true
        else
                        else
            return '{{#vardefine:' .. prefix .. name .. '|{{formatnum:' .. dmg .. '}}%}}'
                            if k == 'provided' then
        end
                                output_value = false
    end
                            else
                                output_value[damage_type] = {}
                            end
                        end
                    end)


                    -- Both values found, we can now break the loop.
    -- Apply ranges.
                    if isValueFound.min and isValueFound.max then
    function getRangeCount(arg)
                        break
        if inArgs(arg) then
                    end
            data[arg] = split(args[arg])
                end
            if data[arg][2] == nil then
                if DAMAGE_PARSED[mode][config_key] == nil then
                data[arg][2] = data[arg][1]
                    DAMAGE_PARSED[mode][config_key] = {}
                end
                DAMAGE_PARSED[mode][config_key][k] = output_value
             end
             end
         end
         end
     end
     end


     forEach(parseConfig)
     getRangeCount('range_min_count');
    getRangeCount('range_max_count')


    -- Detected "count", for skills like Clementine, Enough Mineral, etc.
     function determineRange(minmax)
     function doEachDamage()
         if inArgs('range_' .. minmax) then
         local WITH_EACH = table.deep_copy(DAMAGE_PARSED)
            data['range_' .. minmax] = split(args['range_' .. minmax])
        for mode, mode_content in pairs(DAMAGE_PARSED) do
             if data['range_' .. minmax][2] == nil then
             for damage_key, damage_value in pairs(mode_content) do
                 data['range_' .. minmax][2] = data['range_' .. minmax][1]
                 if string.find(damage_key, 'total_') then
            end
                    local new_value = table.deep_copy(damage_value)
            if inArgs('range_' .. minmax .. '_count') then
 
                local i = 1;
                    forEachDamageType(function(damage_type)
                for k, v in spairs(data['range_' .. minmax]) do
                        for k, hit_count in ipairs(new_value.hit_counts[damage_type]) do
                    data['range_' .. minmax][i] = 1 + (-1 + data['range_' .. minmax][i]) *
                            hit_count = hit_count == '' and 1 or hit_count
                                                      data['range_' .. minmax .. '_count'][i]
                            new_value.hit_counts[damage_type][k] = hit_count *
                     i = i + 1
                                ((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
             end
         end
         end
        return WITH_EACH
     end
     end


     if args.count then
     determineRange('min');
        DAMAGE_PARSED = doEachDamage()
     determineRange('max');
     end


     function doBasicDamage()
     -- If maximum range is specified, but not minimum, and minimum count is specified.
        for mode, mode_content in pairs(DAMAGE_PARSED) do
    -- By default, it would just do the same as with max, don't want that.
            for damage_key, damage_value in pairs(mode_content) do
    if inArgs('range_max') and not inArgs('range_min') then
                forEachDamageType(function(damage_type)
        data['range_min'] = {1, 1}
                    local i = 1
        if inArgs('range_min_count') then
                    local output = 0
            local range_max_arg = split(args.range_max);
                    -- Check if to even generate the damage.
            if range_max_arg[2] == nil then
                    if damage_value.provided then
                range_max_arg[2] = range_max_arg[1]
                        -- 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
            data['range_min'] = {1 + range_max_arg[1] * data['range_min_count'][1],
                                1 + range_max_arg[2] * data['range_min_count'][2]}
         end
         end
     end
     end


     doBasicDamage()
     local out_min = {}
    local out_max = {}


    -- Adding missing cancel part damage to full, so that repetition wouldn't be a problem.
     function applyRange(minmax)
     function addCancelDamage()
         local temp_tab = {};
         for mode, mode_content in pairs(BASIC_DAMAGE) do
        if minmax == 'min' then
             for damage_key, damage_value in pairs(mode_content) do
            temp_tab = out_min
                 local cancel_candidate = BASIC_DAMAGE[mode]['cancel_' .. damage_key]
        else
                forEachDamageType(function(damage_type)
            temp_tab = out_max
                     if not string.find(damage_key, 'cancel_') and cancel_candidate then
        end
                         BASIC_DAMAGE[mode][damage_key][damage_type] = damage_value[damage_type] +
        if inArgs('range_max') then
                            cancel_candidate[damage_type]
             for k, v in spairs(out) do
                 if not (string.starts(k, 'min_') or string.starts(k, 'max_')) then
                     if (string.find(k, '_pvp')) then
                         temp_tab[minmax .. '_' .. k] = v * data['range_' .. minmax][2];
                    else
                        temp_tab[minmax .. '_' .. k] = v * data['range_' .. minmax][1];
                     end
                     end
                 end)
                 end
             end
             end
         end
         end
        tableMerge(out, temp_tab)
     end
     end


     if args.cancel_dmg then
     applyRange('min');
         addCancelDamage()
    applyRange('max');
 
    -- Get the actual variables with MW syntax.
    local vars = {}
    for k, v in spairs(out) do
         table.insert(vars, var(k, v, args.prefix))
     end
     end


     local WITH_TRAITS = createDamageDataTable()
    -- Transform ranges to variables.
     function doTraits()
     local vars_range = {}
        -- Handle traits here
     if (inArgs('range_max')) then
         for mode, mode_content in pairs(BASIC_DAMAGE) do
         for k, v in spairs(out) do
             for damage_key, damage_value in pairs(mode_content) do
             if not (string.starts(k, 'min_') or string.starts(k, 'max_')) then
                for _, trait in pairs(TRAITS) do
                local prefix = ''
                    --[[
                if args.prefix ~= nil then
                    Suffix all damage values with existing traits.
                    prefix = args.prefix .. '_'
                    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
                table.insert(vars_range,
                    '{{#vardefine: ' .. prefix .. 'range_' .. k .. '|{{formatnum:' .. round(out_min['min_' .. k]) ..
                        '}}% ~ {{formatnum:' .. round(out_max['max_' .. k]) .. '}}%}}');
             end
             end
         end
         end
        indexTableMerge(vars, vars_range);
    end
    -- Dump all values if wanted.
    if args.dump == 'true' then
        local ret = {}
        for k, v in spairs(out) do
            table.insert(ret, k .. ': ' .. v)
        end
        return frame:preprocess(table.concat(ret, "<br/>"))
     end
     end


     doTraits()
     -- Parse all variables
    local parsed = frame:preprocess('{{ ' .. table.concat(vars) .. 'trim2}}')


     local WITH_PASSIVES = createDamageDataTable()
    if args[1] ~= 'true' and args.table ~= 'true' then
        return parsed
    end
 
    local char = args.char or args[2] or 'Elsword'
 
    -- Generate the table
     local tbl = mw.html.create('table'):attr({
        ['cellpadding'] = 5,
        ['border'] = 1,
        ['style'] = 'border-collapse: collapse; text-align: center',
        ['class'] = 'colortable-' .. char
    })
 
    -- For rowspan, colspan shenanigans
    function increaseSpace(el, type, num)
        if type == 'row' then
            type = 'rowspan'
        elseif type == 'col' then
            type = 'colspan'
        end
        num = num or 1
        return el:attr(type, tonumber(el:getAttr(type) or 1) + num)
    end


     --[[
     function multiplySpace(el, type, m)
    Generates passives with every possible combinations of all subsets.
         if not el then
    For example: 3 passives are given, so it will generate the following:
             return false
    (1), (2), (3), (1, 2), (1, 3), (1, 2, 3), (2, 3)
        end
    ]]
        if type == 'row' then
    function doPassives()
            type = 'rowspan'
         for mode, mode_content in pairs(WITH_TRAITS) do
        else
             for damage_key, damage_value in pairs(mode_content) do
            type = 'colspan'
                forEachDamageType(function(damage_type)
        end
                    local combinations = { {} }
        if m == nil then
                    for passive_key, passive in pairs(PASSIVES) do
             m = 2
                        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
        local span = el:getAttr(type) or 1
        return el:attr(type, tonumber(span) * m)
     end
     end


     doPassives()
     local combine = split(args.combine)
    local combine_suffix = args.combine_suffix
    local append = split(args.append)[1]
    local append_alias = split(args.append)[2]
 
    local tbl_order = {'extra', 'passives_normal', 'passives_switch', 'awk', 'traits', 'hit_count'}
    local STR = {
        BASE = 'Base',
        MODE = 'Mode',
        REGULAR = 'Regular',
        NORMAL = 'Normal',
        AWK = 'Awakening Mode',
        AVG = 'Average',
        MAX = 'Max',
        TRAIT = {'Enhanced', 'Empowered', 'Useful', 'Heavy'},
        PER = 'Per',
        INSTANCE = 'Instance'
    }
 
    local trait_args = {}
    for k, v in ipairs(STR.TRAIT) do
        table.insert(trait_args, string.lower(v))
    end
 
    local trait_count = 0
    for k, v in ipairs(trait_args) do
        if inArrayHas(v, out) then
            trait_count = trait_count + 1
        end
    end


     local RANGE = {
     local tbl_content = {
         min_count = args.range_min_count and args.range_min_count[1],
         extra = {
         max_count = args.range_max_count and args.range_max_count[1],
            mode = STR.MODE,
        PvE = {
            long = STR.AVG
             min = args.range_min and args.range_min[1],
        },
             max = args.range_max and args.range_max[1]
         passives_normal = {
            mode = STR.MODE,
            base = STR.BASE,
            combined = {},
             aliases = {args.alias1 or false, args.alias2 or false, args.alias3 or false},
             suffixes = {args.suffix1 or false, args.suffix2 or false, args.suffix3 or false}
         },
         },
         PvP = {
         passives_switch = {
             min = args.range_min and (args.range_min[2] or args.range_min[1]),
             normal = STR.NORMAL,
             max = args.range_max and (args.range_max[2] or args.range_max[1])
            hide = true
        },
        awk = {
            normal = STR.REGULAR,
            awk_link = '[[' .. STR.AWK .. ']]',
            hide = true
        },
        traits = {
             normal = STR.NORMAL,
            hide = true
        },
        hit_count = {
            avg = STR.AVG,
            max = STR.MAX,
            hide = true
         }
         }
     }
     }


     local WITH_RANGE = createDamageDataTable()
     local count_name = args.count_name or STR.INSTANCE
     function doDamageBuffRange()
     if inArgs('count') and not args.use_avg then
         -- Handle damage range here
         tbl_content.hit_count.avg = table.concat({STR.PER, count_name}, ' ')
        for mode, mode_content in pairs(WITH_PASSIVES) do
    end
            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;
    function getRowIndex(row)
                    WITH_RANGE[mode][damage_key][damage_type] = not OPTIONS.format and final_damage_value or
        for k, v in ipairs(tbl_order) do
                        formatDamage(final_damage_value)
            if row == v then
                 end)
                 return k
             end
             end
         end
         end
     end
     end


     doDamageBuffRange()
     for passive_i = 1, 3 do
        repeat
            -- Add normal passives to the first row.
            local passive_name = ''
            -- Alias for appended passives.
            if (inArgs('passive' .. passive_i)) then
                passive_name = args['passive' .. passive_i]
            end
            if indexOf(tostring(passive_i), combine) ~= nil then
                -- Add combined passives.
                tbl_content.passives_normal.combined[passive_i] = passive_name
            elseif tostring(passive_i) == append then
                -- Add switch passives.
                tbl_content.passives_switch[passive_i] = passive_name
                tbl_content.passives_switch.hide = false
                if append_alias ~= nil then
                    tbl_content.passives_switch.display_name = append_alias
                end
            elseif (inArgs('passive' .. passive_i)) then
                -- Add regular passives to the first row.
                tbl_content.passives_normal[passive_i] = passive_name
            end
        until true
    end


     local FINAL_DAMAGE = WITH_RANGE
     local ret = ''


     -- Helper function to iterate over traits.
     for trait_order, trait_arg in ipairs(trait_args) do
    function checkTraits(settings)
        -- Add traits if exist.
        local output
        if (inArgs(trait_arg)) then
        if not settings then
             tbl_content.traits[trait_arg] = args[trait_arg]
             output = false
             tbl_content.traits.hide = false
        else
             output = settings.output or {}
         end
         end
    end
    -- Add Useful trait.
    if inArgs('hits_useful') or inArgs('avg_hits_useful') then
        tbl_content.traits.useful = 'true'
        tbl_content.traits.hide = false
    end
    if inArgs('avg_hits') or inArgs('count') then
        -- Enable average/max if needed.
        tbl_content.hit_count.hide = false
    end
    if inArrayHas('awk_', args) then
        -- Enable Awakening if needed.
        tbl_content.awk.hide = false
    end
    local loop_factor, mode_th;
    local cells = {}
    local passive_normal_count = 0;
    local passive_switch_count = 0;


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


     -- Helper function to iterate over passives.
     local hit_count_table = {'total'}
     function checkPassives(settings)
     local awk_table = {''}
        local output = settings.output or {}
    local levels_exist =
        local PASSIVES_WITH_COMBINED = table.deep_copy(PASSIVES)
        (next(passives) or args.append or args.awk_hits or args.awk_dmg or args.count)
    local no_max = args.no_max == 'true'


        -- Handle combined passives properly.
    function makePassiveLink(passive, alias, suffix, nil_cond)
         if OPTIONS.combine then
         if nil_cond == nil then
             table.insert(PASSIVES_WITH_COMBINED, {
             nil_cond = true
                is_combined = true
            })
         end
         end
        if nil_cond and alias ~= nil and alias ~= false then
            alias = '|' .. alias
        else
            alias = ''
        end
        suffix = suffix or ''
        passive = '[[' .. passive .. alias .. ']]' .. suffix
        return passive
    end
    -- Begin the main loop.
    for k, type in ipairs(tbl_order) do
        repeat
            local tr = tbl:tag('tr')
            local data = tbl_content[type];
            local hide = data.hide;
            cells[type] = {}


        for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do
            function new(wikitext, normal)
            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
                local th = tr:tag('th'):wikitext(wikitext)
                 if type(settings.action) == 'function' then
                 if normal == true then
                     settings.action(passive, output, passive_index)
                    table.insert(cells[type].normal_th, th)
                elseif normal == false then
                     table.insert(cells[type].th, th)
                 else
                 else
                     return true
                     return th
                 end
                 end
             end
             end
        end
        return output
    end


    -- Generate the table
             function multiplySpaceAll(level, num)
    local TABLE = mw.html.create('table'):attr({
                if cells[level] == nil then
        cellpadding = 5,
                     return false
        border = 1,
        style = 'border-collapse: collapse; text-align: center',
        class = 'colortable-' .. OPTIONS.character
    })
 
    -- Our table structure
    local TABLE_CONTENT = {
        {
             type = 'extra',
            text = { 'Average' },
            is_visible = OPTIONS.no_max,
            no_damage = true
        },
        {
            type = 'passives',
            text = checkPassives({
                output = { '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
                 end
            }),
                num = num or 2
            keywords = checkPassives({
                 for k, v in ipairs(cells[level].th) do
                 action = function(passive, output, passive_index)
                     multiplySpace(v, 'col', num)
                    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
            }),
                 for k, v in ipairs(cells[level].normal_th) do
            is_visible = not OPTIONS.no_max or #PASSIVES > 0
                     multiplySpace(v, 'col', num)
        },
        {
            type = 'passive_appended',
            text = {
                 '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 = { 'Regular', (function()
                if OPTIONS.dmp then
                     return link('Dynamo Point System', 'Dynamo Configuration', args.awk_prefix,
                    OPTIONS.dmp ~= 'false' and ('(' .. OPTIONS.dmp .. ' 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
                 end
                return link('Awakening Mode', nil, args.awk_prefix, args.awk_suffix)
             end
             end)()
 
            },
             function reverseMultiplySpace(num, increase_mode)
             keywords = { 'awk' },
                local i = #tbl_order;
            keyword_next_to_main_key = true,
                num = num or 2
            is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or inArgs('avg_awk_hits') or false
 
        },
                 local fix_for_no_max = 0
        {
                 if no_max and not levels_exist then
            type = 'traits',
                     fix_for_no_max = -1
            text = checkTraits({
                 output = { 'Normal' },
                 action = function(trait, output)
                     table.insert(output, trait.name)
                 end
                 end
            }),
 
            keywords = checkTraits({
                 while (i > 1) do
                 action = function(trait, output)
                     multiplySpaceAll(tbl_order[i - 1 + fix_for_no_max], num)
                     table.insert(output, trait.key)
                    i = i - 1
                 end
                 end
            }),
                 if increase_mode ~= false then
            is_visible = checkTraits()
                     increaseSpace(mode_th, 'row')
        },
        {
            type = 'cancel',
            text = {
                 'Cancel', 'Full'
            },
            keywords = { 'cancel' },
            keyword_first = true,
            is_visible = inArgs('cancel_dmg')
        },
        {
            type = 'hit_count',
            text = {
                (inArgs('count') and not OPTIONS.use_avg) and
                (table.concat({ 'Per', args.count_name or 'Group' }, ' ')) or 'Average',
                '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
                 end
                return { 'total' }
             end
             end)(),
            is_visible = ((inArgs('avg_hits') or inArgs('count')) and not OPTIONS.no_max) or false
        }
    }


    function TABLE:new()
            cells[type].normal_th = {}
        return self:tag('tr')
            cells[type].th = {}
    end


    function returnDamageInOrder()
            if (type == 'extra' and no_max) then
        local main_key = 'damage'
                mode_th = new(data.mode)
        local all_list = {}
                new(data.long, true)
            end


        -- Initialize current list with main key
            if (type == 'passives_normal') then
        local current_list = { main_key }
                if not no_max then
                    mode_th = new(data.mode)
                else
                    reverseMultiplySpace(nil, false);
                end


        for i = #TABLE_CONTENT, 1, -1 do
                if (no_max and levels_exist) or not no_max then
            local current_row = TABLE_CONTENT[i]
                    new(data.base, true)
            local new_list = {}
                end


            -- Check if it's the first iteration. If so, append phrases.
                 for i = 1, 3, 1 do
            if not current_row.no_damage then
                    local passive_link = data[i]
                 if i == #TABLE_CONTENT then
                     if (passive_link ~= nil) then
                     for _, keyword in ipairs(current_row.keywords) do
                        local suffix = ''
                         if not OPTIONS.no_max or (OPTIONS.no_max and keyword ~= 'total') then
                         if next(data.suffixes) and data.suffixes[i] ~= false then
                             local new_key = keyword .. '_' .. main_key
                             suffix = ' ' .. data.suffixes[i]
                            table.insert(new_list, new_key)
                         end
                         end
                        passive_link = makePassiveLink(passive_link, data.aliases[i], suffix, next(data.aliases))
                        new(passive_link, false)
                        passive_normal_count = passive_normal_count + 1
                     end
                     end
                 elseif current_row.is_visible then
                 end
                     -- Append suffix for each keyword in current row
 
                     for _, keyword in ipairs(current_row.keywords) do
                -- Handle combining passives.
                         -- Iterate through previous keys
                if next(data.combined) then
                        for _, prev_key in ipairs(all_list) do
                     local combined_str = ''
                            local new_key = prev_key .. '_' .. keyword
                     for k, v in spairs(data.combined) do
                            -- If needed, move the suffix to the rightmost of main_key.
                         combined_str = combined_str .. makePassiveLink(v, data.aliases[k], data.suffixes[k]) .. '/'
                            if current_row.keyword_next_to_main_key then
                    end
                                new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
                    combined_str = combined_str:gsub('/$', '')
                            elseif current_row.keyword_first then
                    if combine_suffix then
                                 new_key = keyword .. '_' .. prev_key
                        combined_str = combined_str .. ' ' .. combine_suffix
                    end
                    new(combined_str, false)
                    passive_normal_count = passive_normal_count + 1
                end
 
            end
 
            if (type == 'passives_switch') then
                if not hidden(type) then
                    multiplySpaceAll('passives_normal')
                    -- For some reason, whenever appending is active, it misses a 1 in rowspan of this cell.
                    increaseSpace(mode_th, 'row')
                end
 
                -- Passives that appear in the second row
                loop_factor = (passive_normal_count + 1)
                for ix = 1, loop_factor, 1 do
                    for i = 1, 3, 1 do
                        if (data[i] ~= nil) then
                            new(data.normal, true);
                            local suffix = ''
                            if next(tbl_content.passives_normal.suffixes) and tbl_content.passives_normal.suffixes[i] ~= false then
                                 suffix = ' ' .. tbl_content.passives_normal.suffixes[i]
                             end
                             end
                             table.insert(new_list, new_key)
                             local passive_link = data[i]
                            passive_link = makePassiveLink(passive_link, data.display_name, suffix)
                            new(passive_link, false)
 
                            if (ix == 1) then
                                -- Count switch passives. Only one iteration.
                                passive_switch_count = passive_switch_count + 1
                            end
 
                         end
                         end
                     end
                     end
                 end
                 end
            end


                 -- Append new_list to all_list
            if (type == 'awk' and not hide) then
                 for _, new_key in ipairs(new_list) do
                 reverseMultiplySpace();
                     table.insert(all_list, sortPassives(new_key))
                 table.insert(awk_table, 'awk')
 
                loop_factor = loop_factor * (passive_switch_count + 1)
 
                for i = 1, loop_factor, 1 do
                     new(data.normal, true)
                    new(data.awk_link, false)
                 end
                 end
             end
             end
        end


        -- Sort the list once more, in order to swap the order of cancel & full.
            if (type == 'traits' and not hide) then
        if inArgs('cancel_dmg') then
                 if trait_count == 2 then
            local new_list = {}
                     reverseMultiplySpace(3);
            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
                 else
                     new_list[i] = prefix .. damage_key
                     reverseMultiplySpace();
                end
 
                -- Manually fix certain situations.
                local has_awk = 1
                if not hidden('awk') then
                    has_awk = 2
                 end
                 end
            end
            all_list = new_list
        end


        return all_list
                local extra = 1
    end
                if hidden('awk') and not hidden('passives_switch') then
                    extra = 2
                end


    function doInitialCell(new_row)
                loop_factor = loop_factor * has_awk
        return new_row:tag('th'):wikitext('Mode')
                for i = 1, loop_factor * extra, 1 do
    end
                    local ix = 1
                    new(data.normal, true)
                    for k, trait_name in ipairs(trait_args) do
                        if data[trait_name] ~= nil then
                            new(STR.TRAIT[ix], false)
                        end
                        ix = ix + 1
                    end
                end
            end


    function doHeaders()
            if (type == 'hit_count' and not hide) then
        local current_multiplier = 0 -- Keeps track of the number of cells to spawn
                if no_max and levels_exist then
        local initial_header_cell    -- The leftmost cell that says "Mode"
                    increaseSpace(mode_th, 'row')
        local iterations = 0        -- Keeps track of iterations that successfully rendered something. Required to tell the initial cell how many columns to span.
                elseif levels_exist or (args.avg_hits and args.hits) then
                    reverseMultiplySpace();
                else
                    reverseMultiplySpace(nil, false);
                end
                local avg_or_each = 'avg'
                if inArgs('count') and not args.use_avg then
                    avg_or_each = 'each'
                end
                table.insert(hit_count_table, 1, avg_or_each)


        for row_index, row in ipairs(TABLE_CONTENT) do
                -- Some things are breaking here, so I needed to implement conditional patches.
            if row.is_visible then
                if hidden('awk') then
                local new_row = TABLE:new()
                    loop_factor = loop_factor * (passive_switch_count + 1)
                 local next_multiplier = 0
                 end


                -- Only spawn the initial cell in the first generated row.
                 if hidden('passives_switch') then
                 if iterations == 0 and not initial_header_cell then
                     loop_factor = passive_normal_count + 1
                     initial_header_cell = doInitialCell(new_row)
                 end
                 end


                 --[[
                 if (hidden('traits') and not hidden('awk')) or (not hidden('awk') and hidden('passives_switch')) then
                We need to know how the colspan will look like.
                    loop_factor = loop_factor * 2
                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
                 end


                 -- Now we can spawn our header cells depending on what is known.
                 loop_factor = loop_factor * (trait_count + 1)
                 for i = 1, (current_multiplier == 0 and 1 or current_multiplier), 1 do
 
                    for _, text in ipairs(row.text) do
                 if not no_max then
                         local new_cell = new_row:tag('th')
                    for i = 1, loop_factor, 1 do
                        new_cell:attr('colspan', colspan_value):wikitext(text)
                        new(data.avg, true)
                        next_multiplier = next_multiplier + 1
                         new(data.max, false)
                     end
                     end
                 end
                 end
                current_multiplier = next_multiplier
                iterations = iterations + 1
             end
             end
         end
 
        -- Apply rowspan of the same value as iteration count.
         until true
         initial_header_cell:attr('rowspan', iterations)
    end
 
    if no_max and args.avg_hits then
         hit_count_table[2] = nil
     end
     end


     -- Helper function to display ranges.
     function concat(tbl)
    function doRangeText(damage_number)
        local returned_str = ''
        if damage_number and damage_number.min == damage_number.max then
        for k, v in ipairs(tbl) do
            damage_number = damage_number.min
            local delimiter = '_'
        elseif damage_number then
            if returned_str == '' then
             damage_number = damage_number.min ..
                delimiter = ''
                 '<span style="white-space: nowrap;"> ~</span> ' .. damage_number.max
            end
             if v ~= '' then
                 returned_str = returned_str .. delimiter .. v
            end
         end
         end
         return damage_number
         return returned_str
     end
     end


     function doContentByMode(mode)
     function makeValueRows(mode_flag)
         local mode_row = TABLE:new()
         local mode_cell = 'PvE'
         mode_row:tag('td'):wikitext(frame:expandTemplate { title = mode })
         if mode_flag == true then
         local damage_entries = returnDamageInOrder()
            mode_flag = '_pvp'
        local last_number
            mode_cell = 'PvP'
         local last_unique_cell
         else
            mode_flag = ''
         end


         for _, damage_key in ipairs(damage_entries) do
         local value_row = tbl:tag('tr')
            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
        function display(name, range_flag)
                    -- Display ranges.
            local range_factor;
                    local new_cell = mode_row:tag('td'):wikitext(damage_number
            local cell_content = {}
                         -- Error out if it doesn't exist
            if range_flag == true then
                         or frame:expandTemplate {
                range_factor = 2
                            title = 'color',
            else
                            args = { 'red', '&#35;ERROR' }
                range_factor = 1
                         })
            end
                     last_unique_cell = new_cell
            for i = 1, range_factor, 1 do
                else
                local range_prefix = '';
                    last_unique_cell:attr('colspan', (last_unique_cell:getAttr('colspan') or 1) + 1)
                if range_flag == true then
                    if i == 1 then
                         range_prefix = 'min_'
                    else
                         range_prefix = 'max_'
                    end
                end
                local value = out[range_prefix .. name];
                if (value ~= nil) then
                    if (args.dump_names == 'true') then
                         table.insert(cell_content, name)
                     elseif value ~= 0 then
                        table.insert(cell_content, formatnum(math.round(value, 2)) .. '%')
                    else
                        table.insert(cell_content, 'N/A')
                    end
                 end
                 end
                 last_number = damage_number
            end
            if next(cell_content) then
                 return value_row:tag('td'):wikitext(table.concat(cell_content, '<span style="white-space:nowrap"> ~</span> '));
             else
             else
                 mode_row:tag('td'):wikitext(damage_key)
                 if args.dump_names == 'true' then
                    return value_row:tag('td'):wikitext(name)
                end
                return value_row:tag('td'):wikitext(frame:expandTemplate{
                    title = 'color',
                    args = {'red', '&#35;ERROR'}
                })
            end
 
        end
 
        local ret2 = ''
 
        value_row:tag('td'):wikitext(frame:expandTemplate{
            title = mode_cell
        })
 
        for passive_normal_i = 0, 3, 1 do
            local combine_now = tostring(passive_normal_i) == combine[1]
            if tbl_content.passives_normal[passive_normal_i] or combine_now or passive_normal_i == 0 then
 
                local passive_normal_str = 'passive' .. passive_normal_i
                if combine_now then
                    for k, v in ipairs(combine) do
                        if k ~= 1 then
                            passive_normal_str = passive_normal_str .. '_passive' .. v
                        end
                    end
                end
                if passive_normal_i == 0 then
                    passive_normal_str = ''
                end
                for passive_switch_i = 0, 3, 1 do
                    local current_switch = tbl_content.passives_switch[passive_switch_i]
                    if current_switch or passive_switch_i == 0 then
                        local passive_switch_str = 'passive' .. passive_switch_i
                        if passive_switch_i == 0 then
                            passive_switch_str = ''
                        end
                        for _, awk_str in ipairs(awk_table) do
                            for trait_i = 0, #trait_args do
                                repeat
                                    for hit_i, hit_v in ipairs(hit_count_table) do
                                        local trait_str = trait_args[trait_i] or ''
                                        local str_tbl = {'damage'}
                                        local passive_tbl = {}
                                        table.insert(str_tbl, awk_str)
                                        if tbl_content.traits[trait_str] then
                                            table.insert(str_tbl, trait_str)
                                        elseif trait_i > 0 then
                                            do
                                                break
                                            end
                                        end
                                        table.insert(str_tbl, 1, hit_v)
                                        table.insert(passive_tbl, passive_switch_str)
                                        table.insert(passive_tbl, passive_normal_str)
 
                                        table.sort(passive_tbl)
 
                                        for k, v in ipairs(passive_tbl) do
                                            table.insert(str_tbl, v)
                                        end
 
                                        if inArgs('range_max') then
                                            display(concat(str_tbl) .. mode_flag, true)
                                        else
                                            display(concat(str_tbl) .. mode_flag)
                                        end
                                    end
                                until true
                            end
                        end
                    end
                end
             end
             end
         end
         end
     end
     end


     function doTable()
     -- For debugging purposes
         doHeaders()
    if (args.debug == 'true') then
        forEach(doContentByMode)
         ret = ''
    end
        for i = 1, #tbl_order, 1 do
            ret = ret .. "'''" .. tbl_order[i] .. "''': <br/>"
            for k2, v2 in pairs(tbl_content[tbl_order[i]]) do
                if (v2 == true) then
                    v2 = 'true'
                end
                local output = tostring(v2)
                if (type(v2) == 'table') then
                    output = ''
                    output = output .. '<br/>--<br/>'
                    for k3, v3 in pairs(v2) do
                        output = output .. k3 .. ': ' .. tostring(v3) .. '<br/>'
                    end
                    output = output .. '--'
                end
                ret = ret .. k2 .. ': ' .. output .. '<br/>'
            end
            ret = ret .. '<br/>'
        end


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


     -- Dump all values if wanted.
     makeValueRows();
    if OPTIONS.dump_table_data then
     makeValueRows(true);
        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 = ''
     local bug = ''
     if OPTIONS.bug then
     if args.bug == 'true' then
         bug = frame:expandTemplate {
         bug = frame:expandTemplate{
             title = 'SkillText',
             title = 'SkillText',
             args = { 'FreeTraining' }
             args = {'FreeTraining'}
         }
         }
     end
     end


     -- Transform into variables
     return parsed .. bug .. tostring(tbl)
    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
end


return p
return p
-- pyend
-- pyend

Revision as of 00:34, 5 January 2024

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

-- pystart
require('Module:CommonFunctions');
local getArgs = require('Module:Arguments').getArgs
local p = {}

-- Main process
function p.main(frame)
    local args = getArgs(frame)

    function inArgs(key)
        if args[key] ~= nil then
            return true
        end
    end

    -- Collect data from the input
    local data = {}
    local data_types = {
        'dmg',
        'awk_dmg',
        'hits',
        'avg_hits',
        'awk_hits',
        'avg_awk_hits',
        'hits_useful',
        'avg_hits_useful',
        'awk_hits_useful',
        'avg_awk_hits_useful',
        'perm_buff'
    }

    -- Handle the PvP split values
    for k, v in spairs(data_types) do
        table.insert(data_types, 'pvp_' .. v)
    end

    for k, v in spairs(data_types) do
        local i = 1
        if inArgs(v) then
            for k2, v2 in spairs(split(args[v])) do
                -- Check for operators. If detected, evaluate.
                if string.find(v2, '*') or string.find(v2, '+') then
                    v2 = frame:preprocess('{{#expr:' .. v2 .. '}}')
                end

                -- Check if proper hit count values provided. If empty string detected, inherit from 'hits'.
                if string.find(v, 'avg_') and string.find(v, '_hits') and v2 == '' then
                    data[v .. i] = data['avg_hits' .. i]
                elseif string.find(v, 'hits') and v2 == '' then
                    data[v .. i] = data['hits' .. i]
                elseif string.find(v, 'awk_dmg') and v2 == '' then
                    if string.find(v, 'pvp') then
                        data[v .. i] = data['pvp_dmg' .. i]
                    else
                        data[v .. i] = data['dmg' .. i]
                    end
                else
                    data[v .. i] = v2
                end
                i = i + 1
            end
        end
    end

    -- For weird skills
    function inheritMissing(keyTable, inheritTable)
        local n = 1; -- counter for the func. argument loop
        local i;
        for k_key, v_key in spairs(keyTable) do
            if inArgs(inheritTable[n]) and not inArgs(v_key) then
                i = 1
                for k, v in spairs(split(args.dmg)) do
                    data[v_key .. i] = data[inheritTable[n] .. i]
                    i = i + 1
                end
            end
            n = n + 1
        end
    end

    inheritMissing({'awk_dmg', 'pvp_awk_dmg', 'awk_hits', 'avg_awk_hits'}, {'dmg', 'pvp_dmg', 'hits', 'avg_hits'})

    -- Laziness
    if args.hits and args.awk_dmg and not args.awk_hits then
        data.awk_hits = args.hits
    end

    if args.awk_dmg and args.avg_hits and not args.avg_awk_hits then
        data.avg_awk_hits = args.avg_hits
    end

    if args.awk_dmg and args.avg_hits_useful and not args.avg_awk_hits_useful then
        data.avg_awk_hits_useful = args.avg_hits_useful
    end

    -- Handle trait table
    local traits = {}

    if inArgs('heavy') then
        traits.heavy = 1.44
    end

    if inArgs('enhanced') then
        traits.enhanced = 0.8
    end

    -- Customizable for empowered, it had to be special lol.
    if inArgs('empowered') then
        if (args.empowered == 'true') then
            traits.empowered = 1.2
        else
            traits.empowered = args.empowered
        end
    end

    if args.useful == 'true' then
        args.useful = 0.7
    end

    if args.useful_penalty == 'true' then
        args.useful_penalty = 0.7
    end

    -- Output passives if provided
    local passives = {}
    for i = 1, 3 do
        if inArgs('passive' .. i) then
            passives[i] = args['passive' .. i]
            passives[i] = split(frame:preprocess('{{:' .. passives[i] .. '}}{{#arrayprint:' .. passives[i] .. '}}'))
        end
    end

    function list(ispvp)

        -- Define tables that hold the subsequent damage values.
        -- I know this isn't the best, but I don't want to work with nested tables in this language.
        local fvals = {}
        local tvals = {}
        local pvals = {
            [1] = {},
            [2] = {},
            [3] = {},
            [12] = {},
            [13] = {},
            [23] = {},
            [123] = {}
        }

        -- Check the specified mode and define the prefixes/suffixes first.
        local pr = ''
        local su = ''
        local p_index = 1
        if ispvp then
            p_index = 2
        end

        if (ispvp) then
            pr = 'pvp_'
            su = '_pvp'
        end

        -- Define total/average damage calculation based on damage per hit and hit amount.
        function getTotal(dmg, hits, fval, count)
            -- Handle PvP prefixes/suffixes
            if inArgs(pr .. dmg) then
                dmg = pr .. dmg
            end
            if inArgs(pr .. hits) then
                hits = pr .. hits
            end

            if dmg == 'awk_dmg' and ispvp and not inArgs(pr .. 'awk_dmg') then
                dmg = pr .. 'dmg'
            end

            fval = fval .. su

            local i = 1
            fvals[fval] = 0
            for k, v in spairs(split(args.dmg)) do
                if -- If 'hits' defined, but 'avg_hits' not defined, inherit from 'hits'.
                (data[hits .. i] == nil and data['hits' .. i] ~= nil and hits == 'avg_hits') then
                    data[hits .. i] = data['hits' .. i]
                elseif -- If 'hits' undefined, assume they're equal to 1.
                (data[hits .. i] == nil) then
                    data[hits .. i] = 1
                end
                -- Proceed to combine
                fvals[fval] = fvals[fval] + data[dmg .. i] * data[hits .. i] * (data[pr .. 'perm_buff' .. i] or data['perm_buff' .. i] or 1)
                i = i + 1
            end
            -- For skills with multiple same parts, ex. Clementine, Enough Mineral
            if count == true then
                fvals[fval] = fvals[fval] * args.count
                if inArgs('count_extra' .. su) then
                    if args.count_extra_hits == nil then
                        args.count_extra_hits = 1
                    end
                    if not string.find(fval, "each_") then
                        fvals[fval] = fvals[fval] + (args['count_extra' .. su] * args['count_extra_hits'])
                    end
                end
            end
            -- Apply Useful modifier.
            if string.find(fval, 'useful') then
                fvals[fval] = fvals[fval] * (args.useful_penalty or args.useful)
            end
        end

        -- Actually generate the values depending on arguments provided.
        if inArgs(pr .. 'dmg') then
            if (inArgs('count')) then
                getTotal('dmg', 'hits', 'each_damage')
                getTotal('dmg', 'hits', 'total_damage', true)
            else
                getTotal(pr .. 'dmg', 'hits', 'total_damage')
            end

            if inArgs('avg_hits') then
                getTotal('dmg', 'avg_hits', 'avg_damage')
            end
        end

        if inArgs(pr .. 'awk_dmg') or inArrayStarts(pr .. 'awk_dmg', data) then
            getTotal('awk_dmg', 'awk_hits', 'total_damage_awk')

            if (inArgs('avg_hits') and (inArgs('awk_dmg') or inArgs('awk_hits'))) or inArgs('avg_awk_hits') then
                getTotal('awk_dmg', 'avg_awk_hits', 'avg_damage_awk')
            end
        end

        -- Handling traits
        -- Useful handled separately
        if inArgs('useful_penalty') or inArgs('useful') then
            getTotal(pr .. 'dmg', 'hits_useful', 'total_damage_useful')

            if inArgs('avg_hits_useful') then
                getTotal('dmg', 'avg_hits_useful', 'avg_damage_useful')
            end

            if inArgs(pr .. 'awk_dmg') and inArgs('awk_hits_useful') then
                getTotal('awk_dmg', 'awk_hits_useful', 'total_damage_awk_useful')
            end

            if inArgs(pr .. 'avg_awk_hits') and inArgs('avg_awk_hits_useful') then
                getTotal('awk_dmg', 'avg_awk_hits_useful', 'avg_damage_awk_useful')
            end
        end

        -- Multiply all values with traits and store them in another table.
        for k, v in spairs(fvals) do
            if not string.find(k, 'useful') then
                for kt, vt in spairs(traits) do
                    if inArgs(kt) then
                        local dmg_name = k .. '_' .. kt
                        if ispvp then
                            dmg_name = dmg_name:gsub(su, '') .. su
                        end
                        local dmg_formula = v * vt
                        tvals[dmg_name] = dmg_formula
                    end
                end
            end
        end

        -- Get a table of merged base & trait values
        local ftvals = fvals
        tableMerge(ftvals, tvals)

        function addPassive(num, loop_table)
            local pval_index
            if loop_table == nil then
                pval_index = num
                loop_table = ftvals
            else
                pval_index = tonumber(loop_table .. num)
                loop_table = pvals[loop_table]
            end
            for k, v in spairs(loop_table) do
                local dmg_name = k .. '_passive' .. num
                if ispvp then
                    dmg_name = dmg_name:gsub(su, '') .. su
                end
                local dmg_formula = v * passives[num][p_index]
                pvals[pval_index][dmg_name] = dmg_formula
            end
        end

        -- Add passives and combine them.
        if inArgs('passive2') then
            addPassive(2)
            if inArgs('passive3') then
                addPassive(3, 2)
            end
        end

        if inArgs('passive1') then
            addPassive(1)
            if inArgs('passive2') then
                addPassive(2, 1)
                if inArgs('passive3') then
                    addPassive(3, 12)
                end
            end
            if inArgs('passive3') then
                addPassive(3, 1)
            end
        end

        if inArgs('passive3') then
            addPassive(3)
        end

        -- Merge all tables into one.
        tableMerge(fvals, tvals)
        for k, v in spairs(pvals) do
            tableMerge(fvals, v)
        end

        return fvals
    end

    local out = list(false)
    local out_pvp = list(true)

    -- Merge the output to a unified table.
    tableMerge(out, out_pvp)

    -- Function wrapper for vardefine syntax in MW.
    function var(name, dmg, prefix)
        if prefix == nil then
            prefix = ''
        else
            prefix = prefix .. '_'
        end
        if dmg == 0 then
            dmg = 'N/A'
        else
            dmg = round(dmg)
        end
        if (args.format == 'false' or dmg == 'N/A') then
            return '{{#vardefine:' .. prefix .. name .. '|' .. dmg .. '}}'
        else
            return '{{#vardefine:' .. prefix .. name .. '|{{formatnum:' .. dmg .. '}}%}}'
        end
    end

    -- Apply ranges.
    function getRangeCount(arg)
        if inArgs(arg) then
            data[arg] = split(args[arg])
            if data[arg][2] == nil then
                data[arg][2] = data[arg][1]
            end
        end
    end

    getRangeCount('range_min_count');
    getRangeCount('range_max_count')

    function determineRange(minmax)
        if inArgs('range_' .. minmax) then
            data['range_' .. minmax] = split(args['range_' .. minmax])
            if data['range_' .. minmax][2] == nil then
                data['range_' .. minmax][2] = data['range_' .. minmax][1]
            end
            if inArgs('range_' .. minmax .. '_count') then
                local i = 1;
                for k, v in spairs(data['range_' .. minmax]) do
                    data['range_' .. minmax][i] = 1 + (-1 + data['range_' .. minmax][i]) *
                                                      data['range_' .. minmax .. '_count'][i]
                    i = i + 1
                end
            end
        end
    end

    determineRange('min');
    determineRange('max');

    -- If maximum range is specified, but not minimum, and minimum count is specified.
    -- By default, it would just do the same as with max, don't want that.
    if inArgs('range_max') and not inArgs('range_min') then
        data['range_min'] = {1, 1}
        if inArgs('range_min_count') then
            local range_max_arg = split(args.range_max);
            if range_max_arg[2] == nil then
                range_max_arg[2] = range_max_arg[1]
            end
            data['range_min'] = {1 + range_max_arg[1] * data['range_min_count'][1],
                                 1 + range_max_arg[2] * data['range_min_count'][2]}
        end
    end

    local out_min = {}
    local out_max = {}

    function applyRange(minmax)
        local temp_tab = {};
        if minmax == 'min' then
            temp_tab = out_min
        else
            temp_tab = out_max
        end
        if inArgs('range_max') then
            for k, v in spairs(out) do
                if not (string.starts(k, 'min_') or string.starts(k, 'max_')) then
                    if (string.find(k, '_pvp')) then
                        temp_tab[minmax .. '_' .. k] = v * data['range_' .. minmax][2];
                    else
                        temp_tab[minmax .. '_' .. k] = v * data['range_' .. minmax][1];
                    end
                end
            end
        end
        tableMerge(out, temp_tab)
    end

    applyRange('min');
    applyRange('max');

    -- Get the actual variables with MW syntax.
    local vars = {}
    for k, v in spairs(out) do
        table.insert(vars, var(k, v, args.prefix))
    end

    -- Transform ranges to variables.
    local vars_range = {}
    if (inArgs('range_max')) then
        for k, v in spairs(out) do
            if not (string.starts(k, 'min_') or string.starts(k, 'max_')) then
                local prefix = ''
                if args.prefix ~= nil then
                    prefix = args.prefix .. '_'
                end
                table.insert(vars_range,
                    '{{#vardefine: ' .. prefix .. 'range_' .. k .. '|{{formatnum:' .. round(out_min['min_' .. k]) ..
                        '}}% ~ {{formatnum:' .. round(out_max['max_' .. k]) .. '}}%}}');
            end
        end
        indexTableMerge(vars, vars_range);
    end

    -- Dump all values if wanted.
    if args.dump == 'true' then
        local ret = {}
        for k, v in spairs(out) do
            table.insert(ret, k .. ': ' .. v)
        end
        return frame:preprocess(table.concat(ret, "<br/>"))
    end

    -- Parse all variables
    local parsed = frame:preprocess('{{ ' .. table.concat(vars) .. 'trim2}}')

    if args[1] ~= 'true' and args.table ~= 'true' then
        return parsed
    end

    local char = args.char or args[2] or 'Elsword'

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

    -- For rowspan, colspan shenanigans
    function increaseSpace(el, type, num)
        if type == 'row' then
            type = 'rowspan'
        elseif type == 'col' then
            type = 'colspan'
        end
        num = num or 1
        return el:attr(type, tonumber(el:getAttr(type) or 1) + num)
    end

    function multiplySpace(el, type, m)
        if not el then
            return false
        end
        if type == 'row' then
            type = 'rowspan'
        else
            type = 'colspan'
        end
        if m == nil then
            m = 2
        end
        local span = el:getAttr(type) or 1
        return el:attr(type, tonumber(span) * m)
    end

    local combine = split(args.combine)
    local combine_suffix = args.combine_suffix
    local append = split(args.append)[1]
    local append_alias = split(args.append)[2]

    local tbl_order = {'extra', 'passives_normal', 'passives_switch', 'awk', 'traits', 'hit_count'}
    local STR = {
        BASE = 'Base',
        MODE = 'Mode',
        REGULAR = 'Regular',
        NORMAL = 'Normal',
        AWK = 'Awakening Mode',
        AVG = 'Average',
        MAX = 'Max',
        TRAIT = {'Enhanced', 'Empowered', 'Useful', 'Heavy'},
        PER = 'Per',
        INSTANCE = 'Instance'
    }

    local trait_args = {}
    for k, v in ipairs(STR.TRAIT) do
        table.insert(trait_args, string.lower(v))
    end

    local trait_count = 0
    for k, v in ipairs(trait_args) do
        if inArrayHas(v, out) then
            trait_count = trait_count + 1
        end
    end

    local tbl_content = {
        extra = {
            mode = STR.MODE,
            long = STR.AVG
        },
        passives_normal = {
            mode = STR.MODE,
            base = STR.BASE,
            combined = {},
            aliases = {args.alias1 or false, args.alias2 or false, args.alias3 or false},
            suffixes = {args.suffix1 or false, args.suffix2 or false, args.suffix3 or false}
        },
        passives_switch = {
            normal = STR.NORMAL,
            hide = true
        },
        awk = {
            normal = STR.REGULAR,
            awk_link = '[[' .. STR.AWK .. ']]',
            hide = true
        },
        traits = {
            normal = STR.NORMAL,
            hide = true
        },
        hit_count = {
            avg = STR.AVG,
            max = STR.MAX,
            hide = true
        }
    }

    local count_name = args.count_name or STR.INSTANCE
    if inArgs('count') and not args.use_avg then
        tbl_content.hit_count.avg = table.concat({STR.PER, count_name}, ' ')
    end

    function getRowIndex(row)
        for k, v in ipairs(tbl_order) do
            if row == v then
                return k
            end
        end
    end

    for passive_i = 1, 3 do
        repeat
            -- Add normal passives to the first row.
            local passive_name = ''
            -- Alias for appended passives.
            if (inArgs('passive' .. passive_i)) then
                passive_name = args['passive' .. passive_i]
            end
            if indexOf(tostring(passive_i), combine) ~= nil then
                -- Add combined passives.
                tbl_content.passives_normal.combined[passive_i] = passive_name
            elseif tostring(passive_i) == append then
                -- Add switch passives.
                tbl_content.passives_switch[passive_i] = passive_name
                tbl_content.passives_switch.hide = false
                if append_alias ~= nil then
                    tbl_content.passives_switch.display_name = append_alias
                end
            elseif (inArgs('passive' .. passive_i)) then
                -- Add regular passives to the first row.
                tbl_content.passives_normal[passive_i] = passive_name
            end
        until true
    end

    local ret = ''

    for trait_order, trait_arg in ipairs(trait_args) do
        -- Add traits if exist.
        if (inArgs(trait_arg)) then
            tbl_content.traits[trait_arg] = args[trait_arg]
            tbl_content.traits.hide = false
        end
    end

    -- Add Useful trait.
    if inArgs('hits_useful') or inArgs('avg_hits_useful') then
        tbl_content.traits.useful = 'true'
        tbl_content.traits.hide = false
    end

    if inArgs('avg_hits') or inArgs('count') then
        -- Enable average/max if needed.
        tbl_content.hit_count.hide = false
    end

    if inArrayHas('awk_', args) then
        -- Enable Awakening if needed.
        tbl_content.awk.hide = false
    end

    local loop_factor, mode_th;
    local cells = {}
    local passive_normal_count = 0;
    local passive_switch_count = 0;

    function hidden(level)
        return tbl_content[level].hide
    end

    local hit_count_table = {'total'}
    local awk_table = {''}
    local levels_exist =
        (next(passives) or args.append or args.awk_hits or args.awk_dmg or args.count)
    local no_max = args.no_max == 'true'

    function makePassiveLink(passive, alias, suffix, nil_cond)
        if nil_cond == nil then
            nil_cond = true
        end
        if nil_cond and alias ~= nil and alias ~= false then
            alias = '|' .. alias
        else
            alias = ''
        end
        suffix = suffix or ''
        passive = '[[' .. passive .. alias .. ']]' .. suffix
        return passive
    end

    -- Begin the main loop.
    for k, type in ipairs(tbl_order) do
        repeat
            local tr = tbl:tag('tr')
            local data = tbl_content[type];
            local hide = data.hide;
            cells[type] = {}

            function new(wikitext, normal)
                local th = tr:tag('th'):wikitext(wikitext)
                if normal == true then
                    table.insert(cells[type].normal_th, th)
                elseif normal == false then
                    table.insert(cells[type].th, th)
                else
                    return th
                end
            end

            function multiplySpaceAll(level, num)
                if cells[level] == nil then
                    return false
                end
                num = num or 2
                for k, v in ipairs(cells[level].th) do
                    multiplySpace(v, 'col', num)
                end
                for k, v in ipairs(cells[level].normal_th) do
                    multiplySpace(v, 'col', num)
                end
            end

            function reverseMultiplySpace(num, increase_mode)
                local i = #tbl_order;
                num = num or 2

                local fix_for_no_max = 0
                if no_max and not levels_exist then
                    fix_for_no_max = -1
                end

                while (i > 1) do
                    multiplySpaceAll(tbl_order[i - 1 + fix_for_no_max], num)
                    i = i - 1
                end
                if increase_mode ~= false then
                    increaseSpace(mode_th, 'row')
                end
            end

            cells[type].normal_th = {}
            cells[type].th = {}

            if (type == 'extra' and no_max) then
                mode_th = new(data.mode)
                new(data.long, true)
            end

            if (type == 'passives_normal') then
                if not no_max then
                    mode_th = new(data.mode)
                else
                    reverseMultiplySpace(nil, false);
                end

                if (no_max and levels_exist) or not no_max then
                    new(data.base, true)
                end

                for i = 1, 3, 1 do
                    local passive_link = data[i]
                    if (passive_link ~= nil) then
                        local suffix = ''
                        if next(data.suffixes) and data.suffixes[i] ~= false then
                            suffix = ' ' .. data.suffixes[i]
                        end
                        passive_link = makePassiveLink(passive_link, data.aliases[i], suffix, next(data.aliases))
                        new(passive_link, false)
                        passive_normal_count = passive_normal_count + 1
                    end
                end

                -- Handle combining passives.
                if next(data.combined) then
                    local combined_str = ''
                    for k, v in spairs(data.combined) do
                        combined_str = combined_str .. makePassiveLink(v, data.aliases[k], data.suffixes[k]) .. '/'
                    end
                    combined_str = combined_str:gsub('/$', '')
                    if combine_suffix then
                        combined_str = combined_str .. ' ' .. combine_suffix
                    end
                    new(combined_str, false)
                    passive_normal_count = passive_normal_count + 1
                end

            end

            if (type == 'passives_switch') then
                if not hidden(type) then
                    multiplySpaceAll('passives_normal')
                    -- For some reason, whenever appending is active, it misses a 1 in rowspan of this cell.
                    increaseSpace(mode_th, 'row')
                end

                -- Passives that appear in the second row
                loop_factor = (passive_normal_count + 1)
                for ix = 1, loop_factor, 1 do
                    for i = 1, 3, 1 do
                        if (data[i] ~= nil) then
                            new(data.normal, true);
                            local suffix = ''
                            if next(tbl_content.passives_normal.suffixes) and tbl_content.passives_normal.suffixes[i] ~= false then
                                suffix = ' ' .. tbl_content.passives_normal.suffixes[i]
                            end
                            local passive_link = data[i]
                            passive_link = makePassiveLink(passive_link, data.display_name, suffix)
                            new(passive_link, false)

                            if (ix == 1) then
                                -- Count switch passives. Only one iteration.
                                passive_switch_count = passive_switch_count + 1
                            end

                        end
                    end
                end
            end

            if (type == 'awk' and not hide) then
                reverseMultiplySpace();
                table.insert(awk_table, 'awk')

                loop_factor = loop_factor * (passive_switch_count + 1)

                for i = 1, loop_factor, 1 do
                    new(data.normal, true)
                    new(data.awk_link, false)
                end
            end

            if (type == 'traits' and not hide) then
                if trait_count == 2 then
                    reverseMultiplySpace(3);
                else
                    reverseMultiplySpace();
                end

                -- Manually fix certain situations.
                local has_awk = 1
                if not hidden('awk') then
                    has_awk = 2
                end

                local extra = 1
                if hidden('awk') and not hidden('passives_switch') then
                    extra = 2
                end

                loop_factor = loop_factor * has_awk
                for i = 1, loop_factor * extra, 1 do
                    local ix = 1
                    new(data.normal, true)
                    for k, trait_name in ipairs(trait_args) do
                        if data[trait_name] ~= nil then
                            new(STR.TRAIT[ix], false)
                        end
                        ix = ix + 1
                    end
                end
            end

            if (type == 'hit_count' and not hide) then
                if no_max and levels_exist then
                    increaseSpace(mode_th, 'row')
                elseif levels_exist or (args.avg_hits and args.hits) then
                    reverseMultiplySpace();
                else
                     reverseMultiplySpace(nil, false);
                end
                local avg_or_each = 'avg'
                if inArgs('count') and not args.use_avg then
                    avg_or_each = 'each'
                end
                table.insert(hit_count_table, 1, avg_or_each)

                -- Some things are breaking here, so I needed to implement conditional patches.
                if hidden('awk') then
                    loop_factor = loop_factor * (passive_switch_count + 1)
                end

                if hidden('passives_switch') then
                    loop_factor = passive_normal_count + 1
                end

                if (hidden('traits') and not hidden('awk')) or (not hidden('awk') and hidden('passives_switch')) then
                    loop_factor = loop_factor * 2
                end

                loop_factor = loop_factor * (trait_count + 1)

                if not no_max then
                    for i = 1, loop_factor, 1 do
                        new(data.avg, true)
                        new(data.max, false)
                    end
                end
            end

        until true
    end

    if no_max and args.avg_hits then
        hit_count_table[2] = nil
    end

    function concat(tbl)
        local returned_str = ''
        for k, v in ipairs(tbl) do
            local delimiter = '_'
            if returned_str == '' then
                delimiter = ''
            end
            if v ~= '' then
                returned_str = returned_str .. delimiter .. v
            end
        end
        return returned_str
    end

    function makeValueRows(mode_flag)
        local mode_cell = 'PvE'
        if mode_flag == true then
            mode_flag = '_pvp'
            mode_cell = 'PvP'
        else
            mode_flag = ''
        end

        local value_row = tbl:tag('tr')

        function display(name, range_flag)
            local range_factor;
            local cell_content = {}
            if range_flag == true then
                range_factor = 2
            else
                range_factor = 1
            end
            for i = 1, range_factor, 1 do
                local range_prefix = '';
                if range_flag == true then
                    if i == 1 then
                        range_prefix = 'min_'
                    else
                        range_prefix = 'max_'
                    end
                end
                local value = out[range_prefix .. name];
                if (value ~= nil) then
                    if (args.dump_names == 'true') then
                        table.insert(cell_content, name)
                    elseif value ~= 0 then
                        table.insert(cell_content, formatnum(math.round(value, 2)) .. '%')
                    else
                        table.insert(cell_content, 'N/A')
                    end
                end
            end
            if next(cell_content) then
                return value_row:tag('td'):wikitext(table.concat(cell_content, '<span style="white-space:nowrap"> ~</span> '));
            else
                if args.dump_names == 'true' then
                    return value_row:tag('td'):wikitext(name)
                end
                return value_row:tag('td'):wikitext(frame:expandTemplate{
                    title = 'color',
                    args = {'red', '&#35;ERROR'}
                })
            end

        end

        local ret2 = ''

        value_row:tag('td'):wikitext(frame:expandTemplate{
            title = mode_cell
        })

        for passive_normal_i = 0, 3, 1 do
            local combine_now = tostring(passive_normal_i) == combine[1]
            if tbl_content.passives_normal[passive_normal_i] or combine_now or passive_normal_i == 0 then

                local passive_normal_str = 'passive' .. passive_normal_i
                if combine_now then
                    for k, v in ipairs(combine) do
                        if k ~= 1 then
                            passive_normal_str = passive_normal_str .. '_passive' .. v
                        end
                    end
                end
                if passive_normal_i == 0 then
                    passive_normal_str = ''
                end
                for passive_switch_i = 0, 3, 1 do
                    local current_switch = tbl_content.passives_switch[passive_switch_i]
                    if current_switch or passive_switch_i == 0 then
                        local passive_switch_str = 'passive' .. passive_switch_i
                        if passive_switch_i == 0 then
                            passive_switch_str = ''
                        end
                        for _, awk_str in ipairs(awk_table) do
                            for trait_i = 0, #trait_args do
                                repeat
                                    for hit_i, hit_v in ipairs(hit_count_table) do
                                        local trait_str = trait_args[trait_i] or ''
                                        local str_tbl = {'damage'}
                                        local passive_tbl = {}
                                        table.insert(str_tbl, awk_str)
                                        if tbl_content.traits[trait_str] then
                                            table.insert(str_tbl, trait_str)
                                        elseif trait_i > 0 then
                                            do
                                                break
                                            end
                                        end
                                        table.insert(str_tbl, 1, hit_v)
                                        table.insert(passive_tbl, passive_switch_str)
                                        table.insert(passive_tbl, passive_normal_str)

                                        table.sort(passive_tbl)

                                        for k, v in ipairs(passive_tbl) do
                                            table.insert(str_tbl, v)
                                        end

                                        if inArgs('range_max') then
                                            display(concat(str_tbl) .. mode_flag, true)
                                        else
                                            display(concat(str_tbl) .. mode_flag)
                                        end
                                    end
                                until true
                            end
                        end
                    end
                end
            end
        end
    end

    -- For debugging purposes
    if (args.debug == 'true') then
        ret = ''
        for i = 1, #tbl_order, 1 do
            ret = ret .. "'''" .. tbl_order[i] .. "''': <br/>"
            for k2, v2 in pairs(tbl_content[tbl_order[i]]) do
                if (v2 == true) then
                    v2 = 'true'
                end
                local output = tostring(v2)
                if (type(v2) == 'table') then
                    output = ''
                    output = output .. '<br/>--<br/>'
                    for k3, v3 in pairs(v2) do
                        output = output .. k3 .. ': ' .. tostring(v3) .. '<br/>'
                    end
                    output = output .. '--'
                end
                ret = ret .. k2 .. ': ' .. output .. '<br/>'
            end
            ret = ret .. '<br/>'
        end

        return ret
    end

    makeValueRows();
    makeValueRows(true);

    local bug = ''
    if args.bug == 'true' then
        bug = frame:expandTemplate{
            title = 'SkillText',
            args = {'FreeTraining'}
        }
    end

    return parsed .. bug .. tostring(tbl)

end

return p
-- pyend