Anonymous

Module:Sandbox/AuronTest: Difference between revisions

From Melvor Idle
m
Test Items module without cloning in order to assess impact on memory consumption
mNo edit summary
m (Test Items module without cloning in order to assess impact on memory consumption)
Line 1: Line 1:
--This module contains all sorts of functions for getting data on items
--Several functions related to use tables can be found at Module:Items/UseTables
--Functions related to source tables can be found at Module:Items/SourceTables
--Other functions moved to Module:Items/ComparisonTables
local p = {}
local p = {}


local MonsterData = mw.loadData('Module:Monsters/data')
local ItemData = mw.loadData('Module:Items/data')


local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
local Areas = require('Module:CombatAreas')
local Magic = require('Module:Magic')
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Items = require('Module:Items')


function p.getMonster(name)
p.EasterEggs = {'Amulet of Calculated Promotion', 'Clue Chasers Insignia', '8', 'Lemon', 'Easter Egg', 'Abnormal Log', 'Red Herring', 'Cool Glasses'}
  local result = nil
p.EventItems = {'Christmas Cracker', 'Christmas Coal', 'Christmas Sweater',
  if name == 'Spider (lv. 51)' or name == 'Spider' then
'Christmas Wreath', 'Candy Cane', 'Santa Hat',
    return p.getMonsterByID(50)
'Friendship Bracelet', 'Event Clue 1', 'Event Clue 2',
  elseif name == 'Spider (lv. 52)' or name == 'Spider2' then
'Event Clue 3', 'Event Clue 4', 'Candle', 'Cake Base',
    return p.getMonsterByID(51)
'Magical Flavouring', 'Magical Icing', 'Birthday Cake',
  end
'Purple Party Hat', 'Birthday Token', 'Christmas Present (Yellow)',
'Christmas Present (Blue)', 'Christmas Present (Green)', 'Christmas Present (White)',
'Christmas Present (Purple)', 'Christmas Present (Standard)', 'Event Token - Holiday 2021',
'Holiday Scarf', 'Gingerbread House', 'Gingerbread Man', 'Edible Candy Cane',
'Locked Chest', 'Locked Chest Key', 'Event Token (Holiday 2021)'}
p.OtherShopItems = {'Cooking Gloves', 'Mining Gloves', 'Gem Gloves', 'Smithing Gloves', 'Thieving Gloves'}
--This is hardcoded, so there's no easy way to scrape it. Hopefully it doesn't change
p.GemTable = {["Topaz"] = {name = 'Topaz', id = 128, chance = 50},
["Sapphire"] = {name = "Sapphire", id = 129, chance = 17.5},
["Ruby"] = {name = "Ruby", id = 130, chance = 17.5},
["Emerald"] = {name = "Emerald", id = 131, chance = 10},
["Diamond"] = {name = "Diamond", id = 132, chance = 5}}
--The base chance to receive a gem while mining
p.GemChance = .01
--The number of different fishing junk items
p.junkCount = 8
--Items (aside from bars & gems) which can be created via Alt Magic
p.AltMagicProducts = {'Rune Essence', 'Bones', 'Holy Dust'}
--The kinds of gloves with cost & charges
p.GloveTable = {['Cooking Gloves'] = {cost=50000, charges=500},
['Mining Gloves'] = {cost=75000, charges=500},
['Smithing Gloves'] = {cost=100000, charges=500},
['Thieving Gloves'] = {cost=100000, charges=500},
['Gem Gloves'] = {cost=500000, charges=2000}}


  for i, monster in pairs(MonsterData.Monsters) do
    if(monster.name == name) then
      result = Shared.clone(monster)
      --Make sure every monster has an ID, and account for the 1-based indexing of Lua
      result.id = i - 1
      break
    end
  end
  return result
end


function p.getMonsterByID(ID)
p.specialFishWt = 6722
  local result = Shared.clone(MonsterData.Monsters[ID + 1])
p.specialFishLoot = {{128, 2000}, {129, 1600}, {130, 1400}, {131, 1000}, {132, 400}, {667, 10}, {668, 10}, {902, 1}, {670, 1}, {669, 50}, {120, 250}}
  result.id = ID
  return result
end


function p.getPassive(name)
function p.buildSpecialFishingTable()
  local result = nil
--This shouldn't ever be included in a page
--This is for generating the above 'specialFishLoot' variable if it ever needs to change
--To re-run, edit the module, type in "console.log(p.buildSpecialFishingTable())" and copy+paste the result as the new value of the variable
--Also gives you the total fishing weight for saving time later
local lootArray = {}
local totalWt = 0


  for i, passive in pairs(MonsterData.Passives) do
for i, item in pairs(ItemData.Items) do
    if passive.name == name then
if item.fishingCatchWeight ~= nil then
      result = Shared.clone(passive)
totalWt = totalWt + item.fishingCatchWeight
      --Make sure every passive has an ID, and account for the 1-based indexing of Lua
table.insert(lootArray, '{'..(i - 1)..', '..item.fishingCatchWeight..'}')
      result.id = i - 1
end
      break
end
    end
  end
  return result
end


function p.getPassiveByID(ID)
local result = 'p.specialFishWt = '..totalWt..'\r\n'
  return MonsterData.Passives[ID + 1]
result = result..'p.specialFishLoot = {'..table.concat(lootArray, ', ')..'}'
return result
end
end


-- Given a list of monster IDs, calls statFunc with each monster and returns
function p.getItemByID(ID)
-- the lowest & highest values
return ItemData.Items[ID + 1]
function p.getLowHighStat(idList, statFunc)
  local lowVal, highVal = nil, nil
  for i, monID in ipairs(idList) do
    local monster = p.getMonsterByID(monID)
    local statVal = statFunc(monster)
    if lowVal == nil or statVal < lowVal then lowVal = statVal end
    if highVal == nil or statVal > highVal then highVal = statVal end
  end
  return lowVal, highVal
end
end


function p._getMonsterStat(monster, statName)
function p.getItem(name)
  if statName == 'HP' then
name = string.gsub(name, "%%27", "'")
    return p._getMonsterHP(monster)
name = string.gsub(name, "&#39;", "'")
  elseif statName == 'maxHit' then
for i, item in ipairs(ItemData.Items) do
    return p._getMonsterMaxHit(monster)
local itemName = string.gsub(item.name, '#', '')
  elseif statName == 'accuracyRating' then
if name == itemName then
    return p._getMonsterAR(monster)
return item
  elseif statName == 'meleeEvasionRating' then
end
    return p._getMonsterER(monster, 'Melee')
end
  elseif statName == 'rangedEvasionRating' then
return nil
    return p._getMonsterER(monster, 'Ranged')
  elseif statName == 'magicEvasionRating' then
    return p._getMonsterER(monster, 'Magic')
  elseif statName == 'damageReduction' then
    return p.getEquipmentStat(monster, 'damageReduction')
  end
 
  return monster[statName]
end
end


function p.getMonsterStat(frame)
function p.getItems(checkFunc)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
local result = {}
  local StatName = frame.args ~= nil and frame.args[2] or frame[2]
local itemCount = 0
  local monster = p.getMonster(MonsterName)
for i, item in ipairs(ItemData.Items) do
  if monster == nil then
if checkFunc(item) then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
itemCount = itemCount + 1
  end
result[itemCount] = item
 
end
  return p._getMonsterStat(monster, StatName)
end
return result
end
end


function p._getMonsterStyleIcon(frame)
function p._getItemStat(item, StatName, ZeroIfNil)
  local args = frame.args ~= nil and frame.args or frame
local result = item[StatName]
  local monster = args[1]
--Special Overrides:
  local notext = args.notext
-- Equipment stats first
  local nolink = args.nolink
if Shared.contains(ItemData.EquipmentStatKeys, StatName) and item.equipmentStats ~= nil then
 
result = item.equipmentStats[StatName]
local iconText = ''
elseif StatName == 'isTwoHanded' then
if monster.attackType == 'melee' then
if item.validSlots ~= nil and item.occupiesSlots ~= nil then
iconText = Icons.Icon({'Melee', notext=notext, nolink=nolink})
result = Shared.contains(item.validSlots, 'Weapon') and Shared.contains(item.occupiesSlots, 'Shield')
elseif monster.attackType == 'ranged' then
else
iconText = Icons.Icon({'Ranged', type='skill', notext=notext, nolink=nolink})
result = false
elseif monster.attackType == 'magic' then
end
iconText = Icons.Icon({'Magic', type='skill', notext=notext, nolink=nolink})
elseif string.find(StatName, '^(.+)LevelRequired$') ~= nil and item.equipRequirements ~= nil and item.equipRequirements.Level ~= nil then
elseif monster.attackType == 'random' then
local skillName = Shared.titleCase(string.match(StatName, '^(.+)LevelRequired$'))
iconText = Icons.Icon({'Bane', notext=notext, nolink=nolink, img='Question'})
if skillName ~= nil then
local skillID = Constants.getSkillID(skillName)
if skillID ~= nil then
result = item.equipRequirements.Level[skillID]
end
end
elseif StatName == 'attackType' then
result = p._getWeaponAttackType(item)
elseif StatName == 'description' then
result = item.description
if result == nil or result == '' then result = 'No Description' end
elseif StatName == 'completionReq' then
if item.ignoreCompletion == nil or not item.ignoreCompletion then
result = 'Yes'
else
result = 'No'
end
elseif StatName == 'slayerBonusXP' then
return p._getItemModifier(item, 'increasedSkillXP', 'Slayer', false)
elseif StatName == 'hasCombatStats' then
return tostring(p.hasCombatStats(item) or p._hasLevelRequirements(item))
end
end
 
if result == nil and ZeroIfNil then result = 0 end
return iconText
return result
end
end


function p.getMonsterStyleIcon(frame)
function p.getItemStat(frame)
  local args = frame.args ~= nil and frame.args or frame
local args = frame.args ~= nil and frame.args or frame
  local MonsterName = args[1]
local ItemName = args[1]
  local monster = p.getMonster(MonsterName)
local StatName = args[2]
 
local ZeroIfNil = args.ForceZero ~= nil and args.ForceZero ~= '' and args.ForceZero ~= 'false'
  if monster == nil then
local formatNum = args.formatNum ~= nil and args.formatNum ~= '' and args.formatNum ~= 'false'
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
local item = p.getItem(ItemName)
  end
if item == nil then
 
return "ERROR: No item named "..ItemName.." exists in the data module[[Category:Pages with script errors]]"
  args[1] = monster
end
  return p._getMonsterStyleIcon(args)
local result = p._getItemStat(item, StatName, ZeroIfNil)
if formatNum then result = Shared.formatnum(result) end
return result
end
end


function p._getMonsterHP(monster)
--Gets the value of a given modifier for a given item
  return 10 * p._getMonsterLevel(monster, 'Hitpoints')
--asString is false by default, when true it writes the full bonus text
end
function p._getItemModifier(item, modifier, skill, asString)
if asString == nil then asString = false end
if skill == '' then
skill = nil
elseif type(skill) == 'string' then
skill = Constants.getSkillID(skill)
end


function p.getMonsterEffectiveHP(frame)
local result = 0
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
  if monster ~= nil then
    return math.floor((p._getMonsterHP(monster)/(1 - p._getMonsterStat(monster, 'damageReduction')/100)) + 0.5)
  else
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
end


function p.getMonsterHP(frame)
if item.modifiers ~= nil and item.modifiers[modifier] ~= nil then
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
if type(item.modifiers[modifier]) == 'table' then
  local monster = p.getMonster(MonsterName)
for i, subVal in Shared.skpairs(item.modifiers[modifier]) do
  if monster ~= nil then
if subVal[1] == skill then
    return p._getMonsterHP(monster)
result = subVal[2]
  else
break
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
end
  end
end
end
else
result = item.modifiers[modifier]
end
end


function p._getMonsterLevel(monster, skillName)
if asString then
  local result = 0
if skill ~= nil then
  if monster.levels[skillName] ~= nil then
return Constants._getModifierText(modifier, {skill, result})
    result = monster.levels[skillName]
else
  end
return Constants._getModifierText(modifier, result)
  return result
end
else
return result
end
end
end


function p.getMonsterLevel(frame)
function p.hasCombatStats(item)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
if item.isEquipment or (item.validSlots == nil and item.equipmentStats ~= nil) then
  local SkillName = frame.args ~= nil and frame.args[2] or frame[2]
-- Ensure at least one stat has a non-zero value
  local monster = p.getMonster(MonsterName)
for statName, statVal in pairs(item.equipmentStats) do
 
if statVal ~= 0 then return true end
  if monster == nil then
end
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
end
  end
return false
 
  return p._getMonsterLevel(monster, SkillName)
end
end


function p.getEquipmentStat(monster, statName)
function p._hasLevelRequirements(item)
  local result = 0
--Function true if an item has at least one level requirement to equip
  for i, stat in Shared.skpairs(monster.equipmentStats) do
if item.equipRequirements ~= nil and item.equipRequirements.Level ~= nil then
    if stat.key == statName then
for skillID, lvl in pairs(item.equipRequirements.Level) do
      result = stat.value
if lvl ~= nil and lvl > 1 then
      break
return true
    end
end
  end
end
  return result
return false
else
return false
end
end
end


function p.calculateStandardStat(effectiveLevel, bonus)
function p.getItemModifier(frame)
  --Based on calculateStandardStat in Characters.js
local itemName = frame.args ~= nil and frame.args[1] or frame[1]
  return (effectiveLevel + 9) * (bonus + 64)
local modName = frame.args ~= nil and frame.args[2] or frame[2]
end
local skillName = frame.args ~= nil and frame.args[3] or frame[3]
local asString = frame.args ~= nil and frame.args[4] or frame[4]
if asString ~= nil then
asString = (string.upper(asString) ~= 'FALSE')
end


function p.calculateStandardMaxHit(baseLevel, strengthBonus)
local item = p.getItem(itemName)
  --Based on calculateStandardMaxHit in Characters.js
if item == nil then
  local effectiveLevel = baseLevel + 9
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  return math.floor(10 * (1.3 + effectiveLevel / 10 + strengthBonus / 80 + effectiveLevel * strengthBonus / 640))
end
end


function p._getMonsterAttackSpeed(monster)
return p._getItemModifier(item, modName, skillName, asString)
  return p.getEquipmentStat(monster, 'attackSpeed') / 1000
end
end


function p.getMonsterAttackSpeed(frame)
function p._getWeaponAttackType(item)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
if item.isEquipment == true and (item.validSlots ~= nil and Shared.contains(item.validSlots, 'Weapon')) or
  local monster = p.getMonster(MonsterName)
(item.occupiesSlots ~= nil and Shared.contains(item.occupiesSlots, 'Weapon')) then
  if monster ~= nil then
if Shared.contains({'melee', 'ranged', 'magic'}, item.attackType) then
    return p._getMonsterAttackSpeed(monster)
local iconType = item.attackType ~= 'melee' and 'skill' or nil
  else
return Icons.Icon({Shared.titleCase(item.attackType), type=iconType, nolink='true'})
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
end
  end
end
return 'Invalid'
end
end


function p._getMonsterCombatLevel(monster)
function p.getWeaponAttackType(frame)
  local base = 0.25 * (p._getMonsterLevel(monster, 'Defence') + p._getMonsterLevel(monster, 'Hitpoints'))
local itemName = frame.args ~= nil and frame.args[1] or frame
  local melee = 0.325 * (p._getMonsterLevel(monster, 'Attack') + p._getMonsterLevel(monster, 'Strength'))
local item = p.getItem(itemName)
  local range = 0.325 * (1.5 * p._getMonsterLevel(monster, 'Ranged'))
if item == nil then
  local magic = 0.325 * (1.5 * p._getMonsterLevel(monster, 'Magic'))
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  if melee > range and melee > magic then
end
    return math.floor(base + melee)
return p._getWeaponAttackType(item)
  elseif range > magic then
    return math.floor(base + range)
  else
    return math.floor(base + magic)
  end
end
end


function p.getMonsterCombatLevel(frame)
function p.getPotionTable(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local potionName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local tiers = {'I', 'II', 'III', 'IV'}


  if monster == nil then
local resultPart = {}
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
table.insert(resultPart, '{| class="wikitable"')
  end
table.insert(resultPart, '\r\n!Potion!!Tier!!Charges!!Effect')


  return p._getMonsterCombatLevel(monster)
local tier1potion = p.getItem(potionName..' I')
end
if tier1potion == nil then
 
return 'ERROR: No potion named "' .. potionName .. '" was found[[Category:Pages with script errors]]'
function p._getMonsterAR(monster)
end
  local baseLevel = 0
for i, tier in pairs(tiers) do
  local bonus = 0
local tierName = potionName..' '..tier
  if monster.attackType == 'melee' then
local potion = p.getItemByID(tier1potion.id + i - 1)
    baseLevel = p._getMonsterLevel(monster, 'Attack')
if potion ~= nil then
    bonus = p.getEquipmentStat(monster, 'stabAttackBonus')
table.insert(resultPart, '\r\n|-')
  elseif monster.attackType == 'ranged' then
table.insert(resultPart, '\r\n|'..Icons.Icon({tierName, type='item', notext=true, size='60'}))
    baseLevel = p._getMonsterLevel(monster, 'Ranged')
table.insert(resultPart, '||'..Icons.Icon({tierName, tier, type='item', noicon=true}))
    bonus = p.getEquipmentStat(monster, 'rangedAttackBonus')
table.insert(resultPart, '||'..potion.potionCharges..'||'..potion.description)
  elseif monster.attackType == 'magic' then
end
    baseLevel = p._getMonsterLevel(monster, 'Magic')
end
    bonus = p.getEquipmentStat(monster, 'magicAttackBonus')
  elseif monster.attackType == 'random' then
  --Bane has the same AR with every attack type so being lazy and just showing the one.
    baseLevel = p._getMonsterLevel(monster, 'Attack')
    bonus = p.getEquipmentStat(monster, 'stabAttackBonus')
  else
    return "ERROR: This monster has an invalid attack type somehow[[Category:Pages with script errors]]"
  end


  return p.calculateStandardStat(baseLevel, bonus)
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
end


function p.getMonsterAR(frame)
function p._getOtherItemBoxText(item)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
resultPart = {}
  local monster = p.getMonster(MonsterName)
--For equipment, show the slot they go in
 
if item.validSlots ~= nil then
  if monster == nil then
local slotLinkMap = {
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
["Helmet"] = 'Equipment#Helmets',
  end
["Platebody"] = 'Equipment#Platebodies',
 
["Platelegs"] = 'Equipment#Platelegs',
  return p._getMonsterAR(monster)
["Boots"] = 'Equipment#Boots',
["Weapon"] = 'Equipment#Weapons',
["Shield"] = 'Equipment#Offhand',
["Amulet"] = 'Equipment#Amulets',
["Ring"] = 'Equipment#Rings',
["Gloves"] = 'Equipment#Gloves',
["Quiver"] = 'Equipment#Ammunition',
["Cape"] = 'Equipment#Capes',
["Passive"] = 'Combat Passive Slot',
["Summon1"] = 'Summoning',
["Summon2"] = 'Summoning'
}
local slotText = {}
for i, slot in ipairs(item.validSlots) do
local slotLink = slotLinkMap[slot]
if slotLink == nil then
table.insert(slotText, slot)
else
table.insert(slotText, '[[' .. slotLink .. '|' .. slot .. ']]')
end
end
table.insert(resultPart, "\r\n|-\r\n|'''Equipment Slot:''' "..table.concat(slotText, ', '))
end
--For weapons with a special attack, show the details
if item.hasSpecialAttack then
table.insert(resultPart, "\r\n|-\r\n|'''Special Attack:'''")
for i, spAtt in ipairs(item.specialAttacks) do
table.insert(resultPart, '\r\n* ' .. spAtt.defaultChance .. '% chance for ' .. spAtt.name .. ':')
table.insert(resultPart, '\r\n** ' .. spAtt.description)
end
end
--For potions, show the number of charges
if item.potionCharges ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''Charges:''' "..item.potionCharges)
end
--For food, show how much it heals for
if item.healsFor ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''Heals for:''' "..Icons.Icon({"Hitpoints", type="skill", notext="true"})..' '..(item.healsFor * 10))
end
--For Prayer Points, show how many you get
if item.prayerPoints ~= nil then
table.insert(resultPart, "\r\n|-\r\n|'''"..Icons.Icon({'Prayer', type='skill'}).." Points:''' "..item.prayerPoints)
end
--For items with modifiers, show what those are
if item.modifiers ~= nil and Shared.tableCount(item.modifiers) > 0 then
table.insert(resultPart, "\r\n|-\r\n|'''Modifiers:'''\r\n"..Constants.getModifiersText(item.modifiers, true))
end
return table.concat(resultPart)
end
end


function p._getMonsterER(monster, style)
function p.getOtherItemBoxText(frame)
  local baseLevel= 0
local itemName = frame.args ~= nil and frame.args[1] or frame
  local bonus = 0
local item = p.getItem(itemName)
 
local asList = false
  if style == "Melee" then
if frame.args ~= nil then
    baseLevel = p._getMonsterLevel(monster, 'Defence')
asList = frame.args.asList ~= nil and frame.args.asList ~= '' and frame.args.asList ~= 'false'
    bonus = p.getEquipmentStat(monster, 'meleeDefenceBonus')
end
  elseif style == "Ranged" then
if item == nil then
    baseLevel = p._getMonsterLevel(monster, 'Defence')
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
    bonus = p.getEquipmentStat(monster, 'rangedDefenceBonus')
end
  elseif style == "Magic" then
    baseLevel = math.floor(p._getMonsterLevel(monster, 'Magic') * 0.7 + p._getMonsterLevel(monster, 'Defence') * 0.3)
    bonus = p.getEquipmentStat(monster, 'magicDefenceBonus')
  else
    return "ERROR: Must choose Melee, Ranged, or Magic[[Category:Pages with script errors]]"
  end


  return p.calculateStandardStat(baseLevel, bonus)
return p._getOtherItemBoxText(item, asList)
end
end


function p.getMonsterER(frame)
function p._getItemCategories(item)
  local args = frame.args ~= nil and frame.args or frame
local resultPart = {}
  local MonsterName = args[1]
if item.category ~= nil then table.insert(resultPart, '[[Category:'..item.category..']]') end
  local style = args[2]
if item.type ~= nil then table.insert(resultPart, '[[Category:'..item.type..']]') end
  local monster = p.getMonster(MonsterName)
if item.tier ~= nil then table.insert(resultPart, '[[Category:'..Shared.titleCase(item.tier)..' '..item.type..']]') end
 
if item.hasSpecialAttack then table.insert(resultPart, '[[Category:Items With Special Attacks]]') end
  if monster == nil then
if item.validSlots ~= nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
local slotRemap = {
  end
['Passive'] = 'Passive Items',
 
['Summon1'] = 'Summoning Familiars',
  return p._getMonsterER(monster, style)
['Summon2'] = ''
}
for i, slotName in ipairs(item.validSlots) do
local slotRemapName = slotName
if slotRemap[slotName] ~= nil then slotRemapName = slotRemap[slotName] end
if slotRemapName ~= '' then table.insert(resultPart, '[[Category:' .. slotRemapName .. ']]') end
end
end
if item.modifiers ~= nil then
local modsDL = {
'increasedChanceToDoubleLootCombat',
'decreasedChanceToDoubleLootCombat',
'increasedChanceToDoubleLootThieving',
'decreasedChanceToDoubleLootThieving',
'increasedChanceToDoubleItemsGlobal',
'decreasedChanceToDoubleItemsGlobal'
}
for modName, val in pairs(item.modifiers) do
if Shared.contains(modsDL, modName) then
table.insert(resultPart, '[[Category:Double Loot Chance Items]]')
break
end
end
end
return table.concat(resultPart)
end
end


function p._isDungeonOnlyMonster(monster)
function p.getItemCategories(frame)
  local areaList = Areas.getMonsterAreas(monster.id)
local itemName = frame.args ~= nil and frame.args[1] or frame
  local inDungeon = false
local item = p.getItem(itemName)
if item == nil then
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
end


  for i, area in ipairs(areaList) do
return p._getItemCategories(item)
  if area.type == 'dungeon' then
    inDungeon = true
  else
    return false
  end
  end
  return inDungeon
end
end


function p.isDungeonOnlyMonster(frame)
function p.getSkillcapeTable(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local skillName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local cape = p.getItem(skillName..' Skillcape')
 
local resultPart = {}
  if monster == nil then
table.insert(resultPart, '{| class="wikitable"\r\n')
    return "ERROR: No monster with name "..monsterName.." found[[Category:Pages with script errors]]"
table.insert(resultPart, '!Skillcape!!Name!!Effect')
  end
table.insert(resultPart, '\r\n|-\r\n|'..Icons.Icon({cape.name, type='item', size='60', notext=true}))
 
table.insert(resultPart, '||'..Icons.Icon({cape.name, type='item', noicon=true})..'||'..cape.description)
  return p._isDungeonOnlyMonster(monster)
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
end


function p._getMonsterAreas(monster, excludeDungeons)
function p.getItemGrid(frame)
  local result = ''
local resultPart = {}
  local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
table.insert(resultPart, '{|')
  local areaList = Areas.getMonsterAreas(monster.id)
for i, item in Shared.skpairs(ItemData.Items) do
  for i, area in pairs(areaList) do
if i % 17 == 1 then
    if area.type ~= 'dungeon' or not hideDungeons then
table.insert(resultPart, '\r\n|-\r\n|')
      if i > 1 then result = result..'<br/>' end
else
      result = result..Icons.Icon({area.name, type = area.type})
table.insert(resultPart, '||')
    end
end
  end
table.insert(resultPart, 'style="padding:3px"|'..Icons.Icon({item.name, type='item', notext=true, size='40'}))
  return result
end
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
end


function p.getMonsterAreas(frame)
function p.getWeaponStatsBox(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local itemName = frame.args ~= nil and frame.args[1] or frame
  local hideDungeons = frame.args ~= nil and frame.args[2] or nil
local item = p.getItem(itemName)
  local monster = p.getMonster(MonsterName)
if item == nil then
 
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  if monster == nil then
end
    return "ERROR: No monster with name "..monsterName.." found[[Category:Pages with script errors]]"
  end
local ico = {
 
["Attack"] = Icons.Icon({'Attack', type='skill', notext=true}),
  return p._getMonsterAreas(monster, hideDungeons)
["Combat"] = Icons.Icon({'Combat', notext=true}),
["Defence"] = Icons.Icon({'Defence', type='skill', notext=true}),
["Magic"] = Icons.Icon({'Magic', type='skill', notext=true}),
["Ranged"] = Icons.Icon({'Ranged', type='skill', notext=true}),
["Strength"] = Icons.Icon({'Strength', type='skill', notext=true}),
["Slayer"] = Icons.Icon({'Slayer', type='skill', notext=true})
}
local resultPart = {}
table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="4" style="border-bottom:solid medium black;"| Weapon Stats')
table.insert(resultPart, '\r\n|-\r\n!colspan="2" style="border-bottom:solid thin black;"| Offensive Stats')
table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Defensive Stats')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Attack Speed')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. Shared.round(p._getItemStat(item, 'attackSpeed', true) / 1000, 3, 1) .. 's')
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'meleeDefenceBonus', true))
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| Attack Type')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'attackType'))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Damage Reduction')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'damageReduction', true) .. '%')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Strength'] .. ' Strength Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'meleeStrengthBonus', true))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedDefenceBonus', true))
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Stab Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'stabAttackBonus', true))
table.insert(resultPart, '\r\n!style="text-align:right;border-bottom:solid thin black;"| ' .. ico['Magic'] .. ' Defence Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;border-bottom:solid thin black;"| ' .. p._getItemStat(item, 'magicDefenceBonus', true))
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Slash Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'slashAttackBonus', true))
table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Other')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Block Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'blockAttackBonus', true))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Slayer'] .. ' Bonus Slayer XP')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'slayerBonusXP', true) .. '%')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Attack Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedAttackBonus', true))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Attack'] .. ' Level Required')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'attackLevelRequired', true))
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Strength Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedStrengthBonus', true))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Level Required')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedLevelRequired', true))
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' Attack Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'magicAttackBonus', true))
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' Level Required')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'magicLevelRequired', true))
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' % Damage Bonus')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'magicDamageBonus', true) .. '%')
table.insert(resultPart, '\r\n!style="text-align:right;"| Two Handed?')
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. (p._getItemStat(item, 'isTwoHanded') and 'Yes' or 'No'))
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
end


function p.getSpecAttackMaxHit(specAttack, normalMaxHit)
function p.getArmourStatsBox(frame)
  local result = 0
local itemName = frame.args ~= nil and frame.args[1] or frame
  for i, dmg in pairs(specAttack.damage) do
local item = p.getItem(itemName)
    if dmg.maxRoll == 'Fixed' then
if item == nil then
      result = dmg.maxPercent * 10
return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
    elseif dmg.maxRoll == 'MaxHit' then
end
      if dmg.character == 'Target' then
        --Confusion applied damage based on the player's max hit. Gonna just ignore that one
local ico = {
        result = 0
["Attack"] = Icons.Icon({'Attack', type='skill', notext=true}),
      else
["Combat"] = Icons.Icon({'Combat', notext=true}),
        result = dmg.maxPercent * normalMaxHit * 0.01
["Defence"] = Icons.Icon({'Defence', type='skill', notext=true}),
      end
["Magic"] = Icons.Icon({'Magic', type='skill', notext=true}),
    elseif Shared.contains({'Bleeding', 'Poisoned'}, dmg.maxRoll) then
["Ranged"] = Icons.Icon({'Ranged', type='skill', notext=true}),
      -- TODO: This is limited in that there is no verification that bleed/poison
["Strength"] = Icons.Icon({'Strength', type='skill', notext=true}),
      -- can be applied to the target, it is assumed that it can and so this applies
["Slayer"] = Icons.Icon({'Slayer', type='skill', notext=true})
      result = result + dmg.maxPercent * 10
}
    end
local resultPart = {}
  end
table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="4" style="border-bottom:solid medium black;"| Armour Stats')
  return result
table.insert(resultPart, '\r\n|-\r\n!colspan="2" style="border-bottom:solid thin black;"| Offensive Stats')
end
table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Defensive Stats')
 
function p.canSpecAttackApplyEffect(specAttack, effectType)
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Strength'] .. ' Strength Bonus')
  local result = false
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'meleeStrengthBonus', true))
  for i, effect in pairs(specAttack.prehitEffects) do
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Defence Bonus')
    if effect.type == effectType then
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'meleeDefenceBonus', true))
      result = true
      break
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Stab Bonus')
    end
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'stabAttackBonus', 0))
  end
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Damage Reduction')
 
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'damageReduction', true) .. '%')
  for i, effect in pairs(specAttack.onhitEffects) do
    if effect.type == effectType then
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Slash Bonus')
      result = true
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'slashAttackBonus', true))
      break
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Defence Bonus')
    end
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedDefenceBonus', true))
  end
  return result
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Combat'] .. ' Block Bonus')
end
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'blockAttackBonus', true))
 
table.insert(resultPart, '\r\n!style="text-align:right;border-bottom:solid thin black;"| ' .. ico['Magic'] .. ' Defence Bonus')
function p._getMonsterMaxHit(monster, doStuns)
table.insert(resultPart, '\r\n|style="text-align:right;border-bottom:solid thin black;"| ' .. p._getItemStat(item, 'magicDefenceBonus', true))
  -- 2021-06-11 Adjusted for v0.20 stun/sleep changes, where damage multiplier now applies
  -- to all enemy attacks if stun/sleep is present on at least one special attack
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Attack Bonus')
  if doStuns == nil then
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedAttackBonus', true))
    doStuns = true
table.insert(resultPart, '\r\n!colspan="2" style="border-bottom:solid thin black;"| Other')
  elseif type(doStuns) == 'string' then
    doStuns = string.upper(doStuns) == 'TRUE'
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Strength Bonus')
  end
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedStrengthBonus', true))
 
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Slayer'] .. ' Bonus Slayer XP')
  local normalChance = 100
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'slayerBonusXP', true) .. '%')
  local specialMaxHit = 0
  local normalMaxHit = p._getMonsterBaseMaxHit(monster)
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' Attack Bonus')
  local hasActiveBuffSpec = false
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'magicAttackBonus', true))
  local damageMultiplier = 1
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Defence'] .. ' Level Required')
  if monster.specialAttacks[1] ~= nil then
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'defenceLevelRequired', true))
    local canStun, canSleep = false, false
    for i, specAttack in pairs(monster.specialAttacks) do
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' % Damage Bonus')
      if monster.overrideSpecialChances ~= nil then
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'magicDamageBonus', true) .. '%')
        normalChance = normalChance - monster.overrideSpecialChances[i]
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Ranged'] .. ' Level Required')
      else
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'rangedLevelRequired', true))
        normalChance = normalChance - specAttack.defaultChance
      end
table.insert(resultPart, '\r\n|-\r\n| colspan="2"|')
      local thisMax = p.getSpecAttackMaxHit(specAttack, normalMaxHit)
table.insert(resultPart, '\r\n!style="text-align:right;"| ' .. ico['Magic'] .. ' Level Required')
      if not canStun and p.canSpecAttackApplyEffect(specAttack, 'Stun') then canStun = true end
table.insert(resultPart, '\r\n|style="text-align:right;"| ' .. p._getItemStat(item, 'magicLevelRequired', true))
      if not canSleep and p.canSpecAttackApplyEffect(specAttack, 'Sleep') then canSleep = true end
 
      if thisMax > specialMaxHit then specialMaxHit = thisMax end
      if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then
        hasActiveBuffSpec = true
      end
    end
 
    if canSleep and doStuns then damageMultiplier = damageMultiplier * 1.2 end
    if canStun and doStuns then damageMultiplier = damageMultiplier * 1.3 end
  end
  --Ensure that if the monster never does a normal attack, the normal max hit is irrelevant
  if normalChance == 0 and not hasActiveBuffSpec then normalMaxHit = 0 end
  return math.floor(math.max(specialMaxHit, normalMaxHit) * damageMultiplier)
end
 
function p.getMonsterMaxHit(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local doStuns = frame.args ~= nil and frame.args[2] or true
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterMaxHit(monster, doStuns)
end
 
function p._getMonsterBaseMaxHit(monster)
  --8/27/21 - Now references p.calculateStandardMaxHit for Melee & Ranged
  local result = 0
  local baseLevel = 0
  local bonus = 0
  if monster.attackType == 'melee' then
    baseLevel = p._getMonsterLevel(monster, 'Strength')
    bonus = p.getEquipmentStat(monster, 'meleeStrengthBonus')
    result = p.calculateStandardMaxHit(baseLevel, bonus)
  elseif monster.attackType == 'ranged' then
    baseLevel = p._getMonsterLevel(monster, 'Ranged')
    bonus = p.getEquipmentStat(monster, 'rangedStrengthBonus')
    result = p.calculateStandardMaxHit(baseLevel, bonus)
  elseif monster.attackType == 'magic' then
    local mSpell = nil
    if monster.selectedSpell ~= nil then mSpell = Magic.getSpellByID('Spells', monster.selectedSpell) end
     
    bonus = p.getEquipmentStat(monster, 'magicDamageBonus')
    baseLevel = p._getMonsterLevel(monster, 'Magic')
 
    result = math.floor(10 * mSpell.maxHit * (1 + bonus / 100) * (1 + (baseLevel + 1) / 200))
  elseif monster.attackType == 'random' then
  local hitArray = {}
  local iconText = Icons.Icon({'Melee', notext=true})
    baseLevel = p._getMonsterLevel(monster, 'Strength')
    bonus = p.getEquipmentStat(monster, 'meleeStrengthBonus')
    table.insert(hitArray, p.calculateStandardMaxHit(baseLevel, bonus))
   
    iconText = Icons.Icon({'Ranged', type='skill', notext=true})
    baseLevel = p._getMonsterLevel(monster, 'Ranged')
    bonus = p.getEquipmentStat(monster, 'rangedStrengthBonus')
    table.insert(hitArray, p.calculateStandardMaxHit(baseLevel, bonus))
   
    iconText = Icons.Icon({'Magic', type='skill', notext=true})
    local mSpell = nil
    if monster.selectedSpell ~= nil then mSpell = Magic.getSpellByID('Spells', monster.selectedSpell) end
    bonus = p.getEquipmentStat(monster, 'magicDamageBonus')
    baseLevel = p._getMonsterLevel(monster, 'Magic')
    local magicDmg = math.floor(10 * mSpell.maxHit * (1 + bonus / 100) * (1 + (baseLevel + 1) / 200))
    table.insert(hitArray, magicDmg)
   
    local max = 0
    for i, val in pairs(hitArray) do
    if val > max then max = val end
    end
    result = max
  else
    return "ERROR: This monster has an invalid attack type somehow[[Category:Pages with script errors]]"
  end
 
  return result
end
 
function p.getMonsterBaseMaxHit(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterBaseMaxHit(monster)
end
 
function p.getMonsterAttacks(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = ''
  local iconText = p._getMonsterStyleIcon({monster, notext=true})
  local typeText = ''
  if  monster.attackType == 'melee' then
    typeText = 'Melee'
  elseif monster.attackType == 'ranged' then
    typeText = 'Ranged'
  elseif monster.attackType == 'magic' then
    typeText = 'Magic'
  elseif monster.attackType == 'random' then
  typeText = "Random"
  end
 
  local buffAttacks = {}
  local hasActiveBuffSpec = false
 
  local normalAttackChance = 100
  if monster.specialAttacks[1] ~= nil then
    for i, specAttack in pairs(monster.specialAttacks) do
      local attChance = 0
      if monster.overrideSpecialChances ~= nil then
        attChance = monster.overrideSpecialChances[i]
      else
        attChance = specAttack.defaultChance
      end
      normalAttackChance = normalAttackChance - attChance
 
      result = result..'\r\n* '..attChance..'% '..iconText..' '..specAttack.name..'\r\n** '..specAttack.description
 
      if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then
        table.insert(buffAttacks, specAttack.name)
        hasActiveBuffSpec = true
      end
    end
  end
  if normalAttackChance == 100 then
    result = iconText..' 1 - '..p._getMonsterBaseMaxHit(monster)..' '..typeText..' Damage'
  elseif normalAttackChance > 0 then
    result = '* '..normalAttackChance..'% '..iconText..' 1 - '..p.getMonsterBaseMaxHit(frame)..' '..typeText..' Damage'..result
  elseif hasActiveBuffSpec then
    --If the monster normally has a 0% chance of doing a normal attack but some special attacks can't be repeated, include it
    --(With a note about when it does it)
    result = '* '..iconText..' 1 - '..p._getMonsterBaseMaxHit(monster)..' '..typeText..' Damage (Instead of repeating '..table.concat(buffAttacks, ' or ')..' while the effect is already active)'..result
  end
 
  return result
end
 
function p.getMonsterPassives(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = ''
 
  if monster.hasPassive then
    result = result .. '===Passives==='
    for i, passiveID in pairs(monster.passiveID) do
      local passive = p.getPassiveByID(passiveID)
      result = result .. '\r\n* ' .. passive.name .. '\r\n** ' .. passive.description
    end
  end
  return result
end
 
function p.getMonsterCategories(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = '[[Category:Monsters]]'
 
  if monster.attackType == 'melee' then
    result = result..'[[Category:Melee Monsters]]'
  elseif monster.attackType == 'ranged' then
    result = result..'[[Category:Ranged Monsters]]'
  elseif monster.attackType == 'magic' then
    result = result..'[[Category:Magic Monsters]]'
  end
 
  if monster.specialAttacks[1] ~= nil then
    result = result..'[[Category:Monsters with Special Attacks]]'
  end
 
  if monster.isBoss then
    result = result..'[[Category:Bosses]]'
  end
 
  return result
end
 
function p.getOtherMonsterBoxText(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = ''
 
  --Going through and finding out which damage bonuses will apply to this monster
  local monsterTypes = {}
  if monster.isBoss then table.insert(monsterTypes, 'Boss') end
 
  local areaList = Areas.getMonsterAreas(monster.id)
  local counts = {combat = 0, slayer = 0, dungeon = 0}
  for i, area in Shared.skpairs(areaList) do
    counts[area.type] = counts[area.type] + 1
  end
 
  if counts.combat > 0 then table.insert(monsterTypes, 'Combat Area') end
  if counts.slayer > 0 then table.insert(monsterTypes, 'Slayer Area') end
  if counts.dungeon > 0 then table.insert(monsterTypes, 'Dungeon') end
 
  result = result.."\r\n|-\r\n|'''Monster Types:''' "..table.concat(monsterTypes, ", ")
 
  local SlayerTier = 'N/A'
  if monster.canSlayer then
    SlayerTier = Constants.getSlayerTierNameByLevel(p._getMonsterCombatLevel(monster))
  end
 
  result = result.."\r\n|-\r\n|'''"..Icons.Icon({'Slayer', type='skill'}).." [[Slayer#Slayer Tier Monsters|Tier]]:''' "
  if monster.canSlayer then
    result = result.."[[Slayer#"..SlayerTier.."|"..SlayerTier.."]]"
  else
    result = result..SlayerTier
  end
 
  return result
end
 
function p.getMonsterDrops(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = ''
 
  if monster.bones ~= nil and monster.bones >= 0 then
    local bones = Items.getItemByID(monster.bones)
    --Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
    if not p._isDungeonOnlyMonster(monster) or Shared.contains(bones.name, 'Shard') then
      result = result.."'''Always Drops:'''"
      result = result..'\r\n{|class="wikitable"'
      result = result..'\r\n!Item !! Qty'
      result = result..'\r\n|-\r\n|'..Icons.Icon({bones.name, type='item'})
      result = result..'||'..(monster.boneQty ~= nil and monster.boneQty or 1)..'\r\n'..'|}'
    end
  end
 
  --Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
  if not p._isDungeonOnlyMonster(monster) then
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    local lootValue = 0
 
    result = result.."'''Loot:'''"
    local avgGp = 0
 
    if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
      avgGp = (monster.dropCoins[1] + monster.dropCoins[2]) / 2
      local gpTxt = Icons.GP(monster.dropCoins[1], monster.dropCoins[2])
      result = result.."\r\nIn addition to loot, the monster will also drop "..gpTxt..'.'
    end
 
    local multiDrop = Shared.tableCount(monster.lootTable) > 1
    local totalWt = 0
    for i, row in pairs(monster.lootTable) do
      totalWt = totalWt + row[2]
    end
    result = result..'\r\n{|class="wikitable sortable"'
    result = result..'\r\n!Item!!Qty'
    result = result..'!!Price!!colspan="2"|Chance'
 
    --Sort the loot table by weight in descending order
    table.sort(monster.lootTable, function(a, b) return a[2] > b[2] end)
    for i, row in Shared.skpairs(monster.lootTable) do
      local thisItem = Items.getItemByID(row[1])
     
      local maxQty = row[3]
      if thisItem ~= nil then
    result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
  else
      result = result..'\r\n|-\r\n|Unknown Item[[Category:Pages with script errors]]'
    end
      result = result..'||style="text-align:right" data-sort-value="'..maxQty..'"|'
 
      if maxQty > 1 then
        result = result.. '1 - '
      end
      result = result..Shared.formatnum(row[3])
 
      --Adding price columns
  local itemPrice = 0
      if thisItem == nil then
      result = result..'||data-sort-value="0"|???'
      else
        itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
      if itemPrice == 0 or maxQty == 1 then
        result = result..'||'..Icons.GP(itemPrice)
      else
        result = result..'||'..Icons.GP(itemPrice, itemPrice * maxQty)
      end
      end
 
      --Getting the drop chance
      local dropChance = (row[2] / totalWt * lootChance)
      if dropChance ~= 100 then
        --Show fraction as long as it isn't going to be 1/1
        result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
        result = result..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
        result = result..'||'
      else
        result = result..'||colspan="2" data-sort-value="'..row[2]..'"'
      end
      result = result..'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
 
      --Adding to the average loot value based on price & dropchance
      lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
    end
    if multiDrop then
      result = result..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
      if lootChance < 100 then
        result = result..'\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||'
      else
        result = result..'\r\n|colspan="2" '
      end
      result = result..'style="text-align:right"|'..Shared.round(lootChance, 2, 2)..'%'
    end
    result = result..'\r\n|}'
    result = result..'\r\nThe loot dropped by the average kill is worth '..Icons.GP(Shared.round(lootValue, 2, 0)).." if sold."
    if avgGp > 0 then
      result = result..'<br/>Including GP, the average kill is worth '..Icons.GP(Shared.round(avgGp + lootValue, 2, 0))..'.'
    end
  end
 
  --If no other drops, make sure to at least say so.
  if result == '' then result = 'None' end
  return result
end
 
-- Find drop chance of specified item from specified monster.
-- Usage: |Monster Name|Item Name
function p.getItemDropChance(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
  local ItemName = frame.args ~= nil and frame.args[2] or frame[2]
 
  local monster = p.getMonster(MonsterName)
  local item = Items.getItem(ItemName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
  if item == nil then
    return "ERROR: No item with that name found[[Category:Pages with script errors]]"
  end
 
  if not p._isDungeonOnlyMonster(monster) then
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
 
local totalWt = 0
--for i, row in pairs(monster.lootTable) do
--totalWt = totalWt + row[2]
--end
local dropChance = 0
table.insert(resultPart, '\r\n|}')
local dropWt = 0
return table.concat(resultPart)
for i, row in Shared.skpairs(monster.lootTable) do
mw.log(row[2])
local thisItem = Items.getItemByID(row[1])
totalWt = totalWt + row[2]
if item['id'] == thisItem['id'] then
    dropWt = row[2]
    end
end
dropChance = (dropWt / totalWt * lootChance)
return Shared.round(dropChance, 2, 2)
  end
end
 
function p.getChestDrops(frame)
  local ChestName = frame.args ~= nil and frame.args[1] or frame
  local chest = Items.getItem(ChestName)
 
  if chest == nil then
    return "ERROR: No item named "..ChestName..' found[[Category:Pages with script errors]]'
  end
 
  local result = ''
 
  if chest.dropTable == nil then
    return "ERROR: "..ChestName.." does not have a drop table[[Category:Pages with script errors]]"
  else
    local lootChance = 100
    local lootValue = 0
 
    local multiDrop = Shared.tableCount(chest.dropTable) > 1
    local totalWt = 0
    for i, row in pairs(chest.dropTable) do
      totalWt = totalWt + row[2]
    end
    result = result..'\r\n{|class="wikitable sortable"'
    result = result..'\r\n!Item!!Qty'
    result = result..'!!colspan="2"|Chance!!Price'
 
    --Sort the loot table by weight in descending order
    for i, row in pairs(chest.dropTable) do
      if chest.dropQty ~= nil then
        table.insert(row, chest.dropQty[i])
      else
        table.insert(row, 1)
      end
    end
    table.sort(chest.dropTable, function(a, b) return a[2] > b[2] end)
    for i, row in Shared.skpairs(chest.dropTable) do
      local thisItem = Items.getItemByID(row[1])
      local qty = row[3]
      result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
      result = result..'||style="text-align:right" data-sort-value="'..qty..'"|'
 
      if qty > 1 then
        result = result.. '1 - '
      end
      result = result..Shared.formatnum(qty)
 
      local dropChance = (row[2] / totalWt) * 100
      result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
      result = result..'|'..Shared.fraction(row[2], totalWt)
 
      result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
 
      result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
      if qty > 1 then
        result = result..'|'..Icons.GP(thisItem.sellsFor, thisItem.sellsFor * qty)
      else
        result = result..'|'..Icons.GP(thisItem.sellsFor)
      end
      lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor * ((1 + qty)/ 2))
    end
    result = result..'\r\n|}'
    result = result..'\r\nThe average value of the contents of one chest is '..Icons.GP(Shared.round(lootValue, 2, 0))..'.'
  end
 
  return result
end
 
function p.getAreaMonsterTable(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
  if area == nil then
    return "ERROR: Could not find an area named "..areaName..'[[Category:Pages with script errors]]'
  end
 
  if area.type == 'dungeon' then
    return p.getDungeonMonsterTable(frame)
  end
 
  local tableTxt = '{| class="wikitable sortable"'
  tableTxt = tableTxt..'\r\n! Name !! Combat Level !! Hitpoints !! Max Hit !! [[Combat Triangle|Combat Style]]'
  for i, monsterID in pairs(area.monsters) do
    local monster = p.getMonsterByID(monsterID)
    tableTxt = tableTxt..'\r\n|-\r\n|'..Icons.Icon({monster.name, type='monster'})
    tableTxt = tableTxt..'||'..p._getMonsterCombatLevel(monster)
    tableTxt = tableTxt..'||'..Shared.formatnum(p.getMonsterHP(monster.name))
    tableTxt = tableTxt..'||'..Shared.formatnum(p.getMonsterMaxHit(monster.name))
    tableTxt = tableTxt..'||'..p.getMonsterStyleIcon({monster.name, nolink=true})
  end
  tableTxt = tableTxt..'\r\n|}'
  return tableTxt
end
end
function p.getDungeonMonsterTable(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
  if area == nil then
    return "ERROR: Could not find a dungeon named "..areaName..'[[Category:Pages with script errors]]'
  end
  --For Dungeons, go through and count how many of each monster are in the dungeon first
  local monsterCounts = {}
  for i, monsterID in pairs(area.monsters) do
    if monsterCounts[monsterID] == nil then
      monsterCounts[monsterID] = 1
    else
      monsterCounts[monsterID] = monsterCounts[monsterID] + 1
    end
  end
  local usedMonsters = {}
  -- Declare function for building table rows to avoid repeating code
  local buildRow = function(entityID, monsterCount, specialType)
    local monIcon, monLevel, monHP, monMaxHit, monStyle, monCount
    local monData = {}
    if specialType ~= nil and Shared.contains({'Afflicted', 'SlayerArea'}, specialType) then
    -- Special handling for Into the Mist
  if specialType == 'Afflicted' then
    local iconQ = Icons.Icon({'Into the Mist', notext=true, nolink=true, img='Question'})
    monIcon = Icons.Icon({'Into the Mist', 'Afflicted Monster', nolink=true, img='Question'})
    monLevel, monHP, monMaxHit, monStyle, monCount = iconQ, iconQ, iconQ, iconQ, monsterCount
  elseif specialType == 'SlayerArea' then
    -- entityID corresponds to a slayer area
    local area = Areas.getAreaByID('slayer', entityID)
        monIcon = Icons.Icon({area.name, type='combatArea'}) .. ' Monsters'
    monLevel = {p.getLowHighStat(area.monsters, function(monster) return p._getMonsterCombatLevel(monster) end)}
    monHP = {p.getLowHighStat(area.monsters, function(monster) return p._getMonsterHP(monster) end)}
    local lowMaxHit, highMaxHit = p.getLowHighStat(area.monsters, function(monster) return p._getMonsterMaxHit(monster) end)
    monMaxHit = highMaxHit
    monStyle = Icons.Icon({area.name, area.name, notext=true, nolink=true, img='Question'})
    monCount = monsterCount
  end
      else
        -- entityID corresponds to a monster
        local monster = p.getMonsterByID(entityID)
        monIcon = Icons.Icon({monster.name, type='monster'})
        monLevel = p._getMonsterCombatLevel(monster)
        monHP = p._getMonsterHP(monster)
        monMaxHit = p._getMonsterMaxHit(monster)
        monStyle = p._getMonsterStyleIcon({monster})
        monCount = monsterCount
      end
      local getValSort = function(val)
      if type(val) == 'table' then
        if type(val[1]) == 'number' and type(val[2]) == 'number' then
        return (val[1] + val[2]) / 2
        else
          return (type(val[1]) == 'number' and val[1]) or 0
        end
      else
        return (type(val) == 'number' and val) or 0
      end
      end
      local getValText = function(val)
      if type(val) == 'table' and Shared.tableCount(val) == 2 then
        if type(val[1]) == 'number' and type(val[2]) == 'number' then
        return Shared.formatnum(val[1]) .. ' - ' .. Shared.formatnum(val[2])
        else
          return val[1] .. ' - ' .. val[2]
        end
      elseif type(val) == 'number' then
        return Shared.formatnum(val)
      else
        return val
      end
      end
       
      local resultPart = {}
      table.insert(resultPart, '\r\n|-\r\n| ' .. monIcon)
      table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monLevel) .. '"| ' .. getValText(monLevel))
      table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monHP) .. '"| ' .. getValText(monHP))
      table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monMaxHit) .. '"| ' .. getValText(monMaxHit))
      table.insert(resultPart, '\r\n| ' .. monStyle)
      table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monCount) .. '"| ' .. getValText(monCount))
      return table.concat(resultPart)
  end
  local returnPart = {}
  table.insert(returnPart, '{| class="wikitable sortable"')
  table.insert(returnPart, '\r\n! Name !! Combat Level !! Hitpoints !! Max Hit !! [[Combat Triangle|Combat Style]] !! Count')
  -- Special handing for Impending Darkness event
  -- TODO needs to be revised once there is a better understanding of how the event works
  --if area.isEvent ~= nil and area.isEvent then
  -- for i, eventAreaID in ipairs(Areas.eventData.slayerAreas) do
  --   table.insert(returnPart, buildRow(eventAreaID, {5, 8}, 'SlayerArea'))
  -- end
  --  -- Add Bane * 4
  --  table.insert(returnPart, buildRow(152, 4))
  --end
  for i, monsterID in pairs(area.monsters) do
    if not Shared.contains(usedMonsters, monsterID) then
      if monsterID >= 0 then
        table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID]))
      else
        --Special handling for Into the Mist
        table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], 'Afflicted'))
      end
      table.insert(usedMonsters, monsterID)
    end
  end
  table.insert(returnPart, '\r\n|}')
  return table.concat(returnPart)
end
function p.getDungeonTotalHp(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
  if area == nil then
    return "ERROR: Could not find a dungeon named "..areaName..'[[Category:Pages with script errors]]'
  end
  local totalHP = 0
  for i, monsterID in pairs(area.monsters) do
    if not Shared.contains(usedMonsters, monsterID) then
      local monster = p.getMonsterByID(monsterID)
      totalHP = totalHP + p._getMonsterHP(monster)
    end
  end
  return totalHP
end
function p._getAreaMonsterList(area)
  local monsterList = {}
  for i, monsterID in pairs(area.monsters) do
    local monster = p.getMonsterByID(monsterID)
    table.insert(monsterList, Icons.Icon({monster.name, type='monster'}))
  end
  return table.concat(monsterList, '<br/>')
end
function p._getDungeonMonsterList(area)
  local monsterList = {}
  local lastMonster = nil
  local lastID = -2
  local count = 0
  -- Special handing for Impending Darkness event
  -- TODO needs to be revised once there is a better understanding of how the event works
  --if area.isEvent ~= nil and area.isEvent then
  -- for i, eventAreaID in ipairs(Areas.eventData.slayerAreas) do
  --   local eventArea = Areas.getAreaByID('slayer', eventAreaID)
  --   table.insert(monsterList, '5-8 ' .. Icons.Icon({eventArea.name, type='combatArea'}) .. ' Monsters')
  -- end
  -- table.insert(monsterList, '4 ' .. Icons.Icon({'Bane', type='monster'}))
  --end
  for i, monsterID in Shared.skpairs(area.monsters) do
    if monsterID ~= lastID then
      local monster = nil
      if monsterID ~= -1 then monster = p.getMonsterByID(monsterID) end
      if lastID ~= -2 then
        if lastID == -1 then
          --Special handling for Afflicted Monsters
          table.insert(monsterList, Icons.Icon({'Affliction', 'Afflicted Monster', img='Question', qty=count}))
        else
          local name = lastMonster.name
          table.insert(monsterList, Icons.Icon({name, type='monster', qty=count}))
        end
      end
      lastMonster = monster
      lastID = monsterID
      count = 1
    else
      count = count + 1
    end
    --Make sure the final monster in the dungeon gets counted
    if i == Shared.tableCount(area.monsters) then
      local name = lastMonster.name
      table.insert(monsterList, Icons.Icon({lastMonster.name, type='monster', qty=count}))
    end
  end
  return table.concat(monsterList, '<br/>')
end
function p.getAreaMonsterList(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
  if area == nil then
    return "ERROR: Could not find an area named "..areaName..'[[Category:Pages with script errors]]'
  end
  if area.type == 'dungeon' then
    return p._getDungeonMonsterList(area)
  else
    return p._getAreaMonsterList(area)
  end
end
function p.getFoxyTable(frame)
  local result = 'Monster,Min GP,Max GP,Average GP'
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
    if not p._isDungeonOnlyMonster(monster) then
      if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
      local avgGp = (monster.dropCoins[1] + monster.dropCoins[2]) / 2
      result = result..'<br/>'..monster.name..','..monster.dropCoins[1]..','..(monster.dropCoins[2])..','..avgGp
      end
    end
  end
  return result
end
function p._getMonsterAverageGP(monster)
  local result = ''
  local totalGP = 0
  if monster.bones ~= nil and monster.bones >= 0 then
    local bones = Items.getItemByID(monster.bones)
    --Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
    if not p._isDungeonOnlyMonster(monster) or Shared.contains(bones.name, 'Shard') then
      totalGP = totalGP + bones.sellsFor
    end
  end
  --Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
  if not p._isDungeonOnlyMonster(monster) then
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    local lootValue = 0
    local avgGp = 0
    if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
      avgGp = (monster.dropCoins[1] + monster.dropCoins[2]) / 2
    end
    totalGP = totalGP + avgGp
    local multiDrop = Shared.tableCount(monster.lootTable) > 1
    local totalWt = 0
    for i, row in pairs(monster.lootTable) do
      totalWt = totalWt + row[2]
    end
    for i, row in Shared.skpairs(monster.lootTable) do
      local thisItem = Items.getItemByID(row[1])
      local maxQty = row[3]
      local itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
      --Getting the drop chance
      local dropChance = (row[2] / totalWt * lootChance)
      --Adding to the average loot value based on price & dropchance
      lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
    end
    totalGP = totalGP + lootValue
  end
  return Shared.round(totalGP, 2, 2)
end
function p.getMonsterAverageGP(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
  return p._getMonsterAverageGP(monster)
end
function p.getMonsterEVTable(frame)
  local result = '{| class="wikitable sortable"'
  result = result..'\r\n!Monster!!Combat Level!!Average GP'
  for i, monsterTemp in Shared.skpairs(MonsterData.Monsters) do
    local monster = Shared.clone(monsterTemp)
    monster.id = i - 1
    if not p._isDungeonOnlyMonster(monster) then
      local monsterGP = p._getMonsterAverageGP(monster)
      local combatLevel = p._getMonsterCombatLevel(monster, 'Combat Level')
      result = result..'\r\n|-\r\n|'..Icons.Icon({monster.name, type='monster', noicon=true})..'||'..combatLevel..'||'..monsterGP
    end
  end
  result = result..'\r\n|}'
  return result
end
function p.getSlayerTierMonsterTable(frame)
  -- Input validation
  local tier = frame.args ~= nil and frame.args[1] or frame
  local slayerTier = nil
  if tier == nil then
    return "ERROR: No tier specified[[Category:Pages with script errors]]"
  end
  if tonumber(tier) ~= nil then
    slayerTier = Constants.getSlayerTierByID(tonumber(tier))
  else
    slayerTier = Constants.getSlayerTier(tier)
  end
  if slayerTier == nil then
    return "ERROR: Invalid slayer tier[[Category:Pages with script errors]]"
  end
  -- Obtain required tier details
  local minLevel, maxLevel = slayerTier.minLevel, slayerTier.maxLevel
  -- Build list of monster IDs
  -- Right now hiddenMonsterIDs is empty
  local hiddenMonsterIDs = {}
  local monsterIDs = {}
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
    if monster.canSlayer and not Shared.contains(hiddenMonsterIDs, i - 1) then
      local cmbLevel = p._getMonsterCombatLevel(monster)
      if cmbLevel >= minLevel and (maxLevel == nil or cmbLevel <= maxLevel) then
        table.insert(monsterIDs, i - 1)
      end
    end
  end
  if Shared.tableCount(monsterIDs) == 0 then
    -- Somehow no monsters are in the tier, return nothing
    return ''
  else
    return p._getMonsterTable(monsterIDs, true)
  end
end
function p.getFullMonsterTable(frame)
  local monsterIDs = {}
  for i = 0, Shared.tableCount(MonsterData.Monsters) - 1, 1 do
    table.insert(monsterIDs, i)
  end
  return p._getMonsterTable(monsterIDs, false)
end
function p._getMonsterTable(monsterIDs, excludeDungeons)
  --Making a single function for getting a table of monsters given a list of IDs.
  local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
  local tableParts = {}
  table.insert(tableParts, '{| class="wikitable sortable stickyHeader"')
  -- First header row
  table.insert(tableParts, '\r\n|- class="headerRow-0"\r\n! colspan="5" | !! colspan="4" |Offensive Stats !! colspan="3" |Evasion Rating !! colspan="4" |')
  -- Second header row
  table.insert(tableParts, '\r\n|- class="headerRow-1"\r\n!Monster !!Name !!ID !!Combat Level ')
  table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Hitpoints', type='skill'}))
  table.insert(tableParts, '!!Attack Speed (s) !!colspan="2"|Max Hit !!Accuracy ')
  table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Defence', type='skill', notext=true}))
  table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Ranged', type='skill', notext=true}))
  table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Magic', type='skill', notext=true}))
  table.insert(tableParts, '!!' .. Icons.Icon({'Coins', notext=true, nolink=true}) .. ' Coins !!Bones !!Locations')
  -- Generate row per monster
  for i, monsterID in Shared.skpairs(monsterIDs) do
    local monster = p.getMonsterByID(monsterID)
    local cmbLevel = p._getMonsterCombatLevel(monster)
    local atkSpeed = p._getMonsterAttackSpeed(monster)
    local maxHit = p._getMonsterMaxHit(monster)
    local accR = p._getMonsterAR(monster)
    local evaR = {p._getMonsterER(monster, "Melee"), p._getMonsterER(monster, "Ranged"), p._getMonsterER(monster, "Magic")}
    local gpRange = {0, 0}
    if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
      gpRange = {monster.dropCoins[1], monster.dropCoins[2]}
    end
    local gpTxt = nil
    if gpRange[1] >= gpRange[2] then
      gpTxt = Shared.formatnum(gpRange[1])
    else
      gpTxt = Shared.formatnum(gpRange[1]) .. ' - ' .. Shared.formatnum(gpRange[2])
    end
    local boneTxt = 'None'
    if monster.bones ~= nil and monster.bones >= 0 then
      local bones = Items.getItemByID(monster.bones)
      boneTxt = Icons.Icon({bones.name, type='item', notext=true})
    end
    table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({monster.name, type='monster', size=50, notext=true}))
    table.insert(tableParts, '\r\n|style="text-align:left" |' .. Icons.Icon({monster.name, type='monster', noicon=true}))
    table.insert(tableParts, '\r\n|style="text-align:right" |' .. monsterID)
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. cmbLevel .. '" |' .. Shared.formatnum(cmbLevel))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. p._getMonsterHP(monster) .. '" |' .. Shared.formatnum(p._getMonsterHP(monster)))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. atkSpeed .. '" |' .. Shared.round(atkSpeed, 1, 1))
    table.insert(tableParts, '\r\n|style="text-align:center;border-right:hidden" |' .. p._getMonsterStyleIcon({monster, notext=true}))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. maxHit .. '" |' .. Shared.formatnum(maxHit))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. accR .. '" |' .. Shared.formatnum(accR))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[1] .. '" |' .. Shared.formatnum(evaR[1]))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[2] .. '" |' .. Shared.formatnum(evaR[2]))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[3] .. '" |' .. Shared.formatnum(evaR[3]))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. (gpRange[1] + gpRange[2]) / 2 .. '" |' .. gpTxt)
    table.insert(tableParts, '\r\n|style="text-align:center" |' .. boneTxt)
    table.insert(tableParts, '\r\n|style="text-align:right;width:190px" |' .. p._getMonsterAreas(monster, hideDungeons))
  end
  table.insert(tableParts, '\r\n|}')
  return table.concat(tableParts)
end
function p.getSpecialAttackTable(frame)
    local spAttTable = {}
    for i, monster in ipairs(MonsterData.Monsters) do
        if monster.specialAttacks ~= nil and Shared.tableCount(monster.specialAttacks) > 0 then
            local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
            for j, spAtt in ipairs(monster.specialAttacks) do
                local attChance = (overrideChance and monster.overrideSpecialChances[j] or spAtt.defaultChance)
                if spAttTable[spAtt.id] == nil then
                    spAttTable[spAtt.id] = { ['defn'] = spAtt, ['icons'] = {} }
                end
                if spAttTable[spAtt.id]['icons'][attChance] == nil then
                    spAttTable[spAtt.id]['icons'][attChance] = {}
                end
                table.insert(spAttTable[spAtt.id]['icons'][attChance], Icons.Icon({ monster.name, type = 'monster' }))
            end
        end
    end
    local resultPart = {}
    table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
    table.insert(resultPart, '\r\n|- class="headerRow-0"')
    table.insert(resultPart, '\r\n!Name!!style="min-width:225px"|Monsters!!Chance!!Effect')
    for i, spAttData in Shared.skpairs(spAttTable) do
        local spAtt = spAttData.defn
        local firstRow = true
        local rowsSpanned = Shared.tableCount(spAttData.icons)
        local rowSuffix = ''
        if rowsSpanned > 1 then
            rowSuffix = '|rowspan="' .. rowsSpanned .. '"'
        end
        for chance, iconList in Shared.skpairs(spAttData.icons) do
            table.insert(resultPart, '\r\n|-')
            if firstRow then
                table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.name)
            end
            table.insert(resultPart, '\r\n|data-sort-value="' .. spAtt.name .. '"| ' .. table.concat(iconList, '<br/>'))
            table.insert(resultPart, '\r\n|data-sort-value="' .. chance .. '"| ' .. Shared.round(chance, 2, 0) .. '%')
            if firstRow then
                table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.description)
                firstRow = false
            end
        end
    end
    table.insert(resultPart, '\r\n|}')
    return table.concat(resultPart)
end


return p
return p