Dead by Daylight Wiki
mNo edit summary
mNo edit summary
 
(648 intermediate revisions by 6 users not shown)
Line 1: Line 1:
 
local p = {}
 
local p = {}
 
local mathOps = require("Module:MathOps")
 
local mathOps = require("Module:MathOps")
  +
local str = require("Module:Strings")
 
local frame = mw.getCurrentFrame()
 
local frame = mw.getCurrentFrame()
 
local _brigtnessTreshold = 0.36
 
local _brigtnessTreshold = 0.36
  +
local _isValidFileNameCounter = 0
  +
local _colorOrder = {2, 6, 8, 4, 3, 10, 11, 12, 14, 15, 21, 22, 25, 26, 9, 27, 28, 29, 13, 31, 32}
  +
  +
local strings = {
  +
--genders, not currently used
  +
male = "Male",
  +
female = "Female",
  +
transmale = "Trans Male",
  +
transfemale = "Trans Female",
  +
nonhuman = "Not applicable (not human)",
  +
undefined = "Undefined",
  +
  +
--charNotFound = "Character not found!",
  +
patch = "Patch",
  +
tabberHeaderNotFound = "Header not found",
  +
category = "Category", --used for [[Category:...]]
  +
boxTypeNotFound = b("Dynamic box is missing type!"),
  +
notSpecified = "notSpecified", --used in IconLink for assemble machine function
  +
clrTestString = "An extremely long string that serves as a test string for automatic and ordered coloured text that colour-highlights important values on our beautiful Dead by Daylight wiki. For this testing purposes we consider only 3-characters or longer words",
  +
notEnoughColors = "no more colours in order",
  +
}
  +
p.strings = strings
  +
  +
local months = {
  +
January = "January",
  +
February = "February",
  +
March = "March",
  +
April = "April",
  +
May = "May",
  +
June = "June",
  +
July = "July",
  +
August = "August",
  +
September = "September",
  +
October = "October",
  +
November = "November",
  +
December = "December"
  +
}
  +
  +
local days = {
  +
Monday = "Monday",
  +
Tuesday = "Tuesday",
  +
Wednesday = "Wednesday",
  +
Thursday = "Thursday",
  +
Friday = "Friday",
  +
Saturday = "Saturday",
  +
Sunday = "Sunday"
  +
}
  +
----------------------------------------------
  +
  +
p.timeFormat1 = "%d.%m.%Y" -- 5.9.2022
  +
p.timeContants = {
  +
second = 1,
  +
minute = 60, --in seconds
  +
hour = 3600,
  +
day = 86400,
  +
week = 604800
  +
}
  +
local timeContants = p.timeContants
  +
  +
--Bridge to Language Module, as more appropriate place to have this function
  +
function p.lang()
  +
return require("Module:Languages").lang()
  +
end
  +
  +
--If params will be passed {...} then index should equals 0, so this list will be passeb back: ((index == 0 and params.args) or params)
  +
function p.resolveParameter(param, index, returnCanBeNil)
  +
local retArg
  +
if type(param) == "table" then retArg = ((index == 0 and param.args) or (param.args and param.args[(index or 1)] and p.replaceSpecialCharacters(param.args[(index or 1)]))) or nil end --if parameter is passed from wiki, not other function
  +
if retArg == nil and type(param) == "table" and param.args ~= nil and next(param.args) == nil and not returnCanBeNil then retArg = getParamOrPageName() end --param.args ~= nil and next(param.args): this means that params.args is not nil but empty table
  +
if retArg == nil and not returnCanBeNil then retArg = getParamOrPageName(param, index, false) end --if parameter was passed directly or not at all
  +
if retArg ~= nil and type(param) == "string" then retArg = p.replaceSpecialCharacters(retArg) end --final processing
  +
return retArg
  +
end
  +
  +
function getParamOrPageName(param, index, returnCanBeNil)
  +
if param and
  +
(type(param) ~= types.table or
  +
(type(param) == types.table and not param.args))
  +
then
  +
return param
  +
else
  +
local pageName = pageName or mw.title.getCurrentTitle().text
  +
local pageParts = string.split(pageName, '/') -- remove page suffixes such as language codes
  +
if #pageParts > 1 then
  +
_unofficialLang = pageParts[#pageParts]
  +
return pageParts[#pageParts - 1] --take second to last part of page name
  +
end
  +
return pageParts[1] --if there is no other part than the name itself
  +
end
  +
end
  +
  +
--translates string containing #1# #2# ... to individual strings (resp. words) contained in dyntable
  +
function p.getDynamicString(dynTable, templateString)
  +
--dynTable = {"Testing Chapter", nil, "DLC Str World", nil, 999};
  +
--templateString = "Retracted #1# #2# #3# #4# #5# #6# #7# #8#"
  +
local skipList = {skip = {}, spans = {}}
  +
local spanRegex = "(<span .-</span>)" --mostly clr case
  +
local spanSkipRegex = "&&span" --it needs to use a DIFFERENT symbol than hashtag (#) as the style of spans can contain one due to colorhex colour value
  +
local skipRegex = "#(.-)#" --skip anything inside hashtags
  +
if type(dynTable) == types.string then --is parameter is string then just split it
  +
dynTable = string.split(dynTable)
  +
elseif type(dynTable) == types.table then
  +
local tmp = {}
  +
local skipCounter = 1
  +
local spanCounter = 1
  +
for i, el in pairs(dynTable) do
  +
if el:find(spanRegex) then --if you find any spans to skip then ...
  +
for m in el:gmatch(spanRegex) do --let's find spans (and pull them out) to skip them with splitting by "space"
  +
table.insert(skipList.spans, m)
  +
el = el:gsub(m, spanSkipRegex .. spanCounter)
  +
spanCounter = spanCounter + 1
  +
end
  +
end
  +
  +
--finding any strings DOES NOT need to be first as span regex WILL NOT create elements matching this regex
  +
if el:find(skipRegex) then --if you find any string to skip then ...
  +
for m in el:gmatch(skipRegex) do
  +
table.insert(skipList.skip, m)
  +
el = el:gsub(skip(m:gsub('%W','%%%1')), "#skip" .. skipCounter) --We have to escape special characters as the string is being used as a pattern and for an instance "[[ ]]" are magic chars for LUA patterns
  +
skipCounter = skipCounter + 1
  +
end
  +
end
  +
  +
table.insert(tmp, string.split(tostring(el))) --if parameter is table, split all strings inside
  +
end
  +
dynTable = table.flatten(tmp) --format from table.insert is {{"splitted", "string"}, {"splitted", "another", "string"}}, so it's needed to be flattened
  +
--mw.log(mw.dumpObject(dynTable))
  +
end
  +
  +
if templateString then
  +
for m in templateString:gmatch("#(%d+)#") do --replace all #1# occurences
  +
local currentRegexString = "#" .. tonumber(m) .. "#"
  +
if tonumber(m) <= #dynTable then
  +
templateString = templateString:gsub(currentRegexString, dynTable[tonumber(m)])
  +
else
  +
templateString = templateString:gsub(currentRegexString, cstr.empty) --remove all remaining marks
  +
end
  +
end
  +
else
  +
templateString = table.join(dynTable, space)
  +
end
  +
  +
templateString = string.trim(templateString)
  +
  +
--lg(templateString)
  +
for m in templateString:gmatch("#skip(%d)") do --now we put spans back into string
  +
local currentRegexString = "#skip" .. tonumber(m)
  +
templateString = templateString:gsub(currentRegexString, skipList.skip[tonumber(m)])
  +
end
  +
  +
for m in templateString:gmatch(spanSkipRegex .. "(%d)") do --now we put spans back into string
  +
local currentRegexString = spanSkipRegex .. tonumber(m)
  +
templateString = templateString:gsub(currentRegexString, skipList.spans[tonumber(m)])
  +
end
  +
  +
--mw.log(templateString)
  +
return templateString
  +
end
  +
--------------------------------------------------------------------------------
   
 
function p.getCount(subject)
 
function p.getCount(subject)
 
local list
 
local list
  +
subject = p.resolveParameter(subject)
 
 
--if you have another list just add it into a list then call appropriate function
 
--if you have another list just add it into a list then call appropriate function
if subject == "map" then list = maps
+
if subject == "map" then return p.getMapsCount()
 
elseif subject == "realm" then list = realms
 
elseif subject == "realm" then list = realms
 
elseif subject == "killer" then list = killers
 
elseif subject == "killer" then list = killers
 
elseif subject == "survivor" then list = survivors
 
elseif subject == "survivor" then list = survivors
 
elseif subject == "dlc" then list = dlcs
 
elseif subject == "dlc" then list = dlcs
elseif subject == "chapter" then return getCountDlcType(1)
+
elseif subject == "chapter" then return p.getCountDlcType(1)
elseif subject == "paragraph" then return getCountDlcType(2)
+
elseif subject == "paragraph" then return p.getCountDlcType(2)
elseif subject == "clothing" then return getCountDlcType(3)
+
elseif subject == "clothing" then return p.getCountDlcType(3)
elseif subject == "ost" then return getCountDlcType(4)
+
elseif subject == "ost" then return p.getCountDlcType(4)
elseif subject == "character" then return getCountDlcType(5)
+
elseif subject == "character" then return p.getCountDlcType(5)
 
elseif subject == "ccy" then return getCountCCY(true)
 
elseif subject == "ccy" then return getCountCCY(true)
 
elseif subject == "ccy-gc" then return getCountCCY(true) --redundant option to keep convention
 
elseif subject == "ccy-gc" then return getCountCCY(true) --redundant option to keep convention
 
elseif subject == "ccy-rc" then return getCountCCY(false)
 
elseif subject == "ccy-rc" then return getCountCCY(false)
  +
elseif subject == "killerPerk" then return getPerksCount('K')
  +
elseif subject == "survPerk" then return getPerksCount('S')
 
else return 0
 
else return 0
 
end
 
end
 
 
 
local x = 0
 
local x = 0
for _, item in ipairs(list) do if item.skip then x = x + 1 end end
+
for _, item in ipairs(list) do if item.skip or item.retired then x = x + 1 end end
 
 
 
return #list - x
 
return #list - x
 
end
 
end
   
function getCountDlcType(type)
+
function p.getMapsCount()
  +
local data = require("Module:Datatable")
  +
local result = 0
  +
  +
for _, map in ipairs(data.maps) do
  +
if not (map.skip or map.retired) then
  +
if map.variations then
  +
for _, var in ipairs(map.variations) do
  +
if not (map.skip or map.retired) then
  +
result = result + 1
  +
end
  +
end
  +
else
  +
result = result + 1
  +
end
  +
end
  +
end
  +
  +
return result
  +
end
  +
  +
function p.getCountDlcType(type)
 
local count = 0
 
local count = 0
 
 
for i, dlc in ipairs(dlcs) do
+
for _, dlc in ipairs(dlcs) do
 
if dlc.category == type and not dlc.skip then count = count + 1 end
 
if dlc.category == type and not dlc.skip then count = count + 1 end
 
end
 
end
 
 
  +
return count
  +
end
  +
  +
function getPerksCount(charType)
  +
local perks = mw.loadData("Module:Datatable/Loadout" .. p.lang()).perks
  +
local count = 0
  +
  +
for _, perk in ipairs(perks) do
  +
if perk.charType == charType and not perk.unused then count = count + 1 end
  +
end
  +
 
return count
 
return count
 
end
 
end
Line 47: Line 241:
   
 
return i
 
return i
  +
end
  +
  +
function isRealCcy(ccyId)
  +
local data = require("Module:Datatable" .. p.lang())
  +
for _, currency in ipairs(data.ccy) do
  +
if currency.id == ccyId then return not currency.gc end
  +
end
  +
return false
  +
end
  +
  +
function p.getCcyById(id)
  +
for _, currCcy in ipairs(ccy) do
  +
if currCcy.id == id then return currCcy end
  +
end
  +
return nil
  +
end
  +
  +
function p.getMaxId(tab)
  +
result = 0
  +
for _, item in ipairs(tab) do
  +
result = (item.id > result and item.id) or result
  +
end
  +
return result
 
end
 
end
   
 
-- Function allowing for consistent treatment of boolean-like wikitext input.
 
-- Function allowing for consistent treatment of boolean-like wikitext input.
 
-- It works similarly to the template {{yesno}}.
 
-- It works similarly to the template {{yesno}}.
 
 
function p.bool(val, default)
 
function p.bool(val, default)
 
-- If your wiki uses non-ascii characters for any of "yes", "no", etc., you
 
-- If your wiki uses non-ascii characters for any of "yes", "no", etc., you
Line 98: Line 314:
 
--NEG $22,333,444.563
 
--NEG $22,333,444.563
   
  +
function p.formatNum(amount, decimal, prefix, neg_prefix)
  +
return p.format_num(amount, decimal, prefix, neg_prefix)
  +
end
 
function p.format_num(amount, decimal, prefix, neg_prefix)
 
function p.format_num(amount, decimal, prefix, neg_prefix)
 
local str_amount, formatted, famount, remain
 
local str_amount, formatted, famount, remain
Line 146: Line 365:
   
 
--Converting Arabic numbers to Roman
 
--Converting Arabic numbers to Roman
function ToRomanNumerals(s)
+
function p.toRomanNumerals(s)
 
local numbers = { 1, 5, 10, 50, 100, 500, 1000 }
 
local numbers = { 1, 5, 10, 50, 100, 500, 1000 }
 
local chars = { "I", "V", "X", "L", "C", "D", "M" }
 
local chars = { "I", "V", "X", "L", "C", "D", "M" }
   
 
s = tonumber(s)
 
s = tonumber(s)
if not s or s ~= s then error"Unable to convert to number" end
+
if not s or s ~= s then return false end
if s == math.huge then error"Unable to convert infinity" end
+
if s == math.huge then return false end
 
s = math.floor(s)
 
s = math.floor(s)
 
if s <= 0 then return s end
 
if s <= 0 then return s end
local ret = ""
+
local ret = cstr.empty
 
for i = #numbers, 1, -1 do
 
for i = #numbers, 1, -1 do
 
local num = numbers[i]
 
local num = numbers[i]
Line 202: Line 421:
 
local result
 
local result
 
if string.match(romanNumber, "[MDCLXVI]+$") then --finding ONLY Roman letters in last "word"
 
if string.match(romanNumber, "[MDCLXVI]+$") then --finding ONLY Roman letters in last "word"
--Cowshed
 
mw.log(romanNumber)
 
 
result = str
 
result = str
--result = string.sub(str, 1, index - 1) .. ToNumeral(romanNumber)
 
 
else
 
else
mw.log("Non-Roman form")
 
 
result = str
 
result = str
 
end
 
end
Line 214: Line 429:
 
end
 
end
   
function p.CapitalizeName(str)
+
function p.capitalizeName(str) return p.CapitalizeName(str) end
  +
function p.CapitalizeName(str) --deprecated name
return string.gsub(" "..str, "%W%l", string.upper):sub(2)
 
  +
--mw.log(string.gsub(" "..str, "[%s%-]%a", string.upper):sub(2))
  +
--return string.gsub(" "..str, "[%s%-]%a", string.upper):sub(2) --original version
  +
return string.gsub(" "..str:lower(), "[%s%-]%a", string.upper):sub(2)
 
end
 
end
   
function p.FirstLetterLower(str)
+
function p.firstLetterLower(str) return p.FirstLetterLower(str) end
  +
function p.FirstLetterLower(str) --deprecated name
 
return str:sub(1, 1):lower() .. str:sub(2)
 
return str:sub(1, 1):lower() .. str:sub(2)
 
end
 
end
   
function p.RemoveSpecialCharacters(str)
+
function p.firstLetterUpper(str) return p.FirstLetterUpper(str) end
  +
function p.FirstLetterUpper(str) --deprecated name
str = string.gsub(str, "[']", "")
 
  +
return str:sub(1,1):upper() .. str:sub(2)
str = string.gsub(str, "[®]", "")
 
  +
end
str = string.gsub(str, "[™]", "")
 
  +
str = string.gsub(str, "[:]", "")
 
  +
--[[function p.fixDiacritics(str)
str = string.gsub(str, "[!]", "") --perks
 
  +
local letterSequences = {
str = string.gsub(str, "[&]", "And") --probably can be changed to lower: "and"
 
  +
["195"] = {fix = "â", sequence = {"162"}}
 
  +
}
  +
local fixedSeqsOffset = 0
  +
  +
local currentLen = #str + fixedSeqsOffset
  +
local i = 1
  +
while i <= currentLen do
  +
local ascii = tostring(string.byte(str:sub(i,i)) or 0)
  +
for startSequence, seqData in pairs(letterSequences) do
  +
if ascii == startSequence then
  +
local foundSequence = true
  +
for j, seqCode in ipairs(seqData.sequence) do
  +
local seqAscii = tostring(string.byte(str:sub(i+j,i+j)))
  +
if seqAscii ~= seqCode then
  +
foundSequence = false
  +
break
  +
end
  +
end
  +
  +
if foundSequence then
  +
fixedSeqsOffset = i - (i + #seqData.sequence)
  +
currentLen = #str + fixedSeqsOffset
  +
-- #seqData.sequence + 1 = number of ascii codes that needs to be replaced, the sequence list + the initial ascii code, i.e. the key value from letterSequences
  +
str = str:sub(1, i - 1) .. seqData.fix .. str:sub(i + #seqData.sequence + 1)
  +
end
  +
end
  +
end
  +
i = i + 1
  +
end
  +
return str
  +
end]]
  +
  +
function p.RemoveSpecialCharacters(str, full, replaceDiacritics)
  +
str = string.gsub(str, "'", "")
  +
str = string.gsub(str, "®", "")
  +
str = string.gsub(str, "™", "")
  +
str = string.gsub(str, ":", "")
  +
str = string.gsub(str, "!", "") --perks
  +
str = string.gsub(str, "?", "") --achievements
  +
str = string.gsub(str, "/", "") --achievements
  +
str = string.gsub(str, "%&", "And") --probably can be changed to lower: "and"
  +
  +
if replaceDiacritics then
  +
str = p.replaceDiacritics(str, full)
  +
end
  +
  +
return str
  +
end
  +
  +
function p.replaceDiacritics(str, full)
 
--Diacritics
 
--Diacritics
  +
specialLettersLight = {
  +
["A"] = {"À", "Á", "Â", "Ã", "Ä"},
  +
["a"] = {"à", "á", "â", "ã", "ä"},
  +
["e"] = {"è", "é", "ê", "ë", "ě"},
  +
["O"] = {"Ò", "Ó", "Ô", "Õ", "Ö"},
  +
["o"] = {"ò", "ó", "ô", "õ", "ö"}
  +
}
  +
specialLetters = {
  +
["A"] = {"À", "Á", "Â", "Ã", "Ä"},
  +
["a"] = {"à", "á", "â", "ã", "ä"},
  +
["E"] = {"È", "É", "Ê", "Ë", "Ě"},
  +
["e"] = {"è", "é", "ê", "ë", "ě"},
  +
["I"] = {"Ì", "Í", "Î", "Ñ", "Ï"},
  +
["i"] = {"ì", "í", "î", "ñ", "ï"},
  +
["O"] = {"Ò", "Ó", "Ô", "Õ", "Ö"},
  +
["o"] = {"ò", "ó", "ô", "õ", "ö"},
  +
["U"] = {"Ù", "Ú", "Û", "Ů", "Ü"},
  +
["u"] = {"ù", "ú", "û", "ů", "ü"},
  +
["Y"] = { "Ý", "Ÿ"},
  +
["y"] = { "ý", "ÿ"}
  +
}
  +
--Originally it was grouped by set [ÀÁ], however there is some sort of bug there that makes it not working
 
--str = string.gsub(str, "[ÁÂ]", "A")
 
--str = string.gsub(str, "[ÁÂ]", "A")
  +
--if debugRun then mw.log(mw.dumpObject(specialLetters)) end
str = string.gsub(str, "[áâ]", "a")
 
  +
for letter, row in pairs(((full and specialLetters) or specialLettersLight)) do
str = string.gsub(str, "[ÉĚ]", "E")
 
  +
for _, special in ipairs(row) do
str = string.gsub(str, "[éě]", "e")
 
str = string.gsub(str, "[Í]", "I")
+
str = string.replace(str, special, letter)
  +
end
str = string.gsub(str, "[í]", "i")
 
  +
end
str = string.gsub(str, "[Ó]", "O")
 
str = string.gsub(str, "[ó]", "o")
 
str = string.gsub(str, "[ÚŮ]", "U")
 
str = string.gsub(str, "[úů]", "u")
 
str = string.gsub(str, "[Ý]", "Y")
 
str = string.gsub(str, "[ý]", "y")
 
 
return str
 
return str
 
end
 
end
   
function p.isValidFileName(name)
+
function p.replaceSpecialCharacters(name)
  +
local charList = {
  +
["'"] = '&#39;',
  +
["&"] = '&#38;'
  +
}
  +
if name == nil then error("Name parameter is empty, but it shouldnt?") end
  +
for repl, sChar in pairs(charList) do
  +
name = string.gsub(tostring(name), sChar, repl)
  +
end
  +
return name
  +
end
  +
  +
function p.pageExists(name)
  +
return (not name == cstr.empty) or mw.title.new(name).exists
  +
end
  +
  +
function p.isValidFileName(name, extension)
  +
extension = extension or "png"
 
name = p.RemoveSpecialCharacters(name)
 
name = p.RemoveSpecialCharacters(name)
  +
lg("Validation check, Name: " .. name)
return not (name == "" or not mw.title.new("File:" .. name .. ".png").exists)
 
  +
_isValidFileNameCounter = _isValidFileNameCounter + 1
  +
--mw.log("Counter:" .. tostring(_isValidFileNameCounter) .. ' - ' .. cstr.media .. name .. dot .. extension)
  +
return not (name == cstr.empty or not mw.title.new(cstr.media .. name .. dot .. extension).exists)
 
end
 
end
   
function p.resolveFileName(str, keepSpaces)
+
function p.resolveFileName(str, keepSpaces, removeDiacritics)
 
keepSpaces = keepSpaces or false
 
keepSpaces = keepSpaces or false
 
local result = ""
 
local result = ""
Line 257: Line 562:
 
result = string.lower(str)
 
result = string.lower(str)
 
--mw.log(result)
 
--mw.log(result)
result = p.RemoveSpecialCharacters(result)
+
result = p.RemoveSpecialCharacters(result, keepSpaces, removeDiacritics)
 
--mw.log(result)
 
--mw.log(result)
result = p.CapitalizeName(result)
+
result = p.capitalizeName(result)
 
if not keepSpaces then
 
if not keepSpaces then
 
result = string.gsub(result, "[ ]", "")
 
result = string.gsub(result, "[ ]", "")
Line 265: Line 570:
 
--In future if there will be needed replace charactere such as "é" just add another substitution
 
--In future if there will be needed replace charactere such as "é" just add another substitution
 
 
  +
--mw.log(result)
 
return result
 
return result
 
end
 
end
 
 
 
function p.resolveImageName(name)
 
function p.resolveImageName(name)
if mw.title.new("File:" .. name .. ".png").exists then return name .. ".png" end
+
if mw.title.new(cstr.media .. name .. dot .. cstr.png).exists then return name .. dot .. cstr.png end
if mw.title.new("File:" .. name .. ".jpg").exists then return name .. ".jpg" end
+
if mw.title.new(cstr.media .. name .. dot .. cstr.jpg).exists then return name .. dot .. cstr.jpg end
  +
if mw.title.new(cstr.media .. name .. dot .. cstr.gif).exists then return name .. dot .. cstr.gif end
return name .. ".png"
 
  +
return name .. dot .. cstr.png
 
end
 
end
   
Line 278: Line 585:
 
end
 
end
   
function p.clr(text, color)
+
function p.IconLink(icon, pageLink, displayText, background)
  +
local result = cstr.empty
return frame:expandTemplate{title = "clr", args = {color, text}}
 
  +
background = background or p.resolveParameter(icon, "bg", true) or p.resolveParameter(icon, 4, true)
  +
background = p.bool(background == nil or background)
  +
displayText = displayText or p.resolveParameter(icon, 3, true)
  +
pageLink = pageLink or p.resolveParameter(icon, 2, true)
  +
  +
local linkless = p.resolveParameter(icon, "linkless", true) or pageLink == "linkless" --linkless as a second parameter should be deprecated
  +
icon = p.resolveParameter(icon)
  +
local iconObject, pluralType = p.getIconObject(icon)
  +
if pluralType then --if automatic plural is detected, then links should redirect to singular form
  +
pageLink = pageLink or iconObject.name
  +
displayText = icon --since automatic plurals don't imitate 3-parameters scenario we have to set the display text to be same as the first parameter (icon)
  +
end
  +
local iconFile = iconObject.iconFile
  +
local filename = cstr.file .. iconFile .. tl .. "link=" .. (pageLink or icon)
  +
local text = (displayText and pageLink and pageLink .. tl .. displayText) or pageLink or icon --cstr.empty
  +
--local boxDesc = cstr.empty
  +
  +
if pageLink == "img" then
  +
text = cstr.empty
  +
filename = cstr.file .. iconFile .. tl .. "link=" .. icon
  +
end
  +
if not linkless and text ~= cstr.empty then
  +
text = link(text)
  +
--boxDesc = getBoxDescription(pageLink or icon) //hover box, currently disabled
  +
elseif linkless or pageLink == "linkless" then
  +
text = (displayText or icon)
  +
end
  +
  +
local imageFile
  +
if background and iconObject.assembly then
  +
local loLogic = require("Module:Loadout" .. p.lang())
  +
--this needs to (should) be done sophisticated way
  +
local imgType =
  +
(iconObject.iconFile:lower():find("addon") and "addon") or
  +
(iconObject.iconFile:lower():find("items") and "item") or
  +
(iconObject.iconFile:lower():find("favors") and "offering") or
  +
(iconObject.iconFile:lower():find("powers") and "power") or
  +
(iconObject.iconFile:lower():find("perks") and "perk") or
  +
strings.notSpecified
  +
--if iconObject.iconFile:lower():find("favors") then imgType = "offering" end
  +
local loObj = (loLogic["get" .. p.capitalizeName(imgType) .. "ByName"] and loLogic["get" .. p.capitalizeName(imgType) .. "ByName"](iconObject.name)) or nil
  +
imageFile = p.assembleImage(imgType, icon, 32, {rarity = (loObj and loObj.rarity) or 1, iconLink = true})
  +
else
  +
imageFile = link(filename, "64px") --iconObject.ilRes or "32px")
  +
end
  +
  +
local tooltipText = link(filename, "96px", "link=")
  +
  +
--result = '<span class = "wrap-span pcView" style = "display:none;"><span class="box-span">' .. boxDesc .. '</span>' .. text .. file .. '</span>'
  +
local padding = p.getPaddingsFromIcon(iconObject)
  +
result = text ..
  +
'<span class = "iconLink" style = "vertical-align: middle; padding: ' .. padding.top .. space .. padding.right .. space .. padding.bottom .. space .. padding.left .. '">' ..
  +
'<span style = "display:none; width:' .. (iconObject.ilRes or "32px") .. semi .. 'max-height: ' .. (iconObject.ilRes or "32px") .. '" class = "pcView pcIconLink">' .. p.tooltip(imageFile, tooltipText, true, true, true) .. '</span>' ..
  +
'<span class = "mobileView mobileIconLink">' .. link(filename, iconObject.ilRes or "32px") .. '</span>' ..
  +
'</span>'
  +
  +
--mw.log(result)
  +
return result
  +
--return frame:expandTemplate{title = "IconLink", args = {icon, pageLink, displayText} }
  +
end
  +
  +
function p.loadoutIconLink(icon, pageLink, displayText, params)
  +
displayText = displayText or p.resolveParameter(icon, "displayText", true) or p.resolveParameter(icon, 3, true)
  +
pageLink = pageLink or p.resolveParameter(icon, "link", true) or p.resolveParameter(icon, 2, true)
  +
icon = p.resolveParameter(icon, "icon", true) or p.resolveParameter(icon, 1, true) or icon
  +
local info = {
  +
displayText = displayText or pageLink or icon,
  +
link = pageLink or icon,
  +
icon = icon
  +
}
  +
local iconObject, pluralType = p.getIconObject(info.icon)
  +
local filename = cstr.file .. iconObject.iconFile .. tl .. "link=" .. (info.link or info.icon)
  +
local tooltipText = link(filename, "96px", "link=") --default tooltip
  +
  +
if iconObject.iconFile:lower():find("perk") then
  +
local loLogic = require("Module:Loadout" .. p.lang())
  +
tooltipText = loLogic.getPerkPageTable(params.perk) or "WIP/Error:" .. params.perk.name
  +
end
  +
  +
local imageFile = link(filename, "64px")
  +
local padding = p.getPaddingsFromIcon(iconObject)
  +
local result = link(info.displayText, info.link) ..
  +
'<span class = "iconLink" style = "vertical-align: middle; padding: ' .. padding.top .. space .. padding.right .. space .. padding.bottom .. space .. padding.left .. '">' ..
  +
'<span style = "display:none; width:' .. (iconObject.ilRes or "32px") .. semi .. 'max-height: ' .. (iconObject.ilRes or "32px") .. '" class = "pcView pcIconLink">' .. p.tooltip(imageFile, tooltipText, true, true, true) .. '</span>' ..
  +
--'<span class = "mobileView mobileIconLink">' .. link(filename, iconObject.ilRes or "32px") .. '</span>' ..
  +
'</span>'
  +
return result
  +
end
  +
  +
function p.getPaddingsFromIcon(icon)
  +
return {
  +
top = icon.paddingTop or 0,
  +
right = icon.paddingRight or 0,
  +
bottom = icon.paddingBottom or 0,
  +
left = icon.paddingLeft or 0
  +
}
  +
end
  +
  +
function getBoxDescription(icon) --curently disabled
  +
local ic = p.getIconObject(icon)
  +
local result = cstr.empty
  +
if ic.category == "Perks" then
  +
result = '<h3>' .. icon .. '</h3><hr>'-- .. prkz.getPerkDescriptionByName(perk.name)
  +
end
  +
return result
 
end
 
end
   
Line 291: Line 703:
 
local darkness = (0.299 * red + 0.587 * green + 0.114 * blue) / 255
 
local darkness = (0.299 * red + 0.587 * green + 0.114 * blue) / 255
 
 
mw.log("Red: " .. red .." | Blue: " .. blue .. " | Green: " .. green)
+
--mw.log("Red: " .. red .." \t\tor\t Blue: " .. blue .. " | Green: " .. green)
mw.log(darkness)
+
--mw.log(darkness)
 
if(darkness < _brigtnessTreshold) then
 
if(darkness < _brigtnessTreshold) then
 
return "white"
 
return "white"
Line 304: Line 716:
 
end
 
end
 
---------------------------------------------------------------------------------
 
---------------------------------------------------------------------------------
function p.getSumOfASTiles(row)
+
function p.getSumOfSizes(row)
 
local result = 0
 
local result = 0
 
local i = 1
 
local i = 1
Line 314: Line 726:
 
end
 
end
   
  +
--[[
function compASTiles(row1, row2)
 
  +
function compSize(row1, row2)
local sum1 = row1.ASTiles
 
local sum2 = row2.ASTiles
+
local sum1 = row1.size
  +
local sum2 = row2.size
 
if(type(sum1) == "table") then
 
if(type(sum1) == "table") then
sum1 = p.getSumOfASTiles(sum1) --converting back from table to number
+
sum1 = p.getSumOfSizes(sum1) --converting back from table to number
 
end
 
end
 
if(type(sum2) == "table") then
 
if(type(sum2) == "table") then
sum2 = p.getSumOfASTiles(sum2)
+
sum2 = p.getSumOfSizes(sum2)
 
end
 
end
 
 
Line 334: Line 747:
 
end
 
end
 
end
 
end
  +
]]
 
function compMapRateSize(row1, row2)
 
return row1.size > row2.size --comparison specificly made for mapRates table
 
end
 
   
 
function compName(row1, row2)
 
function compName(row1, row2)
  +
if row1.diacritics then
  +
if row2.diacritics then
  +
return p.RemoveSpecialCharacters(row1.name, true, true) < p.RemoveSpecialCharacters(row2.name, true, true)
  +
else
  +
return p.RemoveSpecialCharacters(row1.name, true, true) < row2.name
  +
end
  +
elseif row2.diacritics then
  +
return row1.name < p.RemoveSpecialCharacters(row2.name, true, true)
  +
end
 
return row1.name < row2.name
 
return row1.name < row2.name
 
end
 
end
   
function compInt(row1, row2)
+
function compFilename(row1, row2)
  +
return row1.filename < row2.filename
  +
end
  +
  +
function compInt(row1, row2) -- 1, 2, 3
 
return row1 < row2
 
return row1 < row2
 
end
 
end
   
function p.sortMapsByASTiles()
+
function compIntReversed(row1, row2) -- 3, 2, 1
  +
return row1 > row2
local m = require("Module:Datatable")
 
--mw.log(mw.dumpObject(maps))
 
table.sort(maps,compASTiles)
 
--mw.log(mw.dumpObject(maps))
 
 
end
 
end
   
function p.sortMapRates(rankTable)
+
function compIntDesc(row1, row2) -- 3, 2, 1
  +
return row1 > row2
--mw.log(mw.dumpObject(rankTable))
 
  +
end
table.sort(rankTable, compMapRateSize)
 
  +
--mw.log(mw.dumpObject(rankTable))
 
  +
function compLevel(row1, row2)
  +
return row1.level < row2.level
  +
end
  +
  +
function compDlcCategory(row1, row2)
  +
if row1.category == row2.category then
  +
return row1.id < row2.id
  +
end
  +
return row1.category < row2.category
  +
end
  +
  +
function compId(row1, row2)
  +
return row1.id < row2.id
  +
end
  +
  +
function compRealCcyFirst(row1, row2)
  +
ccy1 = isRealCcy(row1.ccy)
  +
ccy2 = isRealCcy(row2.ccy)
  +
if (ccy1 and ccy2) or (not ccy1 and not ccy2) then --TRUE and TRUE or FALSE and FALSE
  +
return row1.ccy < row2.ccy
  +
elseif ccy1 and not ccy2 then --TRUE and FALSE
  +
return true
  +
else
  +
return false
  +
end
  +
end
  +
  +
function compCharsKillersFirst(row1, row2)
  +
char1 = row1.power ~= nil
  +
char2 = row2.power ~= nil
  +
if (char1 and char2) or (not char1 and not char2) then --TRUE and TRUE or FALSE and FALSE
  +
return row1.id < row2.id
  +
elseif char1 and not char2 then --TRUE and FALSE
  +
return true
  +
else
  +
return false
  +
end
  +
end
  +
  +
function rarityAndName(row1, row2)
  +
if row1.rarity and row2.rarity and row1.rarity < row2.rarity then
  +
return true
  +
elseif row1.rarity and row2.rarity and row1.rarity > row2.rarity then
  +
return false
  +
elseif row1.pieces and row2.pieces then
  +
local row1Name = cstr.empty
  +
local row2Name = cstr.empty
  +
for _, piece in pairs(row1.pieces) do row1Name = piece.name break end
  +
for _, piece in pairs(row2.pieces) do row2Name = piece.name break end
  +
return row1Name < row2Name
  +
else
  +
return compName(row1, row2)
  +
end
  +
end
  +
  +
function p.sortByKeys(tableList) --only int, TODO check key type via pairs() fc
  +
local result = {}
  +
local orderedKeys = table.keys(tableList)
  +
p.sortTable(orderedKeys, true)
  +
  +
for _, key in pairs(orderedKeys) do
  +
result[key] = tableList[key]
  +
end
  +
return result
  +
end
  +
  +
function p.sortByRDate(tableList, reversed)
  +
table.sort(tableList,
  +
function (row1, row2)
  +
if p.toTimestamp(row1.rDate) < p.toTimestamp(row2.rDate) then
  +
if reversed then
  +
return false
  +
else
  +
return true
  +
end
  +
end
  +
end
  +
)
  +
return tableList
  +
end
  +
  +
  +
function p.sortMapsBySize() --deprecated?
  +
local m = require("Module:Datatable" .. p.lang())
  +
--mw.log(mw.dumpObject(maps))
  +
table.sort(maps,compSize)
  +
--mw.log(mw.dumpObject(maps))
 
end
 
end
   
Line 364: Line 871:
 
end
 
end
   
  +
function p.sortItemsBycompFilename(tableList) --used for charms (as perk object is not retrieved, only cosData record)
function p.sortTable(tableList)
 
table.sort(tableList, compInt)
+
table.sort(tableList, compFilename)
 
end
 
end
   
  +
function p.sortTable(tableList, reversed)
  +
if reversed then
  +
table.sort(tableList, compIntReversed)
  +
else
  +
table.sort(tableList, compInt)
  +
end
  +
end
  +
  +
function p.sortTableDesc(tableList)
  +
table.sort(tableList, compIntDesc)
  +
end
  +
  +
function p.sortDlcByCategory(tableList)
  +
table.sort(tableList, compDlcCategory)
  +
end
  +
  +
function p.sortPerksByLevel(tableList)
  +
table.sort(tableList, compLevel)
  +
end
  +
  +
function p.sortTableById(tableList)
  +
table.sort(tableList, compId)
  +
end
  +
  +
function p.sortRealCcyFirst(tableList)
  +
table.sort(tableList, compRealCcyFirst)
  +
end
  +
  +
function p.sortCharsKillersFirst(tableList)
  +
table.sort(tableList, compCharsKillersFirst)
  +
end
  +
  +
function p.sortCosmeticsByRarityAndName(tableList)
  +
table.sort(tableList, rarityAndName)
  +
end
 
---------------------------------------------------------------------------------
 
---------------------------------------------------------------------------------
  +
-- Time functions --
  +
function p.today()
  +
return os.time(os.date("!*t"))
  +
end
   
  +
function p.getYear(stamp) return p.getTimeAspect(stamp, "year") end
function p.getCountOfSoundtracks()
 
  +
function p.getMonth(stamp) return p.getTimeAspect(stamp, "month") end
return utils.getCount("ost")
 
  +
function p.getDay(stamp) return p.getTimeAspect(stamp, "day") end
  +
function p.getHour(stamp) return p.getTimeAspect(stamp, "hour") end
  +
function p.getMinute(stamp) return p.getTimeAspect(stamp, "minute") end
  +
function p.getSecond(stamp) return p.getTimeAspect(stamp, "second") end
  +
function p.getWeekday(stamp) return p.getTimeAspect(stamp, "weekday") end
  +
  +
function p.getTimeAspect(stamp, aspect) --aspect being selected time portion
  +
if type(stamp) ~= 'number' then
  +
stamp = p.toTimestamp(stamp)
  +
end
  +
local tag = '%c'
  +
if aspect == "year" then tag = '%Y'
  +
elseif aspect == "month" then tag = '%m'
  +
elseif aspect == "day" then tag = '%d'
  +
elseif aspect == "hour" then tag = '%H'
  +
elseif aspect == "minute" then tag = '%M'
  +
elseif aspect == "second" then tag = '%S'
  +
elseif aspect == "weekday" then tag = '%w'
  +
end
  +
  +
local result = os.date(tag, stamp)
  +
return (aspect == "weekday" and tonumber(result)) or result
 
end
 
end
   
function p.resolveDateTime(datetime) --with weekday
+
function p.addTime(portion, amount, stamp)
  +
stamp = stamp or 0
matchYear = "^(##)%.(##)%.(%d%d%d%d)$"
 
  +
if type(stamp) ~= 'number' then
matchMonth = "^(##)%.(%d%d)%.(%d%d%d%d)$"
 
  +
stamp = p.toTimestamp(stamp)
  +
end
  +
  +
local multiplier = timeContants[portion]
  +
stamp = stamp + (amount * multiplier)
  +
  +
return stamp
  +
end
  +
  +
function p.getTimeDiff(timestamp1, timestamp2) --First parameter is first in timeline, Second Parameter is later one
  +
return os.difftime(timestamp2, timestamp1)
  +
end
  +
  +
function p.getTimeDiffFormatting(seconds, unit)
  +
return seconds / timeContants[unit]
  +
end
  +
  +
function p.toTimestamp(datestamp)
  +
if type(datestamp) == 'string' then
  +
datestamp = p.GetDatetime(datestamp)
  +
end
  +
return os.time(datestamp) --if datestamp = nil, os.time() returns current datetime stamp
  +
end
  +
  +
function p.toDate(timestamp, timeFormat)
  +
return os.date(timeFormat or '%d %B %Y', timestamp) --21 October 2021
  +
end
  +
  +
function p.resolveDateTime(datetime, skipDay) --with weekday
  +
local matchYear = "^(..)%.(..)%.(%d%d%d%d)$"
  +
local matchMonth = "^(..)%.(%d%d)%.(....)$"
  +
local matchDay = "^(%d%d)%.(..)%.(....)$"
  +
local year, month, day, dayName = false
 
local rDate = os.time(p.GetDatetime(datetime))
 
local rDate = os.time(p.GetDatetime(datetime))
 
if string.find(datetime, matchYear) then
 
if string.find(datetime, matchYear) then
return os.date("%Y", rDate)
+
year = os.date("%Y", rDate)
elseif string.find(datetime, matchMonth) then
 
return os.date("%B %Y", rDate)
 
 
end
 
end
  +
if string.find(datetime, matchMonth) then
return os.date("%d %B %Y (%A)", rDate)
 
  +
month = os.date("%B", rDate)
  +
end
  +
if string.find(datetime, matchDay) then
  +
day = os.date("%d", rDate)
  +
dayName = os.date("%A", rDate)
  +
end
  +
  +
--(day and month) is to avoid showing a day as the function sets the month to January (first month) by default, if the month is not provided
  +
--day part: ((day and month and day .. space) or cstr.empty)
  +
--month part: ((month and months[month] .. space) or cstr.empty)
  +
--year part: year
  +
--dayName part: ((day and month and not skipDay and space .. brackets(days[dayName])) or cstr.empty)
  +
return ((day and month and day .. space) or cstr.empty) .. ((month and months[month] .. space) or cstr.empty) .. year .. ((day and month and not skipDay and space .. brackets(days[dayName])) or cstr.empty)
 
end
 
end
   
  +
function p.isFullDateTime(datestamp) return p.IsFullDateTime(datestamp) end
 
function p.IsFullDateTime(datestamp)
 
function p.IsFullDateTime(datestamp)
 
local day, month, year = datestamp:match("^(..)%.(..)%.(....)$")
 
local day, month, year = datestamp:match("^(..)%.(..)%.(....)$")
Line 392: Line 1,005:
 
end
 
end
   
  +
--convertToTimeVersion = normally, return table counts as 1.1.1970 (with day = 1, month = 1 and year = 1970).
function p.GetDatetime(datestamp)
 
  +
--But if we want to have time stamp (like remaining time) these default values add 1 day, 1 month and 1970 years to the countdown table (Time stamp version table)
local day, month, year = datestamp:match("^(..)%.(..)%.(%d%d%d%d)$")
 
  +
function p.GetDatetime(datestamp, convertToTimeVersion)
if month == "##" then month = 1 end
 
  +
local result = {}
if day == "##" then day = 1 end
 
  +
if type(datestamp) == types.string then
return {year = year, month = month, day = day}
 
  +
local day, month, year, hour, min, sec = datestamp:match("^(..)%.(..)%.(%d%d%d%d) ?(%d?%d?):?(%d?%d?):?(%d?%d?)$")
  +
if month == "##" then
  +
month = 1
  +
result.fakeMonth = true
  +
end
  +
if day == "##" then
  +
day = 1
  +
result.fakeDay = true
  +
end
  +
  +
result.year = tonumber(year)
  +
result.month = tonumber(month)
  +
result.day = tonumber(day)
  +
result.hour = tonumber((hour ~= cstr.empty and hour) or 0)
  +
result.min = tonumber((min ~= cstr.empty and min) or 0)
  +
result.sec = tonumber((sec ~= cstr.empty and sec) or 0)
  +
result.timestamp = p.toTimestamp(result)
  +
  +
local tempDateObj = os.date("!*t", os.time(result))
  +
result.yday = tempDateObj.yday --year day included
  +
result.wday = p.correctWeekday(tempDateObj).wday --weekDay included
  +
result.week = p.getWeekNumber(result.timestamp)
  +
result.quarter = math.ceil(result.month / 3)
  +
result.dst = tempDateObj.isdst
  +
elseif type(datestamp) == types.number then
  +
result = os.date("!*t", datestamp)
  +
if(convertToTimeVersion) then
  +
result.day = result.day - 1 --we have to subtract it, not set it, as the datetime stamp can reach to days and months
  +
result.month = result.month - 1
  +
result.year = result.year - 1970
  +
end
  +
result = p.correctWeekday(result)
  +
result.timestamp = datestamp
  +
end
  +
  +
return result
 
end
 
end
  +
  +
--.wday is indexed 1 - 7, Sunday being 1 => https://www.lua.org/manual/5.3/manual.html#:~:text=wday%20(weekday%2C%201%E2%80%937%2C%20Sunday%20is%C2%A01)
  +
--%w starts 0 - 6, Sunday being 0 => https://www.lua.org/pil/22.1.html#:~:text=%25w,6%20%3D%20Sunday%2DSaturday%5D
  +
--following row synchronises the convention with '%w' and shifting it to 1 - 7 starting Monday, as for the rest of Wiki
  +
function p.correctWeekday(datetable)
  +
datetable.wday = (datetable.wday == 1 and 7) or datetable.wday - 1
  +
return datetable
  +
end
  +
  +
--ISO: https://en.wikipedia.org/wiki/ISO_week_date
  +
--If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in W01. If it is on a Friday, it is part of W53 of the previous year.
  +
--If it is on a Saturday, it is part of the last week of the previous year which is numbered W52 in a common year and W53 in a leap year.
  +
--If it is on a Sunday, it is part of W52 of the previous year.
  +
function p.getWeekNumber(dateStamp) --in milliseconds, os.time()
  +
firstWeekDayOfYear = tonumber(os.date("%w", os.time({year = os.date("%Y", dateStamp), month = 1, day = 1})))
  +
firstWeekOfYear = tonumber(os.date("%W", os.time({year = os.date("%Y", dateStamp), month = 1, day = 1})))
  +
  +
-- ATTENTION as per description above some years have first week 01
  +
-- because all first weeks of year that starts with Sunday, Saturday, Friday or Thursday must be shifted
  +
if firstWeekDayOfYear > 0 and firstWeekDayOfYear < 4 and firstWeekOfYear == 0 then
  +
return tonumber(os.date("%W", dateStamp)) + 1
  +
else
  +
return tonumber(os.date("%W", dateStamp))
  +
end
  +
end
  +
  +
function p.getDatePart(datestamp, part)
  +
local day, month, year = datestamp:match("^(..)%.(..)%.(....)$")
  +
if year == "####" then month = false end
  +
if month == "##" then month = false end
  +
if day == "##" then day = false end
  +
if part == "day" then return day
  +
elseif part == "month" then return month
  +
elseif part == "year" then return year
  +
end
  +
end
  +
  +
--------------------------------------------------------------------------------
   
 
function p.regularReplace(reggedString, regexTable)
 
function p.regularReplace(reggedString, regexTable)
local result = ""
+
local result = cstr.empty
local regexString = regex --"#pl%((%d)%)" -- looking and extracting number from "#pl(x)"
 
 
for key, value in pairs(regexTable) do
 
for key, value in pairs(regexTable) do
 
reggedString = reggedString:gsub(key, value)
 
reggedString = reggedString:gsub(key, value)
 
end
 
end
--for m in reggedString:gmatch(regex) do
 
-- mw.log(mw.dumpObject(m))
 
-- result = reggedString:gsub(regexString, "string")
 
-- mw.log(result)
 
--end
 
 
return reggedString
 
return reggedString
 
end
 
end
   
 
function p.getIcon(icon)
 
function p.getIcon(icon)
mw.log(icon)
+
return p.getIconObject(icon).iconFile
  +
end
if type(icon) == "table" then
 
  +
icon = icon.args[1] or mw.title.getCurrentTitle().text
 
  +
function p.getIconObject(icon)
end
 
  +
icon = p.resolveParameter(icon)
require("Module:Datatable/Icons")
 
  +
local icons = mw.loadData("Module:Datatable/Icons" .. p.lang()).icons --mw.loadData
  +
local pluralRegex = "^(.+)s$"
  +
local plural2Regex = "^(.+)es$"
  +
local plural3Regex = "^(.+)ies$"
  +
local plural = icon:match(pluralRegex)
  +
local plural2 = icon:match(plural2Regex) --Toolboxes => Toolbox
  +
local plural3 = icon:match(plural3Regex) --Bodies => Bod
  +
local result = getIconElement(icons, icon, true)
 
 
  +
if result then
mw.log(icon)
 
  +
return result
for _, element in ipairs(icons) do
 
  +
elseif plural and icons[plural] ~= nil then --Pallets => Pallet
if icon == element.icon then
 
  +
return getIconElement(icons, plural), 1
mw.log(element.iconFile)
 
  +
elseif plural2 and icons[plural2] ~= nil then --Toolboxes => Toolbox
return element.iconFile
 
  +
return getIconElement(icons, plural2), 2
end
 
  +
elseif plural3 and icons[plural3 .. "y"] ~= nil then --Bodies => Body
  +
return getIconElement(icons, plural3 .. "y"), 3
  +
else
  +
return getIconElement(icons, icon)
 
end
 
end
return icons[1].iconFile --Icon not found, then pick the first one which is supposed to be the Unknown one
 
 
end
 
end
   
function p.getIconTest(icon)
+
function getIconElement(icons, icon, returnNil)
  +
if type(icons[icon]) == types.table then
local iconR = ""
 
  +
local result = table.copy(icons[icon])
if type(icon) == "table" then
 
  +
result.name = icon
icon = icon.args[1] or mw.title.getCurrentTitle().text
 
  +
return result
  +
elseif returnNil then
  +
return nil
  +
elseif icons[icon] == nil then
  +
return icons["Unknown QuestionMark"] or icons[1]
  +
else
  +
return getIconElement(icons, icons[icon])
 
end
 
end
require("Module:Datatable/Icons")
 
 
for _, element in ipairs(icons) do
 
iconR = iconR .. "icon [" .. icon .. "] == [" .. element.icon .. "] element.icon<br>"
 
if icon == element.icon then
 
iconR = iconR .. "FOUND"
 
return iconR
 
end
 
end
 
iconR = iconR .. "NOT FOUND"
 
return iconR
 
 
end
 
end
   
Line 454: Line 1,142:
 
if character.id == id then return character end
 
if character.id == id then return character end
 
end
 
end
return "Character not found!"
 
 
end
 
end
   
  +
function p.getCharacter(object)
  +
local data = require("Module:Datatable" .. p.lang())
  +
  +
if object.killer or object.charType == 'K' then
  +
return p.getCharacterById(object.killer or object.character, data.killers)
  +
else
  +
return p.getCharacterById(object.survivor or object.character, data.survivors)
  +
end
  +
end
  +
  +
--function returns appropriate character name that should be compatible with IL.
  +
--IL itself is not executed to have option to customise the IL call
  +
function p.resolveCharacterIconLinkName(owner)
  +
return (owner and owner.multiName and owner.realName) or (not p.isKiller(owner) and owner.shortName) or owner.name or false
  +
end
  +
  +
function p.resolveCharacterPortraitFileName(character, maxId)
  +
local fileConst = "_charPreview_portrait"
  +
local isKiller = p.isKiller(character)
  +
local unknownChar = (isKiller and "UnknownKiller") or "UnknownSurvivor"
  +
local fileName
  +
  +
fileName = p.getFileNameFromTableById(character.id, (isKiller and killerImages) or survivorImages, "preview") --get custom name from table
  +
if fileName == cstr.empty or not p.isValidFileName(fileName) then --K/S{ID}_charPreview_portrait
  +
fileName = p.getCharacterIdentifier(character) .. fileConst
  +
end
  +
if (maxId and (character.id >= maxId - 1 and not p.isValidFileName(fileName)) or (not maxId and not p.isValidFileName(fileName))) then --File not Found; maxId - 1 to consider last two characters, not only the last one
  +
fileName = unknownChar .. fileConst
  +
end
  +
  +
--mw.log(fileName)
  +
return fileName
  +
end
  +
  +
function p.getCharacterIdentifier(character)
  +
return ((p.isKiller(character) and 'K') or 'S') .. string.format("%02d", character.id)
  +
end
  +
  +
function p.getFileNameFromTableById(id, fileTable, field)
  +
--mw.log(id)
  +
for j, sImage in ipairs(fileTable) do
  +
if sImage.id == id and sImage[field] ~= nil then
  +
return sImage[field]
  +
end
  +
end
  +
return cstr.empty
  +
end
  +
  +
--Not currently used
 
function p.resolveGender(abbr)
 
function p.resolveGender(abbr)
 
if abbr == 'M' then return strings.male
 
if abbr == 'M' then return strings.male
 
elseif abbr == 'F' then return strings.female
 
elseif abbr == 'F' then return strings.female
else return stirngs.undefined
+
elseif abbr == 'N' then return strings.nonhuman
  +
elseif abbr == 'M/F' then return strings.male .. comma .. strings.female
  +
elseif abbr == 'F/M' then return strings.female .. comma .. strings.male
  +
elseif abbr == 'TM' then return strings.transmale
  +
elseif abbr == 'TF' then return strings.transfemale
  +
else return strings.undefined
 
end
 
end
 
end
 
end
   
  +
--classes in form: "sortable, class2, class3 ,..."
  +
function p.wrapBasicTable(content, classes, styles, skipNtl)
  +
return '{| class = ' .. quotes('wikitable' .. ((classes and space .. p.getTableClasses(classes)) or cstr.empty)) .. ((styles and space .. 'style = ' .. quotes(styles)) or cstr.empty) .. nl ..
  +
(not skipNtl and ntl .. nl or cstr.empty) .. content ..'|}'
  +
end
  +
  +
function p.wrapTable(content, classes, styles, skipNtl)
  +
return (classes and '{| class = ' .. quotes((p.getTableClasses(classes))) or cstr.empty) .. ((styles and space .. 'style = ' .. quotes(styles)) or cstr.empty) .. nl ..
  +
(not skipNtl and ntl .. nl or cstr.empty) .. content ..'|}'
  +
end
  +
  +
function p.getTableClasses(classes)
  +
classes = p.resolveParameter(classes, 1)
  +
local unknownClass = "unknownClass"
  +
if not classes then return unknownClass end
  +
  +
if type(classes) == "string" then
  +
classes = classes:gsub(", ", space):gsub(",", space)
  +
else --todo type(classes) == "table"
  +
return unknownClass
  +
end
  +
return classes
  +
end
  +
  +
function p.getCharacterByName(name)
  +
name = name or p.resolveParameter(name)
  +
local str = require("Module:Datatable" .. p.lang())
  +
for _, surv in ipairs(survivors) do
  +
if surv.shortName == name or surv.name == name then
  +
local survivor = table.copy(surv)
  +
survivor.isKiller = false
  +
return survivor
  +
end
  +
end
  +
for _, killer in ipairs(killers) do
  +
if killer.shortName == name or killer.realName == name or the(killer) .. killer.name == name or killer.name == name then
  +
local klr = table.copy(killer)
  +
klr.isKiller = true
  +
return klr
  +
end
  +
end
  +
return nil
  +
end
  +
  +
--deprecated
  +
function p.getCharsByDlc(dlc, charType)
  +
local data = require("Module:Datatable" .. p.lang())
  +
local result = {}
  +
local listTable = (charType == 'S' and data.survivors) or (charType == nil and data.survivors) or data.killers --if the charType is not set then set the table by default to survivors
  +
  +
for _, character in ipairs(listTable) do
  +
if character.dlc == dlc.id then
  +
result[#result + 1] = character
  +
end
  +
end
  +
  +
if charType ~= nil then --if the charType is set, we need loop through only one table. Otherwise charType wasn't set in order to retrieve both types of characters from DLC
  +
return result
  +
end
  +
  +
for _, character in ipairs(data.killers) do --since the default table is survivor, we can hardcode killer table as a second table
  +
if character.dlc == dlc.id then
  +
result[#result + 1] = character
  +
end
  +
end
  +
return result
  +
end
  +
  +
function p.getCharacterFirstName(character)
  +
character = p.resolveParameter(character)
  +
if not character.name then
  +
character = p.getCharacterByName(character)
  +
end
  +
local regex = '([^ ]*) ?' --%a doesn't consider diacritics such as É (for instance: Élodie)
  +
local isKiller = p.isKiller(character)
  +
local text = (isKiller and character.name) or character.shortName or character.name --if the char is killer then use their name, otherwise it's survivor, thus use shortName or name
  +
if isKiller then return text end --if it's killer, then their nickname is already what it's supposed to return
  +
  +
if regex and text:gmatch(regex) then
  +
for m in text:gmatch(regex) do
  +
return m --return first occurence before potential space, otherwise return whole string
  +
end
  +
end
  +
return "Name not found"
  +
end
  +
  +
function p.replaceLastSpaceByNBSP(text) -- Non Breakable SPace
  +
local lastCharSpaceRegex = "^(.+) $"
  +
local regex = "^(.+) (.+)$" --should pick last space(?)
  +
text = text:gsub(lastCharSpaceRegex, "%1" .. nbspC):gsub(regex, "%1" .. nbspC .. "%2")
  +
return text
  +
end
  +
  +
function p.isCharacterKiller(character) return p.isKiller(character) end
  +
function p.isKiller(character)
  +
local character = p.resolveParameter(character)
  +
return character.radius ~= nil
  +
end
  +
  +
function p.getPossessiveName(character, params, fullName)
  +
local langs = require("Module:Languages")
  +
params = params or {link = p.bool(p.resolveParameter(character, "link", true)) or false}
  +
params.fullName = fullName or p.bool(p.resolveParameter(character, "fullName", true)) or false
  +
  +
if not character.name then
  +
character = p.getCharacterByName(p.resolveParameter(character))
  +
end
  +
if not character then return false end
  +
  +
params.firstName = p.getCharacterFirstName(character)
  +
  +
return langs.evaluatePossessive(character, params)
  +
end
  +
  +
--[[
  +
function p.getDativeName(character, params)
  +
params = params or {link = p.bool(p.resolveParameter(character, "link", true)) or false}
  +
if not character.name then
  +
character = p.getCharacterByName(p.resolveParameter(character))
  +
end
  +
if _language ~= "en" then
  +
return langs["dative_" .. _language](character, params) .. firstName
  +
end
  +
  +
return firstName
  +
end
  +
]]
  +
  +
function p.clr(color, text)
  +
text = text or p.resolveParameter(color, 2, true)
  +
color = p.resolveParameter(color)
  +
  +
return clr(color, text)
  +
--return frame:expandTemplate{title = "clr", args = {color, text}}
  +
end
  +
  +
  +
function p.biclr(color, text) return i(p.clr(color, text)) end
  +
function p.ibclr(color, text) return i(p.clr(color, text)) end
  +
  +
--Colour Ordered
  +
function p.clro(text, reset)
  +
reset = reset or p.bool(p.resolveParameter(text, 2, true)) or false
  +
text = p.resolveParameter(text, 1)
  +
  +
if not p.bool(frame:callParserFunction("#varexists:colorOrderCounter")) or reset then
  +
frame:callParserFunction("#vardefine:colorOrderCounter", 1 )
  +
end
  +
local order = tonumber(frame:callParserFunction("#var:colorOrderCounter")) % (#_colorOrder + 1) --making list infinite by going back
  +
order = (order == 0 and 1) or order --adjusting an index from order = #colorOrder => 0 + 1 = 1
  +
frame:callParserFunction("#vardefine:colorOrderCounter", order + 1 )
  +
  +
return ((text ~= nil and text ~= cstr.empty) and b(clr(_colorOrder[order], text))) or clr(_colorOrder[order], text)
  +
end
  +
  +
--Colour Ordered Reset
  +
function p.clror(text)
  +
return p.clro(text, true)
  +
end
  +
  +
function p.clroTest()
  +
local words = string.split(strings.clrTestString, space)
  +
local result = cstr.empty
  +
local clrOffset = 0
  +
for i, word in ipairs(words) do
  +
if #word <= 2 then clrOffset = clrOffset - 1 end
  +
local clrIndex = i + clrOffset
  +
local color = _colorOrder[clrIndex]
  +
result = result .. (#word > 2 and p.tooltip(((i == 1 and p.clror(word)) or p.clro(word)), "order: " .. clrIndex .. br .. "colour: " .. color, true, true) or word) .. (clrIndex < #_colorOrder and space or cstr.empty)
  +
if clrIndex == #_colorOrder then break end
  +
end
  +
return result .. ((#words > #_colorOrder and "...") or (#words < #_colorOrder and dot .. space .. brackets(strings.notEnoughColors)) or cstr.empty)
  +
end
  +
  +
function p.bclr(color, text)
  +
return b(p.clr(color, text))
  +
end
  +
  +
function clr(color, text)
  +
local result = 'inherit' --CSS value, don't change it
  +
--mw.log(tonumber(color, 10))
  +
color = tonumber(color, 10) or color:lower() --if the string is int then convert it to integer
  +
  +
if color == 1 or color == "brown" then result = "ab713c"
  +
elseif color == "1bg" then result = "5d4533"
  +
elseif color == 2 or color == "yellow" then result = "e8c252"
  +
elseif color == "2bg" then result = "d7ad2f"
  +
elseif color == 3 or color == "green" then result = "199b1e"
  +
elseif color == "3bg" then result = "0f791f"
  +
elseif color == 4 or color == "purple" then result = "ac3ee3"
  +
elseif color == "4bg" then result = "672d7f"
  +
elseif color == 5 or color == "pink" then result = "ff0955"
  +
elseif color == "5bg" then result = "cf0b45"
  +
elseif color == 6 or color == "orange" then result = "ff8800"
  +
elseif color == "6bg" then result = "ff5300"
  +
elseif color == 7 or color == "grey" then result = "808080"
  +
elseif color == 8 or color == "red" then result = "d41c1c"
  +
elseif color == 9 or color == "beige" then result = "e7cda2"
  +
elseif color == 10 or color == "blue" then result = "0e98ff"
  +
elseif color == 11 or color == "violet" then result = "b91a9b"
  +
elseif color == "11bg" then result = "800080"
  +
elseif color == 12 or color == "light blue" then result = "9bb0bf"
  +
elseif color == "12bg" then result = "9bb0bf"
  +
elseif color == 13 or color == "faded jade" then result = "418284"
  +
elseif color == 14 or color == "gold" then result = "ffa800"
  +
elseif color == "14bg" then result = "ffa800"
  +
elseif color == 15 or color == "fuchsia" then result = "ec0dea"
  +
elseif color == "15bg" then result = "b30ad2"
  +
elseif color == 16 or color == "white" then result = "ffffff"
  +
elseif color == 17 or color == "vomit green" then result = "8ad672"
  +
elseif color == 18 or color == "blood red" then result = "900a0a"
  +
elseif color == 19 or color == "bright red" then result = "ff0000"
  +
elseif color == 20 or color == "silver" then result = "b5afb0"
  +
elseif color == 21 or color == "turquoise" then result = "37d1c0"
  +
elseif color == 22 or color == "cinnamon" then result = "b74004"
  +
elseif color == 23 or color == "black" then result = "000000"
  +
elseif color == 24 or color == "wiki gold" then result = "b7a269"
  +
elseif color == 25 or color == "bronze" then result = "c2593a"
  +
elseif color == 26 or color == "chartreuse" then result = "b6fa36"
  +
elseif color == 27 or color == "screamin' green" then result = "63ef98"
  +
elseif color == 28 or color == "lavender" then result = "b57edc"
  +
elseif color == 29 or color == "aquamarine" then result = "7fffd4"
  +
elseif color == 30 or color == "ultramarine" then result = "120a8f"
  +
elseif color == 31 or color == "olive" then result = "808000"
  +
elseif color == 32 or color == "pale rose" then result = "f5bfd9"
  +
end
  +
  +
if text ~= nil and text ~= cstr.empty then
  +
result = '<span class="luaClr clr clr' .. color .. '" style="color: ' .. ((result ~= 'inherit' and '#') or cstr.empty) .. result .. ';">' .. text .. '</span>'
  +
end
  +
  +
return result
  +
end
  +
  +
--calls for Strings function Syntax Highlight
  +
function p.sh(value, lang, inline)
  +
inline = p.bool(inline or p.resolveParameter(value, 3, true))
  +
lang = lang or p.resolveParameter(value, 2, true)
  +
value = p.resolveParameter(value)
  +
return sh(value, lang, inline)
  +
end
  +
  +
--text = text containing a tooltip
  +
--tooltip = tooltip content
  +
--iconless = true/false whether the (i) icon should appear after the text containing tooltip (default false)
  +
--borderless = true/false whether underline should be shown (default false)
  +
function p.tooltip(text, tooltip, iconless, borderless, pcViewOnly)
  +
borderless = borderless or p.resolveParameter(text, 4, true) or p.resolveParameter(text, "borderless", true)
  +
iconless = p.bool(iconless or p.resolveParameter(text, 3, true) or (p.resolveParameter(text, "tooltip") and p.resolveParameter(text, 2, true)) or p.resolveParameter(text, "iconless"))
  +
tooltip = tooltip or p.resolveParameter(text, 2, true) or p.resolveParameter(text, "tooltip")
  +
text = p.resolveParameter(text)
  +
local containsLink = p.containsLink(tooltip)
  +
  +
return
  +
'<span class = "tooltip' .. ((containsLink and space .. 'linkIncluded') or cstr.empty) .. ((borderless and space .. 'borderless') or cstr.empty) .. '">' ..
  +
'<span class="tooltipBaseText' .. ((iconless and space .. 'iconless') or cstr.empty) .. '">' .. text .. '</span>'..
  +
(((tooltip ~= nil and tooltip ~= cstr.empty) and
  +
'<span class = "tooltiptext">' ..
  +
((not pcViewOnly and '<span class = "mobileView"> (</span>') or cstr.empty) .. --if it's mobile view then apply add brackets...
  +
'<span class="tooltipTextWrapper">' .. tooltip .. '</span>' ..
  +
((not pcViewOnly and '<span class = "mobileView">) </span>') or cstr.empty) ..
  +
'</span>') or cstr.empty) ..
  +
'</span>'
  +
end
  +
  +
function p.containsLink(text)
  +
local regex = '%[%[.-%]%]'
  +
local regexExternal = '%[.-%]'
  +
local rawRegex = '?%&%#91;%&%#91;.-%&%#93;%&%#93;'
  +
local rawRegexExternal = '%&%#91;.-%&%#93;'
  +
if string.match(text, regex) or string.match(text, rawRegex) or
  +
string.match(text, regexExternal) or string.match(text, rawRegexExternal)
  +
then --we want to return boolean, not the index
  +
return true
  +
else
  +
return false
  +
end
  +
end
  +
  +
function p.ptb(content, title, patch)
  +
return
  +
ptb(
  +
p.resolveParameter(content),
  +
title or p.resolveParameter(content, 2, true),
  +
patch or p.resolveParameter(content, 3, true)
  +
)
  +
end
  +
  +
function ptb(content, title, patch)
  +
title = title or p.resolveParameter(content, 2, true)
  +
patch = patch or p.resolveParameter(content, 3, true)
  +
  +
if not patch or p.isPatchLive(patch) then
  +
return content
  +
else
  +
return
  +
dynamicBox(content, title, "ptb")
  +
end
  +
end
  +
  +
function p.deletedBox(content, title, patch)
  +
return
  +
deletedBox(
  +
p.resolveParameter(content),
  +
title or p.resolveParameter(content, 2, true),
  +
patch or p.resolveParameter(content, 3, true)
  +
)
  +
end
  +
  +
function deletedBox(content, title, patch)
  +
local result = dynamicBox(content, title, "deletedBox")
  +
patch = (patch and string.trim(patch)) or nil
  +
  +
if patch and not p.isPatchLive(patch) then
  +
--result = includeonly(result) .. noinclude(category("DeleteBox Remove Lists", "Remove List " .. patch))
  +
result = result .. category("DeleteBox Remove Lists", "DeleteBox List for " .. patch)
  +
end
  +
  +
return result --no further logic so redirecting straight to dynamic box
  +
end
  +
  +
function p.wipBox(content, title, patch)
  +
return
  +
wipBox(
  +
p.resolveParameter(content),
  +
title or p.resolveParameter(content, 2, true),
  +
patch or p.resolveParameter(content, 3, true)
  +
)
  +
end
  +
  +
function wipBox(content, title, patch)
  +
local result = dynamicBox(content, title, "wipBox")
  +
return result --no further logic so redirecting straight to dynamic box
  +
end
  +
  +
function dynamicBox(content, title, boxType)
  +
title = title or p.resolveParameter(content, 2, true)
  +
boxType = boxType or p.resolveParameter(content, 3, true)
  +
  +
if not boxType then
  +
return strings.boxTypeNotFound
  +
end
  +
  +
return
  +
'<div class = "dynamicBox ' .. boxType .. '">' ..
  +
((title and '<div class = "dynamicTitle ' .. boxType .. '">' .. title .. '</div>' ) or cstr.empty) ..
  +
'<div class = "dynamicContent ' .. boxType .. '">' .. content .. '</div>' ..
  +
'</div>'
  +
end
  +
  +
function p.isPatchLive(patchParam)
  +
local patch = p.getPatch(patchParam)
  +
  +
--dLog(((patchParam and "[" .. patchParam .. ((not patch and " - Not Found") or cstr.empty) .. "]") or cstr.empty) .. " isPatchLive: " .. (patch and (p.IsFullDateTime(patch.rDate) and tostring(p.dateHasPassed(p.GetDatetime(patch.rDate)))) or tostring(false)))
  +
return patch and (p.IsFullDateTime(patch.rDate) and p.dateHasPassed(p.GetDatetime(patch.rDate))) or false
  +
end
  +
  +
function p.isCharacterLive(character)
  +
if character.dlc then
  +
local dlcM = require("Module:DLCs")
  +
local dlc = dlcM.getCharacterMainDlc(character)
  +
return (dlc == nil) or p.IsFullDateTime(dlc.rDate) and p.dateHasPassed(dlc.rDate)
  +
end
  +
return false
  +
end
  +
  +
function p.getPatch(patch)
  +
local data = require("Module:Datatable" .. p.lang())
  +
  +
for _, patchObj in ipairs(data.patches) do
  +
if patchObj.patch == patch then return patchObj end
  +
end
  +
end
  +
  +
function p.getLatestPatch()
  +
return require("Module:Datatable" .. p.lang()).latestPatch.patch
  +
end
  +
  +
--timestamp = {year = 2022, month = 7, day = 19} --example format
  +
function p.dateHasPassed(timestamp)
  +
local currentDate = os.date("*t") --gives current timestamp: {year = 1998, month = 9, day = 16, yday = 259, wday = 4, hour = 23, min = 48, sec = 10, isdst = false}
  +
local releaseOffset = 60 * 60 * 15 --release hour 15:00 (server time is -2 hours from my current time)
  +
timestamp = p.standardiseDateObject(timestamp)
  +
  +
--mw.log(os.difftime(os.time(timestamp) + releaseOffset, os.time(currentDate)))
  +
return os.difftime(os.time(timestamp) + releaseOffset, os.time(currentDate)) < 0 -- subtracting releaseOffset to adjust timing according to actual release hour
  +
end
  +
  +
function p.isEventLive(element)
  +
--not element.event is to skip events from different tables, such as tomes
  +
return not (element.event or element.patch) and (element.rDate and p.dateHasPassed(element.rDate)) and (element.eDate == nil or not p.dateHasPassed(element.eDate))
  +
or (element.patch and element.rDate and p.isFullDateTime(element.rDate) and p.dateHasPassed(element.rDate) and (element.eDate == nil or not p.dateHasPassed(element.eDate)))
  +
end
  +
  +
function p.standardiseDateObject(dateObj)
  +
if type(dateObj) == types.table then --if we pass table, we need to check time values and populate them to 0 by default
  +
if not dateObj.hour then dateObj.hour = 0 end
  +
if not dateObj.min then dateObj.min = 0 end
  +
if not dateObj.sec then dateObj.sec = 0 end
  +
  +
dateObj = os.time(dateObj) --we convert it to seconds constant (resp. variable)
  +
end
  +
  +
dateObj = p.GetDatetime(dateObj)
  +
return dateObj
  +
end
  +
  +
function p.resolvePatchList()
  +
local data = require("Module:Datatable" .. p.lang())
  +
local result = cstr.empty
  +
local yearCounter = 0
  +
local tempResult = cstr.empty
  +
  +
for _, patch in ipairs(data.patches) do
  +
local patchDate = p.standardiseDateObject(patch.rDate)
  +
local suffixLink = getSuffixLink(patch)
  +
local suffixPatchName = getSuffixPatchName(patch) --in case of PTB we need to attach " (PTB)" suffix
  +
  +
if yearCounter ~= patchDate.year then
  +
if yearCounter ~= 0 then
  +
result = result .. frame:callParserFunction{ name = '#tag', args = {"tabber", tempResult} } .. nl
  +
end
  +
tempResult = cstr.empty
  +
yearCounter = patchDate.year
  +
result = result ..
  +
"|-|" .. patchDate.year .. "=" .. nl
  +
end
  +
  +
local patchNotes, result = pcall(function () return frame:expandTemplate{title = strings.patch .. space .. patch.patch .. suffixPatchName} end)
  +
if patchNotes and --[[not patch.ptb and]] patch ~= patches[#patches] then
  +
local patchVersions = string.split(patch.patch, '.')
  +
if patchVersions[3] ~= "0" then patchVersions[3] = "0" end
  +
for i = #patchVersions, 4 , -1 do --in case that patch contains anything else after last patch number let's remove it (example: "6.1.2 / 6.1.3")
  +
table.remove(patchVersions)
  +
end
  +
local patchLink = strings.patch .. space .. patch.patch .. suffixPatchName
  +
local patchLinkModified = strings.patch .. space .. table.concat(patchVersions, ".")
  +
  +
tempResult = tempResult ..
  +
frame:expandTemplate{title = "!"}.. "-" .. frame:expandTemplate{title = "!"} .. patch.patch .. suffixPatchName .. "=" .. nl .. --creating individual patch tabs
  +
  +
frame:expandTemplate{title = "(!"} .. space .. 'class = "wikitable"' .. nl ..
  +
"!" .. nl .. "==== " .. link(patchLinkModified .. ((suffixLink and suffixLink .. tl .. patchLink) or cstr.empty)) .. " - " .. p.toDate(os.time(patchDate)) .. " ====" .. nl .. --28 April 2022
  +
frame:expandTemplate{title = "!-"} .. nl ..
  +
frame:expandTemplate{title = "!"} .. result .. nl ..
  +
frame:expandTemplate{title = "!)"} .. nl
  +
end
  +
  +
end
  +
result = result .. frame:callParserFunction{ name = "#tag", args = {"tabber", tempResult} } .. nl --post loop aiditon for last year
  +
  +
result = frame:extensionTag{name = 'tabber',content = result}
  +
  +
return result
  +
end
  +
  +
function getSuffixPatchName(patch)
  +
if patch.ptb then
  +
return " (PTB)"
  +
end
  +
return cstr.empty
  +
end
  +
  +
function getSuffixLink(patch)
  +
local versions = string.split(patch.patch, '.')
  +
if versions[3] ~= "0" then
  +
return "#Hotfix " .. patch.patch
  +
elseif patch.ptb then
  +
return "#Public Test Build " .. patch.patch --patch needs to not contain "(PTB)" part
  +
end
  +
return nil
  +
end
  +
  +
function p.getPatchLink(patch)
  +
local suffixLink = getSuffixLink(patch)
  +
local suffixPatchName = getSuffixPatchName(patch) --in case of PTB we need to attach " (PTB)" suffix
  +
local patchLink = strings.patch .. space .. patch.patch .. suffixPatchName
  +
local patchVersions = string.split(patch.patch, '.')
  +
if patchVersions[3] ~= "0" then patchVersions[3] = "0" end
  +
local patchLinkModified = strings.patch .. space .. table.concat(patchVersions, ".")
  +
  +
return link(patchLinkModified .. ((suffixLink and suffixLink .. tl .. patchLink) or cstr.empty))
  +
end
  +
  +
--wt = wordTable
  +
function p.getWord(wt, index)
  +
local keys = table.keys(wt)
  +
local maxValue = table.max(keys)
  +
local minValue = table.min(keys)
  +
if index > maxValue then return wt[maxValue] end --if the index is higher than highest amount, return word of highest amount available
  +
if wt[index] then return wt[index] end -- if the index is exactly of plural count then return exact word on index
  +
if index < minValue then return wt[minValue] end --if index not found, it means it's lower than lowest amount in a word table
  +
  +
for i, keyValue in ipairs(keys) do
  +
if index < keyValue then
  +
return wt[keys[i - 1]] --if the index is between two amounts(indexes) use the lower one
  +
end
  +
end
  +
end
  +
  +
--{{header = 2022, content = "..."}, {}, ...}
  +
function p.getTabberFromTable(tabberTable)
  +
local result = cstr.empty
  +
for _, section in ipairs(tabberTable) do
  +
result = result .. nl .. '|-|' .. (section.header or strings.tabberHeaderNotFound) .. '=' .. nl .. section.content .. nl
  +
end
  +
  +
result = frame:callParserFunction{ name = '#tag', args = {"tabber", result} }
  +
  +
return result
  +
end
  +
  +
function p.isTagPresent(el, searchedTag)
  +
if el.tags and type(el.tags) == types.table then
  +
for _, tag in ipairs(el.tags) do
  +
if string.lower(searchedTag) == string.lower(tag) then
  +
return true
  +
end
  +
end
  +
end
  +
return false
  +
end
  +
  +
function p.yesNo(param, note, imgSize)
  +
imgSize = imgSize or p.resolveParameter(param, 3, true) or p.resolveParameter(param, "size", true)
  +
note = note or p.resolveParameter(param, 2, true) or p.resolveParameter(param, "note", true)
  +
param = p.resolveParameter(param, 1, true) or param
  +
local imageFilename = "Unknown QuestionMark.png"
  +
local tickClass = "unknown"
  +
local size = (imgSize or 32) .. "px"
  +
if p.bool(param) == true then
  +
imageFilename = "DBD_UI_Icon_CheckMark.png"
  +
tickClass = "tickYes"
  +
elseif p.bool(param) == false then
  +
imageFilename = "DBD_UI_Icon_X.png"
  +
tickClass = "tickNo"
  +
elseif param:lower() == "nan" then
  +
imageFilename = "NaN.png"
  +
tickClass = "tickNaN"
  +
end
  +
  +
--lg(span(p.tooltip(span(file(imageFilename, size), tickClass),cstr.empty,true,true), "yesNo"))
  +
return
  +
span(
  +
p.tooltip(
  +
span(file(imageFilename, size), tickClass),
  +
note or cstr.empty,
  +
true,
  +
true
  +
)
  +
, "yesNo")
  +
end
  +
  +
--imgType:
  +
-- perk = perk with switching background
  +
-- sosPerk = teachable frame background
  +
--icon icon that is inside
  +
--imgSize currently not used, should be available for manually adjusting the size of image
  +
function p.assembleImage(imgType, icon, imgSize, params)
  +
local variousLogic = require("Module:Various")
  +
  +
params = params or p.resolveParameter(imgType, 4, true) --not sure if table can be passed directly from wiki
  +
local rarity = p.resolveParameter(imgType, "rarity", true)
  +
local isIconLink = p.resolveParameter(imgType, "iconLink", true) or p.resolveParameter(imgType, "il", true) or (params and params.iconLink == true) or false
  +
local rarityNumber = tonumber(rarity) or (params and params.rarity and tonumber(params.rarity))
  +
rarity = string.replace(((rarityNumber and (variousLogic.rarity[rarityNumber].techName or variousLogic.rarity[rarityNumber].name)) or (rarity or (params and params.rarity)) or variousLogic.rarity[1].techName or variousLogic.rarity[1].name):lower(), space, dash) --Common by default
  +
rarity = string.replace(rarity, "&", "and")
  +
  +
imgSize = imgSize or p.resolveParameter(imgType, 3, true) or p.resolveParameter(imgType, "size", true)
  +
if type(imgSize) == types.string then return "Error: " .. imgSize end
  +
icon = icon or p.resolveParameter(imgType, 2, true) or p.resolveParameter(imgType, "icon", true)
  +
imgType = p.resolveParameter(imgType, "type", true) or p.resolveParameter(imgType)
  +
  +
local imgSizeStyle = (imgSize and ' style = "width: ' .. imgSize .. 'px; height: ' .. imgSize .. 'px;"') or cstr.empty
  +
local align = (params and params.align and "float: " .. params.align .. semi) or cstr.empty
  +
  +
if imgType == "sosPerk" then
  +
return
  +
'<div class = "game-element-container inline-flex relative">' ..
  +
'<div class = "game-element-bg-settings game-element-bg-settings-size-sos teachable-perk-element"' .. imgSizeStyle .. '></div>' ..
  +
'<div class = "game-element-bg-settings-size-sos game-element-imgObject absolute"' .. imgSizeStyle .. '>' .. file(p.getIcon(icon), "link=" .. icon) .. '</div>' ..
  +
'<div class = "game-element-bg-settings game-element-bg-settings-size-sos teachable-indicator-perk-element absolute"' .. imgSizeStyle .. '></div>' ..
  +
'</div>'
  +
elseif imgType == "perk" then
  +
return
  +
'<div class = "game-element-container flex relative">' ..
  +
'<div class = "game-element-bg-settings game-element-bg-settings-size-perk animate-perk-settings animate-perk-bg"' .. imgSizeStyle .. '></div> ' ..
  +
'<div class = "game-element-bg-settings game-element-bg-settings-size-perk animate-perk-settings animate-perk-level absolute"' .. imgSizeStyle .. '></div>' ..
  +
'<div class = "game-element-bg-settings-size-sos game-element-imgObject absolute"' .. imgSizeStyle .. '>' .. file(p.getIcon(icon), "link=" .. icon, "center") .. '</div>' ..
  +
'</div>'
  +
elseif table.contains({"addon", "item", "power", "offering"}, imgType) then
  +
--rarity = (rarity and variousLogic.rarity[rarity].name:lower()) or string.replace((params and (params.rarity and (tonumber(params.rarity) and variousLogic.rarity[params.rarity].name) or params.rarity) or variousLogic.rarity[1].name):lower(), space, dash)
  +
addonContainer = (imgType == "addon" and "addon-container") or cstr.empty
  +
imgSize = imgSize or 128
  +
if isIconLink then
  +
local ilOffset = (imgSize / 2)
  +
local addonMarkerScale = 50 * (imgSize / 128)
  +
return
  +
'<span style="display:inline-flex; position:relative; height: ' .. ilOffset .. 'px; width: ' .. imgSize .. 'px">' ..
  +
'<span class = "margin-auto" style = "display: inline-flex; position: absolute; top: -6px; max-width: ' .. imgSize .. 'px;' .. align ..'">' ..
  +
'<span class = "game-element-container ' .. addonContainer .. ' flex relative">' ..
  +
'<span class = "game-element-bg-settings game-element-bg-settings-size-perk ' .. rarity .. '-' .. (table.contains({"item", "offering"}, imgType) and imgType or "item") .. '-element" ' .. imgSizeStyle .. '></span>' ..
  +
((imgType == "addon" and '<span class = "game-element-bg-settings game-element-bg-settings-size-perk addon-marker addon-marker-il absolute" style = "scale: ' .. addonMarkerScale .. '%;"></span>') or cstr.empty) ..
  +
'<span class = "game-element-bg-settings-size-sos absolute" ' .. imgSizeStyle .. '>' .. file(p.getIcon(icon), "link=" .. icon, "300px") .. '</span>' ..
  +
'</span>' ..
  +
'</span>' ..
  +
'</span>'
  +
else
  +
return
  +
'<div class = "margin-auto" style = "max-width: ' .. imgSize .. 'px; scale: calc(' .. imgSize .. '/128); ' .. align ..'">' ..
  +
'<div class = "game-element-container ' .. addonContainer .. ' flex relative">' ..
  +
'<div class = "game-element-bg-settings game-element-bg-settings-size-perk ' .. rarity .. '-' .. (table.contains({"item", "offering"}, imgType) and imgType or "item") .. '-element"></div>' ..
  +
((imgType == "addon" and '<div class = "game-element-bg-settings game-element-bg-settings-size-perk addon-marker absolute"></div>') or cstr.empty) ..
  +
'<div class = "game-element-bg-settings-size-sos absolute">' .. file(p.getIcon(icon), "link=" .. icon, "300px") .. '</div>' ..
  +
'</div>' ..
  +
'</div>'
  +
end
  +
end
  +
end
  +
  +
function p.resolvePageHeader(params)
  +
return nil
  +
end
   
 
return p
 
return p

Latest revision as of 05:32, 17 April 2024

Template-info Documentation
Colour test
An extremely (order: 1
colour: 2
)
long (order: 2
colour: 6
)
string (order: 3
colour: 8
)
that (order: 4
colour: 4
)
serves (order: 5
colour: 3
)
as a test (order: 6
colour: 10
)
string (order: 7
colour: 11
)
for (order: 8
colour: 12
)
automatic (order: 9
colour: 14
)
and (order: 10
colour: 15
)
ordered (order: 11
colour: 21
)
coloured (order: 12
colour: 22
)
text (order: 13
colour: 25
)
that (order: 14
colour: 26
)
colour-highlights (order: 15
colour: 9
)
important (order: 16
colour: 27
)
values (order: 17
colour: 28
)
on our (order: 18
colour: 29
)
beautiful (order: 19
colour: 13
)
Dead (order: 20
colour: 31
)
by Daylight (order: 21
colour: 32
)
...


Expand to view content
Template-info Documentation

Colour Table

Number Hex Value Colour Rarities Ranks/Grades Status HUD Texts Cosmetic Background Colour
1 ab713c Brown Common Rarity Ranks 20-17 #5d4533
2 e8c252 Yellow Uncommon Rarity Ranks 16-13 Status HUD Buffs Aura Colour / Invigorated / General Buffs #d7ad2f
3 199b1e Green Rare Rarity Ranks 12-9 #0f791f
4 ac3ee3 Purple Very Rare Rarity Ranks 8-5 General Reworks #672d7f
5 ff0955 Pink Ultra Rare Rarity Unlockables Removed From The Bloodweb #cf0b45
6 ff8800 Orange Teachable Perks Rarity / Teachable Levels Aura Colour / Quality of Life Change #ff5300
7 808080 Grey Grades Ash IV-I Miscellaneous
8 d41c1c Red Status HUD/Perk Debuffs Aura Colour / General Nerfs
9 e7cda2 Beige Flavour Texts / Aura Colour
10 0e98ff Blue Spiritual Rarity (unused) Aura Colour / Stacking Corrections
11 b91a9b Violet Teachable Perk explanation #800080
12 9bb0bf Light Blue Legendary Rarity Teachable Perk Shrine #9bb0bf
13 418284 Faded Jade Perk Buffs Retired/Decommissioned Unlockables
14 ffa800 Gold Event Rarity Grades Gold IV-I Golden Toolbox #ffa800
15 ec0dea Fuchsia Artefact Rarity Intoxicated #b30ad2
16 ffffff White Aura Colour
17 8ad672 Vomit Green Vile Purge
18 900a0a Blood Red Corrupt Purge
19 ff0000 Bright Red Ranks 4-1/Grades Iridescent IV-I Aura Colour/Miscellaneous
20 b5afb0 Silver Grades Silver IV-I Unused Unlockables / Miscellaneous
21 37d1c0 Turquoise Miscellaneous
22 b74004 Cinnamon Miscellaneous
23 000000 Black Miscellaneous
24 b7a269 Wiki Gold Miscellaneous
25 c2593a Bronze Limited Items Grades Bronze IV-I Aura Colour
26 b6fa36 Chartreuse Miscellaneous
27 63ef98 Screamin' Green Miscellaneous
28 b57edc Lavender Miscellaneous
29 7fffd4 Aquamarine Miscellaneous
30 120a8f Ultramarine Miscellaneous
31 808000 Olive Miscellaneous
32 f5bfd9 Pale Rose Miscellaneous

Colouring Order

In case of highlighting values and modifier values within a section, there is a specific order for the colours used to do so, which originates from the original Perk descriptions and is represented in the first four colours:

Highlight Colour
1 2
2 6
3 8
4 4
5 3
6 10
7 11
8 12
9 14
10 16
11 21
12 22
13 25
14 9
15 26
16 28
17 27

local p = {}
local mathOps = require("Module:MathOps")
local str = require("Module:Strings")
local frame = mw.getCurrentFrame()
local _brigtnessTreshold = 0.36
local _isValidFileNameCounter = 0
local _colorOrder = {2, 6, 8, 4, 3, 10, 11, 12, 14, 15, 21, 22, 25, 26, 9, 27, 28, 29, 13, 31, 32}

local strings = {
	--genders, not currently used
	male = "Male",
	female = "Female",
	transmale = "Trans Male",
	transfemale = "Trans Female",
	nonhuman = "Not applicable (not human)",
	undefined = "Undefined",
	
	--charNotFound = "Character not found!",
	patch = "Patch",
	tabberHeaderNotFound = "Header not found",
	category = "Category", --used for [[Category:...]]
	boxTypeNotFound = b("Dynamic box is missing type!"),
	notSpecified = "notSpecified", --used in IconLink for assemble machine function
	clrTestString = "An extremely long string that serves as a test string for automatic and ordered coloured text that colour-highlights important values on our beautiful Dead by Daylight wiki. For this testing purposes we consider only 3-characters or longer words",
	notEnoughColors = "no more colours in order",
}
p.strings = strings

local months = {
	January = "January",
	February = "February",
	March = "March",
	April = "April",
	May = "May",
	June = "June",
	July = "July",
	August = "August",
	September = "September",
	October = "October",
	November = "November",
	December = "December"
}

local days = {
	Monday = "Monday",
	Tuesday = "Tuesday",
	Wednesday = "Wednesday",
	Thursday = "Thursday",
	Friday = "Friday",
	Saturday = "Saturday",
	Sunday = "Sunday"
}
----------------------------------------------

p.timeFormat1 = "%d.%m.%Y" -- 5.9.2022
p.timeContants = {
	second = 1,
	minute = 60, --in seconds
	hour = 3600,
	day = 86400,
	week = 604800
}
local timeContants = p.timeContants

--Bridge to Language Module, as more appropriate place to have this function
function p.lang()
	return require("Module:Languages").lang()
end

--If params will be passed {...} then index should equals 0, so this list will be passeb back: ((index == 0 and params.args) or params)
function p.resolveParameter(param, index, returnCanBeNil)
	local retArg
	if type(param) == "table" then retArg = ((index == 0 and param.args) or (param.args and param.args[(index or 1)] and p.replaceSpecialCharacters(param.args[(index or 1)]))) or nil end --if parameter is passed from wiki, not other function
	if retArg == nil and type(param) == "table" and param.args ~= nil and next(param.args) == nil and not returnCanBeNil then retArg = getParamOrPageName() end --param.args ~= nil and next(param.args): this means that params.args is not nil but empty table
	if retArg == nil and not returnCanBeNil then retArg = getParamOrPageName(param, index, false) end --if parameter was passed directly or not at all
	if retArg ~= nil and type(param) == "string" then retArg = p.replaceSpecialCharacters(retArg) end --final processing
	return retArg
end

function getParamOrPageName(param, index, returnCanBeNil)
	if param and 
		(type(param) ~= types.table or 
		(type(param) == types.table and not param.args)) 
	then 
		return param
	else
		local pageName = pageName or mw.title.getCurrentTitle().text
		local pageParts = string.split(pageName, '/') -- remove page suffixes such as language codes
		if #pageParts > 1 then
			_unofficialLang = pageParts[#pageParts]
			return pageParts[#pageParts - 1] --take second to last part of page name
		end
		return pageParts[1]  --if there is no other part than the name itself
	end
end

--translates string containing #1# #2# ... to individual strings (resp. words) contained in dyntable
function p.getDynamicString(dynTable, templateString)
	--dynTable = {"Testing Chapter", nil, "DLC Str World", nil, 999};
	--templateString = "Retracted #1# #2# #3# #4# #5# #6# #7# #8#"
	local skipList = {skip = {}, spans = {}}
	local spanRegex = "(<span .-</span>)" --mostly clr case
	local spanSkipRegex = "&&span" --it needs to use a DIFFERENT symbol than hashtag (#) as the style of spans can contain one due to colorhex colour value
	local skipRegex = "#(.-)#" --skip anything inside hashtags
	if type(dynTable) == types.string then --is parameter is string then just split it
		dynTable = string.split(dynTable)
	elseif type(dynTable) == types.table then
		local tmp = {}
		local skipCounter = 1
		local spanCounter = 1
		for i, el in pairs(dynTable) do
			if el:find(spanRegex) then --if you find any spans to skip then ...
				for m in el:gmatch(spanRegex) do --let's find spans (and pull them out) to skip them with splitting by "space"
					table.insert(skipList.spans, m)
					el = el:gsub(m, spanSkipRegex .. spanCounter)
					spanCounter = spanCounter + 1
				end
			end
			
			--finding any strings DOES NOT need to be first as span regex WILL NOT create elements matching this regex
			if el:find(skipRegex) then --if you find any string to skip then ...
				for m in el:gmatch(skipRegex) do
					table.insert(skipList.skip, m)
					el = el:gsub(skip(m:gsub('%W','%%%1')), "#skip" .. skipCounter) --We have to escape special characters as the string is being used as a pattern and for an instance "[[ ]]" are magic chars for LUA patterns
					skipCounter = skipCounter + 1
				end
			end

			table.insert(tmp, string.split(tostring(el))) --if parameter is table, split all strings inside
		end
		dynTable = table.flatten(tmp) --format from table.insert is {{"splitted", "string"}, {"splitted", "another", "string"}}, so it's needed to be flattened
		--mw.log(mw.dumpObject(dynTable))
	end
	
	if templateString then
		for m in templateString:gmatch("#(%d+)#") do --replace all #1# occurences
			local currentRegexString = "#" .. tonumber(m) .. "#"
			if tonumber(m) <= #dynTable then
				templateString = templateString:gsub(currentRegexString, dynTable[tonumber(m)])
			else
				templateString = templateString:gsub(currentRegexString, cstr.empty) --remove all remaining marks
			end
		end
	else
		templateString = table.join(dynTable, space)
	end
	
	templateString = string.trim(templateString)
	
	--lg(templateString)
	for m in templateString:gmatch("#skip(%d)") do --now we put spans back into string
		local currentRegexString = "#skip" .. tonumber(m)
		templateString = templateString:gsub(currentRegexString, skipList.skip[tonumber(m)])
	end
	
	for m in templateString:gmatch(spanSkipRegex .. "(%d)") do --now we put spans back into string
		local currentRegexString = spanSkipRegex .. tonumber(m)
		templateString = templateString:gsub(currentRegexString, skipList.spans[tonumber(m)])
	end
	
	--mw.log(templateString)
	return templateString
end
--------------------------------------------------------------------------------

function p.getCount(subject)
	local list
	subject = p.resolveParameter(subject)
	--if you have another list just add it into a list then call appropriate function
	if		subject == "map"		then return p.getMapsCount()
	elseif	subject == "realm"		then list = realms
	elseif	subject == "killer"		then list = killers
	elseif	subject == "survivor"	then list = survivors
	elseif	subject == "dlc"		then list = dlcs
	elseif	subject == "chapter"	then return p.getCountDlcType(1)
	elseif	subject == "paragraph"	then return p.getCountDlcType(2)
	elseif	subject == "clothing"	then return p.getCountDlcType(3)
	elseif	subject == "ost"		then return p.getCountDlcType(4)
	elseif	subject == "character"	then return p.getCountDlcType(5)
	elseif	subject == "ccy"		then return getCountCCY(true)
	elseif	subject == "ccy-gc"		then return getCountCCY(true) --redundant option to keep convention
	elseif	subject == "ccy-rc"		then return getCountCCY(false)
	elseif	subject == "killerPerk"	then return getPerksCount('K')
	elseif	subject == "survPerk"	then return getPerksCount('S')
	else return 0
	end
	
	local x = 0
	for _, item in ipairs(list) do if item.skip or item.retired then x = x + 1 end end
	
	return #list - x
end

function p.getMapsCount()
	local data = require("Module:Datatable")
	local result = 0
	
	for _, map in ipairs(data.maps) do
		if not (map.skip or map.retired) then
			if map.variations then
				for _, var in ipairs(map.variations) do
					if not (map.skip or map.retired) then
						result = result + 1
					end
				end
			else
				result = result + 1
			end
		end
	end
	
	return result
end

function p.getCountDlcType(type)
	local count = 0
	
	for _, dlc in ipairs(dlcs) do
		if dlc.category == type and not dlc.skip then count = count + 1 end
	end
	
	return count
end

function getPerksCount(charType)
	local perks = mw.loadData("Module:Datatable/Loadout" .. p.lang()).perks
	local count = 0
	
	for _, perk in ipairs(perks) do
		if perk.charType == charType and not perk.unused then count = count + 1 end
	end

	return count
end

function getCountCCY(gc)
	local list = ccy

	local i = 0
	while list[i + 1] and list[i + 1].gc == gc do i = i + 1 end

	return i
end

function isRealCcy(ccyId)
	local data = require("Module:Datatable" .. p.lang())
	for _, currency in ipairs(data.ccy) do
		if currency.id == ccyId then return not currency.gc end
	end
	return false
end

function p.getCcyById(id)
	for _, currCcy in ipairs(ccy) do
		if currCcy.id == id then return currCcy end
	end
	return nil
end

function p.getMaxId(tab)
	result = 0
	for _, item in ipairs(tab) do
		result = (item.id > result and item.id) or result
	end
	return result
end

-- Function allowing for consistent treatment of boolean-like wikitext input.
-- It works similarly to the template {{yesno}}.
function p.bool(val, default)
	-- If your wiki uses non-ascii characters for any of "yes", "no", etc., you
	-- should replace "val:lower()" with "mw.ustring.lower(val)" in the
	-- following line.
	val = type(val) == 'string' and val:lower() or val
	if val == nil then
		return nil
	elseif val == true 
		or val == 'yes'
		or val == 'y'
		or val == 'true'
		or val == 't'
		or val == 'on'
		or tonumber(val) == 1
	then
		return true
	elseif val == false
		or val == 'no'
		or val == 'n'
		or val == 'false'
		or val == 'f'
		or val == 'off'
		or tonumber(val) == 0
	then
		return false
	else
		return default
	end
end

--Example usage:
--amount = 1333444.1
--print(format_num(amount,2))
--print(format_num(amount,-2,"US$"))
--amount = -22333444.5634
--print(format_num(amount,2,"$"))
--print(format_num(amount,2,"$","()"))
--print(format_num(amount,3,"$","NEG "))

--Output:
--1,333,444.10
--US$1,333,400
---$22,333,444.56
--($22,333,444.56)
--NEG $22,333,444.563

function p.formatNum(amount, decimal, prefix, neg_prefix)
	return p.format_num(amount, decimal, prefix, neg_prefix)
end
function p.format_num(amount, decimal, prefix, neg_prefix)
  local str_amount,  formatted, famount, remain

  decimal = decimal or 2  -- default 2 decimal places
  neg_prefix = neg_prefix or "-" -- default negative sign

  famount = math.abs(mathOps.round(amount,decimal))
  famount = math.floor(famount)

  remain = mathOps.round(math.abs(amount) - famount, decimal)

        -- comma to separate the thousands
  formatted = p.commaFormat(famount)

        -- attach the decimal portion
  if (decimal > 0) then
    remain = string.sub(tostring(remain),3)
    formatted = formatted .. "." .. remain ..
                string.rep("0", decimal - string.len(remain))
  end

        -- attach prefix string e.g '$' 
  formatted = (prefix or "") .. formatted 

        -- if value is negative then format accordingly
  if (amount<0) then
    if (neg_prefix=="()") then
      formatted = "("..formatted ..")"
    else
      formatted = neg_prefix .. formatted 
    end
  end

  return formatted
end

function p.commaFormat(amount)
  local formatted = amount
  while true do  
    formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
    if (k==0) then
      break
    end
  end
  return formatted
end

--Converting Arabic numbers to Roman
function p.toRomanNumerals(s)
    local numbers = { 1, 5, 10, 50, 100, 500, 1000 }
	local chars = { "I", "V", "X", "L", "C", "D", "M" }

    s = tonumber(s)
    if not s or s ~= s then return false end
    if s == math.huge then return false end
    s = math.floor(s)
    if s <= 0 then return s end
	local ret = cstr.empty
        for i = #numbers, 1, -1 do
        local num = numbers[i]
        while s - num >= 0 and s > 0 do
            ret = ret .. chars[i]
            s = s - num
        end
        for j = 1, i - 1 do
            local n2 = numbers[j]
            if s - (num - n2) >= 0 and s < num and s > 0 and num - n2 ~= n2 then
                ret = ret .. chars[j] .. chars[i]
                s = s - (num - n2)
                break
            end
        end
    end
    return ret
end

--Converting Roman numbers to Arabic
function ToNumeral(roman)
    local Num = { ["M"] = 1000, ["D"] = 500, ["C"] = 100, ["L"] = 50, ["X"] = 10, ["V"] = 5, ["I"] = 1 }
    local numeral = 0    
 
    local i = 1
    local strlen = string.len(roman)
    while i < strlen do
        local z1, z2 = Num[ string.sub(roman,i,i) ], Num[ string.sub(roman,i+1,i+1) ]
        if z1 < z2 then
            numeral = numeral + ( z2 - z1 )
            i = i + 2
        else
            numeral = numeral + z1
            i = i + 1    
        end        
    end
 
    if i <= strlen then numeral = numeral + Num[ string.sub(roman,i,i) ] end
 
    return numeral    
end

function resolveNameWithRomanNumbers(str)
	local index = string.find(str, " [^ ]*$") + 1
	local romanNumber = (string.sub(str, index))
	local result
	if string.match(romanNumber, "[MDCLXVI]+$") then --finding ONLY Roman letters in last "word"
		result = str
	else
		result = str
	end
	
	return result
end

function p.capitalizeName(str) return p.CapitalizeName(str) end
function p.CapitalizeName(str) --deprecated name
	--mw.log(string.gsub(" "..str, "[%s%-]%a", string.upper):sub(2))
	--return string.gsub(" "..str, "[%s%-]%a", string.upper):sub(2) --original version
	return string.gsub(" "..str:lower(), "[%s%-]%a", string.upper):sub(2)
end

function p.firstLetterLower(str) return p.FirstLetterLower(str) end
function p.FirstLetterLower(str) --deprecated name
	return str:sub(1, 1):lower() .. str:sub(2)
end

function p.firstLetterUpper(str) return p.FirstLetterUpper(str) end
function p.FirstLetterUpper(str) --deprecated name
    return str:sub(1,1):upper() .. str:sub(2)
end

--[[function p.fixDiacritics(str)
	local letterSequences = {
		["195"] = {fix = "â", sequence = {"162"}}
	}
	local fixedSeqsOffset = 0

	local currentLen = #str + fixedSeqsOffset
	local i = 1
	while i <= currentLen do
		local ascii = tostring(string.byte(str:sub(i,i)) or 0)
		for startSequence, seqData in pairs(letterSequences) do
			if ascii == startSequence then
				local foundSequence = true
				for j, seqCode in ipairs(seqData.sequence) do
					local seqAscii = tostring(string.byte(str:sub(i+j,i+j)))
					if seqAscii ~= seqCode then
						foundSequence = false
						break
					end
				end
				
				if foundSequence then
					fixedSeqsOffset = i - (i + #seqData.sequence)
					currentLen = #str + fixedSeqsOffset
					-- #seqData.sequence + 1 = number of ascii codes that needs to be replaced, the sequence list + the initial ascii code, i.e. the key value from letterSequences
					str = str:sub(1, i - 1) .. seqData.fix .. str:sub(i + #seqData.sequence + 1)
				end
			end
		end
		i = i + 1
	end
	return str
end]]

function p.RemoveSpecialCharacters(str, full, replaceDiacritics)
	str = string.gsub(str, "'", "")
	str = string.gsub(str, "®", "")
	str = string.gsub(str, "™", "")
	str = string.gsub(str, ":", "")
	str = string.gsub(str, "!", "") --perks
	str = string.gsub(str, "?", "") --achievements
	str = string.gsub(str, "/", "") --achievements
	str = string.gsub(str, "%&", "And") --probably can be changed to lower: "and"

	if replaceDiacritics then
		str = p.replaceDiacritics(str, full)
	end
		
	return str
end

function p.replaceDiacritics(str, full)
	--Diacritics
	specialLettersLight = {
		["A"] = {"À", "Á", "Â", "Ã", "Ä"},
		["a"] = {"à", "á", "â", "ã", "ä"},
		["e"] = {"è", "é", "ê", "ë", "ě"},
		["O"] = {"Ò", "Ó", "Ô", "Õ", "Ö"},
		["o"] = {"ò", "ó", "ô", "õ", "ö"}
	}
	specialLetters = {
		["A"] = {"À", "Á", "Â", "Ã", "Ä"},
		["a"] = {"à", "á", "â", "ã", "ä"},
		["E"] = {"È", "É", "Ê", "Ë", "Ě"},
		["e"] = {"è", "é", "ê", "ë", "ě"},
		["I"] = {"Ì", "Í", "Î", "Ñ", "Ï"},
		["i"] = {"ì", "í", "î", "ñ", "ï"},
		["O"] = {"Ò", "Ó", "Ô", "Õ", "Ö"},
		["o"] = {"ò", "ó", "ô", "õ", "ö"},
		["U"] = {"Ù", "Ú", "Û", "Ů", "Ü"},
		["u"] = {"ù", "ú", "û", "ů", "ü"},
		["Y"] = {	  "Ý",			 "Ÿ"},
		["y"] = {	  "ý",			 "ÿ"}
	}
	--Originally it was grouped by set [ÀÁ], however there is some sort of bug there that makes it not working
	--str = string.gsub(str, "[ÁÂ]", "A")
	--if debugRun then mw.log(mw.dumpObject(specialLetters)) end
	for letter, row in pairs(((full and specialLetters) or specialLettersLight)) do
		for _, special in ipairs(row) do
			str = string.replace(str, special, letter)
		end
	end
	return str
end

function p.replaceSpecialCharacters(name)
	local charList = { 
		["'"] = '&#39;',
		["&"] = '&#38;'
	}
	if name == nil then error("Name parameter is empty, but it shouldnt?") end
	for repl, sChar in pairs(charList) do
		name = string.gsub(tostring(name), sChar, repl)
	end
	return name
end

function p.pageExists(name)
	return (not name == cstr.empty) or mw.title.new(name).exists
end

function p.isValidFileName(name, extension)
	extension = extension or "png"
	name = p.RemoveSpecialCharacters(name)
	lg("Validation check, Name: " .. name)
	_isValidFileNameCounter = _isValidFileNameCounter + 1
	--mw.log("Counter:" .. tostring(_isValidFileNameCounter) .. ' - ' .. cstr.media .. name .. dot .. extension)
	return not (name == cstr.empty or not mw.title.new(cstr.media .. name .. dot .. extension).exists)
end

function p.resolveFileName(str, keepSpaces, removeDiacritics)
	keepSpaces = keepSpaces or false
	local result = ""
	
	result = string.lower(str)
	--mw.log(result)
	result = p.RemoveSpecialCharacters(result, keepSpaces, removeDiacritics)
	--mw.log(result)
	result = p.capitalizeName(result)
	if not keepSpaces then
		result = string.gsub(result, "[ ]", "")
	end
	--In future if there will be needed replace charactere such as "é" just add another substitution
	
	--mw.log(result)
	return result
end
	
function p.resolveImageName(name)
	if mw.title.new(cstr.media .. name ..  dot .. cstr.png).exists then return name .. dot .. cstr.png end
	if mw.title.new(cstr.media .. name .. dot .. cstr.jpg).exists then return name .. dot .. cstr.jpg end
	if mw.title.new(cstr.media .. name .. dot .. cstr.gif).exists then return name .. dot .. cstr.gif end
	return name .. dot .. cstr.png
end

function p.GetDisplayName(item)
	return (item.tName or item.name)
end

function p.IconLink(icon, pageLink, displayText, background)
	local result = cstr.empty
	background = background or p.resolveParameter(icon, "bg", true) or p.resolveParameter(icon, 4, true)
	background = p.bool(background == nil or background)
	displayText = displayText or p.resolveParameter(icon, 3, true)
	pageLink = pageLink or p.resolveParameter(icon, 2, true)
	
	local linkless = p.resolveParameter(icon, "linkless", true) or pageLink == "linkless" --linkless as a second parameter should be deprecated
	icon = p.resolveParameter(icon)
	local iconObject, pluralType = p.getIconObject(icon)
	if pluralType then --if automatic plural is detected, then links should redirect to singular form
		pageLink = pageLink or iconObject.name
		displayText = icon --since automatic plurals don't imitate 3-parameters scenario we have to set the display text to be same as the first parameter (icon)
	end
	local iconFile = iconObject.iconFile
	local filename = cstr.file .. iconFile .. tl .. "link=" .. (pageLink or icon)
	local text = (displayText and pageLink and pageLink .. tl .. displayText) or pageLink or icon --cstr.empty
	--local boxDesc = cstr.empty
	
	if pageLink == "img" then
		text = cstr.empty
		filename = cstr.file .. iconFile .. tl .. "link=" .. icon
	end
	if not linkless and text ~= cstr.empty then
		text = link(text)
		--boxDesc = getBoxDescription(pageLink or icon) //hover box, currently disabled
	elseif linkless or pageLink == "linkless" then
		text = (displayText or icon)
	end	
	
	local imageFile
	if background and iconObject.assembly then
		local loLogic = require("Module:Loadout" .. p.lang())
		--this needs to (should) be done sophisticated way
		local imgType = 
			(iconObject.iconFile:lower():find("addon") and "addon") or
			(iconObject.iconFile:lower():find("items") and "item") or 
			(iconObject.iconFile:lower():find("favors") and "offering") or 
			(iconObject.iconFile:lower():find("powers") and "power") or
			(iconObject.iconFile:lower():find("perks") and "perk") or
			strings.notSpecified
		--if iconObject.iconFile:lower():find("favors") then imgType = "offering" end
		local loObj = (loLogic["get" ..  p.capitalizeName(imgType) .. "ByName"] and loLogic["get" ..  p.capitalizeName(imgType) .. "ByName"](iconObject.name)) or nil
		imageFile = p.assembleImage(imgType, icon, 32, {rarity = (loObj and loObj.rarity) or 1, iconLink = true})
	else
		imageFile = link(filename, "64px") --iconObject.ilRes or "32px")
	end
	
	local tooltipText = link(filename, "96px", "link=")
	
	--result = '<span class = "wrap-span pcView" style = "display:none;"><span class="box-span">' .. boxDesc .. '</span>' .. text .. file .. '</span>'
	local padding = p.getPaddingsFromIcon(iconObject)
	result = text ..
		'<span class = "iconLink" style = "vertical-align: middle; padding: ' .. padding.top .. space .. padding.right .. space .. padding.bottom .. space .. padding.left .. '">' ..
			'<span style = "display:none; width:' .. (iconObject.ilRes or "32px") .. semi .. 'max-height: ' .. (iconObject.ilRes or "32px") .. '" class = "pcView pcIconLink">' .. p.tooltip(imageFile, tooltipText, true, true, true) .. '</span>' .. 
			'<span class = "mobileView mobileIconLink">' .. link(filename, iconObject.ilRes or "32px") .. '</span>' ..
		'</span>'

	--mw.log(result)
	return result
	--return frame:expandTemplate{title = "IconLink", args = {icon, pageLink, displayText} }	
end

function p.loadoutIconLink(icon, pageLink, displayText, params)
	displayText = displayText or p.resolveParameter(icon, "displayText", true) or p.resolveParameter(icon, 3, true)
	pageLink = pageLink or p.resolveParameter(icon, "link", true) or p.resolveParameter(icon, 2, true)
	icon = p.resolveParameter(icon, "icon", true) or p.resolveParameter(icon, 1, true) or icon
	local info = {
		displayText = displayText or pageLink or icon,
		link = pageLink or icon,
		icon = icon
	}
	local iconObject, pluralType = p.getIconObject(info.icon)
	local filename = cstr.file .. iconObject.iconFile .. tl .. "link=" .. (info.link or info.icon)
	local tooltipText = link(filename, "96px", "link=") --default tooltip
	
	if iconObject.iconFile:lower():find("perk") then
		local loLogic = require("Module:Loadout" .. p.lang())
		tooltipText = loLogic.getPerkPageTable(params.perk) or "WIP/Error:" .. params.perk.name
	end
	
	local imageFile = link(filename, "64px")
	local padding = p.getPaddingsFromIcon(iconObject)
	local result = link(info.displayText, info.link) .. 
		'<span class = "iconLink" style = "vertical-align: middle; padding: ' .. padding.top .. space .. padding.right .. space .. padding.bottom .. space .. padding.left .. '">' ..
			'<span style = "display:none; width:' .. (iconObject.ilRes or "32px") .. semi .. 'max-height: ' .. (iconObject.ilRes or "32px") .. '" class = "pcView pcIconLink">' .. p.tooltip(imageFile, tooltipText, true, true, true) .. '</span>' .. 
			--'<span class = "mobileView mobileIconLink">' .. link(filename, iconObject.ilRes or "32px") .. '</span>' ..
		'</span>'
	return result
end

function p.getPaddingsFromIcon(icon)
	return {
		top = icon.paddingTop or 0,
		right = icon.paddingRight or 0,
		bottom = icon.paddingBottom or 0,
		left = icon.paddingLeft or 0
	}
end

function getBoxDescription(icon) --curently disabled
	local ic = p.getIconObject(icon)
	local result = cstr.empty
	if ic.category == "Perks" then
		result = '<h3>' .. icon .. '</h3><hr>'-- .. prkz.getPerkDescriptionByName(perk.name)
	end
	return result
end

function p.resolveTextColorByBackground(hexBgColor)
	if(type(hexBgColor) == "table") then
		hexBgColor = hexBgColor.args[1]
	end
	local red = tonumber(string.sub(hexBgColor, 1, 2), 16)
	local green = tonumber(string.sub(hexBgColor, 3, 4), 16)
	local blue = tonumber(string.sub(hexBgColor, 3, 4), 16)
	local darkness = (0.299 * red + 0.587 * green + 0.114 * blue) / 255
	
	--mw.log("Red: " .. red .." \t\tor\t Blue: " .. blue .. " | Green: " .. green)
	--mw.log(darkness)
	if(darkness < _brigtnessTreshold) then
		return "white"
	else
		return "black"
	end
end

function p.getPageTitle()
	return mw.title.getCurrentTitle().text
end
---------------------------------------------------------------------------------
function p.getSumOfSizes(row)
	local result = 0
	local i = 1
	while row[i] do
		result = result + row[i][1] -- hardcoded first variable in table
		i = i + 1
	end
	return result
end

--[[
function compSize(row1, row2)
	local sum1 = row1.size
	local sum2 = row2.size
	if(type(sum1) == "table") then
		sum1 = p.getSumOfSizes(sum1) --converting back from table to number
	end
	if(type(sum2) == "table") then
		sum2 = p.getSumOfSizes(sum2)
	end
	
	if sum1 > sum2 then return true
	elseif sum1 < sum2 then return false
	else 
		if row1.realm < row2.realm then return true
		elseif row1.realm > row2.realm then return false
		else
			return resolveNameWithRomanNumbers(row1.name) < resolveNameWithRomanNumbers(row2.name)
		end
	end
end
]]

function compName(row1, row2)
	if row1.diacritics then
		if row2.diacritics then
			return p.RemoveSpecialCharacters(row1.name, true, true) < p.RemoveSpecialCharacters(row2.name, true, true)
		else
			return p.RemoveSpecialCharacters(row1.name, true, true) < row2.name
		end
	elseif row2.diacritics then
		return row1.name < p.RemoveSpecialCharacters(row2.name, true, true)
	end
	return row1.name < row2.name
end

function compFilename(row1, row2)
	return row1.filename < row2.filename
end

function compInt(row1, row2) -- 1, 2, 3
	return row1 < row2
end

function compIntReversed(row1, row2) -- 3, 2, 1
	return row1 > row2
end

function compIntDesc(row1, row2) -- 3, 2, 1
	return row1 > row2
end

function compLevel(row1, row2)
	return row1.level < row2.level
end

function compDlcCategory(row1, row2)
	if row1.category == row2.category then
		return row1.id < row2.id
	end
	return row1.category < row2.category
end

function compId(row1, row2)
	return row1.id < row2.id
end

function compRealCcyFirst(row1, row2)
	ccy1 = isRealCcy(row1.ccy)
	ccy2 = isRealCcy(row2.ccy)
	if (ccy1 and ccy2) or (not ccy1 and not ccy2) then --TRUE and TRUE or FALSE and FALSE
		return row1.ccy < row2.ccy
	elseif ccy1 and not ccy2 then --TRUE and FALSE
		return true
	else
		return false
	end
end

function compCharsKillersFirst(row1, row2)
	char1 = row1.power ~= nil
	char2 = row2.power ~= nil
	if (char1 and char2) or (not char1 and not char2) then --TRUE and TRUE or FALSE and FALSE
		return row1.id < row2.id
	elseif char1 and not char2 then --TRUE and FALSE
		return true
	else
		return false
	end
end

function rarityAndName(row1, row2)
	if row1.rarity and row2.rarity and row1.rarity < row2.rarity then
		return true
	elseif row1.rarity and row2.rarity and row1.rarity > row2.rarity then
		return false
	elseif row1.pieces and row2.pieces then
		local row1Name = cstr.empty
		local row2Name = cstr.empty
		for _, piece in pairs(row1.pieces) do row1Name = piece.name break end
		for _, piece in pairs(row2.pieces) do row2Name = piece.name break end
		return row1Name < row2Name
	else
		return compName(row1, row2)
	end
end

function p.sortByKeys(tableList) --only int, TODO check key type via pairs() fc
	local result = {}
	local orderedKeys = table.keys(tableList)
	p.sortTable(orderedKeys, true)
	
	for _, key in pairs(orderedKeys) do
		result[key] = tableList[key]
	end
	return result
end

function p.sortByRDate(tableList, reversed)
	table.sort(tableList,
		function (row1, row2) 
			if p.toTimestamp(row1.rDate) < p.toTimestamp(row2.rDate) then
				if reversed then
					return false
				else
					return true
				end
			end
		end
	)
	return tableList
end


function p.sortMapsBySize() --deprecated?
	local m = require("Module:Datatable" .. p.lang())
	--mw.log(mw.dumpObject(maps))
	table.sort(maps,compSize)
	--mw.log(mw.dumpObject(maps))
end

function p.sortItemsByName(tableList)
	table.sort(tableList, compName)
end

function p.sortItemsBycompFilename(tableList) --used for charms (as perk object is not retrieved, only cosData record)
	table.sort(tableList, compFilename)
end

function p.sortTable(tableList, reversed)
	if reversed then
		table.sort(tableList, compIntReversed)
	else
		table.sort(tableList, compInt)
	end
end

function p.sortTableDesc(tableList)
	table.sort(tableList, compIntDesc)
end

function p.sortDlcByCategory(tableList)
	table.sort(tableList, compDlcCategory)
end

function p.sortPerksByLevel(tableList)
	table.sort(tableList, compLevel)	
end

function p.sortTableById(tableList)
	table.sort(tableList, compId)
end

function p.sortRealCcyFirst(tableList)
	table.sort(tableList, compRealCcyFirst)
end

function p.sortCharsKillersFirst(tableList)
	table.sort(tableList, compCharsKillersFirst)
end

function p.sortCosmeticsByRarityAndName(tableList)
	table.sort(tableList, rarityAndName)
end
---------------------------------------------------------------------------------
                            -- Time functions --
function p.today()
	return os.time(os.date("!*t"))
end

function p.getYear(stamp)    return p.getTimeAspect(stamp, "year")    end
function p.getMonth(stamp)   return p.getTimeAspect(stamp, "month")   end
function p.getDay(stamp)     return p.getTimeAspect(stamp, "day")     end
function p.getHour(stamp)    return p.getTimeAspect(stamp, "hour")    end
function p.getMinute(stamp)  return p.getTimeAspect(stamp, "minute")  end
function p.getSecond(stamp)  return p.getTimeAspect(stamp, "second")  end
function p.getWeekday(stamp) return p.getTimeAspect(stamp, "weekday") end

function p.getTimeAspect(stamp, aspect) --aspect being selected time portion
	if type(stamp) ~= 'number' then
		stamp = p.toTimestamp(stamp)
	end
	local tag = '%c'
	if     aspect == "year"    then tag = '%Y'
	elseif aspect == "month"   then tag = '%m'
	elseif aspect == "day"     then tag = '%d'
	elseif aspect == "hour"    then tag = '%H'
	elseif aspect == "minute"  then tag = '%M'
	elseif aspect == "second"  then tag = '%S'
	elseif aspect == "weekday" then tag = '%w'
	end
	
	local result = os.date(tag, stamp)
	return (aspect == "weekday" and tonumber(result)) or result
end

function p.addTime(portion, amount, stamp)
	stamp = stamp or 0
	if type(stamp) ~= 'number' then
		stamp = p.toTimestamp(stamp)
	end
	
	local multiplier = timeContants[portion]
	stamp = stamp + (amount * multiplier)
	
	return stamp
end

function p.getTimeDiff(timestamp1, timestamp2) --First parameter is first in timeline, Second Parameter is later one
	return os.difftime(timestamp2, timestamp1)
end

function p.getTimeDiffFormatting(seconds, unit)
	return seconds / timeContants[unit]
end

function p.toTimestamp(datestamp)
	if type(datestamp) == 'string' then
		datestamp = p.GetDatetime(datestamp)
	end
	return os.time(datestamp) --if datestamp = nil, os.time() returns current datetime stamp
end

function p.toDate(timestamp, timeFormat)
	return os.date(timeFormat or '%d %B %Y', timestamp) --21 October 2021
end

function p.resolveDateTime(datetime, skipDay) --with weekday
	local matchYear = "^(..)%.(..)%.(%d%d%d%d)$"
	local matchMonth = "^(..)%.(%d%d)%.(....)$"
	local matchDay = "^(%d%d)%.(..)%.(....)$"
	local year, month, day, dayName = false
	local rDate = os.time(p.GetDatetime(datetime))
	if string.find(datetime, matchYear) then
		year = os.date("%Y", rDate)
	end
	if string.find(datetime, matchMonth) then
		month = os.date("%B", rDate)
	end
	if string.find(datetime, matchDay) then
		day = os.date("%d", rDate)
		dayName = os.date("%A", rDate)
	end
	
	--(day and month) is to avoid showing a day as the function sets the month to January (first month) by default, if the month is not provided
	--day part: ((day and month and day .. space) or cstr.empty)
	--month part: ((month and months[month] .. space) or cstr.empty)
	--year part: year
	--dayName part: ((day and month and not skipDay and space .. brackets(days[dayName])) or cstr.empty)
	return ((day and month and day .. space) or cstr.empty) .. ((month and months[month] .. space) or cstr.empty) .. year .. ((day and month and not skipDay and space .. brackets(days[dayName])) or cstr.empty)
end

function p.isFullDateTime(datestamp) return p.IsFullDateTime(datestamp) end
function p.IsFullDateTime(datestamp)
	local day, month, year = datestamp:match("^(..)%.(..)%.(....)$")
	if month == "##" or day == "##" or year == "####" then return false end
	return true
end

--convertToTimeVersion = normally, return table counts as 1.1.1970 (with day = 1, month = 1 and year = 1970).
--But if we want to have time stamp (like remaining time) these default values add 1 day, 1 month and 1970 years to the countdown table (Time stamp version table)
function p.GetDatetime(datestamp, convertToTimeVersion)
	local result = {}
	if type(datestamp) == types.string then
		local day, month, year, hour, min, sec = datestamp:match("^(..)%.(..)%.(%d%d%d%d) ?(%d?%d?):?(%d?%d?):?(%d?%d?)$")
		if month == "##" then
			month = 1
			result.fakeMonth = true
		end
		if day == "##" then
			day = 1
			result.fakeDay = true
		end
		
		result.year = tonumber(year)
		result.month = tonumber(month)
		result.day = tonumber(day)
		result.hour = tonumber((hour ~= cstr.empty and hour) or 0)
		result.min = tonumber((min ~= cstr.empty and min) or 0)
		result.sec = tonumber((sec ~= cstr.empty and sec) or 0)
		result.timestamp = p.toTimestamp(result)

		local tempDateObj = os.date("!*t", os.time(result))
		result.yday = tempDateObj.yday --year day included
		result.wday = p.correctWeekday(tempDateObj).wday --weekDay included
		result.week = p.getWeekNumber(result.timestamp)
		result.quarter = math.ceil(result.month / 3)
		result.dst = tempDateObj.isdst
	elseif type(datestamp) == types.number then
		result = os.date("!*t", datestamp)
		if(convertToTimeVersion) then
			result.day = result.day - 1 --we have to subtract it, not set it, as the datetime stamp can reach to days and months
			result.month = result.month - 1
			result.year = result.year - 1970
		end
		result = p.correctWeekday(result)
		result.timestamp = datestamp
	end
	
	return result
end

--.wday is indexed 1 - 7, Sunday being 1 => https://www.lua.org/manual/5.3/manual.html#:~:text=wday%20(weekday%2C%201%E2%80%937%2C%20Sunday%20is%C2%A01)
--%w starts 0 - 6, Sunday being 0 => https://www.lua.org/pil/22.1.html#:~:text=%25w,6%20%3D%20Sunday%2DSaturday%5D
--following row synchronises the convention with '%w' and shifting it to 1 - 7 starting Monday, as for the rest of Wiki
function p.correctWeekday(datetable)
	datetable.wday = (datetable.wday == 1 and 7) or datetable.wday - 1
	return datetable
end	

--ISO: https://en.wikipedia.org/wiki/ISO_week_date
--If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in W01. If it is on a Friday, it is part of W53 of the previous year.
--If it is on a Saturday, it is part of the last week of the previous year which is numbered W52 in a common year and W53 in a leap year.
--If it is on a Sunday, it is part of W52 of the previous year.
function p.getWeekNumber(dateStamp) --in milliseconds, os.time()
	firstWeekDayOfYear = tonumber(os.date("%w", os.time({year = os.date("%Y", dateStamp), month = 1, day = 1})))
	firstWeekOfYear = tonumber(os.date("%W", os.time({year = os.date("%Y", dateStamp), month = 1, day = 1})))
	
	-- ATTENTION as per description above some years have first week 01
	-- because all first weeks of year that starts with Sunday, Saturday, Friday or Thursday must be shifted
	if firstWeekDayOfYear > 0 and firstWeekDayOfYear < 4  and firstWeekOfYear == 0 then
		return tonumber(os.date("%W", dateStamp)) + 1
	else
		return tonumber(os.date("%W", dateStamp))
	end
end

function p.getDatePart(datestamp, part)
	local day, month, year = datestamp:match("^(..)%.(..)%.(....)$")
	if year		== "####" then month = false end
	if month	== "##" then month = false end
	if day		== "##" then day = false end
	if		part == "day"	then return day
	elseif	part == "month"	then return month
	elseif	part == "year"	then return year
	end
end

--------------------------------------------------------------------------------

function p.regularReplace(reggedString, regexTable)
	local result = cstr.empty
	for key, value in pairs(regexTable) do
		reggedString = reggedString:gsub(key, value)
	end
	return reggedString
end

function p.getIcon(icon)
	return p.getIconObject(icon).iconFile
end

function p.getIconObject(icon)
	icon = p.resolveParameter(icon)
	local icons = mw.loadData("Module:Datatable/Icons" .. p.lang()).icons --mw.loadData
	local pluralRegex = "^(.+)s$"
	local plural2Regex = "^(.+)es$"
	local plural3Regex = "^(.+)ies$"
	local plural = icon:match(pluralRegex)
	local plural2 = icon:match(plural2Regex) --Toolboxes => Toolbox
	local plural3 = icon:match(plural3Regex) --Bodies => Bod
	local result = getIconElement(icons, icon, true)
	
	if result then
		return result
	elseif plural and icons[plural] ~= nil then --Pallets => Pallet
		return getIconElement(icons, plural), 1
	elseif plural2 and icons[plural2] ~= nil then --Toolboxes => Toolbox
		return getIconElement(icons, plural2), 2
	elseif plural3 and icons[plural3 .. "y"] ~= nil then --Bodies => Body
		return getIconElement(icons, plural3 .. "y"), 3
	else
		return getIconElement(icons, icon)
	end
end

function getIconElement(icons, icon, returnNil)
	if type(icons[icon]) == types.table then
		local result = table.copy(icons[icon])
		result.name = icon
		return result
	elseif returnNil then
		return nil
	elseif icons[icon] == nil then
		return icons["Unknown QuestionMark"] or icons[1]
	else
		return getIconElement(icons, icons[icon])
	end
end

--************************* survivors & killers ******************************--

function p.getCharacterById(id, charTable)
	for _, character in ipairs(charTable) do
		if character.id == id then return character end
	end
end

function p.getCharacter(object)
	local data = require("Module:Datatable" .. p.lang())
	
	if object.killer or object.charType == 'K' then
		return p.getCharacterById(object.killer or object.character, data.killers)
	else
		return p.getCharacterById(object.survivor or object.character, data.survivors)	
	end
end

--function returns appropriate character name that should be compatible with IL.
--IL itself is not executed to have option to customise the IL call
function p.resolveCharacterIconLinkName(owner)
	return (owner and owner.multiName and owner.realName) or (not p.isKiller(owner) and owner.shortName) or owner.name or false
end

function p.resolveCharacterPortraitFileName(character, maxId)
	local fileConst = "_charPreview_portrait"
	local isKiller = p.isKiller(character)
	local unknownChar = (isKiller and "UnknownKiller") or "UnknownSurvivor"
	local fileName
	
	fileName = p.getFileNameFromTableById(character.id, (isKiller and killerImages) or survivorImages, "preview") --get custom name from table
	if fileName == cstr.empty or not p.isValidFileName(fileName) then --K/S{ID}_charPreview_portrait
		fileName = p.getCharacterIdentifier(character) .. fileConst
	end
	if (maxId and (character.id >= maxId - 1 and not p.isValidFileName(fileName)) or (not maxId and not p.isValidFileName(fileName))) then --File not Found; maxId - 1 to consider last two characters, not only the last one
		fileName = unknownChar .. fileConst
	end

	--mw.log(fileName)
	return fileName
end

function p.getCharacterIdentifier(character)
	return ((p.isKiller(character) and 'K') or 'S') .. string.format("%02d", character.id)
end

function p.getFileNameFromTableById(id, fileTable, field)
	--mw.log(id)
	for j, sImage in ipairs(fileTable) do
		if sImage.id == id and sImage[field] ~= nil then
			return sImage[field]
		end
	end
	return cstr.empty
end

--Not currently used
function p.resolveGender(abbr)
	if		abbr == 'M'		then return strings.male
	elseif	abbr == 'F'		then return strings.female
	elseif  abbr == 'N'		then return strings.nonhuman
	elseif	abbr == 'M/F'	then return strings.male .. comma .. strings.female
	elseif	abbr == 'F/M'	then return strings.female .. comma .. strings.male
	elseif	abbr == 'TM'	then return strings.transmale
	elseif	abbr == 'TF'	then return strings.transfemale
	else						 return strings.undefined
	end
end

--classes in form: "sortable, class2, class3 ,..."
function p.wrapBasicTable(content, classes, styles, skipNtl)
	return '{| class = ' .. quotes('wikitable' .. ((classes and space .. p.getTableClasses(classes)) or cstr.empty)) .. ((styles and space .. 'style = ' .. quotes(styles)) or cstr.empty) .. nl ..
	(not skipNtl and ntl .. nl or cstr.empty) .. content ..'|}'
end

function p.wrapTable(content, classes, styles, skipNtl)
	return (classes and '{| class = ' .. quotes((p.getTableClasses(classes))) or cstr.empty) .. ((styles and space .. 'style = ' .. quotes(styles)) or cstr.empty) .. nl ..
	(not skipNtl and ntl .. nl or cstr.empty) .. content ..'|}'
end

function p.getTableClasses(classes)
	classes = p.resolveParameter(classes, 1)
	local unknownClass = "unknownClass"
	if not classes then return unknownClass end
	
	if type(classes) == "string" then
		classes = classes:gsub(", ", space):gsub(",", space)
	else --todo type(classes) == "table"
		return unknownClass
	end
	return classes
end

function p.getCharacterByName(name)
	name = name or p.resolveParameter(name)
	local str = require("Module:Datatable" .. p.lang())
	for _, surv in ipairs(survivors) do
		if surv.shortName == name or surv.name == name then
			local survivor = table.copy(surv)
			survivor.isKiller = false
			return survivor
		end
	end
	for _, killer in ipairs(killers) do
		if killer.shortName == name or killer.realName == name or the(killer) .. killer.name == name or killer.name == name then
			local klr = table.copy(killer)
			klr.isKiller = true
			return klr
		end
	end
	return nil
end

--deprecated
function p.getCharsByDlc(dlc, charType)
	local data = require("Module:Datatable" .. p.lang())
	local result = {}
	local listTable = (charType == 'S' and data.survivors) or (charType == nil and data.survivors) or data.killers --if the charType is not set then set the table by default to survivors
	
	for _, character in ipairs(listTable) do
		if character.dlc == dlc.id then
			result[#result + 1] =  character
		end
	end
	
	if charType ~= nil then --if the charType is set, we need loop through only one table. Otherwise charType wasn't set in order to retrieve both types of characters from DLC
		return result
	end
	
	for _, character in ipairs(data.killers) do --since the default table is survivor, we can hardcode killer table as a second table
		if character.dlc == dlc.id then
			result[#result + 1] =  character
		end
	end
	return result
end

function p.getCharacterFirstName(character)
	character = p.resolveParameter(character)
	if not character.name then
		character = p.getCharacterByName(character)
	end
	local regex = '([^ ]*) ?' --%a doesn't consider diacritics such as É (for instance: Élodie)
	local isKiller = p.isKiller(character)
	local text = (isKiller and character.name) or character.shortName or character.name --if the char is killer then use their name, otherwise it's survivor, thus use shortName or name
	if isKiller then return text end --if it's killer, then their nickname is already what it's supposed to return
	
	if regex and text:gmatch(regex) then
		for m in text:gmatch(regex) do
			return m --return first occurence before potential space, otherwise return whole string
		end
	end
	return "Name not found"
end

function p.replaceLastSpaceByNBSP(text) -- Non Breakable SPace
	local lastCharSpaceRegex = "^(.+) $"
	local regex = "^(.+) (.+)$" --should pick last space(?)
	text = text:gsub(lastCharSpaceRegex, "%1" .. nbspC):gsub(regex, "%1" .. nbspC .. "%2")
	return text
end

function p.isCharacterKiller(character) return p.isKiller(character) end
function p.isKiller(character) 
	local character = p.resolveParameter(character)
	return character.radius ~= nil
end

function p.getPossessiveName(character, params, fullName)
	local langs = require("Module:Languages")
	params = params or {link = p.bool(p.resolveParameter(character, "link", true)) or false}
	params.fullName = fullName or p.bool(p.resolveParameter(character, "fullName", true)) or false
	
	if not character.name then
		character = p.getCharacterByName(p.resolveParameter(character))
	end
	if not character then return false end
	
	params.firstName = p.getCharacterFirstName(character)

	return langs.evaluatePossessive(character, params)
end

--[[
function p.getDativeName(character, params)
	params = params or {link = p.bool(p.resolveParameter(character, "link", true)) or false}
	if not character.name then
		character = p.getCharacterByName(p.resolveParameter(character))
	end
	if _language ~= "en" then
		return langs["dative_" .. _language](character, params) .. firstName
	end
	
	return firstName
end
]]

function p.clr(color, text)
	text = text or p.resolveParameter(color, 2, true)
	color = p.resolveParameter(color)
	
	return clr(color, text)
	--return frame:expandTemplate{title = "clr", args = {color, text}}	
end


function p.biclr(color, text) return i(p.clr(color, text)) end
function p.ibclr(color, text) return i(p.clr(color, text)) end

--Colour Ordered
function p.clro(text, reset)
	reset = reset or p.bool(p.resolveParameter(text, 2, true)) or false
	text = p.resolveParameter(text, 1)
	
	if not p.bool(frame:callParserFunction("#varexists:colorOrderCounter")) or reset then
		frame:callParserFunction("#vardefine:colorOrderCounter", 1 )
	end
	local order = tonumber(frame:callParserFunction("#var:colorOrderCounter")) % (#_colorOrder + 1) --making list infinite by going back
	order = (order == 0 and 1) or order --adjusting an index from order = #colorOrder => 0 + 1 = 1
	frame:callParserFunction("#vardefine:colorOrderCounter", order + 1 )
	
	return ((text ~= nil and text ~= cstr.empty) and b(clr(_colorOrder[order], text))) or clr(_colorOrder[order], text)
end

--Colour Ordered Reset
function p.clror(text)
	return p.clro(text, true)
end

function p.clroTest()
	local words = string.split(strings.clrTestString, space)
	local result = cstr.empty
	local clrOffset = 0
	for i, word in ipairs(words) do
		if #word <= 2 then clrOffset = clrOffset - 1 end
		local clrIndex = i + clrOffset
		local color = _colorOrder[clrIndex]
		result = result .. (#word > 2 and p.tooltip(((i == 1 and p.clror(word)) or p.clro(word)), "order: " .. clrIndex .. br .. "colour: " .. color, true, true) or word) .. (clrIndex < #_colorOrder and space or cstr.empty)
		if clrIndex == #_colorOrder then break end
	end
	return result .. ((#words > #_colorOrder and "...") or (#words < #_colorOrder and dot .. space .. brackets(strings.notEnoughColors)) or cstr.empty)
end

function p.bclr(color, text)
	return b(p.clr(color, text))
end

function clr(color, text)
	local result = 'inherit' --CSS value, don't change it
	--mw.log(tonumber(color, 10))
	color = tonumber(color, 10) or color:lower() --if the string is int then convert it to integer
	
	if	   color == 1		or color ==  "brown"			then result = "ab713c"
	elseif color == "1bg"									then result = "5d4533"
	elseif color == 2		or color ==  "yellow"			then result = "e8c252"
	elseif color == "2bg"									then result = "d7ad2f"
	elseif color == 3		or color ==  "green"			then result = "199b1e"
	elseif color == "3bg"									then result = "0f791f"
	elseif color == 4		or color ==  "purple"			then result = "ac3ee3"
	elseif color == "4bg"									then result = "672d7f"
	elseif color == 5		or color ==  "pink"				then result = "ff0955"
	elseif color == "5bg"									then result = "cf0b45"
	elseif color == 6		or color ==  "orange"			then result = "ff8800"
	elseif color == "6bg"									then result = "ff5300"
	elseif color == 7		or color ==  "grey"				then result = "808080"
	elseif color == 8		or color ==  "red"				then result = "d41c1c"
	elseif color == 9		or color ==  "beige"			then result = "e7cda2"
	elseif color == 10		or color ==  "blue"				then result = "0e98ff"
	elseif color == 11		or color ==  "violet"			then result = "b91a9b"
	elseif color == "11bg"									then result = "800080"
	elseif color == 12		or color ==  "light blue"		then result = "9bb0bf"
	elseif color == "12bg"									then result = "9bb0bf"
	elseif color == 13		or color ==  "faded jade"		then result = "418284"
	elseif color == 14		or color ==  "gold"				then result = "ffa800"
	elseif color == "14bg"									then result = "ffa800"
	elseif color == 15		or color ==  "fuchsia"			then result = "ec0dea"
	elseif color == "15bg"									then result = "b30ad2"
	elseif color == 16		or color ==  "white"			then result = "ffffff"
	elseif color == 17		or color ==  "vomit green"		then result = "8ad672"
	elseif color == 18		or color ==  "blood red"		then result = "900a0a"
	elseif color == 19		or color ==  "bright red"		then result = "ff0000"
	elseif color == 20		or color ==  "silver"			then result = "b5afb0"
	elseif color == 21		or color ==  "turquoise"		then result = "37d1c0"
	elseif color == 22		or color ==  "cinnamon"			then result = "b74004"
	elseif color == 23		or color ==  "black"			then result = "000000"
	elseif color == 24		or color ==  "wiki gold"		then result = "b7a269"
	elseif color == 25		or color ==  "bronze"			then result = "c2593a"
	elseif color == 26		or color ==  "chartreuse"		then result = "b6fa36"
	elseif color == 27		or color ==  "screamin' green"	then result = "63ef98"
	elseif color == 28		or color ==  "lavender"			then result = "b57edc"
	elseif color == 29		or color ==  "aquamarine"		then result = "7fffd4"
	elseif color == 30		or color ==  "ultramarine"		then result = "120a8f"
	elseif color == 31		or color ==  "olive"			then result = "808000"
	elseif color == 32		or color ==  "pale rose"		then result = "f5bfd9"
	end
	
	if text ~= nil and text ~= cstr.empty then
		result = '<span class="luaClr clr clr' .. color .. '" style="color: ' .. ((result ~= 'inherit' and '#') or cstr.empty) .. result .. ';">' .. text .. '</span>'
	end
	
	return result
end

--calls for Strings function Syntax Highlight
function p.sh(value, lang, inline)
	inline = p.bool(inline or p.resolveParameter(value, 3, true))
	lang = lang or p.resolveParameter(value, 2, true)
	value = p.resolveParameter(value)
	return sh(value, lang, inline)
end

--text = text containing a tooltip
--tooltip = tooltip content
--iconless = true/false whether the (i) icon should appear after the text containing tooltip (default false)
--borderless = true/false whether underline should be shown (default false)
function p.tooltip(text, tooltip, iconless, borderless, pcViewOnly)
	borderless = borderless or p.resolveParameter(text, 4, true) or p.resolveParameter(text, "borderless", true) 
	iconless = p.bool(iconless or p.resolveParameter(text, 3, true) or (p.resolveParameter(text, "tooltip") and p.resolveParameter(text, 2, true)) or p.resolveParameter(text, "iconless"))
	tooltip = tooltip or p.resolveParameter(text, 2, true) or p.resolveParameter(text, "tooltip")
	text = p.resolveParameter(text)
	local containsLink = p.containsLink(tooltip)
	
	return
		'<span class = "tooltip' .. ((containsLink and space .. 'linkIncluded') or cstr.empty) .. ((borderless and space .. 'borderless') or cstr.empty) .. '">' .. 
			'<span class="tooltipBaseText' .. ((iconless and space .. 'iconless') or cstr.empty) .. '">' .. text .. '</span>'..
			(((tooltip ~= nil and tooltip ~= cstr.empty) and
			'<span class = "tooltiptext">' ..
				((not pcViewOnly and '<span class = "mobileView"> (</span>') or cstr.empty) .. --if it's mobile view then apply add brackets...
					'<span class="tooltipTextWrapper">' .. tooltip .. '</span>' .. 
				((not pcViewOnly and '<span class = "mobileView">) </span>') or cstr.empty) .. 
			'</span>') or cstr.empty) ..
		'</span>'
end

function p.containsLink(text)
	local regex = '%[%[.-%]%]'
	local regexExternal = '%[.-%]'
	local rawRegex = '?%&%#91;%&%#91;.-%&%#93;%&%#93;'
	local rawRegexExternal = '%&%#91;.-%&%#93;'
	if string.match(text, regex) or string.match(text, rawRegex) or
		string.match(text, regexExternal) or string.match(text, rawRegexExternal)
	then --we want to return boolean, not the index
		return true
	else
		return false
	end
end

function p.ptb(content, title, patch)
	return
		ptb(
			p.resolveParameter(content),
			title or p.resolveParameter(content, 2, true),
			patch or p.resolveParameter(content, 3, true)
		)
end

function ptb(content, title, patch)
	title = title or p.resolveParameter(content, 2, true)
	patch = patch or p.resolveParameter(content, 3, true)
	
	if not patch or p.isPatchLive(patch) then
		return content
	else
		return
			dynamicBox(content, title, "ptb")
	end
end

function p.deletedBox(content, title, patch)
	return 
		deletedBox(
			p.resolveParameter(content),
			title or p.resolveParameter(content, 2, true),
			patch or p.resolveParameter(content, 3, true)
		)
end

function deletedBox(content, title, patch)
	local result = dynamicBox(content, title, "deletedBox")
	patch = (patch and string.trim(patch)) or nil
	
	if patch and not p.isPatchLive(patch) then
		 --result = includeonly(result) .. noinclude(category("DeleteBox Remove Lists", "Remove List " .. patch))
		 result = result .. category("DeleteBox Remove Lists", "DeleteBox List for " .. patch)
	end
	
	return result --no further logic so redirecting straight to dynamic box
end

function p.wipBox(content, title, patch)
	return 
		wipBox(
			p.resolveParameter(content),
			title or p.resolveParameter(content, 2, true),
			patch or p.resolveParameter(content, 3, true)
		)
end

function wipBox(content, title, patch)
	local result = dynamicBox(content, title, "wipBox")
	return result --no further logic so redirecting straight to dynamic box
end

function dynamicBox(content, title, boxType)
	title = title or p.resolveParameter(content, 2, true)
	boxType = boxType or p.resolveParameter(content, 3, true)
	
	if not boxType then
		return strings.boxTypeNotFound
	end
	
	return	
		'<div class = "dynamicBox ' .. boxType .. '">' .. 
			((title and '<div class = "dynamicTitle ' .. boxType .. '">' .. title .. '</div>' ) or cstr.empty) .. 
			'<div class = "dynamicContent ' .. boxType .. '">' .. content .. '</div>' ..
		'</div>'
end

function p.isPatchLive(patchParam)
	local patch = p.getPatch(patchParam)
	
	--dLog(((patchParam and "[" .. patchParam .. ((not patch and " - Not Found") or cstr.empty) .. "]") or cstr.empty) .. " isPatchLive: " .. (patch and (p.IsFullDateTime(patch.rDate) and tostring(p.dateHasPassed(p.GetDatetime(patch.rDate)))) or tostring(false)))
	return patch and (p.IsFullDateTime(patch.rDate) and p.dateHasPassed(p.GetDatetime(patch.rDate))) or false
end

function p.isCharacterLive(character)
	if character.dlc then
		local dlcM = require("Module:DLCs")
		local dlc = dlcM.getCharacterMainDlc(character)
		return (dlc == nil) or p.IsFullDateTime(dlc.rDate) and p.dateHasPassed(dlc.rDate)
	end
	return false
end

function p.getPatch(patch)
	local data = require("Module:Datatable" .. p.lang())
	
	for _, patchObj in ipairs(data.patches) do
		if patchObj.patch == patch then return patchObj end
	end
end

function p.getLatestPatch()
	return require("Module:Datatable" .. p.lang()).latestPatch.patch
end

--timestamp = {year = 2022, month = 7, day = 19} --example format
function p.dateHasPassed(timestamp)
	local currentDate = os.date("*t") --gives current timestamp: {year = 1998, month = 9, day = 16, yday = 259, wday = 4, hour = 23, min = 48, sec = 10, isdst = false}
	local releaseOffset = 60 * 60 * 15 --release hour 15:00 (server time is -2 hours from my current time)
	timestamp = p.standardiseDateObject(timestamp)

	--mw.log(os.difftime(os.time(timestamp) + releaseOffset, os.time(currentDate))) 
	return os.difftime(os.time(timestamp) + releaseOffset, os.time(currentDate)) < 0 -- subtracting releaseOffset to adjust timing according to actual release hour
end

function p.isEventLive(element)
	--not element.event is to skip events from different tables, such as tomes
	return not (element.event or element.patch) and (element.rDate and p.dateHasPassed(element.rDate)) and (element.eDate == nil or not p.dateHasPassed(element.eDate))
		or (element.patch and element.rDate and p.isFullDateTime(element.rDate) and p.dateHasPassed(element.rDate) and (element.eDate == nil or not p.dateHasPassed(element.eDate)))
end

function p.standardiseDateObject(dateObj)
	if type(dateObj) == types.table then --if we pass table, we need to check time values and populate them to 0 by default
		if not dateObj.hour then dateObj.hour = 0 end
		if not dateObj.min then dateObj.min = 0 end
		if not dateObj.sec then dateObj.sec = 0 end
		
		dateObj = os.time(dateObj) --we convert it to seconds constant (resp. variable)
	end
	
	dateObj = p.GetDatetime(dateObj)
	return dateObj
end

function p.resolvePatchList()
	local data = require("Module:Datatable" .. p.lang())
	local result = cstr.empty
	local yearCounter = 0
	local tempResult = cstr.empty
	
	for _, patch in ipairs(data.patches) do
		local patchDate = p.standardiseDateObject(patch.rDate)
		local suffixLink = getSuffixLink(patch)
		local suffixPatchName = getSuffixPatchName(patch) --in case of PTB we need to attach " (PTB)" suffix
		
		if yearCounter ~= patchDate.year then
			if yearCounter ~= 0 then
				result = result .. frame:callParserFunction{ name = '#tag', args = {"tabber", tempResult} } .. nl
			end
			tempResult = cstr.empty
			yearCounter = patchDate.year
			result = result .. 
				"|-|" .. patchDate.year .. "=" .. nl
		end
		
		local patchNotes, result = pcall(function () return frame:expandTemplate{title = strings.patch .. space .. patch.patch .. suffixPatchName} end)
		if patchNotes and --[[not patch.ptb and]] patch ~= patches[#patches] then
			local patchVersions = string.split(patch.patch, '.')
			if patchVersions[3] ~= "0" then patchVersions[3] = "0" end
			for i = #patchVersions, 4 , -1 do --in case that patch contains anything else after last patch number let's remove it (example: "6.1.2 / 6.1.3")
				table.remove(patchVersions)
			end
			local patchLink = strings.patch .. space .. patch.patch .. suffixPatchName
			local patchLinkModified = strings.patch .. space .. table.concat(patchVersions, ".")
			
			tempResult = tempResult .. 
				frame:expandTemplate{title = "!"}.. "-" .. frame:expandTemplate{title = "!"} .. patch.patch .. suffixPatchName .. "=" .. nl .. --creating individual patch tabs
				
				frame:expandTemplate{title = "(!"} .. space .. 'class = "wikitable"' .. nl ..
				"!" .. nl .. "==== " .. link(patchLinkModified .. ((suffixLink and suffixLink .. tl .. patchLink) or cstr.empty)) .. " - " .. p.toDate(os.time(patchDate)) .. " ====" .. nl .. --28 April 2022
				frame:expandTemplate{title = "!-"} .. nl ..
				frame:expandTemplate{title = "!"} .. result .. nl ..
				frame:expandTemplate{title = "!)"} .. nl
		end
		
	end
	result = result .. frame:callParserFunction{ name = "#tag", args = {"tabber", tempResult} } .. nl --post loop aiditon for last year
	
	result = frame:extensionTag{name = 'tabber',content = result}
	
	return result
end

function getSuffixPatchName(patch)
	if patch.ptb then
		return " (PTB)"
	end
	return cstr.empty
end

function getSuffixLink(patch)
	local versions = string.split(patch.patch, '.')
	if versions[3] ~= "0" then
		return "#Hotfix " .. patch.patch
	elseif patch.ptb then
		return "#Public Test Build " .. patch.patch --patch needs to not contain "(PTB)" part
	end
	return nil
end

function p.getPatchLink(patch)
	local suffixLink = getSuffixLink(patch)
	local suffixPatchName = getSuffixPatchName(patch) --in case of PTB we need to attach " (PTB)" suffix
	local patchLink = strings.patch .. space .. patch.patch .. suffixPatchName
	local patchVersions = string.split(patch.patch, '.')
	if patchVersions[3] ~= "0" then patchVersions[3] = "0" end
	local patchLinkModified = strings.patch .. space .. table.concat(patchVersions, ".")
	
	return link(patchLinkModified .. ((suffixLink and suffixLink .. tl .. patchLink) or cstr.empty))
end

--wt = wordTable
function p.getWord(wt, index)
	local keys = table.keys(wt)
	local maxValue = table.max(keys)
	local minValue = table.min(keys)
	if index > maxValue then return wt[maxValue] end --if the index is higher than highest amount, return word of highest amount available
	if wt[index] then return wt[index] end -- if the index is exactly of plural count then return exact word on index
	if index < minValue then return wt[minValue] end --if index not found, it means it's lower than lowest amount in a word table
	
	for i, keyValue in ipairs(keys) do 
		if index < keyValue then
			return wt[keys[i - 1]] --if the index is between two amounts(indexes) use the lower one
		end
	end
end

--{{header = 2022, content = "..."}, {}, ...}
function p.getTabberFromTable(tabberTable)
	local result = cstr.empty
	for _, section in ipairs(tabberTable) do
		result = result .. nl .. '|-|' .. (section.header or strings.tabberHeaderNotFound) .. '=' .. nl .. section.content .. nl
	end
	
	result = frame:callParserFunction{ name = '#tag', args = {"tabber", result} } 
	
	return result
end

function p.isTagPresent(el, searchedTag)
	if el.tags and type(el.tags) == types.table then
		for _, tag in ipairs(el.tags) do
			if string.lower(searchedTag) == string.lower(tag) then
				return true
			end
		end
	end
	return false
end

function p.yesNo(param, note, imgSize)
	imgSize = imgSize or p.resolveParameter(param, 3, true) or p.resolveParameter(param, "size", true)
	note = note or p.resolveParameter(param, 2, true) or p.resolveParameter(param, "note", true)
	param = p.resolveParameter(param, 1, true) or param
	local imageFilename = "Unknown QuestionMark.png"
	local tickClass = "unknown"
	local size = (imgSize or 32) .. "px"
	if p.bool(param) == true then
		imageFilename = "DBD_UI_Icon_CheckMark.png"
		tickClass = "tickYes"
	elseif p.bool(param) == false then
		imageFilename = "DBD_UI_Icon_X.png"
		tickClass = "tickNo"
	elseif param:lower() == "nan" then
		imageFilename = "NaN.png"
		tickClass = "tickNaN"
	end
	
	--lg(span(p.tooltip(span(file(imageFilename, size), tickClass),cstr.empty,true,true), "yesNo"))
	return
		span(
			p.tooltip(
				span(file(imageFilename, size), tickClass),
				note or cstr.empty,
				true,
				true
			)
		, "yesNo")
end

--imgType:
-- perk = perk with switching background
-- sosPerk = teachable frame background
--icon icon that is inside
--imgSize currently not used, should be available for manually adjusting the size of image
function p.assembleImage(imgType, icon, imgSize, params)
	local variousLogic = require("Module:Various")
	
	params = params or p.resolveParameter(imgType, 4, true) --not sure if table can be passed directly from wiki
	local rarity = p.resolveParameter(imgType, "rarity", true) 
	local isIconLink = p.resolveParameter(imgType, "iconLink", true) or p.resolveParameter(imgType, "il", true) or (params and params.iconLink == true) or false
	local rarityNumber = tonumber(rarity) or (params and params.rarity and tonumber(params.rarity))
	rarity = string.replace(((rarityNumber and (variousLogic.rarity[rarityNumber].techName or variousLogic.rarity[rarityNumber].name)) or (rarity or (params and params.rarity)) or variousLogic.rarity[1].techName or variousLogic.rarity[1].name):lower(), space, dash) --Common by default
	rarity = string.replace(rarity, "&", "and")
	
	imgSize = imgSize or p.resolveParameter(imgType, 3, true) or p.resolveParameter(imgType, "size", true)
	if type(imgSize) == types.string then return "Error: " .. imgSize end
	icon = icon or p.resolveParameter(imgType, 2, true) or p.resolveParameter(imgType, "icon", true)
	imgType = p.resolveParameter(imgType, "type", true) or p.resolveParameter(imgType)
	
	local imgSizeStyle = (imgSize and ' style = "width: ' .. imgSize .. 'px; height: ' .. imgSize .. 'px;"') or cstr.empty
	local align = (params and params.align and "float: " .. params.align .. semi) or cstr.empty
	
	if imgType == "sosPerk" then
		return 
			'<div class = "game-element-container inline-flex relative">' ..
				'<div class = "game-element-bg-settings game-element-bg-settings-size-sos teachable-perk-element"' .. imgSizeStyle .. '></div>' ..
				'<div class = "game-element-bg-settings-size-sos game-element-imgObject absolute"' .. imgSizeStyle .. '>' .. file(p.getIcon(icon), "link=" .. icon) .. '</div>' ..
				'<div class = "game-element-bg-settings game-element-bg-settings-size-sos teachable-indicator-perk-element absolute"' .. imgSizeStyle .. '></div>' ..
			'</div>'
	elseif imgType == "perk" then
		return
			'<div class = "game-element-container flex relative">' ..
				'<div class = "game-element-bg-settings game-element-bg-settings-size-perk animate-perk-settings animate-perk-bg"' .. imgSizeStyle .. '></div> ' ..
				'<div class = "game-element-bg-settings game-element-bg-settings-size-perk animate-perk-settings animate-perk-level absolute"' .. imgSizeStyle .. '></div>' ..
				'<div class = "game-element-bg-settings-size-sos game-element-imgObject absolute"'  .. imgSizeStyle .. '>' .. file(p.getIcon(icon), "link=" .. icon, "center") .. '</div>' ..
			'</div>'
	elseif table.contains({"addon", "item", "power", "offering"}, imgType) then
		--rarity = (rarity and variousLogic.rarity[rarity].name:lower()) or string.replace((params and (params.rarity and (tonumber(params.rarity) and variousLogic.rarity[params.rarity].name) or params.rarity) or variousLogic.rarity[1].name):lower(), space, dash) 
		addonContainer = (imgType == "addon" and "addon-container") or cstr.empty
		imgSize = imgSize or 128
		if isIconLink then
			local ilOffset = (imgSize / 2)
			local addonMarkerScale = 50 * (imgSize / 128)
			return 
				'<span style="display:inline-flex; position:relative; height: ' .. ilOffset .. 'px; width: ' .. imgSize .. 'px">' ..
					'<span class = "margin-auto" style = "display: inline-flex; position: absolute; top: -6px; max-width: ' .. imgSize .. 'px;' .. align ..'">' ..
						'<span class = "game-element-container ' ..  addonContainer .. ' flex relative">' ..
							'<span class = "game-element-bg-settings game-element-bg-settings-size-perk ' .. rarity .. '-' .. (table.contains({"item", "offering"}, imgType) and imgType or "item") .. '-element" ' .. imgSizeStyle .. '></span>' ..
							((imgType == "addon" and '<span class = "game-element-bg-settings game-element-bg-settings-size-perk addon-marker addon-marker-il absolute" style = "scale: ' .. addonMarkerScale .. '%;"></span>') or cstr.empty) ..
							'<span class = "game-element-bg-settings-size-sos absolute" ' .. imgSizeStyle .. '>' .. file(p.getIcon(icon), "link=" .. icon, "300px") .. '</span>' ..
						'</span>' ..
					'</span>' ..
				'</span>'
		else
			return
				'<div class = "margin-auto" style = "max-width: ' .. imgSize .. 'px; scale: calc(' .. imgSize .. '/128); ' .. align ..'">' ..
					'<div class = "game-element-container ' ..  addonContainer .. ' flex relative">' ..
						'<div class = "game-element-bg-settings game-element-bg-settings-size-perk ' .. rarity .. '-' .. (table.contains({"item", "offering"}, imgType) and imgType or "item") .. '-element"></div>' ..
						((imgType == "addon" and '<div class = "game-element-bg-settings game-element-bg-settings-size-perk addon-marker absolute"></div>') or cstr.empty) ..
						'<div class = "game-element-bg-settings-size-sos absolute">' .. file(p.getIcon(icon), "link=" .. icon, "300px") .. '</div>' ..
					'</div>' ..
				'</div>'
		end
	end
end

function p.resolvePageHeader(params)
	return nil	
end

return p