Difference between revisions of "Module:Infobox Monster"

From Idlescape Wiki
Jump to navigation Jump to search
(Updated to fetch and use monster stats from single source)
(Add precision of upto two for displaying affinities)
Line 226: Line 226:
 
   local img = mw.html.create('img'):attr(attrs)
 
   local img = mw.html.create('img'):attr(attrs)
 
   return tostring(img)
 
   return tostring(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)
 +
  return formatted:gsub("%.?0+$", "")
 
end
 
end
  
Line 235: Line 245:
 
         args[sl()] = affinitiesIcon[index]
 
         args[sl()] = affinitiesIcon[index]
 
         if affinityValue > 1 then
 
         if affinityValue > 1 then
           args[sd()] = "<span style=\"color:#4caf50\">" .. (affinityValue - 1) * 100 .. "%<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\">" .. (affinityValue - 1) * 100 .. "%<span/>"
+
           args[sd()] = "<span style=\"color:#f44336\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%<span/>"
 
         else
 
         else
           args[sd()] = (affinityValue - 1) * 100 .. "%"
+
           args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 
         end
 
         end
 
       else
 
       else
 
         args[sl()] = affinitiesIcon[index]
 
         args[sl()] = affinitiesIcon[index]
         args[sd()] = (affinityValue - 1) * 100 .. "%"
+
         args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 
       end
 
       end
 
       if index % 3 == 0 then
 
       if index % 3 == 0 then
Line 261: Line 271:
 
         args[sl()] = affinitiesIcon[index]
 
         args[sl()] = affinitiesIcon[index]
 
         if affinityValue > 1 then
 
         if affinityValue > 1 then
           args[sd()] = "<span style=\"color:#4caf50\">" .. (affinityValue - 1) * 100 .. "%<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\">" .. (affinityValue - 1) * 100 .. "%<span/>"
+
           args[sd()] = "<span style=\"color:#f44336\">" .. toFixed((affinityValue - 1) * 100, 2) .. "%<span/>"
 
         else
 
         else
           args[sd()] = (affinityValue - 1) * 100 .. "%"
+
           args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 
         end
 
         end
 
       else
 
       else
 
         args[sl()] = affinitiesIcon[index]
 
         args[sl()] = affinitiesIcon[index]
         args[sd()] = (affinityValue - 1) * 100 .. "%"
+
         args[sd()] = toFixed((affinityValue - 1) * 100, 2) .. "%"
 
       end
 
       end
 
       if index % 3 == 0 then
 
       if index % 3 == 0 then

Revision as of 21:20, 31 March 2025


local p = {}
local findId = require("Module:FindId")
local infoboxModule = require('Module:Infobox')
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
  local orgi_key_numbered
  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(" Of ", " of "):gsub(" The ", " the "):gsub("Ii", "II")
  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 an monster object from monsters/data module
---@param id number
---@return table|nil
local function getMonster(id)
  local mosnter = monstersData[tostring(id)]
  if mosnter then
    return mosnter
  end
  return nil
end

local function getMonsterStats(id)
  local monsterStats = monstersStats[tostring(id)]
  if not monsterStats then
    local dungeonMonsters = mw.loadData("Module:Monsters stats dungeon/data")
    monsterStats = dungeonMonsters[tostring(id)]
    if not monsterStats then
      local normalMonsters = mw.loadData("Module:Monsters stats normal/data")
      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 url = fullUrl(monster.image)
  local attrs = {
    src = fullUrl(monster.image),
    alt = monster.name,
    width = "150px"
  }
  local img = mw.html.create('img'):attr(attrs)
  return tostring(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)
  return formatted:gsub("%.?0+$", "")
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

local function getZonesLink(zones)
  local s = ""
  if zones then
    for index, value in ipairs(zones) do
      s = s .. "[[" .. capitalize(value) .. "]]"
      if tablelength(zones) > index then
        s = s .. ", "
      end
    end
  end
  return s
end

local function createInfobox(monster, monsterStats, zones, dps)
  local args = {}

  args.autoheaders = "y"
  args.subbox = "no"
  args.bodystyle = " "
  args.title = monster.name
  args.image = createImgTag(monster)
  args[l()] = "Zones"
  args[d()] = getZonesLink(zones)
  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 * 100) .. "%"
  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

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

function p._infoboxMonster(_args)
  local dps
  local zones
  if _args["zones"] then
    zones = fromCSV(_args["zones"])
  end
  if _args["DPS"] then
    dps = _args["DPS"]
  else
    dps = "-"
  end
  local name
  local id
  local monster
  local monsterStats
  local infobox
  if _args[1] then
    name = _args[1]
  else
    name = mw.title.getCurrentTitle().text
  end
  id = findId._findId({name, "monster"})
  if not id then
    return "<div style=\"color:red\"> No monster named '" .. name ..
             "'</div>. The Module:Monsters Ids/data maybe outdated."
  end
  monster = getMonster(id)
  if not monster then
    return "<div style=\"color:red\"> No monster named '" .. name .. "'</div>. The Module:Monsters/data maybe outdated."
  end
  monsterStats = getMonsterStats(id)
  if not monsterStats then
    return "<div style=\"color:red\"> No monster named '" .. name ..
             "'</div>. Module:Monsters stats normal/data and Module:Monsters stats dungeon/data maybe outdated."
  end
  infobox = createInfobox(monster, monsterStats, zones, dps)
  return infobox
end

return p