Difference between revisions of "Module:Infobox Monster"

From Idlescape Wiki
Jump to navigation Jump to search
(Fix make a copy of the original array when replacing values)
m (Change Species link name to exclude (species))
 
(4 intermediate revisions by the same user not shown)
Line 10: Line 10:
  
 
local defaultAffinities = {
 
local defaultAffinities = {
  {Melee = 1},
+
    {Melee = 1},
  {Magic = 1},
+
    {Magic = 1},
  {Range = 1},
+
    {Range = 1},
  {Piercing = 1},
+
    {Piercing = 1},
  {Blunt = 1},
+
    {Blunt = 1},
  {Slashing = 1},
+
    {Slashing = 1},
  {Fire = 1},
+
    {Fire = 1},
  {Ice = 1},
+
    {Ice = 1},
  {Nature = 1},
+
    {Nature = 1},
  {Chaos = 1},
+
    {Chaos = 1},
  {Posion = 1},
+
    {Posion = 1},
  {Lightning = 1}
+
    {Lightning = 1}
 
}
 
}
 
local affinitiesIcon = {
 
local affinitiesIcon = {
  [1] = "[[File:Melee splash.png|20px|link=Combat#Affinities]]",
+
    [1] = "[[File:Melee splash.png|20px|link=Combat#Affinities]]",
  [2] = "[[File:Magic splash.png|20px|link=Combat#Affinities]]",
+
    [2] = "[[File:Magic splash.png|20px|link=Combat#Affinities]]",
  [3] = "[[File:Range splash.png|20px|link=Combat#Affinities]]",
+
    [3] = "[[File:Range splash.png|20px|link=Combat#Affinities]]",
  [4] = "[[File:Stab splash.png|20px|link=Combat#Affinities]]",
+
    [4] = "[[File:Stab splash.png|20px|link=Combat#Affinities]]",
  [5] = "[[File:Crush splash.png|20px|link=Combat#Affinities]]",
+
    [5] = "[[File:Crush splash.png|20px|link=Combat#Affinities]]",
  [6] = "[[File:Slash splash.png|20px|link=Combat#Affinities]]",
+
    [6] = "[[File:Slash splash.png|20px|link=Combat#Affinities]]",
  [7] = "[[File:Fire_splash.png|20px|link=Combat#Affinities]]",
+
    [7] = "[[File:Fire_splash.png|20px|link=Combat#Affinities]]",
  [8] = "[[File:Ice_splash.png|20px|link=Combat#Affinities]]",
+
    [8] = "[[File:Ice_splash.png|20px|link=Combat#Affinities]]",
  [9] = "[[File:Nature_splash.png|20px|link=Combat#Affinities]]",
+
    [9] = "[[File:Nature_splash.png|20px|link=Combat#Affinities]]",
  [10] = "[[File:Chaos_splash.png|20px|link=Combat#Affinities]]",
+
    [10] = "[[File:Chaos_splash.png|20px|link=Combat#Affinities]]",
  [11] = "[[File:Poison_splash.png|20px|link=Combat#Affinities]]",
+
    [11] = "[[File:Poison_splash.png|20px|link=Combat#Affinities]]",
  [12] = "[[File:Lightning_splash.png|20px|link=Combat#Affinities]]"
+
    [12] = "[[File:Lightning_splash.png|20px|link=Combat#Affinities]]"
 
}
 
}
  
 
-- Convert from CSV string to table (converts a single line of a CSV file)
 
-- Convert from CSV string to table (converts a single line of a CSV file)
 
local function fromCSV(s)
 
local function fromCSV(s)
  if string.sub(s, -1) ~= ',' then
+
    if string.sub(s, -1) ~= ',' then
    s = s .. ',' -- ending comma
+
        s = s .. ',' -- ending comma
  end
+
    end
  s = string.gsub(s, "[%[%]]", ""):gsub(",%s", ",")
+
    s = string.gsub(s, "[%[%]]", ""):gsub(",%s", ",")
  local t = {} -- table to collect fields
+
    local t = {} -- table to collect fields
  local fieldstart = 1
+
    local fieldstart = 1
  repeat
+
    repeat
    local nexti = string.find(s, ',', fieldstart)
+
        local nexti = string.find(s, ',', fieldstart)
    table.insert(t, string.sub(s, fieldstart, nexti - 1))
+
        table.insert(t, string.sub(s, fieldstart, nexti - 1))
    fieldstart = nexti + 1
+
        fieldstart = nexti + 1
  until fieldstart > string.len(s)
+
    until fieldstart > string.len(s)
  return t
+
    return t
 
end
 
end
  
 
local function pairsByKeys(t, f)
 
local function pairsByKeys(t, f)
  local a = {}
+
    local a = {}
  local orgi_key_type
+
    local orgi_key_type
  for n in pairs(t) do
+
    for n in pairs(t) do
    if tonumber(n) == nil then
+
        if tonumber(n) == nil then
      table.insert(a, n)
+
            table.insert(a, n)
      orgi_key_type = "word"
+
            orgi_key_type = "word"
    elseif type(n) == "number" then
+
        elseif type(n) == "number" then
      table.insert(a, n)
+
            table.insert(a, n)
      orgi_key_type = "int"
+
            orgi_key_type = "int"
    elseif type(n) == "string" and type(tonumber(n) == "number") then
+
        elseif type(n) == "string" and type(tonumber(n) == "number") then
      orgi_key_type = "number"
+
            orgi_key_type = "number"
      table.insert(a, tonumber(n))
+
            table.insert(a, tonumber(n))
 +
        end
 
     end
 
     end
  end
+
    table.sort(a, f)
  table.sort(a, f)
+
    local key
  local key
+
    local value
  local value
+
    local i = 0 -- iterator variable
  local i = 0 -- iterator variable
+
    local iter = function() -- iterator function
  local iter = function() -- iterator function
+
        i = i + 1
    i = i + 1
+
        if a[i] == nil then
    if a[i] == nil then
+
            return nil
      return nil
+
        elseif orgi_key_type == "word" or orgi_key_type == "int" then
    elseif orgi_key_type == "word" or orgi_key_type == "int" then
+
            key = a[i]
      key = a[i]
+
            value = t[a[i]]
      value = t[a[i]]
+
        elseif orgi_key_type == "number" then
    elseif orgi_key_type == "number" then
+
            key = tostring(a[i])
      key = tostring(a[i])
+
            value = t[tostring(a[i])]
      value = t[tostring(a[i])]
+
        end
 +
        return key, value
 
     end
 
     end
     return key, value
+
     return iter
  end
 
  return iter
 
 
end
 
end
  
 
local function tchelper(first, rest)
 
local function tchelper(first, rest)
  return first:upper() .. rest:lower()
+
    return first:upper() .. rest:lower()
 
end
 
end
  
 
local function capitalize(s)
 
local function capitalize(s)
  s = s:gsub("(%a)([%w_']*)", tchelper):gsub(" Of ", " of "):gsub(" The ", " the "):gsub("Ii", "II")
+
    s = s
  return s
+
        :gsub("^(%a)([%w_']*)", tchelper)
 +
        :gsub("( %a)([%w_']*)", tchelper)
 +
        :gsub(" Of ", " of ")
 +
        :gsub(" The ", " the ")
 +
        :gsub(" A(n?) ", " a%1 ")
 +
        :gsub("Ii+$", string.upper)
 +
        :gsub("Ii+[ <]+", string.upper)
 +
    return s
 
end
 
end
  
 
local function tablelength(T)
 
local function tablelength(T)
  local count = 0
+
    local count = 0
  for _ in pairs(T) do
+
    for _ in pairs(T) do
    count = count + 1
+
        count = count + 1
  end
+
    end
  return count
+
    return count
 
end
 
end
  
 
local function h()
 
local function h()
  local s = "header" .. headerCount
+
    local s = "header" .. headerCount
  headerCount = headerCount + 1
+
    headerCount = headerCount + 1
  labelCount = headerCount
+
    labelCount = headerCount
  dataCount = headerCount
+
    dataCount = headerCount
  return s
+
    return s
 
end
 
end
  
 
local function sbreak()
 
local function sbreak()
  local s = "sbreak" .. headerCount
+
    local s = "sbreak" .. headerCount
  headerCount = headerCount + 1
+
    headerCount = headerCount + 1
  labelCount = headerCount
+
    labelCount = headerCount
  dataCount = headerCount
+
    dataCount = headerCount
  return s
+
    return s
 
end
 
end
  
 
local function l()
 
local function l()
  local s = "label" .. labelCount
+
    local s = "label" .. labelCount
  dataCount = labelCount
+
    dataCount = labelCount
  labelCount = labelCount + 1
+
    labelCount = labelCount + 1
  headerCount = labelCount
+
    headerCount = labelCount
  return s
+
    return s
 
end
 
end
  
 
local function d()
 
local function d()
  local s = "data" .. dataCount
+
    local s = "data" .. dataCount
  dataCount = dataCount + 1
+
    dataCount = dataCount + 1
  headerCount = dataCount
+
    headerCount = dataCount
  labelCount = dataCount
+
    labelCount = dataCount
  return s
+
    return s
 
end
 
end
  
 
local function sl()
 
local function sl()
  local s = "s" .. l {}
+
    local s = "s" .. l {}
  return s
+
    return s
 
end
 
end
  
 
local function sd()
 
local function sd()
  local s = "s" .. d {}
+
    local s = "s" .. d {}
  return s
+
    return s
 
end
 
end
  
 
local function rc()
 
local function rc()
  local s = "rowclass" .. labelCount
+
    local s = "rowclass" .. labelCount
  return s
+
    return s
 
end
 
end
  
Line 157: Line 164:
 
---@return table? # The monster object, or nil if not found.
 
---@return table? # The monster object, or nil if not found.
 
local function getMonster(id)
 
local function getMonster(id)
  local monster = monstersData[tostring(id)]
+
    local monster = monstersData[tostring(id)]
  if monster then
+
    if monster then
    return monster
+
        return monster
  end
+
    end
 
end
 
end
  
 
local function getMonsterStats(id)
 
local function getMonsterStats(id)
  local monsterStats = monstersStats[tostring(id)]
+
    local monsterStats = monstersStats[tostring(id)]
  if not monsterStats then
 
    monsterStats =  mw.loadData("Module:Monsters stats dungeon/data")[tostring(id)]
 
 
     if not monsterStats then
 
     if not monsterStats then
      monsterStats = mw.loadData("Module:Monsters stats normal/data")[tostring(id)]
+
        monsterStats =  mw.loadData("Module:Monsters stats dungeon/data")[tostring(id)]
      if not monsterStats then
+
        if not monsterStats then
        return nil
+
            monsterStats = mw.loadData("Module:Monsters stats normal/data")[tostring(id)]
      end
+
            if not monsterStats then
 +
                return nil
 +
            end
 +
        end
 
     end
 
     end
  end
+
    return monsterStats
  return monsterStats
 
 
end
 
end
  
 
local function calcThreat(monster)
 
local function calcThreat(monster)
  local damageThreat = 2
+
    local damageThreat = 2
  local weaponThreat = 3
+
    local weaponThreat = 3
  local armorThreat = 5
+
    local armorThreat = 5
  local attackSpeedThreat = 3
+
    local attackSpeedThreat = 3
  local attackSpeedThreatLevel = 2.4 / monster.attackSpeed
+
    local attackSpeedThreatLevel = 2.4 / monster.attackSpeed
  local attackSpeedThreatFinal = attackSpeedThreatLevel * attackSpeedThreat
+
    local attackSpeedThreatFinal = attackSpeedThreatLevel * attackSpeedThreat
  local potentialDamageThreatFinal = (monster.attack + monster.strength + monster.magic + monster.range) * damageThreat
+
    local potentialDamageThreatFinal = (monster.attack + monster.strength + monster.magic + monster.range) * damageThreat
  local weaponThreatFinal = (monster.weapon.dexterity + monster.weapon.intellect + monster.weapon.strength) * weaponThreat
+
    local weaponThreatFinal = (monster.weapon.dexterity + monster.weapon.intellect + monster.weapon.strength) * weaponThreat
  local targetArmorRating = monster.armor.protection + monster.armor.resistance + monster.armor.agility * 1.5
+
    local targetArmorRating = monster.armor.protection + monster.armor.resistance + monster.armor.agility * 1.5
  local armorThreatFinal = (targetArmorRating + monster.defense * 10) * armorThreat
+
    local armorThreatFinal = (targetArmorRating + monster.defense * 10) * armorThreat
  local baseThreat = (potentialDamageThreatFinal + weaponThreatFinal) * attackSpeedThreatFinal + armorThreatFinal
+
    local baseThreat = (potentialDamageThreatFinal + weaponThreatFinal) * attackSpeedThreatFinal + armorThreatFinal
  return math.floor(baseThreat)
+
    return math.floor(baseThreat)
 
end
 
end
  
 
local function fullUrl(url)
 
local function fullUrl(url)
  local newUrl = url
+
    local newUrl = url
  if url:sub(1, 5) == "https" then
+
    if url:sub(1, 5) == "https" then
 +
        return newUrl
 +
    end
 +
    if url:sub(1, 1) ~= "/" then
 +
        newUrl = "/" .. newUrl
 +
    end
 +
    newUrl = "https://www.play.idlescape.com" .. newUrl
 
     return newUrl
 
     return newUrl
  end
 
  if url:sub(1, 1) ~= "/" then
 
    newUrl = "/" .. newUrl
 
  end
 
  newUrl = "https://www.play.idlescape.com" .. newUrl
 
  return newUrl
 
 
end
 
end
  
 
local function createImgTag(monster)
 
local function createImgTag(monster)
  local attrs = {
+
    local attrs = {
    src = fullUrl(monster.image),
+
        src = fullUrl(monster.image),
    alt = monster.name,
+
        alt = monster.name,
    width = 150
+
        width = 150
  }
+
    }
  local img = mw.html.create('img'):attr(attrs)
+
    local img = mw.html.create('img'):attr(attrs)
  img = tostring(img):gsub("<img(.-) */>", "<img%1>")
+
    img = tostring(img):gsub("<img(.-) */>", "<img%1>")
  return img
+
    return img
 
end
 
end
  
Line 220: Line 227:
 
--- @return string The formatted number as a string.
 
--- @return string The formatted number as a string.
 
local function toFixed(num, digits)
 
local function toFixed(num, digits)
  digits = math.max(0, math.floor(digits))
+
    digits = math.max(0, math.floor(digits))
  local formatted = string.format("%." .. digits .. "f", num):gsub("%.?0+$", "")
+
    local formatted = string.format("%." .. digits .. "f", num):gsub("%.?0+$", "")
  return formatted
+
    return formatted
 
end
 
end
  
 
local function createOffAffinity(args, monsterStats)
 
local function createOffAffinity(args, monsterStats)
  for index, value in pairsByKeys(defaultAffinities) do
+
    for index, value in pairsByKeys(defaultAffinities) do
    for affinity, affinityValue in pairs(value) do
+
        for affinity, affinityValue in pairs(value) do
      if monsterStats.offensiveDamageAffinity[affinity] then
+
            if monsterStats.offensiveDamageAffinity[affinity] then
        affinityValue = monsterStats.offensiveDamageAffinity[affinity]
+
                affinityValue = monsterStats.offensiveDamageAffinity[affinity]
        args[sl()] = affinitiesIcon[index]
+
                args[sl()] = affinitiesIcon[index]
        if affinityValue > 1 then
+
                if affinityValue > 1 then
          args[sd()] = "<span style=\"color:#4caf50\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
+
                    args[sd()] = "<span style=\"color:#4caf50\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
        elseif affinityValue < 1 then
+
                elseif affinityValue < 1 then
          args[sd()] = "<span style=\"color:#f44336\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
+
                    args[sd()] = "<span style=\"color:#f44336\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
        else
+
                else
          args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
+
                    args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 +
                end
 +
            else
 +
                args[sl()] = affinitiesIcon[index]
 +
                args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 +
            end
 +
            if index % 3 == 0 then
 +
                args["bodyclass"] = "equal-space"
 +
                args[sbreak()] = "yes"
 +
            end
 
         end
 
         end
      else
 
        args[sl()] = affinitiesIcon[index]
 
        args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 
      end
 
      if index % 3 == 0 then
 
        args["bodyclass"] = "equal-space"
 
        args[sbreak()] = "yes"
 
      end
 
 
     end
 
     end
  end
+
    return args
  return args
 
 
end
 
end
  
 
local function createDeffAffinity(args, monsterStats)
 
local function createDeffAffinity(args, monsterStats)
  for index, value in pairsByKeys(defaultAffinities) do
+
    for index, value in pairsByKeys(defaultAffinities) do
    for affinity, affinityValue in pairs(value) do
+
        for affinity, affinityValue in pairs(value) do
      if monsterStats.defensiveDamageAffinity[affinity] then
+
            if monsterStats.defensiveDamageAffinity[affinity] then
        affinityValue = monsterStats.defensiveDamageAffinity[affinity]
+
                affinityValue = monsterStats.defensiveDamageAffinity[affinity]
        args[sl()] = affinitiesIcon[index]
+
                args[sl()] = affinitiesIcon[index]
        if affinityValue > 1 then
+
                if affinityValue > 1 then
          args[sd()] = "<span style=\"color:#4caf50\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
+
                    args[sd()] = "<span style=\"color:#4caf50\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
        elseif affinityValue < 1 then
+
                elseif affinityValue < 1 then
          args[sd()] = "<span style=\"color:#f44336\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
+
                    args[sd()] = "<span style=\"color:#f44336\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
        else
+
                else
          args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
+
                    args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 +
                end
 +
            else
 +
                args[sl()] = affinitiesIcon[index]
 +
                args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 +
            end
 +
            if index % 3 == 0 then
 +
                args["bodyclass"] = "equal-space"
 +
                args[sbreak()] = "yes"
 +
            end
 
         end
 
         end
      else
 
        args[sl()] = affinitiesIcon[index]
 
        args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 
      end
 
      if index % 3 == 0 then
 
        args["bodyclass"] = "equal-space"
 
        args[sbreak()] = "yes"
 
      end
 
 
     end
 
     end
  end
+
    return args
  return args
 
 
end
 
end
  
  
 
---Creates a Wikitext link for a list of titles.
 
---Creates a Wikitext link for a list of titles.
---@param titles string[] # The list of titles to link to.
+
---@param titles string[]|string # The list of titles to link to.
 
---@param capitalize_ boolean|nil # If true, capitalizes the first letter of each word.
 
---@param capitalize_ boolean|nil # If true, capitalizes the first letter of each word.
 
---@return string # The Wikitext links.
 
---@return string # The Wikitext links.
 
local function createWikitextLinks(titles, capitalize_)
 
local function createWikitextLinks(titles, capitalize_)
  local s = ""
+
    if type(titles) == "string" then
  if titles then
+
        titles = {titles}
    for index, value in ipairs(titles) do
+
    end
      local title = capitalize_ and capitalize(value) or value
+
    local s = ""
      s = s .. "[[" .. title .. "]]"
+
    if titles then
      if tablelength(titles) > index then
+
        for index, value in ipairs(titles) do
        s = s .. ", "
+
            local title = capitalize_ and capitalize(value) or value
      end
+
            s = s .. "[[" .. title .. "]]"
 +
            if tablelength(titles) > index then
 +
                s = s .. ", "
 +
            end
 +
        end
 
     end
 
     end
  end
+
    return s
  return s
 
 
end
 
end
  
Line 302: Line 312:
 
---@return string # The Wikitext link with the image.
 
---@return string # The Wikitext link with the image.
 
local function createWikitextImage(title, imageAttributes, divCSS)
 
local function createWikitextImage(title, imageAttributes, divCSS)
  divCSS = divCSS or {}
+
    divCSS = divCSS or {}
  divCSS["display"] = divCSS["display"] or "inline-block" --Makes the div same size as the image
+
    divCSS["display"] = divCSS["display"] or "inline-block" --Makes the div same size as the image
  local e = mw.html.create("div")
+
    local e = mw.html.create("div")
    :css(divCSS)
+
        :css(divCSS)
    :tag("img")
+
        :tag("img")
    :attr(imageAttributes)
+
        :attr(imageAttributes)
    :done()
+
        :done()
  e = tostring(e):gsub("<img(.-) */>", "<img%1>")
+
    e = tostring(e):gsub("<img(.-) */>", "<img%1>")
  return string.format('[[%s|%s]]', title, e)
+
    return string.format('[[%s|%s]]', title, e)
 
end
 
end
  
Line 318: Line 328:
 
---@return string # The Wikitext of links and images for the abilities.
 
---@return string # The Wikitext of links and images for the abilities.
 
local function createAbilityRotation(abilities, unique)
 
local function createAbilityRotation(abilities, unique)
  local colors = {
+
    local colors = {
    Melee = "red",
+
        Melee = "red",
    Range = "green",
+
        Range = "green",
    Magic = "blue",
+
        Magic = "blue",
  }
+
    }
  
  local seenIds = {}
+
    local seenIds = {}
  local images = {}
+
    local images = {}
  for _, ability in ipairs(abilities) do
+
    for _, ability in ipairs(abilities) do
    if not seenIds[ability.id] then
+
        if not seenIds[ability.id] then
      seenIds[ability.id] = unique and true or false
+
            seenIds[ability.id] = unique and true or false
      local a = abilitiesData[tostring(ability.id)]
+
            local a = abilitiesData[tostring(ability.id)]
      local attributes = {
+
            local attributes = {
        src="https://www.play.idlescape.com" .. a.abilityImage,
+
                src="https://www.play.idlescape.com" .. a.abilityImage,
        alt=a.abilityName .. ". " .. a.description,
+
                alt=a.abilityName .. ". " .. a.description,
        width=30
+
                width=30
      }
+
            }
      local css = {
+
            local css = {
        ["box-shadow"]=string.format("0 0 2px 1px %s", colors[a.damageType] or "white"),
+
                ["box-shadow"]=string.format("0 0 2px 1px %s", colors[a.damageType] or "white"),
        background=colors[a.damageType] or "white",
+
                background=colors[a.damageType] or "white",
        margin="5px"
+
                margin="5px"
      }
+
            }
      table.insert(images, createWikitextImage(a.abilityName, attributes, css))
+
            table.insert(images, createWikitextImage(a.abilityName, attributes, css))
 +
        end
 
     end
 
     end
  end
 
  
  return table.concat(images, "")
+
    return table.concat(images, "")
 
end
 
end
  
Line 351: Line 361:
 
---@return {dps: number, primaryDamageType: string}
 
---@return {dps: number, primaryDamageType: string}
 
local function getDamageInfo(stats)
 
local function getDamageInfo(stats)
  local dps = 0
+
    local dps = 0
  local timeLeft = 1
+
    local timeLeft = 1
  local attackSpeed = stats.attackSpeed
+
    local attackSpeed = stats.attackSpeed
  local str = stats.strength * 2 + (stats.masteryStrength or 0) + stats.weapon.strength * 2
+
    local str = stats.strength * 2 + (stats.masteryStrength or 0) + stats.weapon.strength * 2
  local mgc = stats.magic * 2 + (stats.masteryMagic or 0) + stats.weapon.intellect * 2
+
    local mgc = stats.magic * 2 + (stats.masteryMagic or 0) + stats.weapon.intellect * 2
  local rng = stats.range * 2 + (stats.masteryRange or 0) + stats.weapon.dexterity * 2
+
    local rng = stats.range * 2 + (stats.masteryRange or 0) + stats.weapon.dexterity * 2
  local minHitMult = stats.hitMults.minimum
+
    local minHitMult = stats.hitMults.minimum
  local maxHitMult = stats.hitMults.maximum
+
    local maxHitMult = stats.hitMults.maximum
  local meleeAbilityCount = 0
+
    local meleeAbilityCount = 0
  local magicAbilityCount = 0
+
    local magicAbilityCount = 0
  local rangeAbilityCount = 0
+
    local rangeAbilityCount = 0
  local bestOffensiveScaling = {[1]="none", [2]=0}
+
    local bestOffensiveScaling = {[1]="none", [2]=0}
  local critChance = stats.offensiveCritical.chance
+
    local critChance = stats.offensiveCritical.chance
  local critDamage = stats.offensiveCritical.damageMultiplier
+
    local critDamage = stats.offensiveCritical.damageMultiplier
  
  for _, value in pairs(stats.abilities or {}) do
+
    for _, value in pairs(stats.abilities or {}) do
    local ability = abilitiesData[tostring(value.id)]
+
        local ability = abilitiesData[tostring(value.id)]
    if ability then
+
        if ability then
      local min = minHitMult * ability.baseMinimumDamageCoeff
+
            local min = minHitMult * ability.baseMinimumDamageCoeff
      local max = maxHitMult * ability.baseMaximumDamageCoeff
+
            local max = maxHitMult * ability.baseMaximumDamageCoeff
      local damage = 0
+
            local damage = 0
  
      if ability.damageType == "Magic" then
+
            if ability.damageType == "Magic" then
        damage = (min * mgc + max * mgc) / 2
+
                damage = (min * mgc + max * mgc) / 2
        magicAbilityCount = magicAbilityCount + 1
+
                magicAbilityCount = magicAbilityCount + 1
      elseif ability.damageType == "Melee" then
+
            elseif ability.damageType == "Melee" then
        damage = (min * str + max * str) / 2
+
                damage = (min * str + max * str) / 2
        meleeAbilityCount = meleeAbilityCount + 1
+
                meleeAbilityCount = meleeAbilityCount + 1
      elseif ability.damageType == "Range" then
+
            elseif ability.damageType == "Range" then
        damage = (min * rng + max * rng) / 2
+
                damage = (min * rng + max * rng) / 2
        rangeAbilityCount = rangeAbilityCount + 1
+
                rangeAbilityCount = rangeAbilityCount + 1
      end
+
            end
  
      local damageBonus = 0
+
            local damageBonus = 0
      for _, damageScale in ipairs(ability.damageScaling) do
+
            for _, damageScale in ipairs(ability.damageScaling) do
        local scalingHere = (stats.offensiveDamageAffinity[damageScale.affinity] or 1) * damageScale.scaling
+
                local scalingHere = (stats.offensiveDamageAffinity[damageScale.affinity] or 1) * damageScale.scaling
        if scalingHere > 1 then
+
                if scalingHere > 1 then
          damageBonus = damageBonus + scalingHere
+
                    damageBonus = damageBonus + scalingHere
        end
+
                end
        if scalingHere > bestOffensiveScaling[2] then
+
                if scalingHere > bestOffensiveScaling[2] then
          bestOffensiveScaling = {damageScale.affinity, scalingHere}
+
                    bestOffensiveScaling = {damageScale.affinity, scalingHere}
        end
+
                end
      end
+
            end
  
      damageBonus = damageBonus / (#ability.damageScaling > 0 and #ability.damageScaling or 1)
+
            damageBonus = damageBonus / (#ability.damageScaling > 0 and #ability.damageScaling or 1)
      damage = damage * damageBonus
+
            damage = damage * damageBonus
      damage = damage * (critChance * critDamage + (1 - critChance))
+
            damage = damage * (critChance * critDamage + (1 - critChance))
  
      local dot = ability.healthChangeEvent
+
            local dot = ability.healthChangeEvent
      if dot and not dot.targetsSelf and not ability.targetFriendly then
+
            if dot and not dot.targetsSelf and not ability.targetFriendly then
        damage = damage + dot.dotCount * -dot.healthChange
+
                damage = damage + dot.dotCount * -dot.healthChange
      end
+
            end
  
      local abilitySpeed = attackSpeed * ability.baseSpeedCoeff
+
            local abilitySpeed = attackSpeed * ability.baseSpeedCoeff
      local cooldown = (ability.cooldown or 0) / 1000
+
            local cooldown = (ability.cooldown or 0) / 1000
      if cooldown == 0 or abilitySpeed / cooldown > timeLeft then
+
            if cooldown == 0 or abilitySpeed / cooldown > timeLeft then
        dps = dps + (damage / abilitySpeed) * timeLeft
+
                dps = dps + (damage / abilitySpeed) * timeLeft
        timeLeft = 0
+
                timeLeft = 0
        break
+
                break
      end
+
            end
  
      dps = dps + damage / cooldown
+
            dps = dps + damage / cooldown
      timeLeft = timeLeft - abilitySpeed / cooldown
+
            timeLeft = timeLeft - abilitySpeed / cooldown
 +
        end
 
     end
 
     end
  end
 
  
  local rotationFocus
+
    local rotationFocus
  if meleeAbilityCount > magicAbilityCount + rangeAbilityCount then
+
    if meleeAbilityCount > magicAbilityCount + rangeAbilityCount then
    rotationFocus = "Melee"
+
        rotationFocus = "Melee"
  elseif magicAbilityCount > meleeAbilityCount + rangeAbilityCount then
+
    elseif magicAbilityCount > meleeAbilityCount + rangeAbilityCount then
    rotationFocus = "Magic"
+
        rotationFocus = "Magic"
  elseif rangeAbilityCount > meleeAbilityCount + magicAbilityCount then
+
    elseif rangeAbilityCount > meleeAbilityCount + magicAbilityCount then
    rotationFocus = "Range"
+
        rotationFocus = "Range"
  else
+
    else
    rotationFocus = "Hybrid"
+
        rotationFocus = "Hybrid"
  end
+
    end
  
  local dataToReturn = {dps = dps, primaryDamageType = rotationFocus}
+
    local dataToReturn = {dps = dps, primaryDamageType = rotationFocus}
  return dataToReturn
+
    return dataToReturn
 
end
 
end
  
---Replaces all instances of a value in an array with another value.
+
---Replaces all instances of values in an array with other values.
 
---@param array any[] # The array to search.
 
---@param array any[] # The array to search.
---@param to any # The value to replace.
+
---@param tbl { from: any, to: any }[] # The table of values to replace.
---@param from any # The value to replace with.
 
 
---@return any[] # The modified array.
 
---@return any[] # The modified array.
local function replaceArrayElement(array, to, from)
+
local function replaceArrayElements(array, tbl)
  local newArray = {}
+
    local newArray = {}
  for i, v in ipairs(newArray) do
+
    for i, v in ipairs(array or {}) do
    if v == to then
+
        for _, t in ipairs(tbl or {}) do
      newArray[i] = from
+
            if v == t.from then
    else
+
                newArray[i] = t.to
      newArray[i] = v
+
            else
 +
                newArray[i] = v
 +
            end
 +
        end
 
     end
 
     end
  end
+
    return newArray
  return newArray
 
 
end
 
end
  
Line 457: Line 468:
 
---@return table # mw.html element; the infobox table.
 
---@return table # mw.html element; the infobox table.
 
local function createInfobox(monster, monsterStats, zones, dps, unique)
 
local function createInfobox(monster, monsterStats, zones, dps, unique)
  local args = {}
+
    local args = {}
  local species = replaceArrayElement(monsterStats.species, "shrimp", "shrimp (species)")
+
    local replace = {
 +
        {from = "shrimp", to = "shrimp (species)|Shrimp"},
 +
        {from = "goblin", to = "goblin (species)|Goblin"},
 +
    }
 +
    local species = replaceArrayElements(monsterStats.species, replace)
 +
    species = (species[1] or ""):gsub("^%l", string.upper)
  
  args.autoheaders = "y"
+
    args.autoheaders = "y"
  args.subbox = "no"
+
    args.subbox = "no"
  args.bodystyle = " "
+
    args.bodystyle = " "
  args.title = monster.name
+
    args.title = monster.name
  args.image = createImgTag(monster)
+
    args.image = createImgTag(monster)
  args[l()] = "Zones"
+
    args[l()] = "Zones"
  args[d()] = createWikitextLinks(zones)
+
    args[d()] = createWikitextLinks(zones)
  args[l()] = "Species"
+
    args[l()] = "Species"
  args[d()] = createWikitextLinks(species, monsterStats.species ~= "shrimp")
+
    args[d()] = createWikitextLinks(species)
  if monsterStats.abilities then
+
    if monsterStats.abilities then
    args[h()] = unique and "Ability list" or "Abilities"
+
        args[h()] = unique and "Ability List" or "Ability Rotation"
    args[sd()] = createAbilityRotation(monsterStats.abilities, unique)
+
        args[sd()] = createAbilityRotation(monsterStats.abilities, unique)
  end
+
    end
  args[h()] = "Offensive Stats"
+
    args[h()] = "Offensive Stats"
  args[sl()] = "Attack Speed"
+
    args[sl()] = "Attack Speed"
  args[sd()] = tostring(monsterStats.attackSpeed)
+
    args[sd()] = tostring(monsterStats.attackSpeed)
  args[sl()] = "DPS"
+
    args[sl()] = "DPS"
  args[sd()] = tostring(dps)
+
    args[sd()] = tostring(dps)
  args[sl()] = "Crit Chance"
+
    args[sl()] = "Crit Chance"
  args[sd()] = tostring(monsterStats.offensiveCritical.chance * 100) .. "%"
+
    args[sd()] = tostring(monsterStats.offensiveCritical.chance * 100) .. "%"
  args[sl()] = "Crit Multiplier"
+
    args[sl()] = "Crit Multiplier"
  args[sd()] = tostring(monsterStats.offensiveCritical.damageMultiplier) .. "x"
+
    args[sd()] = tostring(monsterStats.offensiveCritical.damageMultiplier) .. "x"
  args[h()] = "Offensive Affinities"
+
    args[h()] = "Offensive Affinities"
  createOffAffinity(args, monsterStats)
+
    createOffAffinity(args, monsterStats)
  args[h()] = "Defensive Stats"
+
    args[h()] = "Defensive Stats"
  args[sl()] = "Threat"
+
    args[sl()] = "Threat"
  args[sd()] = calcThreat(monsterStats)
+
    args[sd()] = calcThreat(monsterStats)
  args[sl()] = "Crit Avoidance"
+
    args[sl()] = "Crit Avoidance"
  args[sd()] = tostring(monsterStats.defensiveCritical.chance * 100) .. "%"
+
    args[sd()] = tostring(monsterStats.defensiveCritical.chance * 100) .. "%"
  args[sl()] = "Crit Reduction"
+
    args[sl()] = "Crit Reduction"
  args[sd()] = tostring(monsterStats.defensiveCritical.damageMultiplier * 100) .. "%"
+
    args[sd()] = tostring(monsterStats.defensiveCritical.damageMultiplier * 100) .. "%"
  args[h()] = "Defensive Affinities"
+
    args[h()] = "Defensive Affinities"
  createDeffAffinity(args, monsterStats)
+
    createDeffAffinity(args, monsterStats)
  
  for key, data in pairs(args) do
+
    for key, data in pairs(args) do
    if string.find(key, "data") then
+
        if string.find(key, "data") then
      args[key] = tostring(data)
+
            args[key] = tostring(data)
 +
        end
 
     end
 
     end
  end
 
  
  return infoboxModule.infobox(args)
+
    return infoboxModule.infobox(args)
 
end
 
end
  
Line 508: Line 524:
 
---@return table|nil The stats for monster that overrides, or nil if not found.
 
---@return table|nil The stats for monster that overrides, or nil if not found.
 
local function findMonsterByOverrideName(name)
 
local function findMonsterByOverrideName(name)
  for id, monster in pairs(monstersStats) do
+
    for id, monster in pairs(monstersStats) do
    for overrideName, overrideMonster in pairs(monster.overrides or {}) do
+
        for overrideName, overrideMonster in pairs(monster.overrides or {}) do
      if overrideName == name then
+
            if overrideName == name then
        return id, overrideMonster
+
                return id, overrideMonster
      end
+
            end
 +
        end
 
     end
 
     end
  end
 
 
end
 
end
  
  
 
function p.infoboxMonster(frame)
 
function p.infoboxMonster(frame)
  local args = frame:getParent().args
+
    local args = frame:getParent().args
  return p._infoboxMonster(args)
+
    return p._infoboxMonster(args)
 
end
 
end
  
 
function p._infoboxMonster(_args)
 
function p._infoboxMonster(_args)
  local name = _args[1] or _args["name"] or mw.title.getCurrentTitle().text
+
    local name = _args[1] or _args["name"] or mw.title.getCurrentTitle().text
  local zones = _args["zones"] and fromCSV(_args["zones"]) or nil
+
    local zones = _args["zones"] and fromCSV(_args["zones"]) or nil
  local unique = (_args["unique"] or _args["abilityList"]) and true or false
+
    local unique = (_args["unique"] or _args["abilityList"]) and true or false
  
  local monsterError = "<div style=\"color:red\"> No monster named '" .. name ..
+
    local monsterError = "<div style=\"color:red\"> No monster named '" .. name ..
    "'. Some of the Modules: Monsters/data, Monsters_Ids/data or Monsters_stats/data might be outdated.</div>"
+
        "'. Some of the Modules: Monsters/data, Monsters_Ids/data or Monsters_stats/data might be outdated.</div>"
  
  local overrideStats
+
    local overrideStats
  local id = findId._findId({name, "monster"})
+
    local id = findId._findId({name, "monster"})
  if id == "id not found" then
+
    if id == "id not found" then
    --Some monsters use same id as other monsters so we need to fetch their info differently
+
        --Some monsters use same id as other monsters so we need to fetch their info differently
    id, overrideStats = findMonsterByOverrideName(name)
+
        id, overrideStats = findMonsterByOverrideName(name)
     if not id then
+
        if not id then
      return monsterError
+
            return monsterError
 +
        end
 +
    end
 +
    local monster = overrideStats and {name=name, image=overrideStats.imageOverride} or getMonster(id)
 +
     if not monster then
 +
        return monsterError
 +
    end
 +
    local stats = overrideStats or getMonsterStats(id)
 +
    if not stats then
 +
        return monsterError .. "<div style=\"color:red\">Please add monster stats to Module:Monsters_stats/data " ..
 +
            "if possible or try manually making <code><nowiki>{{Infobox}}</nowiki></code></div>"
 
     end
 
     end
  end
 
  local monster = overrideStats and {name=name, image=overrideStats.imageOverride} or getMonster(id)
 
  if not monster then
 
    return monsterError
 
  end
 
  local stats = overrideStats or getMonsterStats(id)
 
  if not stats then
 
    return monsterError .. "<div style=\"color:red\">Please add monster stats to Module:Monsters_stats/data " ..
 
      "if possible or try manually making <code><nowiki>{{Infobox}}</nowiki></code></div>"
 
  end
 
  
  local dps
+
    local dps
  if _args["DPS"] then
+
    if _args["DPS"] then
    dps = _args["DPS"]
+
        dps = _args["DPS"]
  else
+
    else
    local damageInfo = getDamageInfo(stats)
+
        local damageInfo = getDamageInfo(stats)
    dps = string.format("%d (%s)", damageInfo.dps, damageInfo.primaryDamageType)
+
        dps = string.format("%d (%s)", damageInfo.dps, damageInfo.primaryDamageType)
  end
+
    end
  
  local infobox = createInfobox(monster, stats, zones, dps, unique)
+
    local infobox = createInfobox(monster, stats, zones, dps, unique)
  return infobox
+
    return infobox
 
end
 
end
  
 
return p
 
return p

Latest revision as of 18:52, 30 April 2025


local p = {}
local findId = require("Module:FindId")
local infoboxModule = require('Module:Infobox')
local abilitiesData = mw.loadData("Module:Abilities/data")
local monstersData = mw.loadData("Module:Monsters/data")
local monstersStats = mw.loadData("Module:Monsters stats/data")
local headerCount = 1
local labelCount = 1
local dataCount = 1

local defaultAffinities = {
    {Melee = 1},
    {Magic = 1},
    {Range = 1},
    {Piercing = 1},
    {Blunt = 1},
    {Slashing = 1},
    {Fire = 1},
    {Ice = 1},
    {Nature = 1},
    {Chaos = 1},
    {Posion = 1},
    {Lightning = 1}
}
local affinitiesIcon = {
    [1] = "[[File:Melee splash.png|20px|link=Combat#Affinities]]",
    [2] = "[[File:Magic splash.png|20px|link=Combat#Affinities]]",
    [3] = "[[File:Range splash.png|20px|link=Combat#Affinities]]",
    [4] = "[[File:Stab splash.png|20px|link=Combat#Affinities]]",
    [5] = "[[File:Crush splash.png|20px|link=Combat#Affinities]]",
    [6] = "[[File:Slash splash.png|20px|link=Combat#Affinities]]",
    [7] = "[[File:Fire_splash.png|20px|link=Combat#Affinities]]",
    [8] = "[[File:Ice_splash.png|20px|link=Combat#Affinities]]",
    [9] = "[[File:Nature_splash.png|20px|link=Combat#Affinities]]",
    [10] = "[[File:Chaos_splash.png|20px|link=Combat#Affinities]]",
    [11] = "[[File:Poison_splash.png|20px|link=Combat#Affinities]]",
    [12] = "[[File:Lightning_splash.png|20px|link=Combat#Affinities]]"
}

-- Convert from CSV string to table (converts a single line of a CSV file)
local function fromCSV(s)
    if string.sub(s, -1) ~= ',' then
        s = s .. ',' -- ending comma
    end
    s = string.gsub(s, "[%[%]]", ""):gsub(",%s", ",")
    local t = {} -- table to collect fields
    local fieldstart = 1
    repeat
        local nexti = string.find(s, ',', fieldstart)
        table.insert(t, string.sub(s, fieldstart, nexti - 1))
        fieldstart = nexti + 1
    until fieldstart > string.len(s)
    return t
end

local function pairsByKeys(t, f)
    local a = {}
    local orgi_key_type
    for n in pairs(t) do
        if tonumber(n) == nil then
            table.insert(a, n)
            orgi_key_type = "word"
        elseif type(n) == "number" then
            table.insert(a, n)
            orgi_key_type = "int"
        elseif type(n) == "string" and type(tonumber(n) == "number") then
            orgi_key_type = "number"
            table.insert(a, tonumber(n))
        end
    end
    table.sort(a, f)
    local key
    local value
    local i = 0 -- iterator variable
    local iter = function() -- iterator function
        i = i + 1
        if a[i] == nil then
            return nil
        elseif orgi_key_type == "word" or orgi_key_type == "int" then
            key = a[i]
            value = t[a[i]]
        elseif orgi_key_type == "number" then
            key = tostring(a[i])
            value = t[tostring(a[i])]
        end
        return key, value
    end
    return iter
end

local function tchelper(first, rest)
    return first:upper() .. rest:lower()
end

local function capitalize(s)
    s = s
        :gsub("^(%a)([%w_']*)", tchelper)
        :gsub("( %a)([%w_']*)", tchelper)
        :gsub(" Of ", " of ")
        :gsub(" The ", " the ")
        :gsub(" A(n?) ", " a%1 ")
        :gsub("Ii+$", string.upper)
        :gsub("Ii+[ <]+", string.upper)
    return s
end

local function tablelength(T)
    local count = 0
    for _ in pairs(T) do
        count = count + 1
    end
    return count
end

local function h()
    local s = "header" .. headerCount
    headerCount = headerCount + 1
    labelCount = headerCount
    dataCount = headerCount
    return s
end

local function sbreak()
    local s = "sbreak" .. headerCount
    headerCount = headerCount + 1
    labelCount = headerCount
    dataCount = headerCount
    return s
end

local function l()
    local s = "label" .. labelCount
    dataCount = labelCount
    labelCount = labelCount + 1
    headerCount = labelCount
    return s
end

local function d()
    local s = "data" .. dataCount
    dataCount = dataCount + 1
    headerCount = dataCount
    labelCount = dataCount
    return s
end

local function sl()
    local s = "s" .. l {}
    return s
end

local function sd()
    local s = "s" .. d {}
    return s
end

local function rc()
    local s = "rowclass" .. labelCount
    return s
end

---Fetches a monster object from Module:Monsters/data.
---@param id string|number # The monster's ID.
---@return table? # The monster object, or nil if not found.
local function getMonster(id)
    local monster = monstersData[tostring(id)]
    if monster then
        return monster
    end
end

local function getMonsterStats(id)
    local monsterStats = monstersStats[tostring(id)]
    if not monsterStats then
        monsterStats =  mw.loadData("Module:Monsters stats dungeon/data")[tostring(id)]
        if not monsterStats then
            monsterStats = mw.loadData("Module:Monsters stats normal/data")[tostring(id)]
            if not monsterStats then
                return nil
            end
        end
    end
    return monsterStats
end

local function calcThreat(monster)
    local damageThreat = 2
    local weaponThreat = 3
    local armorThreat = 5
    local attackSpeedThreat = 3
    local attackSpeedThreatLevel = 2.4 / monster.attackSpeed
    local attackSpeedThreatFinal = attackSpeedThreatLevel * attackSpeedThreat
    local potentialDamageThreatFinal = (monster.attack + monster.strength + monster.magic + monster.range) * damageThreat
    local weaponThreatFinal = (monster.weapon.dexterity + monster.weapon.intellect + monster.weapon.strength) * weaponThreat
    local targetArmorRating = monster.armor.protection + monster.armor.resistance + monster.armor.agility * 1.5
    local armorThreatFinal = (targetArmorRating + monster.defense * 10) * armorThreat
    local baseThreat = (potentialDamageThreatFinal + weaponThreatFinal) * attackSpeedThreatFinal + armorThreatFinal
    return math.floor(baseThreat)
end

local function fullUrl(url)
    local newUrl = url
    if url:sub(1, 5) == "https" then
        return newUrl
    end
    if url:sub(1, 1) ~= "/" then
        newUrl = "/" .. newUrl
    end
    newUrl = "https://www.play.idlescape.com" .. newUrl
    return newUrl
end

local function createImgTag(monster)
    local attrs = {
        src = fullUrl(monster.image),
        alt = monster.name,
        width = 150
    }
    local img = mw.html.create('img'):attr(attrs)
    img = tostring(img):gsub("<img(.-) */>", "<img%1>")
    return img
end

--- Formats a number up to a given number of decimal places, removing trailing zeros.
--- @param num number The number to format.
--- @param digits number The number of decimal places to include (must be a non-negative integer).
--- @return string The formatted number as a string.
local function toFixed(num, digits)
    digits = math.max(0, math.floor(digits))
    local formatted = string.format("%." .. digits .. "f", num):gsub("%.?0+$", "")
    return formatted
end

local function createOffAffinity(args, monsterStats)
    for index, value in pairsByKeys(defaultAffinities) do
        for affinity, affinityValue in pairs(value) do
            if monsterStats.offensiveDamageAffinity[affinity] then
                affinityValue = monsterStats.offensiveDamageAffinity[affinity]
                args[sl()] = affinitiesIcon[index]
                if affinityValue > 1 then
                    args[sd()] = "<span style=\"color:#4caf50\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
                elseif affinityValue < 1 then
                    args[sd()] = "<span style=\"color:#f44336\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
                else
                    args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
                end
            else
                args[sl()] = affinitiesIcon[index]
                args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
            end
            if index % 3 == 0 then
                args["bodyclass"] = "equal-space"
                args[sbreak()] = "yes"
            end
        end
    end
    return args
end

local function createDeffAffinity(args, monsterStats)
    for index, value in pairsByKeys(defaultAffinities) do
        for affinity, affinityValue in pairs(value) do
            if monsterStats.defensiveDamageAffinity[affinity] then
                affinityValue = monsterStats.defensiveDamageAffinity[affinity]
                args[sl()] = affinitiesIcon[index]
                if affinityValue > 1 then
                    args[sd()] = "<span style=\"color:#4caf50\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
                elseif affinityValue < 1 then
                    args[sd()] = "<span style=\"color:#f44336\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%</span>"
                else
                    args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
                end
            else
                args[sl()] = affinitiesIcon[index]
                args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
            end
            if index % 3 == 0 then
                args["bodyclass"] = "equal-space"
                args[sbreak()] = "yes"
            end
        end
    end
    return args
end


---Creates a Wikitext link for a list of titles.
---@param titles string[]|string # The list of titles to link to.
---@param capitalize_ boolean|nil # If true, capitalizes the first letter of each word.
---@return string # The Wikitext links.
local function createWikitextLinks(titles, capitalize_)
    if type(titles) == "string" then
        titles = {titles}
    end
    local s = ""
    if titles then
        for index, value in ipairs(titles) do
            local title = capitalize_ and capitalize(value) or value
            s = s .. "[[" .. title .. "]]"
            if tablelength(titles) > index then
                s = s .. ", "
            end
        end
    end
    return s
end

---Creates an Wikitext link with an image inside a div.
---@param title string # The title of the link.
---@param imageAttributes table # The html attributes for the image.
---@param divCSS table # The CSS styles for the div.
---@return string # The Wikitext link with the image.
local function createWikitextImage(title, imageAttributes, divCSS)
    divCSS = divCSS or {}
    divCSS["display"] = divCSS["display"] or "inline-block" --Makes the div same size as the image
    local e = mw.html.create("div")
        :css(divCSS)
        :tag("img")
        :attr(imageAttributes)
        :done()
    e = tostring(e):gsub("<img(.-) */>", "<img%1>")
    return string.format('[[%s|%s]]', title, e)
end

---Creates a Wikitext of links and images for the abilities.
---@param abilities table[] # Array of abilities, from monsters stats.
---@param unique? boolean # If falsy, creates an element for each ability in order, otherwie skips duplicate abilities.
---@return string # The Wikitext of links and images for the abilities.
local function createAbilityRotation(abilities, unique)
    local colors = {
        Melee = "red",
        Range = "green",
        Magic = "blue",
    }

    local seenIds = {}
    local images = {}
    for _, ability in ipairs(abilities) do
        if not seenIds[ability.id] then
            seenIds[ability.id] = unique and true or false
            local a = abilitiesData[tostring(ability.id)]
            local attributes = {
                src="https://www.play.idlescape.com" .. a.abilityImage,
                alt=a.abilityName .. ". " .. a.description,
                width=30
            }
            local css = {
                ["box-shadow"]=string.format("0 0 2px 1px %s", colors[a.damageType] or "white"),
                background=colors[a.damageType] or "white",
                margin="5px"
            }
            table.insert(images, createWikitextImage(a.abilityName, attributes, css))
        end
    end

    return table.concat(images, "")
end

---Gets the monster's DPS and damage type. Converted to Lua from CombatStats.tsx getAbilityInfo()
---@param stats table # ICombatStatsData
---@return {dps: number, primaryDamageType: string}
local function getDamageInfo(stats)
    local dps = 0
    local timeLeft = 1
    local attackSpeed = stats.attackSpeed
    local str = stats.strength * 2 + (stats.masteryStrength or 0) + stats.weapon.strength * 2
    local mgc = stats.magic * 2 + (stats.masteryMagic or 0) + stats.weapon.intellect * 2
    local rng = stats.range * 2 + (stats.masteryRange or 0) + stats.weapon.dexterity * 2
    local minHitMult = stats.hitMults.minimum
    local maxHitMult = stats.hitMults.maximum
    local meleeAbilityCount = 0
    local magicAbilityCount = 0
    local rangeAbilityCount = 0
    local bestOffensiveScaling = {[1]="none", [2]=0}
    local critChance = stats.offensiveCritical.chance
    local critDamage = stats.offensiveCritical.damageMultiplier

    for _, value in pairs(stats.abilities or {}) do
        local ability = abilitiesData[tostring(value.id)]
        if ability then
            local min = minHitMult * ability.baseMinimumDamageCoeff
            local max = maxHitMult * ability.baseMaximumDamageCoeff
            local damage = 0

            if ability.damageType == "Magic" then
                damage = (min * mgc + max * mgc) / 2
                magicAbilityCount = magicAbilityCount + 1
            elseif ability.damageType == "Melee" then
                damage = (min * str + max * str) / 2
                meleeAbilityCount = meleeAbilityCount + 1
            elseif ability.damageType == "Range" then
                damage = (min * rng + max * rng) / 2
                rangeAbilityCount = rangeAbilityCount + 1
            end

            local damageBonus = 0
            for _, damageScale in ipairs(ability.damageScaling) do
                local scalingHere = (stats.offensiveDamageAffinity[damageScale.affinity] or 1) * damageScale.scaling
                if scalingHere > 1 then
                    damageBonus = damageBonus + scalingHere
                end
                if scalingHere > bestOffensiveScaling[2] then
                    bestOffensiveScaling = {damageScale.affinity, scalingHere}
                end
            end

            damageBonus = damageBonus / (#ability.damageScaling > 0 and #ability.damageScaling or 1)
            damage = damage * damageBonus
            damage = damage * (critChance * critDamage + (1 - critChance))

            local dot = ability.healthChangeEvent
            if dot and not dot.targetsSelf and not ability.targetFriendly then
                damage = damage + dot.dotCount * -dot.healthChange
            end

            local abilitySpeed = attackSpeed * ability.baseSpeedCoeff
            local cooldown = (ability.cooldown or 0) / 1000
            if cooldown == 0 or abilitySpeed / cooldown > timeLeft then
                dps = dps + (damage / abilitySpeed) * timeLeft
                timeLeft = 0
                break
            end

            dps = dps + damage / cooldown
            timeLeft = timeLeft - abilitySpeed / cooldown
        end
    end

    local rotationFocus
    if meleeAbilityCount > magicAbilityCount + rangeAbilityCount then
        rotationFocus = "Melee"
    elseif magicAbilityCount > meleeAbilityCount + rangeAbilityCount then
        rotationFocus = "Magic"
    elseif rangeAbilityCount > meleeAbilityCount + magicAbilityCount then
        rotationFocus = "Range"
    else
        rotationFocus = "Hybrid"
    end

    local dataToReturn = {dps = dps, primaryDamageType = rotationFocus}
    return dataToReturn
end

---Replaces all instances of values in an array with other values.
---@param array any[] # The array to search.
---@param tbl { from: any, to: any }[] # The table of values to replace.
---@return any[] # The modified array.
local function replaceArrayElements(array, tbl)
    local newArray = {}
    for i, v in ipairs(array or {}) do
        for _, t in ipairs(tbl or {}) do
            if v == t.from then
                newArray[i] = t.to
            else
                newArray[i] = v
            end
        end
    end
    return newArray
end

---Creates an infobox for the monster.
---@param monster table # The monster object from Module:Monsters/data.
---@param monsterStats table # The monster stats object from Module:Monsters stats/data.
---@param zones string[] # The zones where the monster can be found.
---@param dps string # The monster's DPS and damage type.
---@param unique boolean # If falsy, creates an element for each ability in order, otherwie skips duplicate abilities.
---@return table # mw.html element; the infobox table.
local function createInfobox(monster, monsterStats, zones, dps, unique)
    local args = {}
    local replace = {
        {from = "shrimp", to = "shrimp (species)|Shrimp"},
        {from = "goblin", to = "goblin (species)|Goblin"},
    }
    local species = replaceArrayElements(monsterStats.species, replace)
    species = (species[1] or ""):gsub("^%l", string.upper)

    args.autoheaders = "y"
    args.subbox = "no"
    args.bodystyle = " "
    args.title = monster.name
    args.image = createImgTag(monster)
    args[l()] = "Zones"
    args[d()] = createWikitextLinks(zones)
    args[l()] = "Species"
    args[d()] = createWikitextLinks(species)
    if monsterStats.abilities then
        args[h()] = unique and "Ability List" or "Ability Rotation"
        args[sd()] = createAbilityRotation(monsterStats.abilities, unique)
    end
    args[h()] = "Offensive Stats"
    args[sl()] = "Attack Speed"
    args[sd()] = tostring(monsterStats.attackSpeed)
    args[sl()] = "DPS"
    args[sd()] = tostring(dps)
    args[sl()] = "Crit Chance"
    args[sd()] = tostring(monsterStats.offensiveCritical.chance * 100) .. "%"
    args[sl()] = "Crit Multiplier"
    args[sd()] = tostring(monsterStats.offensiveCritical.damageMultiplier) .. "x"
    args[h()] = "Offensive Affinities"
    createOffAffinity(args, monsterStats)
    args[h()] = "Defensive Stats"
    args[sl()] = "Threat"
    args[sd()] = calcThreat(monsterStats)
    args[sl()] = "Crit Avoidance"
    args[sd()] = tostring(monsterStats.defensiveCritical.chance * 100) .. "%"
    args[sl()] = "Crit Reduction"
    args[sd()] = tostring(monsterStats.defensiveCritical.damageMultiplier * 100) .. "%"
    args[h()] = "Defensive Affinities"
    createDeffAffinity(args, monsterStats)

    for key, data in pairs(args) do
        if string.find(key, "data") then
            args[key] = tostring(data)
        end
    end

    return infoboxModule.infobox(args)
end

---Finds the overriding monster with the given name.
---@param name string The name of the monster that overrides another monster.
---@return string|nil The ID of the monsters, or nil if not found.
---@return table|nil The stats for monster that overrides, or nil if not found.
local function findMonsterByOverrideName(name)
    for id, monster in pairs(monstersStats) do
        for overrideName, overrideMonster in pairs(monster.overrides or {}) do
            if overrideName == name then
                return id, overrideMonster
            end
        end
    end
end


function p.infoboxMonster(frame)
    local args = frame:getParent().args
    return p._infoboxMonster(args)
end

function p._infoboxMonster(_args)
    local name = _args[1] or _args["name"] or mw.title.getCurrentTitle().text
    local zones = _args["zones"] and fromCSV(_args["zones"]) or nil
    local unique = (_args["unique"] or _args["abilityList"]) and true or false

    local monsterError = "<div style=\"color:red\"> No monster named '" .. name ..
        "'. Some of the Modules: Monsters/data, Monsters_Ids/data or Monsters_stats/data might be outdated.</div>"

    local overrideStats
    local id = findId._findId({name, "monster"})
    if id == "id not found" then
        --Some monsters use same id as other monsters so we need to fetch their info differently
        id, overrideStats = findMonsterByOverrideName(name)
        if not id then
            return monsterError
        end
    end
    local monster = overrideStats and {name=name, image=overrideStats.imageOverride} or getMonster(id)
    if not monster then
        return monsterError
    end
    local stats = overrideStats or getMonsterStats(id)
    if not stats then
        return monsterError .. "<div style=\"color:red\">Please add monster stats to Module:Monsters_stats/data " .. 
            "if possible or try manually making <code><nowiki>{{Infobox}}</nowiki></code></div>"
    end

    local dps
    if _args["DPS"] then
        dps = _args["DPS"]
    else
        local damageInfo = getDamageInfo(stats)
        dps = string.format("%d (%s)", damageInfo.dps, damageInfo.primaryDamageType)
    end

    local infobox = createInfobox(monster, stats, zones, dps, unique)
    return infobox
end

return p