Module:Infobox Monster
Revision as of 09:38, 10 April 2025 by Demcookies (talk | contribs) (Undo revision 14570 by Demcookies (talk) uh big F. try debug on summary line :x)
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(" 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 a monster object from Module:Monsters/data. ---@param id 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 local dungeonMonsters = mw.loadData("Module:Monsters stats dungeon/data") monsterStats = dungeonMonsters[tostring(id)] if not monsterStats then monsterStats = 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 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):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 local function createWikitextLinks(titles) local s = "" if titles then for index, value in ipairs(titles) do s = s .. "[[" .. capitalize(value) .. "]]" 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() return string.format('[[%s|%s]]', title, tostring(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) 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 ---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 = {} 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(monsterStats.species) args[h()] = "Abilities" if not unique then args[sl()] = "Ability Rotation" end args[sd()] = createAbilityRotation(monsterStats.abilities, unique) 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 ---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 .. "'</div>. Some of the Modules: Monsters/data, Monsters_Ids/data or Monsters_stats/data might be outdated." 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 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