Module:Test

From Elwiki
Revision as of 13:45, 5 October 2023 by Ritsu (talk | contribs)

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

-- pystart
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)
    local out

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

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

    -- Define the schema for the table
    local tableSchema = {}
    for _, mode in ipairs(modes) do
        tableSchema[mode] = {}
    end

    function forEach(func)
        for _, mode in ipairs(modes) do
            func(mode)
        end
    end

    function forEachDamageType(func)
        for _, damage_type in ipairs({ 'min', 'max' }) do
            func(damage_type)
        end
    end

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

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

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

    -- Define a table with trait names and their values to apply.
    local TRAITS = {
        -- An empty trait so we keep the original values there.
        {
            key = '',
            name = '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)
        return frame:preprocess('{{#expr:' .. s .. '}}')
    end

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

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

            PASSIVES[tonumber(passive_index)] = {
                name = passive_name,
                value = passive_values[1],
                value_pvp = passive_values[2],
                alias = args['alias' .. passive_index] or (passive_index == OPTIONS.append_index and OPTIONS.append_name),
                suffix = args['suffix' .. passive_index] and (' ' .. args['suffix' .. passive_index]) or '',
                prefix = args['prefix' .. passive_index] and (args['prefix' .. passive_index] .. ' ') or '',
                exist = frame:preprocess('{{#ifexist:' .. passive_name .. '|true|false}}') == 'true'
            }
        elseif 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

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

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

    -- Store a configuration that will tell the main function how to behave given different inputs.
    -- It will always take the first value if available. If not, fall back to the other (recursively).
    local BASE_DAMAGE_CONFIG = {
        total_damage = {
            damage_numbers = { 'dmg' },
            hit_counts = { 'hits' },
            provided = { 'dmg' }
        },
        total_damage_awk = {
            damage_numbers = { 'awk_dmg', 'dmg' },
            hit_counts = { 'awk_hits', 'hits' },
            provided = { 'awk_dmg', 'awk_hits' }
        },
        avg_damage = {
            damage_numbers = { 'dmg' },
            hit_counts = { 'avg_hits', 'hits' },
            provided = { 'avg_hits' }
        },
        avg_damage_awk = {
            damage_numbers = { 'awk_dmg', 'dmg' },
            hit_counts = { 'avg_awk_hits', 'awk_hits', 'avg_hits', 'hits' },
            provided = { 'avg_awk_hits', args.awk_dmg and 'avg_hits' or nil }
        },
        -- Store the logic for Useful traits
        total_damage_useful = {
            damage_numbers = { 'dmg' },
            hit_counts = { 'hits_useful', 'hits' },
            provided = { 'hits_useful' }
        },
        total_damage_awk_useful = {
            damage_numbers = { 'awk_dmg', 'dmg' },
            hit_counts = { 'awk_hits_useful', 'awk_hits', 'hits_useful', 'hits' },
            provided = { 'awk_hits_useful' }
        },
        avg_damage_useful = {
            damage_numbers = { 'dmg' },
            hit_counts = { 'avg_hits_useful', 'hits_useful', 'avg_hits', 'hits' },
            provided = { 'avg_hits_useful' }
        },
        avg_damage_awk_useful = {
            damage_numbers = { 'awk_dmg', 'dmg' },
            hit_counts = { 'avg_awk_hits_useful', 'avg_awk_hits', 'hits_useful', 'hits' },
            provided = { 'avg_awk_hits_useful' }
        },
    }

    local DAMAGE_CONFIG = {}
    function handleCancel()
        local processed_keys = {}
        for config_key, config_value in pairs(BASE_DAMAGE_CONFIG) do
            if not config_key:match('cancel_') then
                local new_config_value = {}
                for arg_table_key, arg_table in pairs(config_value) do
                    local new_arg_table = {}
                    for _, arg in ipairs(arg_table) do
                        table.insert(new_arg_table, 'cancel_' .. arg)
                    end
                    new_config_value[arg_table_key] = new_arg_table
                end
                local new_key = 'cancel_' .. config_key
                DAMAGE_CONFIG[new_key] = new_config_value
                processed_keys[new_key] = true
            end
        end
        return processed_keys
    end

    if args.cancel_dmg then
        handleCancel()
        DAMAGE_CONFIG = table.fuse(BASE_DAMAGE_CONFIG, DAMAGE_CONFIG)
    else
        DAMAGE_CONFIG = BASE_DAMAGE_CONFIG
    end

    -- 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 config_key, config_value in pairs(DAMAGE_CONFIG) do
            for arg_table_key, arg_table in pairs(config_value) do
                if arg_table_key ~= 'provided' and arg_table then
                    -- We only do this for the first (main) key
                    local main_key = arg_table[1]
                    local main_key_prefixed = prefix .. main_key
                    local main_arg_values = args[main_key_prefixed]

                    -- Only if the main argument values exist.
                    if main_arg_values then
                        local i = 1
                        --[[
                            Loop over all damage and attempt to inherit in chain.
                            Break the loop if a match was found. Note: For this to work, the value must be an empty string.
                            Alternatively, it can contain an "i" to template the value to inherit.
                        ]]
                        local cancel_dmg_len = args.cancel_dmg and #(args.cancel_dmg) or 0
                        while i <= (#(args.dmg) + cancel_dmg_len) do
                            local main_arg_value = main_arg_values[i]

                            for ix, inherit_key in ipairs(arg_table) do
                                local inherit_arg = args[prefix .. inherit_key] or args[inherit_key]
                                -- No inheritance from itself.
                                if inherit_arg and inherit_arg[i] and inherit_arg[i] ~= '' and ix ~= 1 then
                                    -- Only inherit if empty
                                    if main_arg_value == '' then
                                        args[main_key_prefixed][i] = inherit_arg[i]
                                        break
                                    elseif main_arg_value and string.find(main_arg_value, 'i') and inherit_arg[i] then
                                        args[main_key_prefixed][i] = eval(main_arg_value:gsub('i', inherit_arg[i]))
                                        break
                                    end
                                end
                            end

                            i = i + 1
                        end
                    end
                end
            end
        end
    end

    forEach(inherit)

    local DAMAGE_PARSED = createDamageDataTable()
    function parseConfig(mode)
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
        for config_key, config_value in pairs(DAMAGE_CONFIG) do
            for k, v in pairs(config_value) do
                local output_value = {}

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

                for _, v2 in ipairs(v) do  -- This array holds the argument names with fallbacks

                    forEachDamageType(function(damage_type)
                        -- If there already is a value for this damage type (min or max), do not continue.
                        if isValueFound[damage_type] == true then
                            return
                        end

                        local arg_from_template =
                            args[prefix .. v2 .. '_' .. damage_type]
                            or args[v2 .. '_' .. damage_type]
                            or args[prefix .. v2]
                            or args[v2];

                        if arg_from_template ~= nil then
                            output_value[damage_type] = arg_from_template
                            if k == 'provided' then
                                output_value[damage_type] = 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[damage_type] = false
                                end
                            end
                            -- Mark the value as found.
                            isValueFound[damage_type] = true
                        else
                            if k == 'provided' then
                                output_value[damage_type] = false
                            else
                                output_value[damage_type] = {}
                            end
                        end
                    end)

                    -- Both values found, we can now break the loop.
                    if isValueFound.min and isValueFound.max then
                        break
                    end

                end
                if DAMAGE_PARSED[mode][config_key] == nil then
                    DAMAGE_PARSED[mode][config_key] = {}
                end
                DAMAGE_PARSED[mode][config_key][k] = output_value
            end
        end
    end

    forEach(parseConfig)

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

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

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

    if args.count then
        DAMAGE_PARSED = doEachDamage()
    end

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

    doBasicDamage()

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

    if args.cancel_dmg then
        addCancelDamage()
    end

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

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

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

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

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

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

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

    -- -- Our table structure
    -- local TABLE_CONTENT = {
    --     {
    --         type = 'extra',
    --         text = { '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
    --         }),
    --         keywords = checkPassives({
    --             action = function(passive, output, passive_index)
    --                 if passive.is_combined then
    --                     -- Handling combined passive damage cells.
    --                     table.insert(output, sortPassives('passive' .. table.concat(OPTIONS.combine, '_passive')))
    --                 else
    --                     table.insert(output, 'passive' .. passive_index)
    --                 end
    --             end
    --         }),
    --         is_visible = not OPTIONS.no_max or #PASSIVES > 0
    --     },
    --     {
    --         type = 'passive_appended',
    --         text = {
    --             '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', nil,
    --                     OPTIONS.dmp ~= 'false' and ('(' .. OPTIONS.dmp .. ' DMP)'))
    --             elseif args.awk_alias then
    --                 return link(unpack(args.awk_alias))
    --             end
    --             return link('Awakening Mode')
    --         end)()
    --         },
    --         keywords = { 'awk' },
    --         keyword_next_to_main_key = true,
    --         is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or inArgs('avg_awk_hits') or false
    --     },
    --     {
    --         type = 'traits',
    --         text = checkTraits({
    --             output = { 'Normal' },
    --             action = function(trait, output)
    --                 table.insert(output, trait.name)
    --             end
    --         }),
    --         keywords = checkTraits({
    --             action = function(trait, output)
    --                 table.insert(output, trait.key)
    --             end
    --         }),
    --         is_visible = checkTraits()
    --     },
    --     {
    --         type = 'cancel',
    --         text = {
    --             '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
    --             return { 'total' }
    --         end)(),
    --         is_visible = ((inArgs('avg_hits') or inArgs('count')) and not OPTIONS.no_max) or false
    --     }
    -- }

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

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

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

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

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

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

    --     -- Sort the list once more, in order to swap the order of cancel & full.
    --     if inArgs('cancel_dmg') then
    --         local new_list = {}
    --         local cancel_counter = 1
    --         local full_counter = 2
    --         for i, damage_key in ipairs(all_list) do
    --             local regex = "^(%w+_)"
    --             local prefix = 'cancel_'
    --             local match = string.match(damage_key, regex)
    --             if (match == prefix) then
    --                 new_list[i] = damage_key:gsub(prefix, "")
    --             else
    --                 new_list[i] = prefix .. damage_key
    --             end
    --         end
    --         all_list = new_list
    --     end

    --     return all_list
    -- end

    -- function doInitialCell(new_row)
    --     return new_row:tag('th'):wikitext('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()
    --     local last_number
    --     local last_unique_cell

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

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

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

    -- 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)
    elseif OPTIONS.dump_parsed then
        return inspect_dump(frame, DAMAGE_PARSED)
    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)

    if out ~= nil then
        return inspect_dump(frame, out)
    end

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

return p
-- pyend