Module:Test: Difference between revisions

From Elwiki
No edit summary
No edit summary
 
(108 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
                    -- We only do this for the first (main) key
                    local main_key = arg_table[1]
                     local main_key_prefixed = prefix .. main_key
                    local main_arg_values = args[main_key_prefixed]


                    -- Only if the main argument values exist.
                if inheritArg and inheritArg[i] and
                     if main_arg_values then
                     (damageTypeIndex == 1 and ix ~= 1 or damageTypeIndex ~= 1) and tonumber(inheritArg[i])
                        local i = 1
                then
                        --[[
                    mainArgValues[i] = InheritanceProcessor.applyInheritance(mainArgValues, inheritArg, mainArgValue,
                            Loop over all damage and attempt to inherit in chain.
                         inheritArg[i])
                            Break the loop if a match was found. Note: For this to work, the value must be an empty string.
                    break
                            Alternatively, it can contain an "i" to template the value to inherit.
                end
                        ]]
            end
                        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
            i = i + 1
                                local inherit_arg = args[prefix .. inherit_key] or args[inherit_key]
        end
                                -- No inheritance from itself.
    end
                                if inherit_arg and inherit_arg[i] and inherit_arg[i] ~= '' and ix ~= 1 then
end
                                    -- Only inherit if empty
 
                                    if main_arg_value == '' then
function InheritanceProcessor.inherit(args, damageConfig, eval)
                                        args[main_key_prefixed][i] = inherit_arg[i]
    local function inheritForMode(mode)
                                        break
        local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
                                    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
 
function DamageEngine.addCancelDamage(basicDamage, args)
    if not args.cancel_dmg then
        return basicDamage
    end


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


     if args.cancel_dmg then
     return basicDamage
        addCancelDamage()
end
     end
 
-- ========================================
-- TRAIT PROCESSING
-- ========================================
local TraitProcessor = {}
 
function TraitProcessor.applyTraits(basicDamage, traits)
     local withTraits = Utils.createDamageDataTable()


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


    local RANGE = {
-- ========================================
         min_count = args.range_min_count and args.range_min_count[1] or 1,
-- RANGE PROCESSING
         max_count = args.range_max_count and args.range_max_count[1] or 1,
-- ========================================
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 = {
         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_PARSED
-- 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
function TableGenerator.checkPassives(passives, options, args, link, sortPassives, settings)
    local output = settings.output or {}
    local PASSIVES_WITH_COMBINED = table.deep_copy(passives)


     -- Helper function to iterate over passives.
     -- Handle combined passives properly.
     function checkPassives(settings)
     if options.combine then
         local output = settings.output or {}
         table.insert(PASSIVES_WITH_COMBINED, {
         local PASSIVES_WITH_COMBINED = table.deep_copy(PASSIVES)
            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
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


    -- -- Generate the table
function TableGenerator.returnDamageInOrder(tableContent, options, inArgs, sortPassives)
    -- local TABLE = mw.html.create('table'):attr({
     local main_key = 'damage'
    --    cellpadding = 5,
     local all_list = {}
    --    border = 1,
     --    style = 'border-collapse: collapse; text-align: center',
     --    class = 'colortable-' .. OPTIONS.character
    -- })


     -- -- Our table structure
     for i = #tableContent, 1, -1 do
    -- local TABLE_CONTENT = {
         local current_row = tableContent[i]
    --    {
         local new_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()
        if not current_row.no_damage then
    --    return self:tag('tr')
            if i == #tableContent then
    -- end
                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


    -- function returnDamageInOrder()
            for _, new_key in ipairs(new_list) do
    --    local main_key = 'damage'
                table.insert(all_list, sortPassives(new_key))
     --    local all_list = {}
            end
        end
     end


     --    -- Initialize current list with main key
     if inArgs('cancel_dmg') then
    --    local current_list = { main_key }
        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


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


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


     --    -- Sort the list once more, in order to swap the order of cancel & full.
     local function doInitialCell(new_row)
    --    if inArgs('cancel_dmg') then
        return new_row:tag('th'):wikitext(translate('Mode'))
    --        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 doHeaders()
    -- end
        local current_multiplier = 0
        local initial_header_cell
        local iterations = 0


    -- function doInitialCell(new_row)
        for row_index, row in ipairs(tableContent) do
    --    return new_row:tag('th'):wikitext('Mode')
            if row.is_visible then
    -- end
                local new_row = TABLE:new()
                local next_multiplier = 0


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


    --    for row_index, row in ipairs(TABLE_CONTENT) do
                local colspan_value = 1
    --        if row.is_visible then
                for k, v in ipairs(tableContent) do
    --            local new_row = TABLE:new()
                    if k > row_index and v.is_visible then
    --            local next_multiplier = 0
                        colspan_value = colspan_value * #v.text
                    end
                end


    --            -- Only spawn the initial cell in the first generated row.
                for i = 1, (current_multiplier == 0 and 1 or current_multiplier), 1 do
    --            if iterations == 0 and not initial_header_cell then
                    for _, text in ipairs(row.text) do
    --                 initial_header_cell = doInitialCell(new_row)
                        local new_cell = new_row:tag('th')
     --            end
                        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