Module:Damage: Difference between revisions
From Elwiki
No edit summary |
No edit summary |
||
(368 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
-- pystart | |||
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 getTranslations = i18n.getTranslations | |||
local p = {} | local p = {} | ||
function | -- Main process | ||
local | function p.main(frame) | ||
for k in pairs( | local args = getArgs(frame) | ||
table. | local tr = getTranslations(frame, 'Template:Damage', args.lang, true) | ||
local i = 0 | local out | ||
return function() | |||
i = i + 1 | function translate(key) | ||
if | return i18n.translate(tr, key) | ||
return keys[i], | end | ||
function inArgs(key) | |||
if args[key] ~= nil then | |||
return true | |||
end | |||
end | |||
local modes = { 'PvE', 'PvP' } | |||
-- Define the schema for the table | |||
local tableSchema = {} | |||
for _, mode in ipairs(modes) do | |||
tableSchema[mode] = {} | |||
end | |||
function forEach(func) | |||
for _, mode in ipairs(modes) do | |||
func(mode) | |||
end | |||
end | |||
function forEachDamageType(func) | |||
for _, damage_type in ipairs({ 'min', 'max' }) do | |||
func(damage_type) | |||
end | |||
end | |||
-- Function to create a new table with the desired schema | |||
function createDamageDataTable() | |||
local newTable = {} | |||
for key, value in pairs(tableSchema) do | |||
if type(value) == "table" then | |||
newTable[key] = {} | |||
end | |||
end | |||
return newTable | |||
end | |||
-- User requested options | |||
local OPTIONS = { | |||
do_table = args[1] == 'true', | |||
character = args[2] or args.char or 'Elsword', | |||
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 | |||
} | |||
-- Define a table with parsed damage information of all kind. | |||
local BASIC_DAMAGE = createDamageDataTable() | |||
-- Define a table with trait names and their values to apply. | |||
local TRAITS = { | |||
-- An empty trait so we keep the original values there. | |||
{ | |||
key = '', | |||
name = translate('Normal'), | |||
value = 1 | |||
}, | |||
{ | |||
key = 'enhanced', | |||
name = translate('Enhanced (Trait)'), | |||
value = args.enhanced ~= nil and 0.8 | |||
}, | |||
{ | |||
key = 'empowered', | |||
name = translate('Empowered'), | |||
value = args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false | |||
}, | |||
{ | |||
key = 'useful', | |||
name = translate('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 = translate('Heavy'), | |||
value = args.heavy ~= nil and 1.44 | |||
} | |||
} | |||
function eval(s) | |||
return frame:preprocess('{{#expr:' .. s .. '}}') | |||
end | |||
-- A table with user-requested passive skills (empty by default). | |||
local PASSIVES = {} | |||
-- A table with non-numeric arguments to split. | |||
local TO_SPLIT = { 'append', 'awk_alias' } | |||
for k, v in pairs(args) do | |||
if string.find(k, 'passive') then | |||
--[[ | |||
Fix up the passives and put them into a separate table. | |||
|passive1=... |passive2=... -> { passive1, passive2 } | |||
--]] | |||
local passive_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 | |||
--[[ | |||
Translate page's display title to passive name. | |||
Customized will override this name, thus no need to perform the translation | |||
--]] | |||
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' | |||
} | |||
elseif string.match(v, '^[()+%-*/%d%s,.i]+$') then | |||
--[[ | |||
Change how args are received. | |||
dmg = 500, 700, 800 (string) -> dmg = { 500, 700, 800 } (table) | |||
--]] | |||
local split_values = split(v) | |||
-- Perform automatic math on each value. | |||
for k2, v2 in pairs(split_values) do | |||
if not string.find(v, '[a-zA-Z]+') then | |||
split_values[k2] = eval(v2) | |||
end | |||
end | |||
args[k] = split_values | |||
elseif inArrayHasValue(k, TO_SPLIT) then | |||
args[k] = split(v) | |||
end | |||
end | |||
-- Set basic hit count to 1 for all damage. | |||
for k, v in ipairs(args.dmg) do | |||
if not args.hits then | |||
args.hits = {} | |||
end | |||
if not args.hits[k] then | |||
args.hits[k] = 1 | |||
end | |||
end | |||
-- 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 | |||
-- 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 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', 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 handleCancel() | |||
local processed_keys = {} | |||
for config_key, config_value in pairs(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 | |||
DAMAGE_CONFIG[new_key] = new_config_value | |||
processed_keys[new_key] = true | |||
end | |||
end | |||
return processed_keys | |||
end | |||
if args.cancel_dmg then | |||
handleCancel() | |||
DAMAGE_CONFIG = table.fuse(BASE_DAMAGE_CONFIG, DAMAGE_CONFIG) | |||
else | |||
DAMAGE_CONFIG = BASE_DAMAGE_CONFIG | |||
end | |||
-- Helper function to check if a table is not empty | |||
local function isTableNotEmpty(tbl) | |||
return next(tbl) ~= nil | |||
end | |||
-- Function to apply inheritance for a specific damage type and argument | |||
local function applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritValue) | |||
if mainArgValue == '' then | |||
return inheritValue | |||
elseif mainArgValue and string.find(mainArgValue, 'i') and inheritValue then | |||
return eval(mainArgValue:gsub('i', inheritValue)) | |||
end | |||
return mainArgValue | |||
end | |||
-- Function to apply inheritance for a specific argument key | |||
local function applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType) | |||
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] = applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritArg[i]) | |||
break | |||
end | |||
end | |||
i = i + 1 | |||
end | |||
end | |||
end | |||
-- Inherits values from args if not provided, but usage suggests that they're meant to be generated. | |||
function inherit(mode) | |||
local prefix = mode == 'PvE' and '' or string.lower(mode .. '_') | |||
for configKey, configValue in pairs(DAMAGE_CONFIG) do | |||
for argTableKey, argTable in pairs(configValue) do | |||
if argTableKey ~= 'provided' and isTableNotEmpty(argTable) then | |||
for damageTypeIndex, damageType in ipairs({ '', '_min', '_max' }) do | |||
applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType) | |||
end | |||
end | |||
end | |||
end | |||
end | |||
forEach(inherit) | |||
local DAMAGE_PARSED = createDamageDataTable() | |||
function parseConfig(mode) | |||
local prefix = mode == 'PvE' and '' or string.lower(mode .. '_') | |||
for config_key, config_value in pairs(DAMAGE_CONFIG) do | |||
for k, v in pairs(config_value) do | |||
local output_value = {} | |||
-- 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 | |||
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 | |||
-- 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 DAMAGE_PARSED[mode][config_key] == nil then | |||
DAMAGE_PARSED[mode][config_key] = {} | |||
end | |||
DAMAGE_PARSED[mode][config_key][k] = output_value | |||
end | |||
end | |||
end | |||
forEach(parseConfig) | |||
-- Detected "count", for skills like Clementine, Enough Mineral, etc. | |||
function doEachDamage() | |||
local WITH_EACH = table.deep_copy(DAMAGE_PARSED) | |||
for mode, mode_content in pairs(DAMAGE_PARSED) do | |||
for damage_key, damage_value in pairs(mode_content) do | |||
if string.find(damage_key, 'total_') then | |||
local new_value = table.deep_copy(damage_value) | |||
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) | |||
WITH_EACH[mode][damage_key:gsub("total_", "each_")] = damage_value | |||
WITH_EACH[mode][damage_key] = new_value | |||
end | |||
end | |||
end | |||
return WITH_EACH | |||
end | |||
if args.count then | |||
DAMAGE_PARSED = doEachDamage() | |||
end | |||
function doBasicDamage() | |||
for mode, mode_content in pairs(DAMAGE_PARSED) do | |||
for damage_key, damage_value in pairs(mode_content) do | |||
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 BASIC_DAMAGE[mode][damage_key] then | |||
BASIC_DAMAGE[mode][damage_key] = {} | |||
end | |||
BASIC_DAMAGE[mode][damage_key][damage_type] = output | |||
end | |||
end) | |||
end | |||
end | |||
end | |||
doBasicDamage() | |||
-- Adding missing cancel part damage to full, so that repetition wouldn't be a problem. | |||
function addCancelDamage() | |||
for mode, mode_content in pairs(BASIC_DAMAGE) do | |||
for damage_key, damage_value in pairs(mode_content) do | |||
local cancel_candidate = BASIC_DAMAGE[mode]['cancel_' .. damage_key] | |||
forEachDamageType(function(damage_type) | |||
if not string.find(damage_key, 'cancel_') and cancel_candidate then | |||
BASIC_DAMAGE[mode][damage_key][damage_type] = damage_value[damage_type] + | |||
cancel_candidate[damage_type] | |||
end | |||
end) | |||
end | |||
end | |||
end | |||
if args.cancel_dmg then | |||
addCancelDamage() | |||
end | |||
local WITH_TRAITS = createDamageDataTable() | |||
function doTraits() | |||
-- Handle traits here | |||
for mode, mode_content in pairs(BASIC_DAMAGE) do | |||
for damage_key, damage_value in pairs(mode_content) do | |||
for _, trait in pairs(TRAITS) do | |||
--[[ | |||
Suffix all damage values with existing traits. | |||
Useful already has the prefix, so only multiply with its value. | |||
Also, we don't want other traits to multiply with Useful, | |||
so we skip those situations, as impossible in-game. | |||
--]] | |||
if (trait.value and trait.key ~= 'useful') or (string.find(damage_key, 'useful') and trait.key == 'useful') then | |||
forEachDamageType(function(damage_type) | |||
local new_key = damage_key .. | |||
((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key)); | |||
if not WITH_TRAITS[mode][new_key] then | |||
WITH_TRAITS[mode][new_key] = {} | |||
end | |||
WITH_TRAITS[mode][new_key][damage_type] = damage_value[damage_type] * trait.value | |||
end) | |||
end | |||
end | |||
end | |||
end | |||
end | |||
doTraits() | |||
local WITH_PASSIVES = createDamageDataTable() | |||
--[[ | |||
Generates passives with every possible combinations of all subsets. | |||
For example: 3 passives are given, so it will generate the following: | |||
(1), (2), (3), (1, 2), (1, 3), (1, 2, 3), (2, 3) | |||
]] | |||
function doPassives() | |||
for mode, mode_content in pairs(WITH_TRAITS) do | |||
for damage_key, damage_value in pairs(mode_content) do | |||
forEachDamageType(function(damage_type) | |||
local combinations = { {} } | |||
for passive_key, passive in pairs(PASSIVES) do | |||
local count = #combinations | |||
for i = 1, count do | |||
local new_combination = { unpack(combinations[i]) } | |||
table.insert(new_combination, passive_key) | |||
table.insert(combinations, new_combination) | |||
end | |||
end | |||
for _, combination in pairs(combinations) do | |||
local passive_multiplier = 1 | |||
local name_suffix = '' | |||
if #combination > 0 then | |||
table.sort(combination) | |||
for _, passive_key in pairs(combination) do | |||
passive_multiplier = passive_multiplier * | |||
tonumber(PASSIVES[passive_key][mode == 'PvE' and 'value' or 'value_pvp']) | |||
name_suffix = name_suffix .. '_passive' .. passive_key | |||
end | |||
end | |||
local new_damage_key = damage_key .. name_suffix; | |||
if not WITH_PASSIVES[mode][new_damage_key] then | |||
WITH_PASSIVES[mode][new_damage_key] = {} | |||
end | |||
WITH_PASSIVES[mode][new_damage_key][damage_type] = damage_value[damage_type] * passive_multiplier | |||
end | |||
end) | |||
end | |||
end | |||
end | |||
doPassives() | |||
local RANGE = { | |||
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]) | |||
} | |||
} | |||
local WITH_RANGE = createDamageDataTable() | |||
function doDamageBuffRange() | |||
-- Handle damage range here | |||
for mode, mode_content in pairs(WITH_PASSIVES) do | |||
for damage_key, damage_value in pairs(mode_content) do | |||
WITH_RANGE[mode][damage_key] = { min = 0, max = 0 } | |||
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; | |||
WITH_RANGE[mode][damage_key][damage_type] = not OPTIONS.format and final_damage_value or | |||
formatDamage(final_damage_value) | |||
end) | |||
end | |||
end | |||
end | |||
doDamageBuffRange() | |||
local FINAL_DAMAGE = WITH_RANGE | |||
-- Helper function to iterate over traits. | |||
function checkTraits(settings) | |||
local output | |||
if not settings then | |||
output = false | |||
else | |||
output = settings.output or {} | |||
end | |||
for trait_index, trait in ipairs(TRAITS) do | |||
if trait.value ~= false and trait_index ~= 1 then | |||
if settings and type(settings.action) == 'function' then | |||
settings.action(trait, output, settings) | |||
else | |||
return true | |||
end | |||
end | |||
end | |||
return output | |||
end | |||
-- Helper function to iterate over passives. | |||
function checkPassives(settings) | |||
local output = settings.output or {} | |||
local PASSIVES_WITH_COMBINED = table.deep_copy(PASSIVES) | |||
-- Handle combined passives properly. | |||
if OPTIONS.combine then | |||
table.insert(PASSIVES_WITH_COMBINED, { | |||
is_combined = true | |||
}) | |||
end | |||
for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do | |||
if (not OPTIONS.is_append or (OPTIONS.is_append and OPTIONS.append_index ~= passive_index)) and (not inArrayHasValue(passive_index, OPTIONS.combine or {}) or inArrayHasValue(passive_index, args.display_separated or {})) then | |||
if type(settings.action) == 'function' then | |||
settings.action(passive, output, passive_index) | |||
else | |||
return true | |||
end | |||
end | |||
end | |||
return output | |||
end | |||
-- Generate the table | |||
local TABLE = mw.html.create('table'):attr({ | |||
cellpadding = 5, | |||
border = 1, | |||
style = 'border-collapse: collapse; text-align: center', | |||
class = 'colortable-' .. OPTIONS.character | |||
}) | |||
-- Our table structure | |||
local TABLE_CONTENT = { | |||
{ | |||
type = 'extra', | |||
text = { translate('Average') }, | |||
is_visible = OPTIONS.no_max, | |||
no_damage = true | |||
}, | |||
{ | |||
type = 'passives', | |||
text = checkPassives({ | |||
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 = 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 = { | |||
translate('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 = { 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 = checkTraits({ | |||
output = { translate('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 = { | |||
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 | |||
} | |||
} | |||
function TABLE:new() | |||
return self:tag('tr') | |||
end | |||
function returnDamageInOrder() | |||
local main_key = 'damage' | |||
local all_list = {} | |||
-- Initialize current list with main key | |||
local current_list = { main_key } | |||
for i = #TABLE_CONTENT, 1, -1 do | |||
local current_row = TABLE_CONTENT[i] | |||
local new_list = {} | |||
-- Check if it's the first iteration. If so, append phrases. | |||
if not current_row.no_damage then | |||
if i == #TABLE_CONTENT then | |||
for _, keyword in ipairs(current_row.keywords) do | |||
if not OPTIONS.no_max or (OPTIONS.no_max and keyword ~= 'total') then | |||
local new_key = keyword .. '_' .. main_key | |||
table.insert(new_list, new_key) | |||
end | |||
end | |||
elseif current_row.is_visible then | |||
-- Append suffix for each keyword in current row | |||
for _, keyword in ipairs(current_row.keywords) do | |||
-- Iterate through previous keys | |||
for _, prev_key in ipairs(all_list) do | |||
local new_key = prev_key .. '_' .. keyword | |||
-- If needed, move the suffix to the rightmost of main_key. | |||
if current_row.keyword_next_to_main_key then | |||
new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword) | |||
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 | |||
for _, new_key in ipairs(new_list) do | |||
table.insert(all_list, sortPassives(new_key)) | |||
end | |||
end | |||
end | |||
-- Sort the list once more, in order to swap the order of cancel & full. | |||
if inArgs('cancel_dmg') then | |||
local new_list = {} | |||
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 | |||
end | |||
function doInitialCell(new_row) | |||
return new_row:tag('th'):wikitext(translate('Mode')) | |||
end | |||
function doHeaders() | |||
local current_multiplier = 0 -- Keeps track of the number of cells to spawn | |||
local initial_header_cell -- The leftmost cell that says "Mode" | |||
local iterations = 0 -- Keeps track of iterations that successfully rendered something. Required to tell the initial cell how many columns to span. | |||
for row_index, row in ipairs(TABLE_CONTENT) do | |||
if row.is_visible then | |||
local new_row = TABLE:new() | |||
local next_multiplier = 0 | |||
-- Only spawn the initial cell in the first generated row. | |||
if iterations == 0 and not initial_header_cell then | |||
initial_header_cell = doInitialCell(new_row) | |||
end | |||
--[[ | |||
We need to know how the colspan will look like. | |||
So the solution is to loop through the table again and check how many cells will be spawned. | |||
And also multiply everything, because it is exponential. | |||
]] | |||
local colspan_value = 1 | |||
for k, v in ipairs(TABLE_CONTENT) do | |||
if k > row_index and v.is_visible then | |||
colspan_value = colspan_value * #v.text | |||
end | |||
end | |||
-- Now we can spawn our header cells depending on what is known. | |||
for i = 1, (current_multiplier == 0 and 1 or current_multiplier), 1 do | |||
for _, text in ipairs(row.text) do | |||
local new_cell = new_row:tag('th') | |||
new_cell:attr('colspan', colspan_value):wikitext(text) | |||
next_multiplier = next_multiplier + 1 | |||
end | |||
end | |||
current_multiplier = next_multiplier | |||
iterations = iterations + 1 | |||
end | |||
end | |||
-- Apply rowspan of the same value as iteration count. | |||
initial_header_cell:attr('rowspan', iterations) | |||
end | |||
-- Helper function to display ranges. | |||
function doRangeText(damage_number) | |||
if damage_number and damage_number.min == damage_number.max then | |||
damage_number = damage_number.min | |||
elseif damage_number then | |||
damage_number = damage_number.min .. | |||
'<span style="white-space: nowrap;"> ~</span> ' .. damage_number.max | |||
end | end | ||
return damage_number | |||
end | end | ||
function | function doContentByMode(mode) | ||
local mode_row = TABLE:new() | |||
mode_row:tag('td'):wikitext(frame:expandTemplate { title = translate(mode) }) | |||
local damage_entries = returnDamageInOrder() | |||
local last_number | |||
local last_unique_cell | |||
for _, damage_key in ipairs(damage_entries) do | |||
if args.dump_names ~= 'true' then | |||
local damage_number = FINAL_DAMAGE[mode][damage_key] | |||
damage_number = doRangeText(damage_number) | |||
if last_number ~= damage_number then | |||
-- Display ranges. | |||
local new_cell = mode_row:tag('td'):wikitext(damage_number | |||
-- Error out if it doesn't exist | |||
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 | ||
end | end | ||
function | function doTable() | ||
return | doHeaders() | ||
end | forEach(doContentByMode) | ||
end | |||
if OPTIONS.do_table then | |||
doTable() | |||
end | |||
-- Dump all values if wanted. | |||
if OPTIONS.dump_table_data then | |||
return inspect_dump(frame, TABLE_CONTENT) | |||
elseif OPTIONS.dump then | |||
return inspect_dump(frame, FINAL_DAMAGE) | |||
elseif OPTIONS.dump_parsed then | |||
return inspect_dump(frame, DAMAGE_PARSED) | |||
end | |||
local bug = '' | |||
if OPTIONS.bug then | |||
bug = frame:expandTemplate { | |||
title = translate('SkillText'), | |||
args = { 'FreeTraining' } | |||
} | |||
end | |||
-- Transform into variables | |||
local variables = doVariables(frame, FINAL_DAMAGE, OPTIONS.prefix) | |||
if out ~= nil then | |||
return inspect_dump(frame, out) | |||
end | |||
return variables .. bug .. (OPTIONS.do_table and tostring(TABLE) or '') | |||
end | end | ||
return p | return p | ||
-- pyend |
Latest revision as of 15:55, 18 March 2024
Documentation for this module may be created at Module:Damage/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 = {}
-- 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
local modes = { 'PvE', 'PvP' }
-- Define the schema for the table
local tableSchema = {}
for _, mode in ipairs(modes) do
tableSchema[mode] = {}
end
function forEach(func)
for _, mode in ipairs(modes) do
func(mode)
end
end
function forEachDamageType(func)
for _, damage_type in ipairs({ 'min', 'max' }) do
func(damage_type)
end
end
-- Function to create a new table with the desired schema
function createDamageDataTable()
local newTable = {}
for key, value in pairs(tableSchema) do
if type(value) == "table" then
newTable[key] = {}
end
end
return newTable
end
-- User requested options
local OPTIONS = {
do_table = args[1] == 'true',
character = args[2] or args.char or 'Elsword',
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
}
-- Define a table with parsed damage information of all kind.
local BASIC_DAMAGE = createDamageDataTable()
-- Define a table with trait names and their values to apply.
local TRAITS = {
-- An empty trait so we keep the original values there.
{
key = '',
name = translate('Normal'),
value = 1
},
{
key = 'enhanced',
name = translate('Enhanced (Trait)'),
value = args.enhanced ~= nil and 0.8
},
{
key = 'empowered',
name = translate('Empowered'),
value = args.empowered == 'true' and 1.2 or tonumber(args.empowered) or false
},
{
key = 'useful',
name = translate('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 = translate('Heavy'),
value = args.heavy ~= nil and 1.44
}
}
function eval(s)
return frame:preprocess('{{#expr:' .. s .. '}}')
end
-- A table with user-requested passive skills (empty by default).
local PASSIVES = {}
-- A table with non-numeric arguments to split.
local TO_SPLIT = { 'append', 'awk_alias' }
for k, v in pairs(args) do
if string.find(k, 'passive') then
--[[
Fix up the passives and put them into a separate table.
|passive1=... |passive2=... -> { passive1, passive2 }
--]]
local passive_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
--[[
Translate page's display title to passive name.
Customized will override this name, thus no need to perform the translation
--]]
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'
}
elseif string.match(v, '^[()+%-*/%d%s,.i]+$') then
--[[
Change how args are received.
dmg = 500, 700, 800 (string) -> dmg = { 500, 700, 800 } (table)
--]]
local split_values = split(v)
-- Perform automatic math on each value.
for k2, v2 in pairs(split_values) do
if not string.find(v, '[a-zA-Z]+') then
split_values[k2] = eval(v2)
end
end
args[k] = split_values
elseif inArrayHasValue(k, TO_SPLIT) then
args[k] = split(v)
end
end
-- Set basic hit count to 1 for all damage.
for k, v in ipairs(args.dmg) do
if not args.hits then
args.hits = {}
end
if not args.hits[k] then
args.hits[k] = 1
end
end
-- 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
-- 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 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', 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 handleCancel()
local processed_keys = {}
for config_key, config_value in pairs(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
DAMAGE_CONFIG[new_key] = new_config_value
processed_keys[new_key] = true
end
end
return processed_keys
end
if args.cancel_dmg then
handleCancel()
DAMAGE_CONFIG = table.fuse(BASE_DAMAGE_CONFIG, DAMAGE_CONFIG)
else
DAMAGE_CONFIG = BASE_DAMAGE_CONFIG
end
-- Helper function to check if a table is not empty
local function isTableNotEmpty(tbl)
return next(tbl) ~= nil
end
-- Function to apply inheritance for a specific damage type and argument
local function applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritValue)
if mainArgValue == '' then
return inheritValue
elseif mainArgValue and string.find(mainArgValue, 'i') and inheritValue then
return eval(mainArgValue:gsub('i', inheritValue))
end
return mainArgValue
end
-- Function to apply inheritance for a specific argument key
local function applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType)
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] = applyInheritance(mainArgValues, inheritArg, mainArgValue, inheritArg[i])
break
end
end
i = i + 1
end
end
end
-- Inherits values from args if not provided, but usage suggests that they're meant to be generated.
function inherit(mode)
local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
for configKey, configValue in pairs(DAMAGE_CONFIG) do
for argTableKey, argTable in pairs(configValue) do
if argTableKey ~= 'provided' and isTableNotEmpty(argTable) then
for damageTypeIndex, damageType in ipairs({ '', '_min', '_max' }) do
applyInheritanceForKey(args, prefix, argTable, damageTypeIndex, damageType)
end
end
end
end
end
forEach(inherit)
local DAMAGE_PARSED = createDamageDataTable()
function parseConfig(mode)
local prefix = mode == 'PvE' and '' or string.lower(mode .. '_')
for config_key, config_value in pairs(DAMAGE_CONFIG) do
for k, v in pairs(config_value) do
local output_value = {}
-- 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
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
-- 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 DAMAGE_PARSED[mode][config_key] == nil then
DAMAGE_PARSED[mode][config_key] = {}
end
DAMAGE_PARSED[mode][config_key][k] = output_value
end
end
end
forEach(parseConfig)
-- Detected "count", for skills like Clementine, Enough Mineral, etc.
function doEachDamage()
local WITH_EACH = table.deep_copy(DAMAGE_PARSED)
for mode, mode_content in pairs(DAMAGE_PARSED) do
for damage_key, damage_value in pairs(mode_content) do
if string.find(damage_key, 'total_') then
local new_value = table.deep_copy(damage_value)
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)
WITH_EACH[mode][damage_key:gsub("total_", "each_")] = damage_value
WITH_EACH[mode][damage_key] = new_value
end
end
end
return WITH_EACH
end
if args.count then
DAMAGE_PARSED = doEachDamage()
end
function doBasicDamage()
for mode, mode_content in pairs(DAMAGE_PARSED) do
for damage_key, damage_value in pairs(mode_content) do
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 BASIC_DAMAGE[mode][damage_key] then
BASIC_DAMAGE[mode][damage_key] = {}
end
BASIC_DAMAGE[mode][damage_key][damage_type] = output
end
end)
end
end
end
doBasicDamage()
-- Adding missing cancel part damage to full, so that repetition wouldn't be a problem.
function addCancelDamage()
for mode, mode_content in pairs(BASIC_DAMAGE) do
for damage_key, damage_value in pairs(mode_content) do
local cancel_candidate = BASIC_DAMAGE[mode]['cancel_' .. damage_key]
forEachDamageType(function(damage_type)
if not string.find(damage_key, 'cancel_') and cancel_candidate then
BASIC_DAMAGE[mode][damage_key][damage_type] = damage_value[damage_type] +
cancel_candidate[damage_type]
end
end)
end
end
end
if args.cancel_dmg then
addCancelDamage()
end
local WITH_TRAITS = createDamageDataTable()
function doTraits()
-- Handle traits here
for mode, mode_content in pairs(BASIC_DAMAGE) do
for damage_key, damage_value in pairs(mode_content) do
for _, trait in pairs(TRAITS) do
--[[
Suffix all damage values with existing traits.
Useful already has the prefix, so only multiply with its value.
Also, we don't want other traits to multiply with Useful,
so we skip those situations, as impossible in-game.
--]]
if (trait.value and trait.key ~= 'useful') or (string.find(damage_key, 'useful') and trait.key == 'useful') then
forEachDamageType(function(damage_type)
local new_key = damage_key ..
((trait.key == 'useful' or trait.key == '') and "" or ('_' .. trait.key));
if not WITH_TRAITS[mode][new_key] then
WITH_TRAITS[mode][new_key] = {}
end
WITH_TRAITS[mode][new_key][damage_type] = damage_value[damage_type] * trait.value
end)
end
end
end
end
end
doTraits()
local WITH_PASSIVES = createDamageDataTable()
--[[
Generates passives with every possible combinations of all subsets.
For example: 3 passives are given, so it will generate the following:
(1), (2), (3), (1, 2), (1, 3), (1, 2, 3), (2, 3)
]]
function doPassives()
for mode, mode_content in pairs(WITH_TRAITS) do
for damage_key, damage_value in pairs(mode_content) do
forEachDamageType(function(damage_type)
local combinations = { {} }
for passive_key, passive in pairs(PASSIVES) do
local count = #combinations
for i = 1, count do
local new_combination = { unpack(combinations[i]) }
table.insert(new_combination, passive_key)
table.insert(combinations, new_combination)
end
end
for _, combination in pairs(combinations) do
local passive_multiplier = 1
local name_suffix = ''
if #combination > 0 then
table.sort(combination)
for _, passive_key in pairs(combination) do
passive_multiplier = passive_multiplier *
tonumber(PASSIVES[passive_key][mode == 'PvE' and 'value' or 'value_pvp'])
name_suffix = name_suffix .. '_passive' .. passive_key
end
end
local new_damage_key = damage_key .. name_suffix;
if not WITH_PASSIVES[mode][new_damage_key] then
WITH_PASSIVES[mode][new_damage_key] = {}
end
WITH_PASSIVES[mode][new_damage_key][damage_type] = damage_value[damage_type] * passive_multiplier
end
end)
end
end
end
doPassives()
local RANGE = {
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])
}
}
local WITH_RANGE = createDamageDataTable()
function doDamageBuffRange()
-- Handle damage range here
for mode, mode_content in pairs(WITH_PASSIVES) do
for damage_key, damage_value in pairs(mode_content) do
WITH_RANGE[mode][damage_key] = { min = 0, max = 0 }
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;
WITH_RANGE[mode][damage_key][damage_type] = not OPTIONS.format and final_damage_value or
formatDamage(final_damage_value)
end)
end
end
end
doDamageBuffRange()
local FINAL_DAMAGE = WITH_RANGE
-- Helper function to iterate over traits.
function checkTraits(settings)
local output
if not settings then
output = false
else
output = settings.output or {}
end
for trait_index, trait in ipairs(TRAITS) do
if trait.value ~= false and trait_index ~= 1 then
if settings and type(settings.action) == 'function' then
settings.action(trait, output, settings)
else
return true
end
end
end
return output
end
-- Helper function to iterate over passives.
function checkPassives(settings)
local output = settings.output or {}
local PASSIVES_WITH_COMBINED = table.deep_copy(PASSIVES)
-- Handle combined passives properly.
if OPTIONS.combine then
table.insert(PASSIVES_WITH_COMBINED, {
is_combined = true
})
end
for passive_index, passive in ipairs(PASSIVES_WITH_COMBINED) do
if (not OPTIONS.is_append or (OPTIONS.is_append and OPTIONS.append_index ~= passive_index)) and (not inArrayHasValue(passive_index, OPTIONS.combine or {}) or inArrayHasValue(passive_index, args.display_separated or {})) then
if type(settings.action) == 'function' then
settings.action(passive, output, passive_index)
else
return true
end
end
end
return output
end
-- Generate the table
local TABLE = mw.html.create('table'):attr({
cellpadding = 5,
border = 1,
style = 'border-collapse: collapse; text-align: center',
class = 'colortable-' .. OPTIONS.character
})
-- Our table structure
local TABLE_CONTENT = {
{
type = 'extra',
text = { translate('Average') },
is_visible = OPTIONS.no_max,
no_damage = true
},
{
type = 'passives',
text = checkPassives({
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 = 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 = {
translate('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 = { 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 = checkTraits({
output = { translate('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 = {
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
}
}
function TABLE:new()
return self:tag('tr')
end
function returnDamageInOrder()
local main_key = 'damage'
local all_list = {}
-- Initialize current list with main key
local current_list = { main_key }
for i = #TABLE_CONTENT, 1, -1 do
local current_row = TABLE_CONTENT[i]
local new_list = {}
-- Check if it's the first iteration. If so, append phrases.
if not current_row.no_damage then
if i == #TABLE_CONTENT then
for _, keyword in ipairs(current_row.keywords) do
if not OPTIONS.no_max or (OPTIONS.no_max and keyword ~= 'total') then
local new_key = keyword .. '_' .. main_key
table.insert(new_list, new_key)
end
end
elseif current_row.is_visible then
-- Append suffix for each keyword in current row
for _, keyword in ipairs(current_row.keywords) do
-- Iterate through previous keys
for _, prev_key in ipairs(all_list) do
local new_key = prev_key .. '_' .. keyword
-- If needed, move the suffix to the rightmost of main_key.
if current_row.keyword_next_to_main_key then
new_key = prev_key:gsub(main_key, main_key .. '_' .. keyword)
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
for _, new_key in ipairs(new_list) do
table.insert(all_list, sortPassives(new_key))
end
end
end
-- Sort the list once more, in order to swap the order of cancel & full.
if inArgs('cancel_dmg') then
local new_list = {}
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
end
function doInitialCell(new_row)
return new_row:tag('th'):wikitext(translate('Mode'))
end
function doHeaders()
local current_multiplier = 0 -- Keeps track of the number of cells to spawn
local initial_header_cell -- The leftmost cell that says "Mode"
local iterations = 0 -- Keeps track of iterations that successfully rendered something. Required to tell the initial cell how many columns to span.
for row_index, row in ipairs(TABLE_CONTENT) do
if row.is_visible then
local new_row = TABLE:new()
local next_multiplier = 0
-- Only spawn the initial cell in the first generated row.
if iterations == 0 and not initial_header_cell then
initial_header_cell = doInitialCell(new_row)
end
--[[
We need to know how the colspan will look like.
So the solution is to loop through the table again and check how many cells will be spawned.
And also multiply everything, because it is exponential.
]]
local colspan_value = 1
for k, v in ipairs(TABLE_CONTENT) do
if k > row_index and v.is_visible then
colspan_value = colspan_value * #v.text
end
end
-- Now we can spawn our header cells depending on what is known.
for i = 1, (current_multiplier == 0 and 1 or current_multiplier), 1 do
for _, text in ipairs(row.text) do
local new_cell = new_row:tag('th')
new_cell:attr('colspan', colspan_value):wikitext(text)
next_multiplier = next_multiplier + 1
end
end
current_multiplier = next_multiplier
iterations = iterations + 1
end
end
-- Apply rowspan of the same value as iteration count.
initial_header_cell:attr('rowspan', iterations)
end
-- Helper function to display ranges.
function doRangeText(damage_number)
if damage_number and damage_number.min == damage_number.max then
damage_number = damage_number.min
elseif damage_number then
damage_number = damage_number.min ..
'<span style="white-space: nowrap;"> ~</span> ' .. damage_number.max
end
return damage_number
end
function doContentByMode(mode)
local mode_row = TABLE:new()
mode_row:tag('td'):wikitext(frame:expandTemplate { title = translate(mode) })
local damage_entries = returnDamageInOrder()
local last_number
local last_unique_cell
for _, damage_key in ipairs(damage_entries) do
if args.dump_names ~= 'true' then
local damage_number = FINAL_DAMAGE[mode][damage_key]
damage_number = doRangeText(damage_number)
if last_number ~= damage_number then
-- Display ranges.
local new_cell = mode_row:tag('td'):wikitext(damage_number
-- Error out if it doesn't exist
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
function doTable()
doHeaders()
forEach(doContentByMode)
end
if OPTIONS.do_table then
doTable()
end
-- Dump all values if wanted.
if OPTIONS.dump_table_data then
return inspect_dump(frame, TABLE_CONTENT)
elseif OPTIONS.dump then
return inspect_dump(frame, FINAL_DAMAGE)
elseif OPTIONS.dump_parsed then
return inspect_dump(frame, DAMAGE_PARSED)
end
local bug = ''
if OPTIONS.bug then
bug = frame:expandTemplate {
title = translate('SkillText'),
args = { 'FreeTraining' }
}
end
-- Transform into variables
local variables = doVariables(frame, FINAL_DAMAGE, OPTIONS.prefix)
if out ~= nil then
return inspect_dump(frame, out)
end
return variables .. bug .. (OPTIONS.do_table and tostring(TABLE) or '')
end
return p
-- pyend