Module:Damage: Difference between revisions

From Elwiki
No edit summary
No edit summary
Line 1: Line 1:
-- pystart
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 14: Line 14:
     end
     end


     -- Collect data from the input
     -- Define the schema for the table
     local data = {}
     local tableSchema = {
    local data_types = {
        PvE = {},
        'dmg',
         PvP = {}
         '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
     -- Function to create a new table with the desired schema
    for k, v in spairs(data_types) do
     function createDamageDataTable()
        table.insert(data_types, 'pvp_' .. v)
         local newTable = {}
    end
         for key, value in pairs(tableSchema) do
 
            if type(value) == "table" then
     for k, v in spairs(data_types) do
                 newTable[key] = {}
         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
         end
        return newTable
     end
     end


     -- For weird skills
     -- User requested options
     function inheritMissing(keyTable, inheritTable)
     local OPTIONS = {
         local n = 1; -- counter for the func. argument loop
        do_table = args[1] == 'true',
         local i;
        character = args[2] or 'Elsword',
         for k_key, v_key in spairs(keyTable) do
         format = args.format ~= 'false',
            if inArgs(inheritTable[n]) and not inArgs(v_key) then
         no_max = args.no_max == 'true',
                 i = 1
         is_append = args.append ~= nil,
                for k, v in spairs(split(args.dmg)) do
        append_index = args.append and tonumber(split(args.append)[1]),
                    data[v_key .. i] = data[inheritTable[n] .. i]
        append_name = args.append and split(args.append)[2],
                    i = i + 1
        combine_suffix = args.combine_suffix and (' ' .. args.combine_suffix) or '',
                 end
        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
             end
             n = n + 1
             return output
         end
        end)(),
     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',
        prefix = args.prefix,
        use_avg = args.use_avg == 'true'
     }


     inheritMissing({'awk_dmg', 'pvp_awk_dmg', 'awk_hits', 'avg_awk_hits'}, {'dmg', 'pvp_dmg', 'hits', 'avg_hits'})
     -- Define a table with parsed damage information of all kind.
    local BASIC_DAMAGE = createDamageDataTable()


     -- Laziness
     -- Define a table with trait names and their values to apply.
    if args.hits and args.awk_dmg and not args.awk_hits then
     local TRAITS = {
        data.awk_hits = args.hits
         -- An empty trait so we keep the original values there.
     end
        {
 
            key = '',
    if args.awk_dmg and args.avg_hits and not args.avg_awk_hits then
            name = 'Normal',
        data.avg_awk_hits = args.avg_hits
            value = 1
    end
        },
 
        {
    if args.awk_dmg and args.avg_hits_useful and not args.avg_awk_hits_useful then
            key = 'enhanced',
         data.avg_awk_hits_useful = args.avg_hits_useful
            name = 'Enhanced',
    end
            value = args.enhanced ~= nil and 0.8
 
        },
    -- Handle trait table
        {
    local traits = {}
            key = 'empowered',
 
            name = 'Empowered',
    if inArgs('heavy') then
            value = args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false
        traits.heavy = 1.44
         },
    end
        {
 
            key = 'useful',
    if inArgs('enhanced') then
            name = 'Useful',
        traits.enhanced = 0.8
             value = args.hits_useful ~= nil and (tonumber(args.useful_penalty) or 0.7)
    end
         },
 
         {
    -- Customizable for empowered, it had to be special lol.
             key = 'heavy',
    if inArgs('empowered') then
             name = 'Heavy',
        if (args.empowered == 'true') then
             value = args.heavy ~= nil and 1.44
            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.
    -- Define a table with user-requested passive skills (empty by default).
        local pr = ''
    local PASSIVES = {}
        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.
    for k, v in pairs(args) do
        function getTotal(dmg, hits, fval, count)
         if string.find(k, 'passive') then
             -- Handle PvP prefixes/suffixes
             --[[
             if inArgs(pr .. dmg) then
             Fix up the passives and put them into a separate table.
                dmg = pr .. dmg
            |passive1=... |passive2=... -> { passive1, passive2 }
            end
             --]]
            if inArgs(pr .. hits) then
            local passive_index = string.match(k, "%d")
                hits = pr .. hits
             local passive_values = split(frame:preprocess('{{:' .. v .. '}}{{#arrayprint:' .. v .. '}}'));
             end
             PASSIVES[tonumber(passive_index)] = {
 
                name = v,
             if dmg == 'awk_dmg' and ispvp and not inArgs(pr .. 'awk_dmg') then
                 value = passive_values[1],
                dmg = pr .. 'dmg'
                 value_pvp = passive_values[2],
             end
                alias = args['alias' .. passive_index] or (passive_index == OPTIONS.append_index and OPTIONS.append_name),
 
                 suffix = args['suffix' .. passive_index] and (' ' .. args['suffix' .. passive_index]) or '',
            fval = fval .. su
             }
 
        elseif string.find(k, 'append') or not string.find(v, '[a-zA-Z]+') then
            local i = 1
            --[[
            fvals[fval] = 0
            Change how args are received.
            for k, v in spairs(split(args.dmg)) do
            dmg = 500, 700, 800 (string) -> dmg = { 500, 700, 800 } (table)
                 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
             local split_values = split(v)
                    data[hits .. i] = data['hits' .. i]
             -- Perform automatic math on each value.
                elseif -- If 'hits' undefined, assume they're equal to 1.
             for k2, v2 in pairs(split_values) do
                (data[hits .. i] == nil) then
                 split_values[k2] = frame:preprocess('{{#expr:' .. v2 .. '}}')
                    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
            args[k] = split_values
         end
         end
    end


        -- Actually generate the values depending on arguments provided.
    -- Set basic hit count to 1 for all damage.
        if inArgs(pr .. 'dmg') then
    for k, v in ipairs(args.dmg) do
            if (inArgs('count')) then
        if not args.hits then
                getTotal('dmg', 'hits', 'each_damage')
             args.hits = {}
                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
         end
 
         if not args.hits[k] then
         if inArgs(pr .. 'awk_dmg') or inArrayStarts(pr .. 'awk_dmg', data) then
             args.hits[k] = 1
             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
         end
    end


        -- Handling traits
    -- Store a configuration that will tell the main function how to behave given different inputs.
         -- Useful handled separately
    -- It will always take the first value if available. If not, fall back to the other (recursively).
         if inArgs('useful_penalty') or inArgs('useful') then
    local DAMAGE_CONFIG = {
             getTotal(pr .. 'dmg', 'hits_useful', 'total_damage_useful')
        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' }
        },
    }


            if inArgs('avg_hits_useful') then
    local DAMAGE_PARSED = createDamageDataTable()
                getTotal('dmg', 'avg_hits_useful', 'avg_damage_useful')
            end


            if inArgs(pr .. 'awk_dmg') and inArgs('awk_hits_useful') then
    function parseConfig(mode)
                getTotal('awk_dmg', 'awk_hits_useful', 'total_damage_awk_useful')
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
            end
        for config_key, config_value in pairs(DAMAGE_CONFIG) do
 
             for k, v in pairs(DAMAGE_CONFIG[config_key]) do
            if inArgs(pr .. 'avg_awk_hits') and inArgs('avg_awk_hits_useful') then
                local output_value = v
                getTotal('awk_dmg', 'avg_awk_hits_useful', 'avg_damage_awk_useful')
                 for _, v2 in ipairs(v) do
             end
                    local arg_from_template = args[prefix .. v2] or args[v2]
        end
                     if arg_from_template ~= nil then
 
                         output_value = arg_from_template
        -- Multiply all values with traits and store them in another table.
                        if k == 'provided' then
        for k, v in spairs(fvals) do
                            output_value = true
            if not string.find(k, 'useful') then
                            -- Do not generate total_damage values at all if the skill can't reach them.
                 for kt, vt in spairs(traits) do
                            if string.find(config_key, 'total_') and OPTIONS.no_max then
                     if inArgs(kt) then
                                output_value = false
                         local dmg_name = k .. '_' .. kt
                            end
                         if ispvp then
                        end
                             dmg_name = dmg_name:gsub(su, '') .. su
                        break
                    else
                         if k == 'provided' then
                             output_value = false
                         end
                         end
                        local dmg_formula = v * vt
                        tvals[dmg_name] = dmg_formula
                     end
                     end
                 end
                 end
                if DAMAGE_PARSED[mode][config_key] == nil then
                    DAMAGE_PARSED[mode][config_key] = {}
                end
                DAMAGE_PARSED[mode][config_key][k] = output_value
             end
             end
         end
         end
    end


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


        function addPassive(num, loop_table)
    -- Detected "count", for skills like Clementine, Enough Mineral, etc.
            local pval_index
    function doEachDamage()
            if loop_table == nil then
        for mode, mode_content in pairs(DAMAGE_PARSED) do
                pval_index = num
             for damage_key, damage_value in pairs(mode_content) do
                loop_table = ftvals
                 if string.find(damage_key, 'total_') then
            else
                     local new_value = table.deep_copy(damage_value)
                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.
                    for k, v in ipairs(new_value.hit_counts) do
        if inArgs('passive2') then
                        new_value.hit_counts[k] = v * args.count[1]
            addPassive(2)
                    end
            if inArgs('passive3') then
                addPassive(3, 2)
            end
        end


        if inArgs('passive1') then
                    DAMAGE_PARSED[mode][damage_key:gsub("total_", "each_")] = damage_value
            addPassive(1)
                     DAMAGE_PARSED[mode][damage_key] = new_value
            if inArgs('passive2') then
                addPassive(2, 1)
                if inArgs('passive3') then
                     addPassive(3, 12)
                 end
                 end
             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
         end
        return fvals
     end
     end


     local out = list(false)
     if inArgs('count') then
    local out_pvp = list(true)
         doEachDamage()
 
    -- 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 = '-%'
        else
            dmg = round(dmg)
        end
         if (args.format == 'false' or dmg == '-%') then
            return '{{#vardefine:' .. prefix .. name .. '|' .. dmg .. '}}'
        else
            return '{{#vardefine:' .. prefix .. name .. '|{{formatnum:' .. dmg .. '}}%}}'
        end
     end
     end


    -- Apply ranges.
     function doBasicDamage()
     function getRangeCount(arg)
         for mode, mode_content in pairs(DAMAGE_PARSED) do
         if inArgs(arg) then
             for damage_key, damage_value in pairs(mode_content) do
             data[arg] = split(args[arg])
                local i = 1
            if data[arg][2] == nil then
                local output = 0
                data[arg][2] = data[arg][1]
                -- Check if to even generate the damage.
                if damage_value.provided then
                    -- Loop through damage numbers and multiply them with hits.
                    for k, v in ipairs(damage_value.damage_numbers) do
                        output = output + (v * damage_value.hit_counts[i])
                        i = i + 1
                    end
                    -- Write the result to a separate object.
                    BASIC_DAMAGE[mode][damage_key] = output
                end
             end
             end
         end
         end
     end
     end


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


     function determineRange(minmax)
    local WITH_TRAITS = createDamageDataTable()
         if inArgs('range_' .. minmax) then
     function doTraits()
             data['range_' .. minmax] = split(args['range_' .. minmax])
         -- Handle traits here
            if data['range_' .. minmax][2] == nil then
        for mode, mode_content in pairs(BASIC_DAMAGE) do
                data['range_' .. minmax][2] = data['range_' .. minmax][1]
             for damage_key, damage_value in pairs(mode_content) do
            end
                for _, trait in pairs(TRAITS) do
            if inArgs('range_' .. minmax .. '_count') then
                    --[[
                local i = 1;
                    Suffix all damage values with existing traits.
                for k, v in spairs(data['range_' .. minmax]) do
                    Useful already has the prefix, so only multiply with its value.
                    data['range_' .. minmax][i] = 1 + (-1 + data['range_' .. minmax][i]) *
                    Also, we don't want other traits to multiply with Useful,
                                                      data['range_' .. minmax .. '_count'][i]
                    so we skip those situations, as impossible in-game.
                     i = i + 1
                    --]]
                    if trait.value and (not string.match(damage_key, '_useful') or trait.key == '') then
                        WITH_TRAITS[mode][damage_key .. ((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key))] =
                            damage_value * trait.value
                     end
                 end
                 end
             end
             end
Line 376: Line 286:
     end
     end


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


     -- If maximum range is specified, but not minimum, and minimum count is specified.
     local WITH_PASSIVES = createDamageDataTable()
    -- 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 = {}
    Generates passives with every possible combinations of all subsets.
 
    For example: 3 passives are given, so it will generate the following:
     function applyRange(minmax)
     (1), (2), (3), (1, 2), (1, 3), (1, 2, 3), (2, 3)
         local temp_tab = {};
    ]]
        if minmax == 'min' then
     function doPassives()
            temp_tab = out_min
         for mode, mode_content in pairs(WITH_TRAITS) do
        else
            for damage_key, damage_value in pairs(mode_content) do
            temp_tab = out_max
                local combinations = { {} }
        end
                for passive_key, passive in pairs(PASSIVES) do
        if inArgs('range_max') then
                    local count = #combinations
            for k, v in spairs(out) do
                    for i = 1, count do
                if not (string.starts(k, 'min_') or string.starts(k, 'max_')) then
                        local new_combination = { unpack(combinations[i]) }
                     if (string.find(k, '_pvp')) then
                        table.insert(new_combination, passive_key)
                         temp_tab[minmax .. '_' .. k] = v * data['range_' .. minmax][2];
                        table.insert(combinations, new_combination)
                    else
                    end
                        temp_tab[minmax .. '_' .. k] = v * data['range_' .. minmax][1];
                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
                     end
                    WITH_PASSIVES[mode][damage_key .. name_suffix] = damage_value * passive_multiplier
                 end
                 end
             end
             end
         end
         end
        tableMerge(out, temp_tab)
     end
     end


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


     -- Get the actual variables with MW syntax.
     local RANGE = {
    local vars = {}
        min_count = args.range_min_count and args.range_min_count[1] or 1,
    for k, v in spairs(out) do
        max_count = args.range_max_count and args.range_max_count[1] or 1,
         table.insert(vars, var(k, v, args.prefix))
        PvE = {
     end
            min = args.range_min and args.range_min[1] or 1,
            max = args.range_max and args.range_max[1] or 1
        },
         PvP = {
            min = args.range_min and (args.range_min[2] or args.range_min[1]) or 1,
            max = args.range_max and (args.range_max[2] or args.range_max[1]) or 1
        }
     }


    -- Transform ranges to variables.
     local WITH_RANGE = createDamageDataTable()
     local vars_range = {}
     function doDamageBuffRange()
     if (inArgs('range_max')) then
        -- Handle damage range here
         for k, v in spairs(out) do
         for mode, mode_content in pairs(WITH_PASSIVES) do
             if not (string.starts(k, 'min_') or string.starts(k, 'max_')) then
             for damage_key, damage_value in pairs(mode_content) do
                local prefix = ''
                WITH_RANGE[mode][damage_key] = { min = 0, max = 0 }
                if args.prefix ~= nil then
                for _, range in ipairs({ 'min', 'max' }) do
                     prefix = args.prefix .. '_'
                    local final_damage_value = damage_value * (1 + ((RANGE[mode][range] - 1) * RANGE[range .. '_count'])) *
                        OPTIONS.perm_buff[mode]
                     WITH_RANGE[mode][damage_key][range] = not OPTIONS.format and final_damage_value or
                        formatDamage(final_damage_value)
                 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
     end


     -- Dump all values if wanted.
     doDamageBuffRange()
    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
     local FINAL_DAMAGE = WITH_RANGE
    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)
     -- Helper function to iterate over traits.
         if not el then
    function checkTraits(settings)
            return false
         local output
        end
         if not settings then
         if type == 'row' then
             output = false
             type = 'rowspan'
         else
         else
             type = 'colspan'
             output = settings.output or {}
         end
         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)
         for trait_index, trait in ipairs(TRAITS) do
    local combine_suffix = args.combine_suffix
            if trait.value ~= false and trait_index ~= 1 then
    local append = split(args.append)[1]
                if settings and type(settings.action) == 'function' then
    local append_alias = split(args.append)[2]
                    settings.action(trait, output, settings)
 
                else
    local tbl_order = {'extra', 'passives_normal', 'passives_switch', 'awk', 'traits', 'hit_count'}
                    return true
    local STR = {
                 end
        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
         end
        return output
     end
     end


     for passive_i = 1, 3 do
     -- Helper function to detect combined passives.
        repeat
    function isCombined(index)
            -- Add normal passives to the first row.
        for k, v in ipairs(OPTIONS.combine) do
            local passive_name = ''
             if index == v then
            -- Alias for appended passives.
                 return true
            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
             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
        return false
     end
     end


     -- Add Useful trait.
     -- Helper function to iterate over passives.
     if inArgs('hits_useful') or inArgs('avg_hits_useful') then
     function checkPassives(settings)
         tbl_content.traits.useful = 'true'
         local output = settings.output or {}
        tbl_content.traits.hide = false
         local PASSIVES_WITH_COMBINED = table.deep_copy(PASSIVES)
    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)
         -- Handle combined passives properly.
         return tbl_content[level].hide
         if OPTIONS.combine then
    end
            table.insert(PASSIVES_WITH_COMBINED, {
 
                is_combined = true
    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
         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)
        for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do
                local th = tr:tag('th'):wikitext(wikitext)
            if (not OPTIONS.is_append or (OPTIONS.is_append and OPTIONS.append_index ~= passive_index)) and not inArrayHasValue(passive_index, OPTIONS.combine or {}) then
                 if normal == true then
                 if type(settings.action) == 'function' then
                    table.insert(cells[type].normal_th, th)
                     settings.action(passive, output, passive_index)
                elseif normal == false then
                     table.insert(cells[type].th, th)
                 else
                 else
                     return th
                     return true
                 end
                 end
             end
             end
        end
        return output
    end


             function multiplySpaceAll(level, num)
    -- Generate the table
                if cells[level] == nil then
    local TABLE = mw.html.create('table'):attr({
                     return false
        cellpadding = 5,
        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.suffix)
                        end
                        table.insert(output, table.concat(combo, '/') .. OPTIONS.combine_suffix)
                    else
                        table.insert(output, link(passive.name, passive.alias) .. passive.suffix)
                     end
                 end
                 end
                 num = num or 2
            }),
                for k, v in ipairs(cells[level].th) do
            keywords = checkPassives({
                     multiplySpace(v, 'col', num)
                 action = function(passive, output, passive_index)
                    if passive.is_combined then
                        -- Handling combined passive damage cells.
                        table.insert(output, sortPassives('passive' .. table.concat(OPTIONS.combine, '_passive')))
                     else
                        table.insert(output, 'passive' .. passive_index)
                    end
                 end
                 end
                 for k, v in ipairs(cells[level].normal_th) do
            }),
                    multiplySpace(v, 'col', num)
            is_visible = not OPTIONS.no_max or #PASSIVES > 0,
            is_special = true
        },
        {
            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 },
            keywords = { OPTIONS.is_append and 'passive' .. OPTIONS.append_index or nil },
            is_visible = OPTIONS.is_append or false
        },
        {
            type = 'awakening',
            text = { 'Regular', link('Awakening Mode') },
            keywords = { 'awk' },
            keyword_next_to_main_key = true,
            is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or false
        },
        {
            type = 'traits',
            text = checkTraits({
                output = { 'Normal' },
                action = function(trait, output)
                    table.insert(output, trait.name)
                 end
                 end
             end
             }),
 
            keywords = checkTraits({
            function reverseMultiplySpace(num, increase_mode)
                action = function(trait, output)
                local i = #tbl_order;
                     table.insert(output, trait.key)
                num = num or 2
 
                local fix_for_no_max = 0
                if no_max and not levels_exist then
                     fix_for_no_max = -1
                 end
                 end
 
            }),
                 while (i > 1) do
            is_visible = checkTraits()
                    multiplySpaceAll(tbl_order[i - 1 + fix_for_no_max], num)
        },
                     i = i - 1
        {
            type = 'hit_count',
            text = {
                 (inArgs('count') and not OPTIONS.use_avg) and
                (table.concat({ 'Per', args.count_name or 'Instance' }, ' ')) 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
                 if increase_mode ~= false then
                 return { 'total' }
                    increaseSpace(mode_th, 'row')
            end)(),
                end
            is_visible = ((inArgs('avg_hits') or inArgs('count')) and not OPTIONS.no_max) or false
            end
        }
    }


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


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


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


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


                 for i = 1, 3, 1 do
            -- Check if it's the first iteration. If so, append phrases.
                    local passive_link = data[i]
            if not current_row.no_damage then
                     if (passive_link ~= nil) then
                 if i == #TABLE_CONTENT then
                        local suffix = ''
                     for _, keyword in ipairs(current_row.keywords) do
                         if next(data.suffixes) and data.suffixes[i] ~= false then
                         if not OPTIONS.no_max or (OPTIONS.no_max and keyword ~= 'total') then
                             suffix = ' ' .. data.suffixes[i]
                             local new_key = keyword .. '_' .. main_key
                            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
                 end
                 elseif current_row.is_special and current_row.is_visible then
 
                    -- Append suffix for each keyword in current row
                -- Handle combining passives.
                    for _, keyword in ipairs(current_row.keywords) do
                if next(data.combined) then
                        -- Iterate through previous keys
                    local combined_str = ''
                        for _, prev_key in ipairs(all_list) do
                    for k, v in spairs(data.combined) do
                            local new_key = prev_key .. '_' .. keyword
                        combined_str = combined_str .. makePassiveLink(v, data.aliases[k], data.suffixes[k]) .. '/'
                            table.insert(new_list, new_key)
                        end
                     end
                     end
                     combined_str = combined_str:gsub('/$', '')
                elseif current_row.is_visible then
                    if combine_suffix then
                     -- Iterate through previous keys
                         combined_str = combined_str .. ' ' .. combine_suffix
                    for _, prev_key in ipairs(all_list) do
                    end
                        -- Append suffix for each keyword in current row
                    new(combined_str, false)
                         for _, keyword in ipairs(current_row.keywords) do
                    passive_normal_count = passive_normal_count + 1
                            local new_key = prev_key .. '_' .. keyword
                end
 
            end


            if (type == 'passives_switch') then
                            -- If needed, move the suffix to the rightmost of main_key.
                if not hidden(type) then
                            if current_row.keyword_next_to_main_key then
                    multiplySpaceAll('passives_normal')
                                new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
                    -- 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


                            table.insert(new_list, new_key)
                         end
                         end
                     end
                     end
                 end
                 end
            end


            if (type == 'awk' and not hide) then
                -- Append new_list to all_list
                 reverseMultiplySpace();
                 for _, new_key in ipairs(new_list) do
                table.insert(awk_table, 'awk')
                    table.insert(all_list, sortPassives(new_key))
 
                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


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


                -- Manually fix certain situations.
    function doInitialCell(new_row)
                local has_awk = 1
        return new_row:tag('th'):wikitext('Mode')
                if not hidden('awk') then
    end
                    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
    function doHeaders()
                if no_max and levels_exist then
        local current_multiplier = 0 -- Keeps track of the number of cells to spawn
                    increaseSpace(mode_th, 'row')
        local initial_header_cell    -- The leftmost cell that says "Mode"
                elseif levels_exist or (args.avg_hits and args.hits) then
        local iterations = 0        -- Keeps track of iterations that successfully rendered something. Required to tell the initial cell how many columns to span.
                    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.
        for row_index, row in ipairs(TABLE_CONTENT) do
                if hidden('awk') then
            if row.is_visible then
                    loop_factor = loop_factor * (passive_switch_count + 1)
                local new_row = TABLE:new()
                 end
                 local next_multiplier = 0


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


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


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


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


     function concat(tbl)
     function doContentByMode(mode)
         local returned_str = ''
         local mode_row = TABLE:new()
         for k, v in ipairs(tbl) do
         mode_row:tag('td'):wikitext(frame:expandTemplate { title = mode })
            local delimiter = '_'
        local damage_entries = returnDamageInOrder()
            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)
        for _, damage_key in ipairs(damage_entries) do
        local mode_cell = 'PvE'
            if args.dump_names ~= 'true' then
        if mode_flag == true then
                local damage_number = FINAL_DAMAGE[mode][damage_key]
            mode_flag = '_pvp'
            mode_cell = 'PvP'
        else
            mode_flag = ''
        end


        local value_row = tbl:tag('tr')
                -- Display ranges.
                damage_number = doRangeText(damage_number)


        function display(name, range_flag)
                mode_row:tag('td'):wikitext(damage_number
            local range_factor;
                    -- Error out if it doesn't exist
            local cell_content = {}
                    or frame:expandTemplate {
            if range_flag == true then
                        title = 'color',
                range_factor = 2
                        args = { 'red', '&#35;ERROR' }
                    })
             else
             else
                 range_factor = 1
                 mode_row:tag('td'):wikitext(damage_key)
             end
             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, '&#45;%')
                    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
         end
    end


        local ret2 = ''
    function doTable()
 
         doHeaders()
        value_row:tag('td'):wikitext(frame:expandTemplate{
         doContentByMode('PvE')
            title = mode_cell
        doContentByMode('PvP')
         })
 
         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
    doTable()


     -- For debugging purposes
     -- Dump all values if wanted.
     if (args.debug == 'true') then
     if OPTIONS.dump_table_data then
         ret = ''
         return inspect_dump(frame, TABLE_CONTENT)
        for i = 1, #tbl_order, 1 do
    elseif OPTIONS.dump then
            ret = ret .. "'''" .. tbl_order[i] .. "''': <br/>"
        return inspect_dump(frame, FINAL_DAMAGE)
            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
     end
    makeValueRows();
    makeValueRows(true);


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


     return parsed .. bug .. tostring(tbl)
     -- Transform into variables
    local variables = doVariables(frame, FINAL_DAMAGE, OPTIONS.prefix)


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


return p
return p
-- pyend

Revision as of 19:37, 21 April 2023

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

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

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

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

    -- Define the schema for the table
    local tableSchema = {
        PvE = {},
        PvP = {}
    }

    -- Function to create a new table with the desired schema
    function createDamageDataTable()
        local newTable = {}
        for key, value in pairs(tableSchema) do
            if type(value) == "table" then
                newTable[key] = {}
            end
        end
        return newTable
    end

    -- User requested options
    local OPTIONS = {
        do_table = args[1] == 'true',
        character = args[2] or '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',
        prefix = args.prefix,
        use_avg = args.use_avg == 'true'
    }

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

    -- Define a table with trait names and their values to apply.
    local TRAITS = {
        -- An empty trait so we keep the original values there.
        {
            key = '',
            name = 'Normal',
            value = 1
        },
        {
            key = 'enhanced',
            name = 'Enhanced',
            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 ~= nil and (tonumber(args.useful_penalty) or 0.7)
        },
        {
            key = 'heavy',
            name = 'Heavy',
            value = args.heavy ~= nil and 1.44
        }
    }

    -- Define a table with user-requested passive skills (empty by default).
    local PASSIVES = {}

    for k, v in pairs(args) do
        if string.find(k, 'passive') then
            --[[
            Fix up the passives and put them into a separate table.
            |passive1=... |passive2=... -> { passive1, passive2 }
            --]]
            local passive_index = string.match(k, "%d")
            local passive_values = split(frame:preprocess('{{:' .. v .. '}}{{#arrayprint:' .. v .. '}}'));
            PASSIVES[tonumber(passive_index)] = {
                name = v,
                value = passive_values[1],
                value_pvp = passive_values[2],
                alias = args['alias' .. passive_index] or (passive_index == OPTIONS.append_index and OPTIONS.append_name),
                suffix = args['suffix' .. passive_index] and (' ' .. args['suffix' .. passive_index]) or '',
            }
        elseif string.find(k, 'append') or not string.find(v, '[a-zA-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
                split_values[k2] = frame:preprocess('{{#expr:' .. v2 .. '}}')
            end
            args[k] = split_values
        end
    end

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

    -- Store a configuration that will tell the main function how to behave given different inputs.
    -- It will always take the first value if available. If not, fall back to the other (recursively).
    local 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_PARSED = createDamageDataTable()

    function parseConfig(mode)
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
        for config_key, config_value in pairs(DAMAGE_CONFIG) do
            for k, v in pairs(DAMAGE_CONFIG[config_key]) do
                local output_value = v
                for _, v2 in ipairs(v) do
                    local arg_from_template = args[prefix .. v2] or args[v2]
                    if arg_from_template ~= nil then
                        output_value = arg_from_template
                        if k == 'provided' then
                            output_value = true
                            -- Do not generate total_damage values at all if the skill can't reach them.
                            if string.find(config_key, 'total_') and OPTIONS.no_max then
                                output_value = false
                            end
                        end
                        break
                    else
                        if k == 'provided' then
                            output_value = false
                        end
                    end
                end
                if DAMAGE_PARSED[mode][config_key] == nil then
                    DAMAGE_PARSED[mode][config_key] = {}
                end
                DAMAGE_PARSED[mode][config_key][k] = output_value
            end
        end
    end

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

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

                    for k, v in ipairs(new_value.hit_counts) do
                        new_value.hit_counts[k] = v * args.count[1]
                    end

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

    if inArgs('count') then
        doEachDamage()
    end

    function doBasicDamage()
        for mode, mode_content in pairs(DAMAGE_PARSED) do
            for damage_key, damage_value in pairs(mode_content) do
                local i = 1
                local output = 0
                -- Check if to even generate the damage.
                if damage_value.provided then
                    -- Loop through damage numbers and multiply them with hits.
                    for k, v in ipairs(damage_value.damage_numbers) do
                        output = output + (v * damage_value.hit_counts[i])
                        i = i + 1
                    end
                    -- Write the result to a separate object.
                    BASIC_DAMAGE[mode][damage_key] = output
                end
            end
        end
    end

    doBasicDamage()

    local WITH_TRAITS = createDamageDataTable()
    function doTraits()
        -- Handle traits here
        for mode, mode_content in pairs(BASIC_DAMAGE) do
            for damage_key, damage_value in pairs(mode_content) do
                for _, trait in pairs(TRAITS) do
                    --[[
                    Suffix all damage values with existing traits.
                    Useful already has the prefix, so only multiply with its value.
                    Also, we don't want other traits to multiply with Useful,
                    so we skip those situations, as impossible in-game.
                    --]]
                    if trait.value and (not string.match(damage_key, '_useful') or trait.key == '') then
                        WITH_TRAITS[mode][damage_key .. ((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key))] =
                            damage_value * trait.value
                    end
                end
            end
        end
    end

    doTraits()

    local WITH_PASSIVES = createDamageDataTable()

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

    doPassives()

    local RANGE = {
        min_count = args.range_min_count and args.range_min_count[1] or 1,
        max_count = args.range_max_count and args.range_max_count[1] or 1,
        PvE = {
            min = args.range_min and args.range_min[1] or 1,
            max = args.range_max and args.range_max[1] or 1
        },
        PvP = {
            min = args.range_min and (args.range_min[2] or args.range_min[1]) or 1,
            max = args.range_max and (args.range_max[2] or args.range_max[1]) or 1
        }
    }

    local WITH_RANGE = createDamageDataTable()
    function doDamageBuffRange()
        -- Handle damage range here
        for mode, mode_content in pairs(WITH_PASSIVES) do
            for damage_key, damage_value in pairs(mode_content) do
                WITH_RANGE[mode][damage_key] = { min = 0, max = 0 }
                for _, range in ipairs({ 'min', 'max' }) do
                    local final_damage_value = damage_value * (1 + ((RANGE[mode][range] - 1) * RANGE[range .. '_count'])) *
                        OPTIONS.perm_buff[mode]
                    WITH_RANGE[mode][damage_key][range] = not OPTIONS.format and final_damage_value or
                        formatDamage(final_damage_value)
                end
            end
        end
    end

    doDamageBuffRange()

    local FINAL_DAMAGE = WITH_RANGE

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

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

    -- Helper function to detect combined passives.
    function isCombined(index)
        for k, v in ipairs(OPTIONS.combine) do
            if index == v then
                return true
            end
        end
        return false
    end

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

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

        for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do
            if (not OPTIONS.is_append or (OPTIONS.is_append and OPTIONS.append_index ~= passive_index)) and not inArrayHasValue(passive_index, OPTIONS.combine or {}) then
                if type(settings.action) == 'function' then
                    settings.action(passive, output, passive_index)
                else
                    return true
                end
            end
        end
        return output
    end

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

    -- Our table structure
    local TABLE_CONTENT = {
        {
            type = 'extra',
            text = { '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.suffix)
                        end
                        table.insert(output, table.concat(combo, '/') .. OPTIONS.combine_suffix)
                    else
                        table.insert(output, link(passive.name, passive.alias) .. passive.suffix)
                    end
                end
            }),
            keywords = checkPassives({
                action = function(passive, output, passive_index)
                    if passive.is_combined then
                        -- Handling combined passive damage cells.
                        table.insert(output, sortPassives('passive' .. table.concat(OPTIONS.combine, '_passive')))
                    else
                        table.insert(output, 'passive' .. passive_index)
                    end
                end
            }),
            is_visible = not OPTIONS.no_max or #PASSIVES > 0,
            is_special = true
        },
        {
            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 },
            keywords = { OPTIONS.is_append and 'passive' .. OPTIONS.append_index or nil },
            is_visible = OPTIONS.is_append or false
        },
        {
            type = 'awakening',
            text = { 'Regular', link('Awakening Mode') },
            keywords = { 'awk' },
            keyword_next_to_main_key = true,
            is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or false
        },
        {
            type = 'traits',
            text = checkTraits({
                output = { 'Normal' },
                action = function(trait, output)
                    table.insert(output, trait.name)
                end
            }),
            keywords = checkTraits({
                action = function(trait, output)
                    table.insert(output, trait.key)
                end
            }),
            is_visible = checkTraits()
        },
        {
            type = 'hit_count',
            text = {
                (inArgs('count') and not OPTIONS.use_avg) and
                (table.concat({ 'Per', args.count_name or 'Instance' }, ' ')) 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
                return { 'total' }
            end)(),
            is_visible = ((inArgs('avg_hits') or inArgs('count')) and not OPTIONS.no_max) or false
        }
    }

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

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

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

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

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

                            -- If needed, move the suffix to the rightmost of main_key.
                            if current_row.keyword_next_to_main_key then
                                new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
                            end

                            table.insert(new_list, new_key)
                        end
                    end
                end

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

        return all_list
    end

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

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

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

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

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

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

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

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

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

                -- Display ranges.
                damage_number = doRangeText(damage_number)

                mode_row:tag('td'):wikitext(damage_number
                    -- Error out if it doesn't exist
                    or frame:expandTemplate {
                        title = 'color',
                        args = { 'red', '&#35;ERROR' }
                    })
            else
                mode_row:tag('td'):wikitext(damage_key)
            end
        end
    end

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

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

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

    -- Transform into variables
    local variables = doVariables(frame, FINAL_DAMAGE, OPTIONS.prefix)

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

return p