Module:Test: Difference between revisions
From Elwiki
No edit summary |
No edit summary |
||
(107 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
-- pystart | -- pystart | ||
require('Module:CommonFunctions') | require('Module:CommonFunctions') | ||
local i18n = require('Module:I18n') | |||
local getArgs = require('Module:Arguments').getArgs | local getArgs = require('Module:Arguments').getArgs | ||
local inspect = require('Module:Inspect').inspect | local inspect = require('Module:Inspect').inspect | ||
local getTranslations = i18n.getTranslations | |||
local p = {} | local p = {} | ||
-- | -- ======================================== | ||
function | -- 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 | |||
for _, mode in ipairs( | func(mode) | ||
end | end | ||
end | |||
function Utils.forEachDamageType(func) | |||
for _, damage_type in ipairs(CONSTANTS.DAMAGE_TYPES) do | |||
func(damage_type) | |||
end | end | ||
end | |||
function Utils.createDamageDataTable() | |||
local newTable = {} | |||
for _, mode in ipairs(CONSTANTS.MODES) do | |||
newTable[mode] = {} | |||
end | 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', | 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 | |||
function InputProcessor.parsePassives(args, frame, options) | |||
local passives = {} | |||
local | |||
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 | ||
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)] = { | |||
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 == | 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' | ||
} | } | ||
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) | local split_values = split(v) | ||
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] = | split_values[k2] = frame:preprocess('{{#expr:' .. v2 .. '}}') | ||
end | end | ||
end | end | ||
processed_args[k] = split_values | |||
elseif inArrayHasValue(k, TO_SPLIT) then | elseif inArrayHasValue(k, CONSTANTS.TO_SPLIT) then | ||
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 | |||
-- ======================================== | |||
-- CONFIGURATION BUILDING | |||
-- ======================================== | |||
local ConfigBuilder = {} | |||
local | function ConfigBuilder.buildDamageConfig(args) | ||
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 | ||
processed_keys[new_key] = new_config_value | |||
end | end | ||
end | end | ||
Line 259: | Line 271: | ||
if args.cancel_dmg then | if args.cancel_dmg then | ||
handleCancel() | local cancel_configs = handleCancel() | ||
return table.fuse(CONSTANTS.BASE_DAMAGE_CONFIG, cancel_configs) | |||
else | 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 | 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 | 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 | ||
Line 311: | Line 355: | ||
end | end | ||
forEach( | Utils.forEach(inheritForMode) | ||
end | |||
local | -- ======================================== | ||
function | -- 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( | 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 | 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 | 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 | if damageParsed[mode][config_key] == nil then | ||
damageParsed[mode][config_key] = {} | |||
end | end | ||
damageParsed[mode][config_key][k] = output_value | |||
end | end | ||
end | end | ||
end | end | ||
forEach( | 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 | 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 | end | ||
end | basicDamage[mode][damage_key][damage_type] = output | ||
end | end | ||
end) | |||
end | end | ||
end | end | ||
return basicDamage | |||
end | |||
function DamageEngine.addCancelDamage(basicDamage, args) | |||
if not args.cancel_dmg then | |||
for mode, mode_content in pairs( | 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) | |||
end | if not string.find(damage_key, 'cancel_') and cancel_candidate then | ||
end | basicDamage[mode][damage_key][damage_type] = damage_value[damage_type] + | ||
cancel_candidate[damage_type] | |||
end | |||
end) | |||
end | end | ||
end | end | ||
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)) | |||
end | if not withTraits[mode][new_key] then | ||
withTraits[mode][new_key] = {} | |||
end | |||
withTraits[mode][new_key][damage_type] = damage_value[damage_type] * trait.value | |||
end) | |||
end | end | ||
end | end | ||
Line 472: | Line 531: | ||
end | end | ||
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 | 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( | 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 | ||
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 | ||
-- | return withPassives | ||
end | |||
-- ======================================== | |||
-- RANGE PROCESSING | |||
-- ======================================== | |||
local RangeProcessor = {} | |||
function RangeProcessor.buildRangeConfig(args) | |||
min_count = args.range_min_count and args.range_min_count[1] | return { | ||
max_count = args.range_max_count and args.range_max_count[1] | min_count = args.range_min_count and args.range_min_count[1], | ||
max_count = args.range_max_count and args.range_max_count[1], | |||
PvE = { | PvE = { | ||
min = args.range_min and args.range_min[1] | min = args.range_min and args.range_min[1], | ||
max = args.range_max and args.range_max[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]) | 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]) | max = args.range_max and (args.range_max[2] or args.range_max[1]) | ||
} | } | ||
} | } | ||
end | |||
local | 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 } | |||
for | 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)) | |||
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 | ||
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 | 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 | ||
if | -- 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 | end | ||
if not skip_passive then | |||
if type(settings.action) == 'function' then | |||
settings.action(passive, output, passive_index) | |||
else | |||
return true | |||
end | end | ||
end | 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', '#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. | -- Dump all values if wanted. | ||
if | if options.dump_table_data then | ||
return inspect_dump(frame, | return inspect_dump(frame, tableContent) | ||
elseif | elseif options.dump then | ||
return inspect_dump(frame, | return inspect_dump(frame, finalDamage) | ||
elseif | elseif options.dump_parsed then | ||
return inspect_dump(frame, | return inspect_dump(frame, damageParsed) | ||
end | end | ||
local bug = '' | local bug = '' | ||
if | 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, | 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. | 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', '#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