Module:Test: Difference between revisions

From Elwiki
No edit summary
No edit summary
 
(107 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- pystart
-- pystart
require('Module:CommonFunctions');
require('Module:CommonFunctions')
local i18n = require('Module:I18n')
local getArgs = require('Module:Arguments').getArgs
local getArgs = require('Module:Arguments').getArgs
local inspect = require('Module:Inspect').inspect
local inspect = require('Module:Inspect').inspect
local getTranslations = i18n.getTranslations
local p = {}
local p = {}


-- Main process
-- ========================================
function p.main(frame)
-- CONSTANTS
    local args = getArgs(frame)
-- ========================================
     local out
local CONSTANTS = {
    MODES = { 'PvE', 'PvP' },
    DAMAGE_TYPES = { 'min', 'max' },
    TO_SPLIT = { 'append', 'awk_alias' },
 
    DEFAULT_TRAITS = {
        -- An empty trait so we keep the original values there.
        {
            key = '',
            name = 'Normal', -- Will be translated later
            value = 1
        },
        {
            key = 'enhanced',
            name = 'Enhanced (Trait)', -- Will be translated later
            value_func = function(args) return args.enhanced ~= nil and 0.8 end
        },
        {
            key = 'empowered',
            name = 'Empowered', -- Will be translated later
            value_func = function(args) return args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false end
        },
        {
            key = 'useful',
            name = 'Useful', -- Will be translated later
            value_func = function(args)
                return (args.hits_useful or args.avg_hits_useful) and
                    (args.useful_penalty or args.useful or 0.8) or false
            end
        },
        {
            key = 'heavy',
            name = 'Heavy', -- Will be translated later
            value_func = function(args) return args.heavy ~= nil and 1.44 end
        }
     },


     function inArgs(key)
     BASE_DAMAGE_CONFIG = {
         if args[key] ~= nil then
        total_damage = {
             return true
            damage_numbers = { 'dmg' },
         end
            hit_counts = { 'hits' },
     end
            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', 'awk_dmg_and_avg_hits' } -- Special marker
        },
        -- 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 modes = { 'PvE', 'PvP' }
-- ========================================
-- UTILITY FUNCTIONS
-- ========================================
local Utils = {}


    -- Define the schema for the table
function Utils.forEach(func)
    local tableSchema = {}
     for _, mode in ipairs(CONSTANTS.MODES) do
     for _, mode in ipairs(modes) do
         func(mode)
         tableSchema[mode] = {}
     end
     end
end


    function forEach(func)
function Utils.forEachDamageType(func)
        for _, mode in ipairs(modes) do
    for _, damage_type in ipairs(CONSTANTS.DAMAGE_TYPES) do
            func(mode)
        func(damage_type)
        end
     end
     end
end


    function forEachDamageType(func)
function Utils.createDamageDataTable()
        for _, damage_type in ipairs({ 'min', 'max' }) do
    local newTable = {}
            func(damage_type)
    for _, mode in ipairs(CONSTANTS.MODES) do
         end
         newTable[mode] = {}
     end
     end
    return newTable
end


    -- Function to create a new table with the desired schema
function Utils.isTableNotEmpty(tbl)
     function createDamageDataTable()
     return next(tbl) ~= nil
        local newTable = {}
end
        for key, value in pairs(tableSchema) do
 
            if type(value) == "table" then
-- ========================================
                newTable[key] = {}
-- INPUT PROCESSING
            end
-- ========================================
        end
local InputProcessor = {}
        return newTable
    end


    -- User requested options
function InputProcessor.parseOptions(args)
     local OPTIONS = {
     return {
         do_table = args[1] == 'true',
         do_table = args[1] == 'true',
         character = args[2] or args.char or 'Elsword',
         character = args[2] or args.char or 'Elsword',
        lang_suffix = args.lang and ('/' .. args.lang) or '',
        lang_append = args.lang ~= nil and args.lang ~= '',
         format = args.format ~= 'false',
         format = args.format ~= 'false',
         no_max = args.no_max == 'true',
         no_max = args.no_max == 'true',
Line 82: Line 163:
         dmp = args.dmp == 'true' and 3 or args.dmp
         dmp = args.dmp == 'true' and 3 or args.dmp
     }
     }
end


    -- Define a table with parsed damage information of all kind.
function InputProcessor.parsePassives(args, frame, options)
    local BASIC_DAMAGE = createDamageDataTable()
     local passives = {}
 
    -- 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
     for k, v in pairs(args) do
         if string.find(k, 'passive') then
         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 passive_name = v
            local passive_title = v .. options.lang_suffix
             local is_custom = string.find(k, '_define') ~= nil
             local is_custom = string.find(k, '_define') ~= nil
             local passive_index = string.match(k, "%d")
             local passive_index = string.match(k, "%d")
             local passive_values = split(is_custom and v or
             local passive_values = split(is_custom and v or
                 frame:preprocess('{{:' .. passive_name .. '}}{{#arrayprint:' .. passive_name .. '}}'));
                 frame:preprocess('{{:' .. passive_name .. '}}{{#arrayprint:' .. passive_name .. '}}'))
            local display_title


             if is_custom then
             if is_custom then
                 passive_name = passive_values[#passive_values]
                 passive_name = passive_values[#passive_values]
                 passive_values[#passive_values] = nil
                 passive_values[#passive_values] = nil
            elseif options.lang_append then
                display_title = i18n.getTranslatedTitle(passive_title)
             end
             end


             PASSIVES[tonumber(passive_index)] = {
             passives[tonumber(passive_index)] = {
                 name = passive_name,
                 name = passive_name,
                 value = passive_values[1],
                 value = passive_values[1],
                 value_pvp = passive_values[2],
                 value_pvp = passive_values[2],
                 alias = args['alias' .. passive_index] or (passive_index == OPTIONS.append_index and OPTIONS.append_name),
                 alias = args['alias' .. passive_index] or (passive_index == options.append_index and options.append_name) or
                    display_title,
                 suffix = args['suffix' .. passive_index] and (' ' .. args['suffix' .. passive_index]) or '',
                 suffix = args['suffix' .. passive_index] and (' ' .. args['suffix' .. passive_index]) or '',
                 prefix = args['prefix' .. passive_index] and (args['prefix' .. passive_index] .. ' ') or '',
                 prefix = args['prefix' .. passive_index] and (args['prefix' .. passive_index] .. ' ') or '',
                 exist = frame:preprocess('{{#ifexist:' .. passive_name .. '|true|false}}') == 'true'
                 exist = frame:preprocess('{{#ifexist:' .. passive_name .. '|true|false}}') == 'true'
             }
             }
         elseif not string.find(v, '[a-hj-zA-HJ-Z]+') then
         end
            --[[
    end
            Change how args are received.
 
            dmg = 500, 700, 800 (string) -> dmg = { 500, 700, 800 } (table)
    return passives
            --]]
end
 
function InputProcessor.processNumericArgs(args, frame)
    local processed_args = table.deep_copy(args)
 
    for k, v in pairs(args) do
        if string.match(v, '^[()+%-*/%d%s,.i]+$') then
             local split_values = split(v)
             local split_values = split(v)
            -- Perform automatic math on each value.
             for k2, v2 in pairs(split_values) do
             for k2, v2 in pairs(split_values) do
                 if not string.find(v, '[a-zA-Z]+') then
                 if not string.find(v, '[a-zA-Z]+') then
                     split_values[k2] = eval(v2)
                     split_values[k2] = frame:preprocess('{{#expr:' .. v2 .. '}}')
                 end
                 end
             end
             end
             args[k] = split_values
             processed_args[k] = split_values
         elseif inArrayHasValue(k, TO_SPLIT) then
         elseif inArrayHasValue(k, CONSTANTS.TO_SPLIT) then
             args[k] = split(v)
             processed_args[k] = split(v)
         end
         end
     end
     end


    return processed_args
end
function InputProcessor.setDefaultHitCounts(args)
     -- Set basic hit count to 1 for all damage.
     -- Set basic hit count to 1 for all damage.
     for k, v in ipairs(args.dmg) do
     for k, v in ipairs(args.dmg) do
Line 190: Line 243:
         end
         end
     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).
-- CONFIGURATION BUILDING
    local BASE_DAMAGE_CONFIG = {
-- ========================================
        total_damage = {
local ConfigBuilder = {}
            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 ConfigBuilder.buildDamageConfig(args)
    function handleCancel()
     local function handleCancel()
         local processed_keys = {}
         local processed_keys = {}
         for config_key, config_value in pairs(BASE_DAMAGE_CONFIG) do
         for config_key, config_value in pairs(CONSTANTS.BASE_DAMAGE_CONFIG) do
             if not config_key:match('cancel_') then
             if not config_key:match('cancel_') then
                 local new_config_value = {}
                 local new_config_value = {}
Line 251: Line 264:
                 end
                 end
                 local new_key = 'cancel_' .. config_key
                 local new_key = 'cancel_' .. config_key
                 DAMAGE_CONFIG[new_key] = new_config_value
                 processed_keys[new_key] = new_config_value
                processed_keys[new_key] = true
             end
             end
         end
         end
Line 259: Line 271:


     if args.cancel_dmg then
     if args.cancel_dmg then
         handleCancel()
         local cancel_configs = handleCancel()
         DAMAGE_CONFIG = table.fuse(BASE_DAMAGE_CONFIG, DAMAGE_CONFIG)
         return table.fuse(CONSTANTS.BASE_DAMAGE_CONFIG, cancel_configs)
     else
     else
         DAMAGE_CONFIG = BASE_DAMAGE_CONFIG
         return CONSTANTS.BASE_DAMAGE_CONFIG
    end
end
 
function ConfigBuilder.buildTraits(args, translate)
    local traits = {}
    for _, trait_template in ipairs(CONSTANTS.DEFAULT_TRAITS) do
        local trait = {
            key = trait_template.key,
            name = translate(trait_template.name),
            value = trait_template.value or (trait_template.value_func and trait_template.value_func(args))
        }
        table.insert(traits, trait)
    end
    return traits
end
 
-- ========================================
-- INHERITANCE SYSTEM
-- ========================================
local InheritanceProcessor = {}
 
function InheritanceProcessor.applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritValue)
    if mainArgValue == '' then
        return inheritValue
    elseif mainArgValue and string.find(mainArgValue, 'i') and inheritValue then
        return mainArgValue:gsub('i', inheritValue)
     end
     end
    return mainArgValue
end
function InheritanceProcessor.applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType, eval)
    local mainKey = argTable[1] .. damageType
    local mainKeyPrefixed = prefix .. mainKey
    local mainArgValues = args[mainKeyPrefixed]
    if mainArgValues then
        local i = 1
        local cancelDmgLen = args.cancel_dmg and #args.cancel_dmg or 0
        while i <= (#args.dmg + cancelDmgLen) do
            local mainArgValue = mainArgValues[i]
            for ix, inheritKey in ipairs(argTable) do
                local inheritArg = args[prefix .. inheritKey .. damageType] or args[inheritKey .. damageType]


    -- Inherits values from args if not provided, but usage suggests that they're meant to be generated.
                -- Basic damage/hits inheritance request detected. Ignore min/max.
    function inherit(mode)
                if damageType and mainKey:gsub(damageType, "") == argTable[#argTable] then
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
                    inheritArg = args[prefix .. inheritKey] or args[inheritKey]
        for config_key, config_value in pairs(DAMAGE_CONFIG) do
                end
            for arg_table_key, arg_table in pairs(config_value) do
 
                 if arg_table_key ~= 'provided' and arg_table then
                 if inheritArg and inheritArg[i] and
                    -- We only do this for the first (main) key
                    (damageTypeIndex == 1 and ix ~= 1 or damageTypeIndex ~= 1) and tonumber(inheritArg[i])
                     local main_key = arg_table[1]
                then
                    local main_key_prefixed = prefix .. main_key
                     mainArgValues[i] = InheritanceProcessor.applyInheritance(mainArgValues, inheritArg, mainArgValue,
                    local main_arg_values = args[main_key_prefixed]
                        inheritArg[i])
                    break
                end
            end


                    -- Only if the main argument values exist.
            i = i + 1
                    if main_arg_values then
        end
                        local i = 1
    end
                        --[[
end
                            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
function InheritanceProcessor.inherit(args, damageConfig, eval)
                                local inherit_arg = args[prefix .. inherit_key] or args[inherit_key]
    local function inheritForMode(mode)
                                -- No inheritance from itself.
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
                                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
        for configKey, configValue in pairs(damageConfig) do
                         end
            for argTableKey, argTable in pairs(configValue) do
                if argTableKey ~= 'provided' and Utils.isTableNotEmpty(argTable) then
                    for damageTypeIndex, damageType in ipairs({ '', '_min', '_max' }) do
                         InheritanceProcessor.applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType,
                            eval)
                     end
                     end
                 end
                 end
Line 311: Line 355:
     end
     end


     forEach(inherit)
     Utils.forEach(inheritForMode)
end


     local DAMAGE_PARSED = createDamageDataTable()
-- ========================================
     function parseConfig(mode)
-- DAMAGE CALCULATION ENGINE
-- ========================================
local DamageEngine = {}
 
function DamageEngine.parseConfig(args, damageConfig, options)
     local damageParsed = Utils.createDamageDataTable()
 
     local function parseConfigForMode(mode)
         local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
         local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
         for config_key, config_value in pairs(DAMAGE_CONFIG) do
         for config_key, config_value in pairs(damageConfig) do
             for k, v in pairs(config_value) do
             for k, v in pairs(config_value) do
                 local output_value = {}
                 local output_value = {}
Line 323: Line 375:
                 local isValueFound = { min = false, max = false }
                 local isValueFound = { min = false, max = false }


                 for _, v2 in ipairs(v) do -- This array holds the argument names with fallbacks
                 for _, v2 in ipairs(v) do -- This array holds the argument names with fallbacks
 
                     Utils.forEachDamageType(function(damage_type)
                     forEachDamageType(function(damage_type)
                         -- If there already is a value for this damage type (min or max), do not continue.
                         -- If there already is a value for this damage type (min or max), do not continue.
                         if isValueFound[damage_type] == true then
                         if isValueFound[damage_type] == true then
Line 335: Line 386:
                             or args[v2 .. '_' .. damage_type]
                             or args[v2 .. '_' .. damage_type]
                             or args[prefix .. v2]
                             or args[prefix .. v2]
                             or args[v2];
                             or args[v2]


                         if arg_from_template ~= nil then
                         if arg_from_template ~= nil then
                             if k == 'provided' then
                             if k == 'provided' then
                                 output_value = true
                                 output_value = true
                                -- Special handling for awk_dmg_and_avg_hits marker
                                if v2 == 'awk_dmg_and_avg_hits' then
                                    output_value = args.awk_dmg and args.avg_hits
                                end
                                 -- Do not generate total_damage values at all if the skill can't reach them.
                                 -- 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
                                 if string.find(config_key, 'total_') and options.no_max then
                                     output_value = false
                                     output_value = false
                                 end
                                 end
Line 365: Line 420:
                         break
                         break
                     end
                     end
                 end
                 end
                 if DAMAGE_PARSED[mode][config_key] == nil then
                 if damageParsed[mode][config_key] == nil then
                     DAMAGE_PARSED[mode][config_key] = {}
                     damageParsed[mode][config_key] = {}
                 end
                 end
                 DAMAGE_PARSED[mode][config_key][k] = output_value
                 damageParsed[mode][config_key][k] = output_value
             end
             end
         end
         end
     end
     end


     forEach(parseConfig)
     Utils.forEach(parseConfigForMode)
    return damageParsed
end


    -- Detected "count", for skills like Clementine, Enough Mineral, etc.
function DamageEngine.handleEachDamage(damageParsed, args)
    function doEachDamage()
    local withEach = table.deep_copy(damageParsed)
        local WITH_EACH = table.deep_copy(DAMAGE_PARSED)
    for mode, mode_content in pairs(damageParsed) 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)


                    forEachDamageType(function(damage_type)
                Utils.forEachDamageType(function(damage_type)
                        for k, hit_count in ipairs(new_value.hit_counts[damage_type]) do
                    for k, hit_count in ipairs(new_value.hit_counts[damage_type]) do
                            hit_count = hit_count == '' and 1 or hit_count
                        hit_count = hit_count == '' and 1 or hit_count
                            new_value.hit_counts[damage_type][k] = hit_count *
                        new_value.hit_counts[damage_type][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
                    end)
                end)


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


    if args.count then
function DamageEngine.calculateBasicDamage(damageParsed)
        DAMAGE_PARSED = doEachDamage()
    local basicDamage = Utils.createDamageDataTable()
    end


     function doBasicDamage()
     for mode, mode_content in pairs(damageParsed) 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
            Utils.forEachDamageType(function(damage_type)
                forEachDamageType(function(damage_type)
                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[damage_type]) do
                        for k, damage_number in ipairs(damage_value.damage_numbers[damage_type]) do
                        local hit_count = damage_value.hit_counts[damage_type][i]
                            local hit_count = damage_value.hit_counts[damage_type][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.
                    if not basicDamage[mode][damage_key] then
                        if not BASIC_DAMAGE[mode][damage_key] then
                        basicDamage[mode][damage_key] = {}
                            BASIC_DAMAGE[mode][damage_key] = {}
                        end
                        BASIC_DAMAGE[mode][damage_key][damage_type] = output
                     end
                     end
                 end)
                    basicDamage[mode][damage_key][damage_type] = output
             end
                 end
             end)
         end
         end
     end
     end


     doBasicDamage()
     return basicDamage
end


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


     -- if args.cancel_dmg then
     return basicDamage
    --    addCancelDamage()
end
    -- end


    local WITH_TRAITS = createDamageDataTable()
-- ========================================
     function doTraits()
-- TRAIT PROCESSING
        -- Handle traits here
-- ========================================
        for mode, mode_content in pairs(BASIC_DAMAGE) do
local TraitProcessor = {}
            for damage_key, damage_value in pairs(mode_content) do
 
                for _, trait in pairs(TRAITS) do
function TraitProcessor.applyTraits(basicDamage, traits)
                    --[[
     local withTraits = Utils.createDamageDataTable()
                    Suffix all damage values with existing traits.
 
                    Useful already has the prefix, so only multiply with its value.
    for mode, mode_content in pairs(basicDamage) do
                    Also, we don't want other traits to multiply with Useful,
        for damage_key, damage_value in pairs(mode_content) do
                    so we skip those situations, as impossible in-game.
            for _, trait in pairs(traits) do
                    --]]
                if (trait.value and trait.key ~= 'useful') or (string.find(damage_key, 'useful') and trait.key == 'useful') then
                    if (trait.value and trait.key ~= 'useful') or (string.find(damage_key, 'useful') and trait.key == 'useful') then
                    Utils.forEachDamageType(function(damage_type)
                         WITH_TRAITS[mode][damage_key .. ((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key))] =
                         local new_key = damage_key ..
                            damage_value * trait.value
                            ((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key))
                     end
                        if not withTraits[mode][new_key] then
                            withTraits[mode][new_key] = {}
                        end
                        withTraits[mode][new_key][damage_type] = damage_value[damage_type] * trait.value
                     end)
                 end
                 end
             end
             end
Line 472: Line 531:
     end
     end


     -- doTraits()
     return withTraits
end


    local WITH_PASSIVES = createDamageDataTable()
-- ========================================
-- PASSIVE PROCESSING
-- ========================================
local PassiveProcessor = {}


    --[[
function PassiveProcessor.generatePassiveCombinations(passives)
    Generates passives with every possible combinations of all subsets.
     local combinations = { {} }
    For example: 3 passives are given, so it will generate the following:
    for passive_key, passive in pairs(passives) do
    (1), (2), (3), (1, 2), (1, 3), (1, 2, 3), (2, 3)
        local count = #combinations
    ]]
        for i = 1, count do
     function doPassives()
            local new_combination = { unpack(combinations[i]) }
        for mode, mode_content in pairs(WITH_TRAITS) do
            table.insert(new_combination, passive_key)
            for damage_key, damage_value in pairs(mode_content) do
            table.insert(combinations, new_combination)
                local combinations = { {} }
        end
                for passive_key, passive in pairs(PASSIVES) do
    end
                    local count = #combinations
    return combinations
                    for i = 1, count do
end
                        local new_combination = { unpack(combinations[i]) }
 
                        table.insert(new_combination, passive_key)
function PassiveProcessor.applyPassives(withTraits, passives)
                        table.insert(combinations, new_combination)
    local withPassives = Utils.createDamageDataTable()
                    end
 
                 end
    for mode, mode_content in pairs(withTraits) do
        for damage_key, damage_value in pairs(mode_content) do
            Utils.forEachDamageType(function(damage_type)
                 local combinations = PassiveProcessor.generatePassiveCombinations(passives)
                 for _, combination in pairs(combinations) do
                 for _, combination in pairs(combinations) do
                     local passive_multiplier = 1
                     local passive_multiplier = 1
Line 500: Line 566:
                         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
                     local new_damage_key = damage_key .. name_suffix
                    if not withPassives[mode][new_damage_key] then
                        withPassives[mode][new_damage_key] = {}
                    end
                    withPassives[mode][new_damage_key][damage_type] = damage_value[damage_type] * passive_multiplier
                 end
                 end
             end
             end)
         end
         end
     end
     end


     -- doPassives()
     return withPassives
end
 
-- ========================================
-- RANGE PROCESSING
-- ========================================
local RangeProcessor = {}


     local RANGE = {
function RangeProcessor.buildRangeConfig(args)
         min_count = args.range_min_count and args.range_min_count[1] or 1,
     return {
         max_count = args.range_max_count and args.range_max_count[1] or 1,
         min_count = args.range_min_count and args.range_min_count[1],
         max_count = args.range_max_count and args.range_max_count[1],
         PvE = {
         PvE = {
             min = args.range_min and args.range_min[1] or 1,
             min = args.range_min and args.range_min[1],
             max = args.range_max and args.range_max[1] or 1
             max = args.range_max and args.range_max[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]),
             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])
         }
         }
     }
     }
end


     local WITH_RANGE = createDamageDataTable()
function RangeProcessor.applyRanges(withPassives, range, options, formatDamage)
     function doDamageBuffRange()
     local withRange = Utils.createDamageDataTable()
        -- Handle damage range here
 
        for mode, mode_content in pairs(WITH_PASSIVES) do
     for mode, mode_content in pairs(withPassives) 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 }
            withRange[mode][damage_key] = { min = 0, max = 0 }
                 for _, range in ipairs({ 'min', 'max' }) do
            Utils.forEachDamageType(function(damage_type)
                    local final_damage_value = damage_value * (1 + ((RANGE[mode][range] - 1) * RANGE[range .. '_count'])) *
                 local range_count = range[damage_type .. '_count'] or 1
                        OPTIONS.perm_buff[mode]
                -- If min count preset, use range_max for the multiplier.
                    WITH_RANGE[mode][damage_key][range] = not OPTIONS.format and final_damage_value or
                local range_multiplier = range[mode][damage_type] or
                        formatDamage(final_damage_value)
                    (damage_type == 'min' and range.min_count and range[mode].max) or 1
                end
                local final_range_multiplier = (1 + ((range_multiplier - 1) * range_count))
             end
                local perm_buff = options.perm_buff[mode]
 
                local final_damage_value = damage_value[damage_type] * final_range_multiplier * perm_buff
                withRange[mode][damage_key][damage_type] = not options.format and final_damage_value or
                    formatDamage(final_damage_value)
             end)
         end
         end
     end
     end


     -- doDamageBuffRange()
     return withRange
end


    -- local FINAL_DAMAGE = WITH_RANGE
-- ========================================
    local FINAL_DAMAGE = DAMAGE_CONFIG
-- TABLE GENERATION
-- ========================================
local TableGenerator = {}


    -- Helper function to iterate over traits.
function TableGenerator.checkTraits(traits, 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
        return output
     end
     end
    return output
end


    -- Helper function to iterate over passives.
function TableGenerator.checkPassives(passives, options, args, link, sortPassives, 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.
    if options.combine then
        table.insert(PASSIVES_WITH_COMBINED, {
            is_combined = true
        })
    end


         -- Handle combined passives properly.
    for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do
         if OPTIONS.combine then
         -- Determine if passive should be skipped
             table.insert(PASSIVES_WITH_COMBINED, {
        local skip_passive = false
                 is_combined = true
         if passive.is_combined then
             })
             -- Skip combined passive if any of its constituent passives are being appended
            if options.is_append and options.combine and inArrayHasValue(options.append_index, options.combine) then
                 skip_passive = true
             else
                skip_passive = false
            end
        else
            -- Regular passive logic: skip if it's appended or part of combine (unless display_separated)
            skip_passive = (options.is_append and options.append_index == passive_index) or
                (inArrayHasValue(passive_index, options.combine or {}) and not inArrayHasValue(passive_index, args.display_separated or {}))
         end
         end


         for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do
         if not skip_passive then
            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
                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
        return output
     end
     end
    return output
end


     -- -- Generate the table
function TableGenerator.buildTableContent(options, passives, traits, args, translate, inArgs, link, sortPassives,
    -- local TABLE = mw.html.create('table'):attr({
                                          fillTemplate)
    --     cellpadding = 5,
     return {
    --    border = 1,
        {
    --    style = 'border-collapse: collapse; text-align: center',
            type = 'extra',
    --    class = 'colortable-' .. OPTIONS.character
            text = { translate('Average') },
     -- })
            is_visible = options.no_max,
            no_damage = true
        },
        {
            type = 'passives',
            text = TableGenerator.checkPassives(passives, options, args, link, sortPassives, {
                output = { translate('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 = TableGenerator.checkPassives(passives, options, args, link, sortPassives, {
                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 = {
                translate('Normal'),
                options.is_append and (function()
                    -- Check if the appended passive is part of a combined passive
                    if options.combine and inArrayHasValue(options.append_index, options.combine) then
                        -- Handle combined passive in append
                        local combo = {}
                        for _, passive_key in ipairs(options.combine) do
                            local passive = passives[passive_key]
                            table.insert(combo,
                                link(passive.name, passive.alias, passive.prefix, passive.suffix, passive.exist))
                        end
                        return table.concat(combo, '/') .. options.combine_suffix
                    else
                        -- Handle single passive in append
                        return 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
                        )
                    end
                end)()
            },
            keywords = { options.is_append and (function()
                -- Generate appropriate keyword for appended passive
                if options.combine and inArrayHasValue(options.append_index, options.combine) then
                    return sortPassives('passive' .. table.concat(options.combine, '_passive'))
                else
                    return 'passive' .. options.append_index
                end
            end)() or nil },
            is_visible = options.is_append or false
        },
        {
            type = 'awakening',
            text = { translate('Regular'), (function()
                if options.dmp then
                    return link('Dynamo Point System' .. options.lang_suffix, 'Dynamo Configuration', args.awk_prefix,
                        options.dmp ~= 'false' and
                        (fillTemplate('({1} DMP)', { options.dmp })) ..
                        (args.awk_suffix and (' ' .. args.awk_suffix) or ''))
                elseif args.awk_alias then
                    return link(args.awk_alias[1], args.awk_alias[2], args.awk_prefix, args.awk_suffix)
                end
                return link('Awakening Mode' .. options.lang_suffix, translate('Awakening Mode'), args.awk_prefix,
                    args.awk_suffix)
            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 = TableGenerator.checkTraits(traits, {
                output = { translate('Normal') },
                action = function(trait, output)
                    table.insert(output, trait.name)
                end
            }),
            keywords = TableGenerator.checkTraits(traits, {
                action = function(trait, output)
                    table.insert(output, trait.key)
                end
            }),
            is_visible = TableGenerator.checkTraits(traits)
        },
        {
            type = 'cancel',
            text = {
                translate('Cancel'),
                translate('Full'),
            },
            keywords = { 'cancel' },
            keyword_first = true,
            is_visible = inArgs('cancel_dmg')
        },
        {
            type = 'hit_count',
            text = {
                (inArgs('count') and not options.use_avg) and
                (fillTemplate(translate('Per {1}'), { args.count_name or translate('Group') })) or
                translate('Average'),
                translate('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
        }
     }
end


    -- -- Our table structure
function TableGenerator.returnDamageInOrder(tableContent, options, inArgs, sortPassives)
    -- local TABLE_CONTENT = {
     local main_key = 'damage'
    --    {
     local all_list = {}
    --        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()
     for i = #tableContent, 1, -1 do
    --    return self:tag('tr')
        local current_row = tableContent[i]
    -- end
        local new_list = {}


    -- function returnDamageInOrder()
        if not current_row.no_damage then
    --    local main_key = 'damage'
            if i == #tableContent then
    --    local all_list = {}
                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
                for _, keyword in ipairs(current_row.keywords) do
                    for _, prev_key in ipairs(all_list) do
                        local new_key = prev_key .. '_' .. keyword
                        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


    --    -- Initialize current list with main key
            for _, new_key in ipairs(new_list) do
     --    local current_list = { main_key }
                table.insert(all_list, sortPassives(new_key))
            end
        end
     end


     --    for i = #TABLE_CONTENT, 1, -1 do
     if inArgs('cancel_dmg') then
    --        local current_row = TABLE_CONTENT[i]
        local new_list = {}
    --         local new_list = {}
        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


     --        -- Check if it's the first iteration. If so, append phrases.
     return all_list
    --        if not current_row.no_damage then
end
    --            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
function TableGenerator.generateTable(finalDamage, tableContent, options, frame, translate, args, inArgs, sortPassives)
    --            for _, new_key in ipairs(new_list) do
     local TABLE = mw.html.create('table'):attr({
     --                table.insert(all_list, sortPassives(new_key))
        cellpadding = 5,
    --             end
        border = 1,
    --        end
        style = 'border-collapse: collapse; text-align: center',
     --    end
        class = 'colortable-' .. options.character
     })


     --    -- Sort the list once more, in order to swap the order of cancel & full.
     function TABLE:new()
    --    if inArgs('cancel_dmg') then
         return self:tag('tr')
    --         local new_list = {}
     end
    --        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
     local function doInitialCell(new_row)
     -- end
        return new_row:tag('th'):wikitext(translate('Mode'))
     end


     -- function doInitialCell(new_row)
     local function doHeaders()
    --    return new_row:tag('th'):wikitext('Mode')
        local current_multiplier = 0
    -- end
        local initial_header_cell
        local iterations = 0


    -- function doHeaders()
        for row_index, row in ipairs(tableContent) do
    --    local current_multiplier = 0 -- Keeps track of the number of cells to spawn
            if row.is_visible then
    --    local initial_header_cell    -- The leftmost cell that says "Mode"
                local new_row = TABLE:new()
    --    local iterations = 0         -- Keeps track of iterations that successfully rendered something. Required to tell the initial cell how many columns to span.
                local next_multiplier = 0


    --    for row_index, row in ipairs(TABLE_CONTENT) do
                if iterations == 0 and not initial_header_cell then
    --        if row.is_visible then
                    initial_header_cell = doInitialCell(new_row)
    --            local new_row = TABLE:new()
                end
    --            local next_multiplier = 0


    --            -- Only spawn the initial cell in the first generated row.
                local colspan_value = 1
    --            if iterations == 0 and not initial_header_cell then
                for k, v in ipairs(tableContent) do
    --                 initial_header_cell = doInitialCell(new_row)
                    if k > row_index and v.is_visible then
     --            end
                        colspan_value = colspan_value * #v.text
                    end
                end
 
                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
        initial_header_cell:attr('rowspan', iterations)
     end


     --            --[[
     local function doRangeText(damage_number)
    --            We need to know how the colspan will look like.
        if damage_number and damage_number.min == damage_number.max then
    --            So the solution is to loop through the table again and check how many cells will be spawned.
             damage_number = damage_number.min
    --             And also multiply everything, because it is exponential.
        elseif damage_number then
    --            ]]
             damage_number = damage_number.min ..
    --             local colspan_value = 1
                '<span style="white-space: nowrap;"> ~</span> ' .. damage_number.max
    --            for k, v in ipairs(TABLE_CONTENT) do
        end
    --                if k > row_index and v.is_visible then
        return damage_number
    --                    colspan_value = colspan_value * #v.text
     end
    --                end
     --            end


     --            -- Now we can spawn our header cells depending on what is known.
     local function doContentByMode(mode)
    --            for i = 1, (current_multiplier == 0 and 1 or current_multiplier), 1 do
        local mode_row = TABLE:new()
    --                for _, text in ipairs(row.text) do
        mode_row:tag('td'):wikitext(frame:expandTemplate { title = translate(mode) })
    --                    local new_cell = new_row:tag('th')
        local damage_entries = TableGenerator.returnDamageInOrder(tableContent, options, inArgs, sortPassives)
    --                    new_cell:attr('colspan', colspan_value):wikitext(text)
        local last_number
    --                    next_multiplier = next_multiplier + 1
        local last_unique_cell
    --                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.
        for _, damage_key in ipairs(damage_entries) do
    -- function doRangeText(damage_number)
            if args.dump_names ~= 'true' then
    --    if damage_number and damage_number.min == damage_number.max then
                local damage_number = finalDamage[mode][damage_key]
    --        damage_number = damage_number.min
                damage_number = doRangeText(damage_number)
    --    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)
                if last_number ~= damage_number then
    --    local mode_row = TABLE:new()
                    local new_cell = mode_row:tag('td'):wikitext(damage_number
    --    mode_row:tag('td'):wikitext(frame:expandTemplate { title = mode })
                        or frame:expandTemplate {
    --    local damage_entries = returnDamageInOrder()
                            title = 'color',
    --    local last_number
                            args = { 'red', '&#35;ERROR' }
     --    local last_unique_cell
                        })
                    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


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


     --            if last_number ~= damage_number then
     return TABLE
    --                -- Display ranges.
end
    --                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()
-- OUTPUT PROCESSING
    --     forEach(doContentByMode)
-- ========================================
    -- end
local OutputProcessor = {}


    -- doTable()
function OutputProcessor.generateOutput(frame, args, finalDamage, damageParsed, tableContent, options, translate,
                                        doVariables, inspect_dump)
    local out = nil


     -- Dump all values if wanted.
     -- Dump all values if wanted.
     if OPTIONS.dump_table_data then
     if options.dump_table_data then
         return inspect_dump(frame, TABLE_CONTENT)
         return inspect_dump(frame, tableContent)
     elseif OPTIONS.dump then
     elseif options.dump then
         return inspect_dump(frame, FINAL_DAMAGE)
         return inspect_dump(frame, finalDamage)
     elseif OPTIONS.dump_parsed then
     elseif options.dump_parsed then
         return inspect_dump(frame, DAMAGE_PARSED)
         return inspect_dump(frame, damageParsed)
     end
     end


     local bug = ''
     local bug = ''
     if OPTIONS.bug then
     if options.bug then
         bug = frame:expandTemplate {
         bug = frame:expandTemplate {
             title = 'SkillText',
             title = translate('SkillText'),
             args = { 'FreeTraining' }
             args = { 'FreeTraining' }
         }
         }
Line 894: Line 1,007:


     -- Transform into variables
     -- Transform into variables
     local variables = doVariables(frame, FINAL_DAMAGE, OPTIONS.prefix)
     local variables = doVariables(frame, finalDamage, options.prefix)
 
    local table_output = ''
    if options.do_table then
        local TABLE = TableGenerator.generateTable(finalDamage, tableContent, options, frame, translate, args,
            function(key) return args[key] ~= nil end, sortPassives)
        table_output = tostring(TABLE)
    end


     if out ~= nil then
     if out ~= nil then
Line 900: Line 1,020:
     end
     end


     return variables .. bug .. (OPTIONS.do_table and tostring(TABLE) or '')
     return variables .. bug .. table_output
end
 
-- ========================================
-- MAIN PROCESS
-- ========================================
function p.main(frame)
    local args = getArgs(frame)
    local tr = getTranslations(frame, 'Template:Damage', args.lang, true)
    local out
 
    function translate(key)
        return i18n.translate(tr, key)
    end
 
    function inArgs(key)
        if args[key] ~= nil then
            return true
        end
    end
 
    -- User requested options
    local OPTIONS = InputProcessor.parseOptions(args)
 
    -- Define a table with parsed damage information of all kind.
    local BASIC_DAMAGE = Utils.createDamageDataTable()
 
    -- Define a table with trait names and their values to apply.
    local TRAITS = ConfigBuilder.buildTraits(args, translate)
 
    function eval(s)
        return frame:preprocess('{{#expr:' .. s .. '}}')
    end
 
    -- A table with user-requested passive skills (empty by default).
    local PASSIVES = InputProcessor.parsePassives(args, frame, OPTIONS)
 
    InputProcessor.setDefaultHitCounts(args)
 
    -- 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 = ConfigBuilder.buildDamageConfig(args)
 
    InheritanceProcessor.inherit(args, DAMAGE_CONFIG, eval)
 
    local DAMAGE_PARSED = DamageEngine.parseConfig(args, DAMAGE_CONFIG, OPTIONS)
 
    if args.count then
        DAMAGE_PARSED = DamageEngine.handleEachDamage(DAMAGE_PARSED, args)
    end
 
    local basicDamage = DamageEngine.calculateBasicDamage(DAMAGE_PARSED)
 
    -- Adding missing cancel part damage to full, so that repetition wouldn't be a problem.
    basicDamage = DamageEngine.addCancelDamage(basicDamage, args)
 
    local WITH_TRAITS = TraitProcessor.applyTraits(basicDamage, TRAITS)
 
    local WITH_PASSIVES = PassiveProcessor.applyPassives(WITH_TRAITS, PASSIVES)
 
    local RANGE = RangeProcessor.buildRangeConfig(args)
 
    local WITH_RANGE = RangeProcessor.applyRanges(WITH_PASSIVES, RANGE, OPTIONS, formatDamage)
 
    local FINAL_DAMAGE = WITH_RANGE
 
    -- Build table structure
    local TABLE_CONTENT = TableGenerator.buildTableContent(OPTIONS, PASSIVES, TRAITS, args, translate, inArgs, link,
        sortPassives, fillTemplate)
 
    -- Generate and return output
    return OutputProcessor.generateOutput(frame, args, FINAL_DAMAGE, DAMAGE_PARSED, TABLE_CONTENT, OPTIONS, translate,
        doVariables, inspect_dump)
end
end


return p
return p
-- pyend
-- pyend

Latest revision as of 10:37, 17 June 2025

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

-- pystart
require('Module:CommonFunctions')
local i18n = require('Module:I18n')
local getArgs = require('Module:Arguments').getArgs
local inspect = require('Module:Inspect').inspect
local getTranslations = i18n.getTranslations
local p = {}

-- ========================================
-- CONSTANTS
-- ========================================
local CONSTANTS = {
    MODES = { 'PvE', 'PvP' },
    DAMAGE_TYPES = { 'min', 'max' },
    TO_SPLIT = { 'append', 'awk_alias' },

    DEFAULT_TRAITS = {
        -- An empty trait so we keep the original values there.
        {
            key = '',
            name = 'Normal', -- Will be translated later
            value = 1
        },
        {
            key = 'enhanced',
            name = 'Enhanced (Trait)', -- Will be translated later
            value_func = function(args) return args.enhanced ~= nil and 0.8 end
        },
        {
            key = 'empowered',
            name = 'Empowered', -- Will be translated later
            value_func = function(args) return args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false end
        },
        {
            key = 'useful',
            name = 'Useful', -- Will be translated later
            value_func = function(args)
                return (args.hits_useful or args.avg_hits_useful) and
                    (args.useful_penalty or args.useful or 0.8) or false
            end
        },
        {
            key = 'heavy',
            name = 'Heavy', -- Will be translated later
            value_func = function(args) return args.heavy ~= nil and 1.44 end
        }
    },

    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', 'awk_dmg_and_avg_hits' } -- Special marker
        },
        -- 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' }
        },
    }
}

-- ========================================
-- UTILITY FUNCTIONS
-- ========================================
local Utils = {}

function Utils.forEach(func)
    for _, mode in ipairs(CONSTANTS.MODES) do
        func(mode)
    end
end

function Utils.forEachDamageType(func)
    for _, damage_type in ipairs(CONSTANTS.DAMAGE_TYPES) do
        func(damage_type)
    end
end

function Utils.createDamageDataTable()
    local newTable = {}
    for _, mode in ipairs(CONSTANTS.MODES) do
        newTable[mode] = {}
    end
    return newTable
end

function Utils.isTableNotEmpty(tbl)
    return next(tbl) ~= nil
end

-- ========================================
-- INPUT PROCESSING
-- ========================================
local InputProcessor = {}

function InputProcessor.parseOptions(args)
    return {
        do_table = args[1] == 'true',
        character = args[2] or args.char or 'Elsword',
        lang_suffix = args.lang and ('/' .. args.lang) or '',
        lang_append = args.lang ~= nil and args.lang ~= '',
        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
    }
end

function InputProcessor.parsePassives(args, frame, options)
    local passives = {}

    for k, v in pairs(args) do
        if string.find(k, 'passive') then
            local passive_name = v
            local passive_title = v .. options.lang_suffix
            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 .. '}}'))
            local display_title

            if is_custom then
                passive_name = passive_values[#passive_values]
                passive_values[#passive_values] = nil
            elseif options.lang_append then
                display_title = i18n.getTranslatedTitle(passive_title)
            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) or
                    display_title,
                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'
            }
        end
    end

    return passives
end

function InputProcessor.processNumericArgs(args, frame)
    local processed_args = table.deep_copy(args)

    for k, v in pairs(args) do
        if string.match(v, '^[()+%-*/%d%s,.i]+$') then
            local split_values = split(v)
            for k2, v2 in pairs(split_values) do
                if not string.find(v, '[a-zA-Z]+') then
                    split_values[k2] = frame:preprocess('{{#expr:' .. v2 .. '}}')
                end
            end
            processed_args[k] = split_values
        elseif inArrayHasValue(k, CONSTANTS.TO_SPLIT) then
            processed_args[k] = split(v)
        end
    end

    return processed_args
end

function InputProcessor.setDefaultHitCounts(args)
    -- 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
end

-- ========================================
-- CONFIGURATION BUILDING
-- ========================================
local ConfigBuilder = {}

function ConfigBuilder.buildDamageConfig(args)
    local function handleCancel()
        local processed_keys = {}
        for config_key, config_value in pairs(CONSTANTS.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
                processed_keys[new_key] = new_config_value
            end
        end
        return processed_keys
    end

    if args.cancel_dmg then
        local cancel_configs = handleCancel()
        return table.fuse(CONSTANTS.BASE_DAMAGE_CONFIG, cancel_configs)
    else
        return CONSTANTS.BASE_DAMAGE_CONFIG
    end
end

function ConfigBuilder.buildTraits(args, translate)
    local traits = {}
    for _, trait_template in ipairs(CONSTANTS.DEFAULT_TRAITS) do
        local trait = {
            key = trait_template.key,
            name = translate(trait_template.name),
            value = trait_template.value or (trait_template.value_func and trait_template.value_func(args))
        }
        table.insert(traits, trait)
    end
    return traits
end

-- ========================================
-- INHERITANCE SYSTEM
-- ========================================
local InheritanceProcessor = {}

function InheritanceProcessor.applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritValue)
    if mainArgValue == '' then
        return inheritValue
    elseif mainArgValue and string.find(mainArgValue, 'i') and inheritValue then
        return mainArgValue:gsub('i', inheritValue)
    end
    return mainArgValue
end

function InheritanceProcessor.applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType, eval)
    local mainKey = argTable[1] .. damageType
    local mainKeyPrefixed = prefix .. mainKey
    local mainArgValues = args[mainKeyPrefixed]

    if mainArgValues then
        local i = 1
        local cancelDmgLen = args.cancel_dmg and #args.cancel_dmg or 0

        while i <= (#args.dmg + cancelDmgLen) do
            local mainArgValue = mainArgValues[i]

            for ix, inheritKey in ipairs(argTable) do
                local inheritArg = args[prefix .. inheritKey .. damageType] or args[inheritKey .. damageType]

                -- Basic damage/hits inheritance request detected. Ignore min/max.
                if damageType and mainKey:gsub(damageType, "") == argTable[#argTable] then
                    inheritArg = args[prefix .. inheritKey] or args[inheritKey]
                end

                if inheritArg and inheritArg[i] and
                    (damageTypeIndex == 1 and ix ~= 1 or damageTypeIndex ~= 1) and tonumber(inheritArg[i])
                then
                    mainArgValues[i] = InheritanceProcessor.applyInheritance(mainArgValues, inheritArg, mainArgValue,
                        inheritArg[i])
                    break
                end
            end

            i = i + 1
        end
    end
end

function InheritanceProcessor.inherit(args, damageConfig, eval)
    local function inheritForMode(mode)
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')

        for configKey, configValue in pairs(damageConfig) do
            for argTableKey, argTable in pairs(configValue) do
                if argTableKey ~= 'provided' and Utils.isTableNotEmpty(argTable) then
                    for damageTypeIndex, damageType in ipairs({ '', '_min', '_max' }) do
                        InheritanceProcessor.applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType,
                            eval)
                    end
                end
            end
        end
    end

    Utils.forEach(inheritForMode)
end

-- ========================================
-- DAMAGE CALCULATION ENGINE
-- ========================================
local DamageEngine = {}

function DamageEngine.parseConfig(args, damageConfig, options)
    local damageParsed = Utils.createDamageDataTable()

    local function parseConfigForMode(mode)
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
        for config_key, config_value in pairs(damageConfig) 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
                    Utils.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
                            if k == 'provided' then
                                output_value = true
                                -- Special handling for awk_dmg_and_avg_hits marker
                                if v2 == 'awk_dmg_and_avg_hits' then
                                    output_value = args.awk_dmg and args.avg_hits
                                end
                                -- 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
                            else
                                if type(output_value) ~= "table" then
                                    output_value = {}
                                end
                                output_value[damage_type] = arg_from_template
                            end
                            -- Mark the value as found.
                            isValueFound[damage_type] = true
                        else
                            if k == 'provided' then
                                output_value = 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 damageParsed[mode][config_key] == nil then
                    damageParsed[mode][config_key] = {}
                end
                damageParsed[mode][config_key][k] = output_value
            end
        end
    end

    Utils.forEach(parseConfigForMode)
    return damageParsed
end

function DamageEngine.handleEachDamage(damageParsed, args)
    local withEach = table.deep_copy(damageParsed)
    for mode, mode_content in pairs(damageParsed) 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)

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

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

function DamageEngine.calculateBasicDamage(damageParsed)
    local basicDamage = Utils.createDamageDataTable()

    for mode, mode_content in pairs(damageParsed) do
        for damage_key, damage_value in pairs(mode_content) do
            Utils.forEachDamageType(function(damage_type)
                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[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 basicDamage[mode][damage_key] then
                        basicDamage[mode][damage_key] = {}
                    end
                    basicDamage[mode][damage_key][damage_type] = output
                end
            end)
        end
    end

    return basicDamage
end

function DamageEngine.addCancelDamage(basicDamage, args)
    if not args.cancel_dmg then
        return basicDamage
    end

    for mode, mode_content in pairs(basicDamage) do
        for damage_key, damage_value in pairs(mode_content) do
            local cancel_candidate = basicDamage[mode]['cancel_' .. damage_key]
            Utils.forEachDamageType(function(damage_type)
                if not string.find(damage_key, 'cancel_') and cancel_candidate then
                    basicDamage[mode][damage_key][damage_type] = damage_value[damage_type] +
                        cancel_candidate[damage_type]
                end
            end)
        end
    end

    return basicDamage
end

-- ========================================
-- TRAIT PROCESSING
-- ========================================
local TraitProcessor = {}

function TraitProcessor.applyTraits(basicDamage, traits)
    local withTraits = Utils.createDamageDataTable()

    for mode, mode_content in pairs(basicDamage) do
        for damage_key, damage_value in pairs(mode_content) do
            for _, trait in pairs(traits) do
                if (trait.value and trait.key ~= 'useful') or (string.find(damage_key, 'useful') and trait.key == 'useful') then
                    Utils.forEachDamageType(function(damage_type)
                        local new_key = damage_key ..
                            ((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key))
                        if not withTraits[mode][new_key] then
                            withTraits[mode][new_key] = {}
                        end
                        withTraits[mode][new_key][damage_type] = damage_value[damage_type] * trait.value
                    end)
                end
            end
        end
    end

    return withTraits
end

-- ========================================
-- PASSIVE PROCESSING
-- ========================================
local PassiveProcessor = {}

function PassiveProcessor.generatePassiveCombinations(passives)
    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
    return combinations
end

function PassiveProcessor.applyPassives(withTraits, passives)
    local withPassives = Utils.createDamageDataTable()

    for mode, mode_content in pairs(withTraits) do
        for damage_key, damage_value in pairs(mode_content) do
            Utils.forEachDamageType(function(damage_type)
                local combinations = PassiveProcessor.generatePassiveCombinations(passives)
                for _, combination in pairs(combinations) do
                    local passive_multiplier = 1
                    local name_suffix = ''
                    if #combination > 0 then
                        table.sort(combination)
                        for _, passive_key in pairs(combination) do
                            passive_multiplier = passive_multiplier *
                                tonumber(passives[passive_key][mode == 'PvE' and 'value' or 'value_pvp'])
                            name_suffix = name_suffix .. '_passive' .. passive_key
                        end
                    end
                    local new_damage_key = damage_key .. name_suffix
                    if not withPassives[mode][new_damage_key] then
                        withPassives[mode][new_damage_key] = {}
                    end
                    withPassives[mode][new_damage_key][damage_type] = damage_value[damage_type] * passive_multiplier
                end
            end)
        end
    end

    return withPassives
end

-- ========================================
-- RANGE PROCESSING
-- ========================================
local RangeProcessor = {}

function RangeProcessor.buildRangeConfig(args)
    return {
        min_count = args.range_min_count and args.range_min_count[1],
        max_count = args.range_max_count and args.range_max_count[1],
        PvE = {
            min = args.range_min and args.range_min[1],
            max = args.range_max and args.range_max[1]
        },
        PvP = {
            min = args.range_min and (args.range_min[2] or args.range_min[1]),
            max = args.range_max and (args.range_max[2] or args.range_max[1])
        }
    }
end

function RangeProcessor.applyRanges(withPassives, range, options, formatDamage)
    local withRange = Utils.createDamageDataTable()

    for mode, mode_content in pairs(withPassives) do
        for damage_key, damage_value in pairs(mode_content) do
            withRange[mode][damage_key] = { min = 0, max = 0 }
            Utils.forEachDamageType(function(damage_type)
                local range_count = range[damage_type .. '_count'] or 1
                -- If min count preset, use range_max for the multiplier.
                local range_multiplier = range[mode][damage_type] or
                    (damage_type == 'min' and range.min_count and range[mode].max) or 1
                local final_range_multiplier = (1 + ((range_multiplier - 1) * range_count))
                local perm_buff = options.perm_buff[mode]

                local final_damage_value = damage_value[damage_type] * final_range_multiplier * perm_buff
                withRange[mode][damage_key][damage_type] = not options.format and final_damage_value or
                    formatDamage(final_damage_value)
            end)
        end
    end

    return withRange
end

-- ========================================
-- TABLE GENERATION
-- ========================================
local TableGenerator = {}

function TableGenerator.checkTraits(traits, 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

function TableGenerator.checkPassives(passives, options, args, link, sortPassives, 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
        -- Determine if passive should be skipped
        local skip_passive = false
        if passive.is_combined then
            -- Skip combined passive if any of its constituent passives are being appended
            if options.is_append and options.combine and inArrayHasValue(options.append_index, options.combine) then
                skip_passive = true
            else
                skip_passive = false
            end
        else
            -- Regular passive logic: skip if it's appended or part of combine (unless display_separated)
            skip_passive = (options.is_append and options.append_index == passive_index) or
                (inArrayHasValue(passive_index, options.combine or {}) and not inArrayHasValue(passive_index, args.display_separated or {}))
        end

        if not skip_passive then
            if type(settings.action) == 'function' then
                settings.action(passive, output, passive_index)
            else
                return true
            end
        end
    end
    return output
end

function TableGenerator.buildTableContent(options, passives, traits, args, translate, inArgs, link, sortPassives,
                                          fillTemplate)
    return {
        {
            type = 'extra',
            text = { translate('Average') },
            is_visible = options.no_max,
            no_damage = true
        },
        {
            type = 'passives',
            text = TableGenerator.checkPassives(passives, options, args, link, sortPassives, {
                output = { translate('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 = TableGenerator.checkPassives(passives, options, args, link, sortPassives, {
                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 = {
                translate('Normal'),
                options.is_append and (function()
                    -- Check if the appended passive is part of a combined passive
                    if options.combine and inArrayHasValue(options.append_index, options.combine) then
                        -- Handle combined passive in append
                        local combo = {}
                        for _, passive_key in ipairs(options.combine) do
                            local passive = passives[passive_key]
                            table.insert(combo,
                                link(passive.name, passive.alias, passive.prefix, passive.suffix, passive.exist))
                        end
                        return table.concat(combo, '/') .. options.combine_suffix
                    else
                        -- Handle single passive in append
                        return 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
                        )
                    end
                end)()
            },
            keywords = { options.is_append and (function()
                -- Generate appropriate keyword for appended passive
                if options.combine and inArrayHasValue(options.append_index, options.combine) then
                    return sortPassives('passive' .. table.concat(options.combine, '_passive'))
                else
                    return 'passive' .. options.append_index
                end
            end)() or nil },
            is_visible = options.is_append or false
        },
        {
            type = 'awakening',
            text = { translate('Regular'), (function()
                if options.dmp then
                    return link('Dynamo Point System' .. options.lang_suffix, 'Dynamo Configuration', args.awk_prefix,
                        options.dmp ~= 'false' and
                        (fillTemplate('({1} DMP)', { options.dmp })) ..
                        (args.awk_suffix and (' ' .. args.awk_suffix) or ''))
                elseif args.awk_alias then
                    return link(args.awk_alias[1], args.awk_alias[2], args.awk_prefix, args.awk_suffix)
                end
                return link('Awakening Mode' .. options.lang_suffix, translate('Awakening Mode'), args.awk_prefix,
                    args.awk_suffix)
            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 = TableGenerator.checkTraits(traits, {
                output = { translate('Normal') },
                action = function(trait, output)
                    table.insert(output, trait.name)
                end
            }),
            keywords = TableGenerator.checkTraits(traits, {
                action = function(trait, output)
                    table.insert(output, trait.key)
                end
            }),
            is_visible = TableGenerator.checkTraits(traits)
        },
        {
            type = 'cancel',
            text = {
                translate('Cancel'),
                translate('Full'),
            },
            keywords = { 'cancel' },
            keyword_first = true,
            is_visible = inArgs('cancel_dmg')
        },
        {
            type = 'hit_count',
            text = {
                (inArgs('count') and not options.use_avg) and
                (fillTemplate(translate('Per {1}'), { args.count_name or translate('Group') })) or
                translate('Average'),
                translate('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
        }
    }
end

function TableGenerator.returnDamageInOrder(tableContent, options, inArgs, sortPassives)
    local main_key = 'damage'
    local all_list = {}

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

        if not current_row.no_damage then
            if i == #tableContent 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
                for _, keyword in ipairs(current_row.keywords) do
                    for _, prev_key in ipairs(all_list) do
                        local new_key = prev_key .. '_' .. keyword
                        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

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

    if inArgs('cancel_dmg') then
        local new_list = {}
        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 TableGenerator.generateTable(finalDamage, tableContent, options, frame, translate, args, inArgs, sortPassives)
    local TABLE = mw.html.create('table'):attr({
        cellpadding = 5,
        border = 1,
        style = 'border-collapse: collapse; text-align: center',
        class = 'colortable-' .. options.character
    })

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

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

    local function doHeaders()
        local current_multiplier = 0
        local initial_header_cell
        local iterations = 0

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

                if iterations == 0 and not initial_header_cell then
                    initial_header_cell = doInitialCell(new_row)
                end

                local colspan_value = 1
                for k, v in ipairs(tableContent) do
                    if k > row_index and v.is_visible then
                        colspan_value = colspan_value * #v.text
                    end
                end

                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
        initial_header_cell:attr('rowspan', iterations)
    end

    local 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

    local function doContentByMode(mode)
        local mode_row = TABLE:new()
        mode_row:tag('td'):wikitext(frame:expandTemplate { title = translate(mode) })
        local damage_entries = TableGenerator.returnDamageInOrder(tableContent, options, inArgs, sortPassives)
        local last_number
        local last_unique_cell

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

                if last_number ~= damage_number then
                    local new_cell = mode_row:tag('td'):wikitext(damage_number
                        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

    doHeaders()
    Utils.forEach(doContentByMode)

    return TABLE
end

-- ========================================
-- OUTPUT PROCESSING
-- ========================================
local OutputProcessor = {}

function OutputProcessor.generateOutput(frame, args, finalDamage, damageParsed, tableContent, options, translate,
                                        doVariables, inspect_dump)
    local out = nil

    -- Dump all values if wanted.
    if options.dump_table_data then
        return inspect_dump(frame, tableContent)
    elseif options.dump then
        return inspect_dump(frame, finalDamage)
    elseif options.dump_parsed then
        return inspect_dump(frame, damageParsed)
    end

    local bug = ''
    if options.bug then
        bug = frame:expandTemplate {
            title = translate('SkillText'),
            args = { 'FreeTraining' }
        }
    end

    -- Transform into variables
    local variables = doVariables(frame, finalDamage, options.prefix)

    local table_output = ''
    if options.do_table then
        local TABLE = TableGenerator.generateTable(finalDamage, tableContent, options, frame, translate, args,
            function(key) return args[key] ~= nil end, sortPassives)
        table_output = tostring(TABLE)
    end

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

    return variables .. bug .. table_output
end

-- ========================================
-- MAIN PROCESS
-- ========================================
function p.main(frame)
    local args = getArgs(frame)
    local tr = getTranslations(frame, 'Template:Damage', args.lang, true)
    local out

    function translate(key)
        return i18n.translate(tr, key)
    end

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

    -- User requested options
    local OPTIONS = InputProcessor.parseOptions(args)

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

    -- Define a table with trait names and their values to apply.
    local TRAITS = ConfigBuilder.buildTraits(args, translate)

    function eval(s)
        return frame:preprocess('{{#expr:' .. s .. '}}')
    end

    -- A table with user-requested passive skills (empty by default).
    local PASSIVES = InputProcessor.parsePassives(args, frame, OPTIONS)

    InputProcessor.setDefaultHitCounts(args)

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

    InheritanceProcessor.inherit(args, DAMAGE_CONFIG, eval)

    local DAMAGE_PARSED = DamageEngine.parseConfig(args, DAMAGE_CONFIG, OPTIONS)

    if args.count then
        DAMAGE_PARSED = DamageEngine.handleEachDamage(DAMAGE_PARSED, args)
    end

    local basicDamage = DamageEngine.calculateBasicDamage(DAMAGE_PARSED)

    -- Adding missing cancel part damage to full, so that repetition wouldn't be a problem.
    basicDamage = DamageEngine.addCancelDamage(basicDamage, args)

    local WITH_TRAITS = TraitProcessor.applyTraits(basicDamage, TRAITS)

    local WITH_PASSIVES = PassiveProcessor.applyPassives(WITH_TRAITS, PASSIVES)

    local RANGE = RangeProcessor.buildRangeConfig(args)

    local WITH_RANGE = RangeProcessor.applyRanges(WITH_PASSIVES, RANGE, OPTIONS, formatDamage)

    local FINAL_DAMAGE = WITH_RANGE

    -- Build table structure
    local TABLE_CONTENT = TableGenerator.buildTableContent(OPTIONS, PASSIVES, TRAITS, args, translate, inArgs, link,
        sortPassives, fillTemplate)

    -- Generate and return output
    return OutputProcessor.generateOutput(frame, args, FINAL_DAMAGE, DAMAGE_PARSED, TABLE_CONTENT, OPTIONS, translate,
        doVariables, inspect_dump)
end

return p
-- pyend