Module:Test

From Elwiki
Revision as of 16:35, 21 April 2023 by Ritsu (talk | contribs) (Bot: Automated import of articles *** existing text overwritten ***)

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

    function link(text)
        return '[[' .. text .. ']]'
    end

    -- User requested options
    local OPTIONS = {
        should_do_table = args[1] == 'true',
        character = args[2] or 'Elsword',
        format = args.format ~= 'false',
        no_max = args.no_max == 'true',
        is_append = args.append ~= nil,
        append_index = args.append and split(args.append)[1],
        append_name = args.append and split(args.append)[2]
    }

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

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

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

    for k, v in pairs(args) do
        if string.find(k, 'passive') then
            --[[
            Fix up the passives and put them into a separate table.
            |passive1=... |passive2=... -> { passive1, passive2 }
            --]]
            local passive_index = string.match(k, "%d")
            local passive_values = split(frame:preprocess('{{:' .. v .. '}}{{#arrayprint:' .. v .. '}}'));
            PASSIVES[tonumber(passive_index)] = {
                name = v,
                value = passive_values[1],
                value_pvp = passive_values[2],
                alias = args['alias' .. passive_index] or split(args.append or '')[2],
                suffix = args['suffix' .. passive_index],
            }
        elseif string.find(k, 'append') or not string.find(v, '[a-zA-Z]+') then
            --[[
            Change how args are received.
            dmg = 500, 700, 800 (string) -> dmg = { 500, 700, 800 } (table)
            --]]
            local split_values = split(v)
            -- Perform automatic math on each value.
            for k2, v2 in pairs(split_values) do
                split_values[k2] = frame:preprocess(v2)
            end
            args[k] = split_values
        end
    end

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

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

    local DAMAGE_PARSED = createDamageDataTable()

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

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

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

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

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

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

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

    doBasicDamage()

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

    doTraits()

    local WITH_PASSIVES = createDamageDataTable()

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

    --[[
    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
        }
    }

    function formatDamage(number)
        local formattedDamage = number > 0 and (formatnum(math.round(number, 2)) .. '%') or '-%'
        return formattedDamage
    end

    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']))
                    WITH_RANGE[mode][damage_key][range] = not OPTIONS.format and final_damage_value or
                        formatDamage(final_damage_value)
                end
            end
        end
    end

    doDamageBuffRange()

    -- 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(args.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)

        if args.combine then
            table.insert(PASSIVES_WITH_COMBINED, {
                
            })
        end

        for passive_index, passive in ipairs(PASSIVES) do
            if (not OPTIONS.is_append or (OPTIONS.is_append and tonumber(OPTIONS.append_index) ~= passive_index)) and not inArrayHasValue(passive_index, args.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
        },
        {
            type = 'passives',
            text = checkPassives({
                output = { 'Base' },
                action = function(passive, output)
                    table.insert(output, link(passive.name))
                end
            }),
            keywords = checkPassives({
                action = function(passive, output, passive_index)
                    table.insert(output, 'passive' .. passive_index)
                end
            }),
            is_visible = OPTIONS.no_max == false
        },
        {
            type = 'passives_appended',
            text = { 'Normal', OPTIONS.is_append and link(OPTIONS.append_name or PASSIVES[OPTIONS.append_index].name) },
            keywords = { OPTIONS.is_append and 'passive' .. OPTIONS.append_index },
            is_visible = OPTIONS.is_append or false
        },
        {
            type = 'awakening',
            text = { 'Regular', link('Awakening Mode') },
            keywords = { 'awk' },
            keyword_next_to_main_key = true,
            is_visible = inArgs('awk_dmg') or inArgs('awk_hits') or false
        },
        {
            type = 'traits',
            text = checkTraits({
                output = { 'Normal' },
                action = function(trait, output)
                    table.insert(output, trait.name)
                end
            }),
            keywords = checkTraits({
                action = function(trait, output)
                    table.insert(output, trait.key)
                end
            }),
            is_visible = checkTraits()
        },
        {
            type = 'hit_count',
            text = {
                (inArgs('count') and not inArgs('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 { '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 sortPassives(s)
        local passives = {}
        for passive in s:gmatch("_passive%d+") do
            table.insert(passives, passive)
        end
        if #passives == 0 then
            return s
        end
        table.sort(passives)
        local base = s:gsub("_passive%d+", "")
        return base .. table.concat(passives)
    end

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

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

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

            -- Check if it's the first iteration. If so, append phrases.
            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
                -- Iterate through previous keys
                for _, prev_key in ipairs(all_list) do
                    -- Append suffix for each keyword in current row
                    for _, keyword in ipairs(current_row.keywords) do
                        local new_key = prev_key .. '_' .. keyword

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

                        table.insert(new_list, new_key)
                    end
                end
            end

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

        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

    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 = WITH_RANGE[mode][damage_key]

                -- Display ranges.
                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

                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 args.dump == 'true' then
        return inspect_dump(WITH_RANGE, frame)
    end

    return tostring(TABLE)
end

return p