Module:Test: Difference between revisions

From Elwiki
(Bot: Automated import of articles *** existing text overwritten ***)
(Bot: Automated import of articles *** existing text overwritten ***)
Line 220: Line 220:


                             for ix, inherit_key in ipairs(arg_table) do
                             for ix, inherit_key in ipairs(arg_table) do
                                inherit_key = prefix .. inherit_key
                                 local inherit_arg = args[prefix .. inherit_key] or args[inherit_key]
                                 local inherit_arg = args[prefix .. inherit_key] or args[inherit_key]
                                 -- No inheritance from itself.
                                 -- No inheritance from itself.
Line 282: Line 281:


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


    --                for k, hit_count in ipairs(new_value.hit_counts) do
                    for k, hit_count in ipairs(new_value.hit_counts) do
    --                    hit_count = hit_count == '' and 1 or hit_count
                        hit_count = hit_count == '' and 1 or hit_count
    --                    new_value.hit_counts[k] = 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])
                            ((string.find(damage_key, 'awk_') and args.awk_count) and args.awk_count[1] or args.count[1])
    --                end
                    end


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


     -- if args.count then
     if args.count then
    --    doEachDamage()
        doEachDamage()
     -- end
     end


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


     -- doBasicDamage()
     doBasicDamage()


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


     -- doTraits()
     doTraits()


     -- local WITH_PASSIVES = createDamageDataTable()
     local WITH_PASSIVES = createDamageDataTable()


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


     -- doPassives()
     doPassives()


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


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


     -- doDamageBuffRange()
     doDamageBuffRange()


     -- local FINAL_DAMAGE = WITH_RANGE
     local FINAL_DAMAGE = WITH_RANGE


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


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


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


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


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


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


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


     -- -- Our table structure
     -- Our table structure
     -- local TABLE_CONTENT = {
     local TABLE_CONTENT = {
    --    {
        {
    --        type = 'extra',
            type = 'extra',
    --        text = { 'Average' },
            text = { 'Average' },
    --        is_visible = OPTIONS.no_max,
            is_visible = OPTIONS.no_max,
    --        no_damage = true
            no_damage = true
    --    },
        },
    --    {
        {
    --        type = 'passives',
            type = 'passives',
    --        text = checkPassives({
            text = checkPassives({
    --            output = { 'Base' },
                output = { 'Base' },
    --            action = function(passive, output)
                action = function(passive, output)
    --                if passive.is_combined then
                    if passive.is_combined then
    --                    -- Handling combined passive header name.
                        -- Handling combined passive header name.
    --                    local combo = {}
                        local combo = {}
    --                    for _, passive_key in ipairs(OPTIONS.combine) do
                        for _, passive_key in ipairs(OPTIONS.combine) do
    --                        passive = PASSIVES[passive_key]
                            passive = PASSIVES[passive_key]
    --                        table.insert(combo, link(passive.name, passive.alias) .. passive.suffix)
                            table.insert(combo, link(passive.name, passive.alias) .. passive.suffix)
    --                    end
                        end
    --                    table.insert(output, table.concat(combo, '/') .. OPTIONS.combine_suffix)
                        table.insert(output, table.concat(combo, '/') .. OPTIONS.combine_suffix)
    --                else
                    else
    --                    table.insert(output, link(passive.name, passive.alias) .. passive.suffix)
                        table.insert(output, link(passive.name, passive.alias) .. passive.suffix)
    --                 end
                    end
    --             end
                 end
    --        }),
             }),
    --        keywords = checkPassives({
            keywords = checkPassives({
    --            action = function(passive, output, passive_index)
                action = function(passive, output, passive_index)
    --                if passive.is_combined then
                    if passive.is_combined then
    --                    -- Handling combined passive damage cells.
                        -- Handling combined passive damage cells.
    --                    table.insert(output, sortPassives('passive' .. table.concat(OPTIONS.combine, '_passive')))
                        table.insert(output, sortPassives('passive' .. table.concat(OPTIONS.combine, '_passive')))
    --                else
                    else
    --                    table.insert(output, 'passive' .. passive_index)
                        table.insert(output, 'passive' .. passive_index)
    --                 end
                    end
    --             end
                 end
    --        }),
             }),
    --        is_visible = not OPTIONS.no_max or #PASSIVES > 0
            is_visible = not OPTIONS.no_max or #PASSIVES > 0
    --    },
        },
    --    {
        {
    --        type = 'passive_appended',
            type = 'passive_appended',
    --        text = { 'Normal',
            text = { 'Normal',
    --            OPTIONS.is_append and
                OPTIONS.is_append and
    --            link(PASSIVES[OPTIONS.append_index].name, PASSIVES[OPTIONS.append_index].alias or OPTIONS.append_name) },
                link(PASSIVES[OPTIONS.append_index].name, PASSIVES[OPTIONS.append_index].alias or OPTIONS.append_name) },
    --        keywords = { OPTIONS.is_append and ('passive' .. OPTIONS.append_index) or nil },
            keywords = { OPTIONS.is_append and ('passive' .. OPTIONS.append_index) or nil },
    --        is_visible = OPTIONS.is_append or false
            is_visible = OPTIONS.is_append or false
    --    },
        },
    --    {
        {
    --        type = 'awakening',
            type = 'awakening',
    --        text = { 'Regular', (function()
            text = { 'Regular', (function()
    --            if OPTIONS.dmp then
                if OPTIONS.dmp then
    --                return link('Dynamo Point System', 'Dynamo Configuration',
                    return link('Dynamo Point System', 'Dynamo Configuration',
    --                    OPTIONS.dmp ~= 'false' and ('(' .. OPTIONS.dmp .. ' DMP)'))
                        OPTIONS.dmp ~= 'false' and ('(' .. OPTIONS.dmp .. ' DMP)'))
    --            elseif args.awk_alias then
                elseif args.awk_alias then
    --                return link(unpack(args.awk_alias))
                    return link(unpack(args.awk_alias))
    --            end
                end
    --            return link('Awakening Mode')
                return link('Awakening Mode')
    --        end)()
            end)()
    --        },
            },
    --        keywords = { 'awk' },
            keywords = { 'awk' },
    --        keyword_next_to_main_key = true,
            keyword_next_to_main_key = true,
    --        is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or inArgs('avg_awk_hits') or false
            is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or inArgs('avg_awk_hits') or false
    --    },
        },
    --    {
        {
    --        type = 'traits',
            type = 'traits',
    --        text = checkTraits({
            text = checkTraits({
    --            output = { 'Normal' },
                output = { 'Normal' },
    --            action = function(trait, output)
                action = function(trait, output)
    --                table.insert(output, trait.name)
                    table.insert(output, trait.name)
    --            end
                end
    --        }),
            }),
    --        keywords = checkTraits({
            keywords = checkTraits({
    --            action = function(trait, output)
                action = function(trait, output)
    --                table.insert(output, trait.key)
                    table.insert(output, trait.key)
    --            end
                end
    --        }),
            }),
    --        is_visible = checkTraits()
            is_visible = checkTraits()
    --    },
        },
    --    {
        {
    --        type = 'hit_count',
            type = 'hit_count',
    --        text = {
            text = {
    --            (inArgs('count') and not OPTIONS.use_avg) and
                (inArgs('count') and not OPTIONS.use_avg) and
    --            (table.concat({ 'Per', args.count_name or 'Instance' }, ' ')) or 'Average',
                (table.concat({ 'Per', args.count_name or 'Instance' }, ' ')) or 'Average',
    --            'Max'
                'Max'
    --        },
            },
    --        keywords = (function()
            keywords = (function()
    --            if inArgs('avg_hits') or inArgs('count') then
                if inArgs('avg_hits') or inArgs('count') then
    --                return { (inArgs('count') and not OPTIONS.use_avg) and 'each' or 'avg', 'total' }
                    return { (inArgs('count') and not OPTIONS.use_avg) and 'each' or 'avg', 'total' }
    --            end
                end
    --            return { 'total' }
                return { 'total' }
    --        end)(),
            end)(),
    --        is_visible = ((inArgs('avg_hits') or inArgs('count')) and not OPTIONS.no_max) or false
            is_visible = ((inArgs('avg_hits') or inArgs('count')) and not OPTIONS.no_max) or false
    --    }
        }
     -- }
     }


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


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


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


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


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


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


    --    return all_list
        return all_list
     -- end
     end


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


     -- function doHeaders()
     function doHeaders()
    --    local current_multiplier = 0 -- Keeps track of the number of cells to spawn
        local current_multiplier = 0 -- Keeps track of the number of cells to spawn
    --    local initial_header_cell    -- The leftmost cell that says "Mode"
        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.
        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
        for row_index, row in ipairs(TABLE_CONTENT) do
    --        if row.is_visible then
            if row.is_visible then
    --            local new_row = TABLE:new()
                local new_row = TABLE:new()
    --            local next_multiplier = 0
                local next_multiplier = 0


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


    --            --[[
                --[[
    --            We need to know how the colspan will look like.
                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.
                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.
                And also multiply everything, because it is exponential.
    --            ]]
                ]]
    --            local colspan_value = 1
                local colspan_value = 1
    --            for k, v in ipairs(TABLE_CONTENT) do
                for k, v in ipairs(TABLE_CONTENT) do
    --                if k > row_index and v.is_visible then
                    if k > row_index and v.is_visible then
    --                    colspan_value = colspan_value * #v.text
                        colspan_value = colspan_value * #v.text
    --                end
                    end
    --            end
                end


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


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


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


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


    --            -- Display ranges.
                -- Display ranges.
    --            damage_number = doRangeText(damage_number)
                damage_number = doRangeText(damage_number)


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


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


     -- doTable()
     doTable()


     -- Dump all values if wanted.
     -- Dump all values if wanted.

Revision as of 00:56, 25 April 2023

Documentation for this module may be created at Module:Test/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 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',
            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 and (args.useful_penalty or args.useful or 0.7) 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_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 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

    -- 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' }
        },
    }
    -- 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.
                        ]]
                        while i <= #(args.dmg) 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 or nil
                                    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

    inherit('PvE')
    inherit('PvP')

    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 = 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, 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

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

    if args.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, damage_number in ipairs(damage_value.damage_numbers) do
                        local hit_count = damage_value.hit_counts[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.
                    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 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
        },
        {
            type = 'passive_appended',
            text = { 'Normal',
                OPTIONS.is_append and
                link(PASSIVES[OPTIONS.append_index].name, PASSIVES[OPTIONS.append_index].alias or OPTIONS.append_name) },
            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',
                        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 = '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_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)
                            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)
    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)

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

return p