Module:Monsters: Difference between revisions

From Melvor Idle
(Added range of prices for chest drop tables, fixed column order)
(Add column name for average healing)
 
(202 intermediate revisions by 11 users not shown)
Line 1: Line 1:
local p = {}
local p = {}


local Constants = mw.loadData('Module:Constants/data')
local Constants = require('Module:Constants')
local MonsterData = mw.loadData('Module:Monsters/data')
local Shared = require('Module:Shared')
local AreaData = mw.loadData('Module:CombatAreas/data')
local GameData = require('Module:GameData')
 
local Areas = require('Module:CombatAreas')
local Magic = require('Module:Magic')
local Magic = require('Module:Magic')
local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Items = require('Module:Items')


function p.getMonster(name)
function p.getMonster(name)
  local result = nil
if name == 'Earth Golem (AoD)' then
  if name == 'Spider (lv. 51)' or name == 'Spider' then
-- Special case for ambiguous monster name
    return p.getMonsterByID(50)
return p.getMonsterByID('melvorAoD:EarthGolem')
  elseif name == 'Spider (lv. 52)' or name == 'Spider2' then
else
    return p.getMonsterByID(51)
    return GameData.getEntityByName('monsters', name)
  end
 
  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
     end
     end
  end
  return result
end
end


function p.getMonsterByID(ID)
function p.getMonsterByID(ID)
  return MonsterData.Monsters[ID + 1]
    return GameData.getEntityByID('monsters', ID)
end
 
function p.getMonsterName(monster)
if monster.id == 'melvorAoD:EarthGolem' then
-- Special case for ambiguous monster name
return 'Earth Golem (AoD)'
else
return monster.name
end
end
 
function p.getPassive(name)
    return GameData.getEntityByName('combatPassives', name)
end
end


function p.getSpecialAttack(name)
function p.getPassiveByID(ID)
  local result = nil
    return GameData.getEntityByID('combatPassives', ID)
end


  for i, attack in pairs(MonsterData.SpecialAttacks) do
-- Given a list of monster IDs, calls statFunc with each monster and returns
    if(attack.name == name) then
-- the lowest & highest values
      result = Shared.clone(attack)
function p.getLowHighStat(idList, statFunc)
      --Make sure every monster has an ID, and account for the 1-based indexing of Lua
local lowVal, highVal = nil, nil
      result.id = i - 1
for i, monID in ipairs(idList) do
    end
local monster = p.getMonsterByID(monID)
  end
local statVal = statFunc(monster)
  return result
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.getSpecialAttackByID(ID)
function p._getMonsterStat(monster, statName)
  return MonsterData.SpecialAttacks[ID + 1]
if statName == 'Barrier' then
return p._getMonsterBarrier(monster)
elseif statName == 'HP' then
return p._getMonsterHP(monster)
elseif statName == 'maxHit' then
return p._getMonsterMaxHit(monster)
elseif statName == 'accuracyRating' then
return p._getMonsterAR(monster)
elseif statName == 'meleeEvasionRating' then
return p._getMonsterER(monster, 'Melee')
elseif statName == 'rangedEvasionRating' then
return p._getMonsterER(monster, 'Ranged')
elseif statName == 'magicEvasionRating' then
return p._getMonsterER(monster, 'Magic')
elseif statName == 'damageReduction' then
return p.getEquipmentStat(monster, 'damageReduction')
elseif statName == 'drReduction' then
return p._getMonsterDrReduction(monster)
end
 
return monster[statName]
end
end


function p.getMonsterStat(frame)
function p.getMonsterStat(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
  local StatName = frame.args ~= nil and frame.args[2] or frame[2]
local StatName = frame.args ~= nil and frame.args[2] or frame[2]
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)
  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end


  if StatName == 'HP' then
return p._getMonsterStat(monster, StatName)
    return p.getMonsterHP(MonsterName)
end
  elseif StatName == 'maxHit' then
    return p.getMonsterMaxHit(MonsterName)
  elseif StatName == 'accuracyRating' then
    return p.getMonsterAR(MonsterName)
  elseif StatName == 'meleeEvasionRating' then
    return p.getMonsterER({MonsterName, 'Melee'})
  elseif StatName == 'rangedEvasionRating' then
    return p.getMonsterER({MonsterName, 'Ranged'})
  elseif StatName == 'magicEvasionRating' then
    return p.getMonsterER({MonsterName, 'Magic'})
  end


  return monster[StatName]
function p._getMonsterStyleIcon(frame)
local args = frame.args ~= nil and frame.args or frame
local monster = args[1]
local notext = args.notext
local nolink = args.nolink
 
local iconText = ''
if monster.attackType == 'melee' then
iconText = Icons.Icon({'Melee', notext=notext, nolink=nolink})
elseif monster.attackType == 'ranged' then
iconText = Icons.Icon({'Ranged', type='skill', notext=notext, nolink=nolink})
elseif monster.attackType == 'magic' then
iconText = Icons.Icon({'Magic', type='skill', notext=notext, nolink=nolink})
elseif monster.attackType == 'random' then
iconText = Icons.Icon({p.getMonsterName(monster), notext=notext, nolink=nolink, img='Question'})
end
 
return iconText
end
end


function p.getMonsterStyleIcon(frame)
function p.getMonsterStyleIcon(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 MonsterName = args[1]
  local notext = args.notext
local monster = p.getMonster(MonsterName)
  local nolink = args.nolink
  local monster = p.getMonster(MonsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end


  local iconText = ''
args[1] = monster
  if  monster.attackType == Constants.attackType.Melee then
return p._getMonsterStyleIcon(args)
    iconText = Icons.Icon({'Melee', notext=notext, nolink=nolink})
end
  elseif monster.attackType == Constants.attackType.Ranged then
 
    iconText = Icons.Icon({'Ranged', type='skill', notext=notext, nolink=nolink})
function p._getMonsterHP(monster)
  else
return 10 * p._getMonsterLevel(monster, 'Hitpoints')
    iconText = Icons.Icon({'Magic', type='skill', notext=notext, nolink=nolink})
end
  end


  return iconText
function p._getMonsterBarrier(monster)
--Monster Barrier is a percentage of its max health
local barPercent = 0
if monster.barrierPercent ~= nil then
barPercent = monster.barrierPercent
end
return p._getMonsterHP(monster) * barPercent * 0.01
end
 
function p.getMonsterEffectiveHP(frame)
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 Shared.printError('No monster with that name found')
end
end
 
function p.getMonsterEffectiveBarrier(frame)
local MonsterName = frame.args ~= nil and frame.args[1] or frame
local monster = p.getMonster(MonsterName)
if monster ~= nil then
return math.floor((p._getMonsterBarrier(monster)/(1 - p._getMonsterStat(monster, 'damageReduction')/100)) + 0.5)
else
return Shared.printError('No monster with that name found')
end
end
 
function p.getMonsterBarrier(frame)
local MonsterName = frame.args ~= nil and frame.args[1] or frame
local monster = p.getMonster(MonsterName)
if monster ~= nil then
return p._getMonsterBarrier(monster)
else
return Shared.printError('No monster with that name found')
end
end
end


function p.getMonsterHP(frame)
function p.getMonsterHP(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)
  if monster ~= nil then
if monster ~= nil then
    return monster.hitpoints * 10
return p._getMonsterHP(monster)
  else
else
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end
end
 
function p._getMonsterLevel(monster, skillName)
local result = 0
if monster.levels[skillName] ~= nil then
result = monster.levels[skillName]
end
return result
end
 
function p.getMonsterLevel(frame)
local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
local SkillName = frame.args ~= nil and frame.args[2] or frame[2]
local monster = p.getMonster(MonsterName)
 
if monster == nil then
return Shared.printError('No monster with that name found')
end
 
return p._getMonsterLevel(monster, SkillName)
end
 
function p.getEquipmentStat(monster, statName)
if monster.equipmentStats == nil then
return 0
else
    return monster.equipmentStats[statName] or 0
    end
end
 
function p.calculateStandardStat(effectiveLevel, bonus)
--Based on calculateStandardStat in Characters.js
return (effectiveLevel + 9) * (bonus + 64)
end
 
function p.calculateStandardMaxHit(baseLevel, strengthBonus)
--Based on calculateStandardMaxHit in Characters.js
local effectiveLevel = baseLevel + 9
return math.floor(10 * (1.3 + effectiveLevel / 10 + strengthBonus / 80 + effectiveLevel * strengthBonus / 640))
end
 
function p._getMonsterAttackSpeed(monster)
return p.getEquipmentStat(monster, 'attackSpeed') / 1000
end
end


function p.getMonsterAttackSpeed(frame)
function p.getMonsterAttackSpeed(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)
  if monster ~= nil then
if monster ~= nil then
    return monster.attackSpeed / 1000
return p._getMonsterAttackSpeed(monster)
  else
else
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end
end
 
function p._getMonsterCombatLevel(monster)
local base = 0.25 * (p._getMonsterLevel(monster, 'Defence') + p._getMonsterLevel(monster, 'Hitpoints'))
local melee = 0.325 * (p._getMonsterLevel(monster, 'Attack') + p._getMonsterLevel(monster, 'Strength'))
local range = 0.325 * (1.5 * p._getMonsterLevel(monster, 'Ranged'))
local magic = 0.325 * (1.5 * p._getMonsterLevel(monster, 'Magic'))
return math.floor(base + math.max(melee, range, magic))
end
end


function p.getMonsterCombatLevel(frame)
function p.getMonsterCombatLevel(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end
 
 
  local base = 0.25 * (monster.defenceLevel + monster.hitpoints)
return p._getMonsterCombatLevel(monster)
  local melee = 0.325 * (monster.attackLevel + monster.strengthLevel)
end
  local range = 0.325 * (1.5 * monster.rangedLevel)
 
  local magic = 0.325 * (1.5 * monster.magicLevel)
function p._getMonsterAR(monster)
  if melee > range and melee > magic then
local baseLevel = 0
    return math.floor(base + melee)
local bonus = 0
  elseif range > magic then
if monster.attackType == 'melee' then
    return math.floor(base + range)
baseLevel = p._getMonsterLevel(monster, 'Attack')
  else
bonus = p.getEquipmentStat(monster, 'stabAttackBonus')
    return math.floor(base + magic)
elseif monster.attackType == 'ranged' then
  end
baseLevel = p._getMonsterLevel(monster, 'Ranged')
bonus = p.getEquipmentStat(monster, 'rangedAttackBonus')
elseif monster.attackType == 'magic' then
baseLevel = p._getMonsterLevel(monster, 'Magic')
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 Shared.printError('This monster has an invalid attack type somehow')
end
 
return p.calculateStandardStat(baseLevel, bonus)
end
end


function p.getMonsterAR(frame)
function p.getMonsterAR(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end
 
  local effAttLvl = 0
  local attBonus = 0
  if monster.attackType == Constants.attackType.Melee then
    effAttLvl = monster.attackLevel + 9
    attBonus = monster.attackBonus + 64
  elseif monster.attackType == Constants.attackType.Ranged then
    effAttLvl = monster.rangedLevel + 9
    attBonus = monster.attackBonusRanged + 64
  elseif monster.attackType == Constants.attackType.Magic then
    effAttLvl = monster.magicLevel + 9
    attBonus = monster.attackBonusMagic + 64
  else
    return "ERROR: This monster has an invalid attack type somehow"
  end


  return effAttLvl * attBonus
return p._getMonsterAR(monster)
end
 
function p._getMonsterER(monster, style)
local baseLevel= 0
local bonus = 0
 
if style == "Melee" then
baseLevel = p._getMonsterLevel(monster, 'Defence')
bonus = p.getEquipmentStat(monster, 'meleeDefenceBonus')
elseif style == "Ranged" then
baseLevel = p._getMonsterLevel(monster, 'Defence')
bonus = p.getEquipmentStat(monster, 'rangedDefenceBonus')
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 Shared.printError('Must choose Melee, Ranged, or Magic')
end
 
return p.calculateStandardStat(baseLevel, bonus)
end
end


function p.getMonsterER(frame)
function p.getMonsterER(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 MonsterName = args[1]
  local style = args[2]
local style = args[2]
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end
 
 
  local effDefLvl = 0
return p._getMonsterER(monster, style)
  local defBonus = 0
end
  if style == "Melee" then
 
    effDefLvl = monster.defenceLevel + 9
-- Determines if the monster is capable of dropping bones, and returns the bones
    defBonus = monster.defenceBonus + 64
-- item if so, or nil otherwise
  elseif style == "Ranged" then
function p._getMonsterBones(monster)
    effDefLvl = monster.defenceLevel + 9
if monster.bones ~= nil then
    defBonus = monster.defenceBonusRanged + 64
local boneItem = Items.getItemByID(monster.bones.itemID)
  elseif style == "Magic" then
        local boneObj = { ["item"] = boneItem, ["quantity"] = monster.bones.quantity }
    effDefLvl = math.floor(monster.magicLevel * 0.7 + monster.defenceLevel * 0.3) + 9
if boneItem.prayerPoints == nil then
    defBonus = monster.defenceBonusMagic + 64
-- Assume bones without prayer points are shards (from God dungeons),
  else
-- and drop unconditionally
    return "ERROR: Must choose Melee, Ranged, or Magic"
return boneObj
  end
elseif not monster.isBoss and not p._isDungeonOnlyMonster(monster) then
  return effDefLvl * defBonus
-- Otherwise, bones drop when the monster isn't dungeon exclusive
return boneObj
end
end
end
 
function p._isDungeonOnlyMonster(monster)
local areaList = Areas.getMonsterAreas(monster.id)
local inDungeon = false
 
for i, area in ipairs(areaList) do
if area.type == 'dungeon' then
inDungeon = true
else
return false
end
end
return inDungeon
end
 
function p.isDungeonOnlyMonster(frame)
local monsterName = frame.args ~= nil and frame.args[1] or frame
local monster = p.getMonster(monsterName)
 
if monster == nil then
return Shared.printError('No monster with name ' .. monsterName .. ' found')
end
 
return p._isDungeonOnlyMonster(monster)
end
 
function p._getMonsterAreas(monster, excludeDungeons, includeEffects)
if includeEffects == nil then includeEffects = false end
local resultPart = {}
local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
local areaList = Areas.getMonsterAreas(monster.id)
for i, area in ipairs(areaList) do
if area.type ~= 'dungeon' or not hideDungeons then
local imgType = (area.type == 'slayerArea' and 'combatArea') or area.type
local txt = Icons.Icon({area.name, type = imgType})
if area.type == 'slayerArea' then
local areaDescrip = Areas._getAreaStat(area, 'areaEffectDesc')
if areaDescrip ~= 'None' then
txt = txt.." - ''"..areaDescrip.."''"
end
end
table.insert(resultPart, txt)
end
end
return table.concat(resultPart, '<br/>')
end
end


function p.getMonsterAreas(frame)
function p.getMonsterAreas(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local monsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local hideDungeons = frame.args ~= nil and frame.args[2] or nil
local includeEffects = frame.args ~= nil and frame.args[3] or true
local monster = p.getMonster(monsterName)
 
if monster == nil then
return Shared.printError('No monster with name ' .. monsterName .. ' found')
end
 
return p._getMonsterAreas(monster, hideDungeons, includeEffects)
end


  if monster == nil then
function p.getSpecAttackMaxHit(specAttack, normalMaxHit, monster)
    return "ERROR: No monster with name "..monsterName.." found"
local bestHit = 0
  end
for i, dmg in pairs(specAttack.damage) do
local thisHit = 0
if dmg.damageType == 'Normal' then
--Account for special attacks that include a normal attack hit
thisHit = normalMaxHit
if dmg.amplitude ~= nil then
thisHit = thisHit * (dmg.amplitude / 100)
end
elseif dmg.maxRoll == 'Fixed' then
thisHit = dmg.maxPercent * 10
elseif dmg.maxRoll == 'MaxHit' then
if dmg.character == 'Target' then
--Confusion applied damage based on the player's max hit. Gonna just ignore that one
thisHit = 0
else
thisHit = dmg.maxPercent * normalMaxHit * 0.01
end
elseif Shared.contains(dmg.maxRoll, "Fixed100") then
--Handles attacks that are doubled when conditions are met like Trogark's double damage if the player is burning
thisHit = dmg.maxPercent * 20
elseif dmg.maxRoll == 'MaxHitScaledByHP2x' then
thisHit = normalMaxHit * 2
elseif dmg.maxRoll == 'PoisonMax35' then
thisHit = normalMaxHit * 1.35
elseif dmg.maxRoll == "MaxHitDR" then
local monsterDR = 0
if monster ~= nil then
monsterDR = p._getMonsterStat(monster, 'damageReduction')
end
thisHit = normalMaxHit * dmg.maxPercent * 0.01 * (1 + monsterDR * 0.01)
elseif Shared.contains({'Bleeding', 'Poisoned'}, dmg.maxRoll) then
-- TODO: This is limited in that there is no verification that bleed/poison
-- can be applied to the target, it is assumed that it can and so this applies
thisHit = thisHit + dmg.maxPercent * 10
end
if thisHit > bestHit then
bestHit = thisHit
end
end
return bestHit
end


  local result = ''
function p.canSpecAttackApplyEffect(specAttack, effectType)
  for i, area in pairs(AreaData.combatAreas) do
local effectKeys = { 'prehitEffects', 'onhitEffects' }
    if Shared.contains(area.monsters, monster.id) then
for i, effectKey in ipairs(effectKeys) do
      if string.len(result) > 0 then result = result..'<br/>' end
if type(specAttack[effectKey]) == 'table' then
      result = result..Icons.Icon({area.areaName, type = 'combatArea'})
for j, effect in pairs(specAttack[effectKey]) do
    end
if effect.type == effectType or p.canModifiersApplyEffect(effect.modifiers, effectType) then
  end
return true
  for i, area in pairs(AreaData.slayerAreas) do
end
    if Shared.contains(area.monsters, monster.id) then
end
      if string.len(result) > 0 then result = result..'<br/>' end
end
      result = result..Icons.Icon({area.areaName, type = 'combatArea'})..'[[Category:Slayer Monsters]]'
end
    end
return false
  end
end
  for i, area in pairs(AreaData.dungeons) do
 
    if Shared.contains(area.monsters, monster.id) then
function p.canModifiersApplyEffect(modifiers, effectType)
      if string.len(result) > 0 then result = result..'<br/>' end
-- List of modifiers which can result in the application of status effects
      result = result..Icons.Icon({area.name, type = 'dungeon'})..'[[Category:Dungeon Monsters]]'
local statusModsAll = {
    end
["Stun"] = { 'increasedGlobalStunChance', 'increasedMeleeStunChance' },
  end
["Sleep"] = { 'increasedGlobalSleepChance' },
  return result
["Poison"] = { 'increasedChanceToApplyPoison' },
["Slow"] = { 'increased15SlowStunChance2Turns', 'increased30Slow5TurnsChance' }
}
 
local statusMods = statusModsAll[effectType]
if statusMods ~= nil and type(modifiers) == 'table' then
for modName, modMagnitude in pairs(modifiers) do
if Shared.contains(statusMods, modName) then
return true
end
end
end
return false
end
 
function p._getMonsterMaxHit(monster, doStuns)
-- 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
if doStuns == nil then
doStuns = true
elseif type(doStuns) == 'string' then
doStuns = string.upper(doStuns) == 'TRUE'
end
 
-- Damage adjustments are defined as follows:
-- multiplier - Damage from modifier 'increasedDamageTaken' & additional damage while
-- stunned, asleep, or poisoned. Defined by in-game function
-- getDamageModifiers(). Applies after other percentage of flat adjustments.
-- percent - Percentage adjustments to the max hit. Applies before flat & multiplier
-- adjustments.
-- flat - Flat adjustments to the max hit. Applies after percent adjustments, and
-- after multiplier adjustments.
local dmgAdjust = { ["percent"] = 100, ["flat"] = 0, ["multiplier"] = 100 }
-- Check passives & effects that apply pre or on hit for damage modifiers
local dmgMods = {
-- List of modifiers which affect damage dealt, and whether they are percentage or flat adjustments
["increasedDamageTaken"] = { type = 'multiplier', mult = 1 },
["increasedMaxHitPercent"] = { type = 'percent', mult = 1 },
["increasedMeleeMaxHit"] = { type = 'percent', mult = 1 },
["increasedRangedMaxHit"] = { type = 'percent', mult = 1 },
["increasedMagicMaxHit"] = { type = 'percent', mult = 1 },
["increasedMaxHitFlat"] = { type = 'flat', mult = 10 },
["increasedMeleeMaxHitFlat"] = { type = 'flat', mult = 10 },
["increasedRangedMaxHitFlat"] = { type = 'flat', mult = 10 },
["increasedMagicMaxHitFlat"] = { type = 'flat', mult = 10 },
-- Rage: +2% max hit per stack, maximum of 10 stacks
["increasedRage"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 10 },
-- Dark Blade: +1% max hit per successful hit, maximum of 30 stacks
["increasedChanceDarkBlade"] = { type = 'percent', mult = 1, magnitude = 1, maxStacks = 30 },
-- Growing Madness/Moment in Time/Reign Over Time: +2% max hit per stack, maximum of 25 stacks
["growingMadnessPassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 },
["momentInTimePassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 },
["reignOverTimePassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 }
}
local effectKeys = { 'prehitEffects', 'onhitEffects' }
local dmgStatuses = {
-- List of status effects which can affect damage dealt
["Stun"] = { type = 'multiplier', magnitude = 30 },
["Sleep"] = { type = 'multiplier', magnitude = 20 }
}
local canApplyStatus = {}
-- Initialize table
for statusName, def in pairs(dmgStatuses) do
canApplyStatus[statusName] = false
end
 
local adjustForMod = function(mod, modMagnitude, effect)
local magnitude = mod.magnitude or modMagnitude
local maxStacks = mod.maxStacks or (effect ~= nil and effect.maxStacks) or 1
dmgAdjust[mod.type] = dmgAdjust[mod.type] + magnitude * mod.mult * maxStacks
end
 
local adjustForCurse = function(curseID, effect)
local curse = Magic.getSpellByID(curseID, 'curse')
if type(curse) == 'table' and type(curse.targetModifiers) == 'table' then
for modName, modMagnitude in pairs(curse.targetModifiers) do
local mod = dmgMods[modName]
if mod ~= nil then
-- The modifier is one which affects damage dealt
adjustForMod(mod, modMagnitude, effect)
end
end
end
end
 
-- Check monster passives for modifiers which affect damage dealt, and alo if any modifiers
-- present can apply stun or sleep
if monster ~= nil and type(monster.passives) ~= nil then
for i, passiveID in ipairs(monster.passives) do
local passive = p.getPassiveByID(passiveID)
if passive ~= nil and type(passive.modifiers) == 'table' then
for modName, modMagnitude in pairs(passive.modifiers) do
local mod = dmgMods[modName]
if modName == 'applyRandomCurseOnSpawn' then
-- Special case in which the enemy can apply a random curse. Currently
-- Anguish III is the curse with the highest +% damage taken, so use this.
adjustForCurse('melvorF:AnguishIII')
elseif mod ~= nil then
-- The modifier is one which affects damage dealt
adjustForMod(mod, modMagnitude)
end
end
-- Check for application of relevant status effects
if doStuns then
for statusName, statusDef in pairs(dmgStatuses) do
if not canApplyStatus[statusName] and p.canModifiersApplyEffect(passive.modifiers, statusName) then
canApplyStatus[statusName] = true
end
end
end
end
end
end
 
local normalChance = 100
local specialMaxHit = 0
local normalMaxHit = p._getMonsterBaseMaxHit(monster)
local hasActiveBuffSpec = false
if monster.specialAttacks ~= nil then
for i, specAttackID in pairs(monster.specialAttacks) do
            local specAttack = GameData.getEntityByID('attacks', specAttackID)
for i, effectKey in ipairs(effectKeys) do
if type(specAttack[effectKey]) == 'table' then
for j, effect in ipairs(specAttack[effectKey]) do
local countsOnPlayer = (effect.countsOn == nil or effect.countsOn == 'Attacker')
if countsOnPlayer then
-- Check for pre or on hit effects for modifiers which affect damage dealt
if type(effect.modifiers) == 'table' then
for modName, modMagnitude in pairs(effect.modifiers) do
local mod = dmgMods[modName]
if mod ~= nil then
-- The modifier is one which affects damage dealt
adjustForMod(mod, modMagnitude, effect)
end
end
end
-- Check for curses which may cause the player to incur additional damage
if effect.effectType == 'Curse' then
-- If isRandom is true then a random curse is selected. Currently
-- Anguish III is the curse with the highest +% damage taken, so
-- use this.
local curseID = (effect.isRandom and 'melvorF:AnguishIII') or effect.curse
if curseID ~= nil then
adjustForCurse(curseID, effect)
end
end
end
end
end
end
 
if monster.overrideSpecialChances ~= nil then
normalChance = normalChance - monster.overrideSpecialChances[i]
else
normalChance = normalChance - specAttack.defaultChance
end
-- Check for application of relevant status effects
if doStuns then
for statusName, statusDef in pairs(dmgStatuses) do
if not canApplyStatus[statusName] and p.canSpecAttackApplyEffect(specAttack, statusName) then
canApplyStatus[statusName] = true
end
end
end
 
local thisMax = p.getSpecAttackMaxHit(specAttack, normalMaxHit, monster)
if thisMax > specialMaxHit then specialMaxHit = thisMax end
if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then
hasActiveBuffSpec = true
end
end
 
if doStuns then
for statusName, statusDef in pairs(dmgStatuses) do
if canApplyStatus[statusName] then
local adjType = statusDef.type
dmgAdjust[adjType] = dmgAdjust[adjType] + statusDef.magnitude
end
end
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
local maxHit = math.floor(math.max(specialMaxHit, normalMaxHit) * dmgAdjust.percent / 100) + dmgAdjust.flat
return math.floor(maxHit * dmgAdjust.multiplier / 100)
end
end


function p.getMonsterMaxHit(frame)
function p.getMonsterMaxHit(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local doStuns = frame.args ~= nil and frame.args[2] or true
local monster = p.getMonster(MonsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end


  local normalChance = 100
return p._getMonsterMaxHit(monster, doStuns)
  local specialMaxHit = 0
end
  local normalMaxHit = p.getMonsterBaseMaxHit(frame)
 
  if monster.hasSpecialAttack then
function p._getMonsterBaseMaxHit(monster)
    for i, specID in pairs(monster.specialAttackID) do
--8/27/21 - Now references p.calculateStandardMaxHit for Melee & Ranged
      local specAttack = p.getSpecialAttackByID(specID)
local result = 0
      if monster.overrideSpecialChances ~= nil then
local baseLevel = 0
         normalChance = normalChance - monster.overrideSpecialChances[i]
local bonus = 0
      else
if monster.attackType == 'melee' then
         normalChance = normalChance - specAttack.chance
baseLevel = p._getMonsterLevel(monster, 'Strength')
      end
bonus = p.getEquipmentStat(monster, 'meleeStrengthBonus')
      local thisMax = 0
result = p.calculateStandardMaxHit(baseLevel, bonus)
      if specAttack.setDamage ~= nil then
elseif monster.attackType == 'ranged' then
        thisMax = specAttack.setDamage * 10
baseLevel = p._getMonsterLevel(monster, 'Ranged')
      else
bonus = p.getEquipmentStat(monster, 'rangedStrengthBonus')
        thisMax = normalMaxHit
result = p.calculateStandardMaxHit(baseLevel, bonus)
      end
elseif monster.attackType == 'magic' then
      if thisMax > specialMaxHit then specialMaxHit = thisMax end
        if monster.selectedSpell == nil then
    end
            result = 0
  end
         else
  --Ensure that if the monster never does a normal attack, the normal max hit is irrelevant
            local mSpell = Magic.getSpellByID(monster.selectedSpell, 'standard')
  if normalChance == 0 then normalMaxHit = 0 end
            if mSpell == nil then
  return math.max(specialMaxHit, normalMaxHit)
                result = 0
            else
                baseLevel = p._getMonsterLevel(monster, 'Magic')
                bonus = p.getEquipmentStat(monster, 'magicDamageBonus')
                result = math.floor(10 * mSpell.maxHit * (1 + bonus / 100) * (1 + (baseLevel + 1) / 200))
            end
         end
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 magicDmg = 0
        if monster.selectedSpell ~= nil then
            local mSpell = Magic.getSpellByID(monster.selectedSpell, 'standard')
            if mSpell ~= nil then
                baseLevel = p._getMonsterLevel(monster, 'Magic')
                bonus = p.getEquipmentStat(monster, 'magicDamageBonus')
                magicDmg = math.floor(10 * mSpell.maxHit * (1 + bonus / 100) * (1 + (baseLevel + 1) / 200))
            end
        end
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 Shared.printError('This monster has an invalid attack type somehow')
end
 
return result
end
end


function p.getMonsterBaseMaxHit(frame)
function p.getMonsterBaseMaxHit(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end
 
  local effStrLvl = 0
  local strBonus = 0
  if monster.attackType == Constants.attackType.Melee then
    effStrLvl = monster.strengthLevel + 9
    strBonus = monster.strengthBonus
  elseif monster.attackType == Constants.attackType.Ranged then
    effStrLvl = monster.rangedLevel + 9
    strBonus = monster.strengthBonusRanged
  elseif monster.attackType == Constants.attackType.Magic then
    local mSpell = nil
    if monster.selectedSpell ~= nil then mSpell = Magic.getSpellByID(monster.selectedSpell) end
    if mSpell == nil then
      return math.floor(10 * (monster.setMaxHit + (monster.setMaxHit * monster.damageBonusMagic / 100)))
    else
      return math.floor(10 * (mSpell.maxHit + (mSpell.maxHit * monster.damageBonusMagic / 100)))
    end
  else
    return "ERROR: This monster has an invalid attack type somehow"
  end


  --Should only get here for Melee/Ranged, which use functionally the same damage formula
return p._getMonsterBaseMaxHit(monster)
  return math.floor(10 * (1.3 + (effStrLvl/10) + (strBonus / 80) + ((effStrLvl * strBonus) / 640)))
end
end


function p.getMonsterAttacks(frame)
function p.getMonsterAttacks(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found"
return Shared.printError('No monster with that name found')
  end
end


  local result = ''
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 iconText = ''
local buffAttacks = {}
  local typeText = ''
local hasActiveBuffSpec = false
  if  monster.attackType == Constants.attackType.Melee then
local isNormalAttackRelevant = false
    iconText = Icons.Icon({'Melee', notext=true})
    typeText = 'Melee'
  elseif monster.attackType == Constants.attackType.Ranged then
    iconText = Icons.Icon({'Ranged', type='skill', notext=true})
    typeText = 'Ranged'
  else
    iconText = Icons.Icon({'Magic', type='skill', notext=true})
    typeText = 'Magic'
  end


  local normalAttackChance = 100
local normalAttackChance = 100
  if monster.hasSpecialAttack then
if monster.specialAttacks ~= nil then
    for i, specID in pairs(monster.specialAttackID) do
for i, specAttackID in pairs(monster.specialAttacks) do
      local specAttack = p.getSpecialAttackByID(specID)
            local specAttack = GameData.getEntityByID('attacks', specAttackID)
      local attChance = 0
local attChance = 0
      if monster.overrideSpecialChances ~= nil then
if monster.overrideSpecialChances ~= nil then
        attChance = monster.overrideSpecialChances[i]
attChance = monster.overrideSpecialChances[i]
      else
else
        attChance = specAttack.chance
attChance = specAttack.defaultChance
      end
end
      normalAttackChance = normalAttackChance - attChance
normalAttackChance = normalAttackChance - attChance


      result = result..'\r\n* '..attChance..'% '..iconText..' '..specAttack.name..'\r\n** '..specAttack.description
result = result..'\r\n* '..attChance..'% '..iconText..' '..specAttack.name..'\r\n** '..specAttack.description
--If this special attack applies a curse, let's actually list what that curse does
if specAttack.onhitEffects ~= nil then
for j, hitEffect in ipairs(specAttack.onhitEffects) do
if hitEffect.effectType == 'Curse' then
local curse = Magic.getSpellByID(hitEffect.curse, 'curse')
result = result..'\r\n*** '..Icons.Icon({curse.name, type='curse'})..': '..Magic._getSpellDescription(curse, true)
end
end
end
 
if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then
table.insert(buffAttacks, specAttack.name)
hasActiveBuffSpec = true
isNormalAttackRelevant = true
end
if not isNormalAttackRelevant and type(specAttack.damage) == 'table' then
-- Determine if the special attack uses normal damage in some form
for j, dmgData in ipairs(specAttack.damage) do
if dmgData.damageType == 'Normal' then
isNormalAttackRelevant = true
break
end
end
end
end
end
if isNormalAttackRelevant or normalAttackChance > 0 then
--Reformatting slightly - If there are any special attacks, specifically label the Normal Attack
local normalDmgText = ' 1 - '..Shared.formatnum(p._getMonsterBaseMaxHit(monster))..' '..typeText..' Damage'
if normalAttackChance > 0 and normalAttackChance < 100 then
normalDmgText = normalAttackChance .. '% ' ..iconText..' Normal Attack\r\n** '..normalDmgText
elseif hasActiveBuffSpec and normalAttackChance == 0 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)
normalDmgText = iconText..' Normal Attack\r\n** '..normalDmgText .. ' (Instead of repeating '..table.concat(buffAttacks, ' or ')..' while the effect is already active)'
end
result = '* ' .. normalDmgText .. result
end
 
return result
end
 
--Function for pulling how much the monster reduces the player DR
--Goes through the passvies to look for the decreasedPlayerDamageReduction modifier
function p._getMonsterDrReduction(monster)
local totalResult = 0
    if type(monster.passives) == 'table' and not Shared.tableIsEmpty(monster.passives) then
for i, passiveID in ipairs(monster.passives) do
local passive = p.getPassiveByID(passiveID)
if passive.modifiers ~= nil then
if passive.modifiers['decreasedPlayerDamageReduction'] ~= nil then
totalResult = totalResult + passive.modifiers['decreasedPlayerDamageReduction']
end
end
end
     end
     end
  end
   
  if normalAttackChance == 100 then
    return totalResult
    result = iconText..'1-'..p.getMonsterBaseMaxHit(frame)..' '..typeText..' Damage'
end
  elseif normalAttackChance > 0 then
 
    result = '* '..normalAttackChance..'% '..iconText..'1-'..p.getMonsterBaseMaxHit(frame)..' '..typeText..' Damage'..result
function p.getMonsterDrReduction(frame)
  end
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  return result
local monster = p.getMonster(MonsterName)
 
if monster == nil then
return Shared.printError('No monster with that name found')
end
return p._getMonsterDrReduction(monster)
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 Shared.printError('No monster with that name found')
end
 
local result = ''
    if type(monster.passives) == 'table' and not Shared.tableIsEmpty(monster.passives) then
result = result .. '===Passives==='
for i, passiveID in ipairs(monster.passives) do
local passive = p.getPassiveByID(passiveID)
result = result .. '\r\n* ' .. passive.name .. '\r\n** ' .. Constants.getDescription(passive.customDescription, passive.modifiers)
end
end
return result
end
end


function p.getMonsterCategories(frame)
function p.getMonsterCategories(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)
 
if monster == nil then
return Shared.printError('No monster with that name found')
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 type(monster.passives) == 'table' and not Shared.tableIsEmpty(monster.passives) then
result = result..'[[Category:Monsters with Special Attacks]]'
end
 
if monster.isBoss then
result = result..'[[Category:Bosses]]'
end
 
return result
end
 
function p.getMonsterBoxBarrierText(frame)
local MonsterName = frame.args ~= nil and frame.args[1] or frame
local monster = p.getMonster(MonsterName)
 
if monster == nil then
return Shared.printError('No monster with that name found')
end
local barrier = p._getMonsterBarrier(monster)
if barrier == 0 then
return ''
end
local result = {}
table.insert(result, '|-\r\n| style="font-weight: bold;" | [[Barrier]]:')
table.insert(result, '\r\n| colspan=15 style="text-align: right" |')
table.insert(result, Icons.Icon({"Barrier", notext="true"}))
table.insert(result, ' '..barrier)
return table.concat(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 Shared.printError('No monster with that name found')
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


  if monster == nil then
local areaList = Areas.getMonsterAreas(monster.id)
    return "ERROR: No monster with that name found"
local counts = {combatArea = 0, slayerArea = 0, dungeon = 0}
  end
for i, area in ipairs(areaList) do
counts[area.type] = counts[area.type] + 1
end


  local result = '[[Category:Monsters]]'
if counts.combatArea > 0 then table.insert(monsterTypes, 'Combat Area') end
 
if counts.slayerArea > 0 then table.insert(monsterTypes, 'Slayer Area') end
  if monster.attackType == Constants.attackType.Melee then
if counts.dungeon > 0 then table.insert(monsterTypes, 'Dungeon') end
    result = result..'[[Category:Melee Monsters]]'
  elseif monster.attackType == Constants.attackType.Ranged then
    result = result..'[[Category:Ranged Monsters]]'
  elseif monster.attackType == Constants.attackType.Magic then
    result = result..'[[Category:Magic Monsters]]'
  end


  if monster.hasSpecialAttack then
result = result.."\r\n|-\r\n|'''Monster Types:''' "..table.concat(monsterTypes, ", ")
    result = result..'[[Category:Monsters with Special Attacks]]'
 
  end
local SlayerTier = 'N/A'
  return result
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
end


function p.getMonsterDrops(frame)
function p.getMonsterDrops(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)
 
if monster == nil then
return Shared.printError('No monster with that name found')
end
 
local result = ''
 
local bones = p._getMonsterBones(monster)
local boneVal = 0
local barrierDust = Items.getItemByID("melvorAoD:Barrier_Dust")
local dustVal = 0
--Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
if bones ~= nil then
local boneQty = (bones.quantity ~= nil and bones.quantity or 1)
local barrier = p._getMonsterBarrier(monster)
result = result.."'''Always Drops:'''"
result = result..'\r\n{|class="wikitable" id="bonedrops"'
result = result..'\r\n!Item !! Qty'
result = result..'\r\n|-\r\n|'..Icons.Icon({bones.item.name, type='item'})
result = result..'||'..boneQty
if barrier > 0 then
local dustQty = math.max(math.floor(barrier / 10 / 20), 1)
result = result..'\r\n|-\r\n|'..Icons.Icon({barrierDust.name, type='item'})
result = result..'||'..dustQty
dustVal = dustQty * barrierDust.sellsFor
end
result = result..'\r\n'..'|}'
boneVal = boneQty * bones.item.sellsFor
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


  if monster == nil then
result = result.."'''Loot:'''"
    return "ERROR: No monster with that name found"
local avgGp = 0
  end


  local result = ''
if monster.gpDrops ~= nil then
avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
local gpTxt = Icons.GP(monster.gpDrops.min, monster.gpDrops.max)
result = result.."\r\nIn addition to loot, the monster will also drop "..gpTxt..'.'
end


  if monster.bones ~= nil then
local multiDrop = Shared.tableCount(monster.lootTable) > 1
    local bones = Items.getItemByID(monster.bones)
local totalWt = 0
    result = result.."'''Always Drops:'''"
for i, row in ipairs(monster.lootTable) do
    result = result..'\r\n{|class="wikitable"'
totalWt = totalWt + row.weight
    result = result..'\r\n!Item !! Qty'
end
    result = result..'\r\n|-\r\n|'..Icons.Icon({bones.name, type='item'})
result = result..'\r\n{|class="wikitable sortable" id="itemdrops"'
    result = result..'||'..(monster.boneQty ~= nil and monster.boneQty or 1)..'\r\n'..'|}'
result = result..'\r\n!Item!!Qty'
  end
result = result..'!!Price!!colspan="2"|Chance'


  if monster.lootTable ~= nil then
--Sort the loot table by weight in descending order
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
local lootTable = Shared.shallowClone(monster.lootTable)
table.sort(lootTable, function(a, b)
if a.weight == b.weight then
local aItem, bItem = Items.getItemByID(a.itemID), Items.getItemByID(b.itemID)
if aItem ~= nil and bItem ~= nil then
return aItem.name < bItem.name
else
return a.itemID < b.itemID
end
else
return a.weight > b.weight
end
end)
for i, row in ipairs(lootTable) do
local thisItem = Items.getItemByID(row.itemID)
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="'..row.maxQuantity..'"|'


    result = result.."'''Loot:'''"
if row.maxQuantity > row.minQuantity then
result = result .. Shared.formatnum(row.minQuantity) .. ' - '
end
result = result .. Shared.formatnum(row.maxQuantity)


    if monster.dropCoins ~= nil then
--Adding price columns
      local gpTxt = Icons.GP(monster.dropCoins[1], monster.dropCoins[2])
local itemPrice = 0
      if lootChance == 100 then
if thisItem == nil then
        result = result.."\r\nIn addition to loot, the monster will also drop "..gpTxt
result = result..'||data-sort-value="0"|???'
      else
else
        result = result.."\r\nIf loot is received, the monster will also drop "..gpTxt
itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
      end
if itemPrice == 0 or row.maxQuantity == row.minQuantity then
    end
result = result..'||'..Icons.GP(itemPrice * row.minQuantity)
else
result = result..'||'..Icons.GP(itemPrice * row.minQuantity, itemPrice * row.maxQuantity)
end
end
 
--Getting the drop chance
local dropChance = (row.weight / 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.weight..'"'
result = result..'|'..Shared.fraction(row.weight * lootChance, totalWt * 100)
result = result..'||'
else
result = result..'||colspan="2" data-sort-value="'..row.weight..'"'
end
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
result = result..'style="text-align:right"|'..string.format(fmt, dropChance)..'%'
 
--Adding to the average loot value based on price & dropchance
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 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"
if boneVal > 0 then
result = result..' and bones'
end
if dustVal > 0 then
result = result..' and barrier dust'
end
result = result..', the average kill is worth '..Icons.GP(Shared.round(avgGp + lootValue + boneVal + dustVal, 2, 0))..'.'
end
end
 
--If no other drops, make sure to at least say so.
if result == '' then result = 'None' end
return result
end
 
function p._getMonsterLootValue(monster)
if monster == nil then
return Shared.printError('No monster with that name found')
end
 
local result = 0
local boneVal = 0
 
local bones = p._getMonsterBones(monster)
--Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
if bones ~= nil then
local boneQty = (bones.quantity ~= nil and bones.quantity) or 1
boneVal = bones.item.sellsFor * boneQty
result = result + boneVal
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.gpDrops ~= nil then
avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
end


    local multiDrop = Shared.tableCount(monster.lootTable) > 1
local multiDrop = Shared.tableCount(monster.lootTable) > 1
    local totalWt = 0
local totalWt = 0
    for i, row in pairs(monster.lootTable) do
for i, row in pairs(monster.lootTable) do
      totalWt = totalWt + row[2]
totalWt = totalWt + row.weight
    end
end
    result = result..'\r\n{|class="wikitable sortable"'
    result = result..'\r\n!Item!!Qty'
    --if multiDrop then result = result..'!!Weight' end


    result = result..'!!colspan="2"|Chance'
for i, row in ipairs(monster.lootTable) do
    --Sort the loot table by weight in descending order
local thisItem = Items.getItemByID(row.itemID)
    table.sort(monster.lootTable, function(a, b) return a[2] > b[2] end)
    for i, row in pairs(monster.lootTable) do
      local thisItem = Items.getItemByID(row[1])
      result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
      result = result..'||style="text-align:right" data-sort-value="'..row[3]..'"|'


      if row[3] > 1 then  
--Adding price columns
        result = result.. '1 - '
local itemPrice = 0
      end
if thisItem ~= nil then
      result = result..Shared.formatnum(row[3])
itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
end


      local dropChance = (row[2] / totalWt * lootChance)
--Getting the drop chance
      result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
local dropChance = (row.weight / totalWt * lootChance)
      result = result..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
--Adding to the average loot value based on price & dropchance
      result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 2))
    end
end
    if multiDrop then
if avgGp > 0 then
      result = result..'\r\n|-class="sortbottom" \r\n!colspan="2"|Total:'
result = result + avgGp + lootValue
      if lootChance < 100 then
end
        result = result..'\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||'
end
      else
        result = result..'\r\n|colspan="2" '
      end
      result = result..'style="text-align:right"|'..lootChance..'.00%'
    end
    result = result..'\r\n|}'
  end


  return result
return result
end
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 Shared.printError('No monster with that name found')
end
if item == nil then
return Shared.printError('No item with that name found')
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
local dropWt = 0
for i, row in ipairs(monster.lootTable) do
totalWt = totalWt + row.weight
if item.id == row.itemID then
dropWt = row.weight
end
end
dropChance = (dropWt / totalWt * lootChance)
return Shared.round(dropChance, 2, 2)
end
end


function p.getChestDrops(frame)
function p.getChestDrops(frame)
  local ChestName = frame.args ~= nil and frame.args[1] or frame
local chestName = frame.args ~= nil and frame.args[1] or frame
  local chest = Items.getItem(ChestName)
local chest = Items.getItem(chestName)
 
if chest == nil then
return Shared.printError('No item named ' .. chestName .. ' found')
end
local result = ''
 
if chest.dropTable == nil then
return Shared.printError(chestName .. ' does not have a drop table')
else
 
local function formatNumRange(minValue, maxValue)
if maxValue ~= nil and maxValue > minValue then
return Shared.formatnum(minValue) .. ' - ' .. Shared.formatnum(maxValue)
else
return Shared.formatnum(minValue)
end
end
 
local lootValue, foodValue = 0, 0
local totalWt = 0
local isAllFood = true
for i, row in ipairs(chest.dropTable) do
totalWt = totalWt + row.weight
if isAllFood then
-- If the container's contents are entirely food then we add additional
-- information to the output, so we determine this here
local item = Items.getItemByID(row.itemID)
if item ~= nil and item.healsFor == nil then
isAllFood = false
end
end
end
result = result..'\r\n{|class="wikitable sortable"'
result = result..'\r\n!Item!!Qty'
result = result..'!!colspan="2"|Chance!!Price' .. (isAllFood and '!!Healing!!Avg. Healing' or '')
 
--Sort the loot table by weight in descending order
local chestDrops = Shared.shallowClone(chest.dropTable)
table.sort(chestDrops, function(a, b) return a.weight > b.weight end)
for i, row in ipairs(chestDrops) do
local thisItem = Items.getItemByID(row.itemID)
result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
result = result..'||style="text-align:right" data-sort-value="'..(row.minQuantity + row.maxQuantity)..'"| ' .. formatNumRange(row.minQuantity, row.maxQuantity)
 
local dropChance = (row.weight / totalWt) * 100
result = result..'||style="text-align:right" data-sort-value="'..row.weight..'"'
result = result..'|'..Shared.fraction(row.weight, totalWt)
 
result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
 
result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
if thisItem.sellsFor == 0 or row.minQuantity == row.maxQuantity then
result = result..'|'..Icons.GP(thisItem.sellsFor * row.minQuantity)
else
result = result..'|'..Icons.GP(thisItem.sellsFor * row.minQuantity, thisItem.sellsFor * row.maxQuantity)
end
lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor * ((row.minQuantity + row.maxQuantity)/ 2))
 
if isAllFood then
local hp = thisItem.healsFor * 10
local minHeal, maxHeal = hp * row.minQuantity, hp * row.maxQuantity
local avgHpPerLoot = (dropChance * 0.01 * (minHeal + maxHeal) / 2)
foodValue = foodValue + avgHpPerLoot
result = result .. '||data-sort-value="' .. thisItem.healsFor .. '"'
result = result .. '|' .. Icons.Icon({'Hitpoints', type='skill', notext=true, nolink=true}) .. ' ' .. formatNumRange(minHeal, maxHeal)
result = result .. '||data-sort-value="' .. avgHpPerLoot .. '"'
result = result .. '|' .. Icons.Icon({'Hitpoints', type='skill', notext=true, nolink=true}) .. ' ' .. Shared.round(avgHpPerLoot, 2, 0)
end
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))..'.'
if isAllFood then
result = result..'\r\n\r\nThe average healing of the contents of one chest is ' .. Icons.Icon({'Hitpoints', type='skill', notext=true, nolink=true}) .. ' ' .. Shared.round(foodValue, 2, 0) .. '.'
end
end
 
return result
end


  if chest == nil then
function p.getAreaMonsterTable(frame)
    return "ERROR: No item named "..ChestName..' found'
local areaName = frame.args ~= nil and frame.args[1] or frame
  end
local area = Areas.getArea(areaName)
if area == nil then
return Shared.printError('Could not find an area named ' .. areaName)
end


  local result = ''
if area.type == 'dungeon' then
return p.getDungeonMonsterTable(frame)
end
local monsters = {}
local hasBarrier = false
for i, monsterID in ipairs(area.monsterIDs) do
local monster = p.getMonsterByID(monsterID)
if not hasBarrier and p._getMonsterBarrier(monster) > 0 then
hasBarrier = true
end
table.insert(monsters, monster)
end


  if chest.dropTable == nil then
local tableBits = {}
    return "ERROR: "..ChestName.." does not have a drop table"
table.insert(tableBits, '{| class="wikitable sortable"')
  else
table.insert(tableBits, '\r\n! Name !! Combat Level ')
    local lootChance = 100
if hasBarrier then
table.insert(tableBits, '!! [[Barrier]] ')
end
table.insert(tableBits, '!! Hitpoints !! colspan=2| Max Hit !! [[Combat Triangle|Combat Style]]')
for i, monster in ipairs(monsters) do
local rowBits = {}
table.insert(tableBits, '\r\n|-\r\n|'..Icons.Icon({p.getMonsterName(monster), type='monster'}))
table.insert(tableBits, '||'..p._getMonsterCombatLevel(monster))
if hasBarrier then
table.insert(tableBits, '||'..Shared.formatnum(p._getMonsterBarrier(monster)))
end
table.insert(tableBits, '||'..Shared.formatnum(p._getMonsterHP(monster)))
local drReduction = p._getMonsterDrReduction(monster)
local maxHit = p._getMonsterMaxHit(monster)
if drReduction > 0 then
table.insert(tableBits, '||style="text-align:right" data-sort-value="'..maxHit..'"| -'..drReduction..'% DR')
table.insert(tableBits, '||style="text-align:right"|'..Shared.formatnum(maxHit))
else
table.insert(tableBits, '||style="text-align:right" colspan="2" data-sort-value="'..maxHit..'"|'..Shared.formatnum(maxHit))
end
table.insert(tableBits, '||'..p._getMonsterStyleIcon({monster, nolink=true}))
end
table.insert(tableBits, '\r\n|}')
return table.concat(tableBits, '')
end


    local multiDrop = Shared.tableCount(chest.dropTable) > 1
function p.getDungeonMonsterTable(frame)
    local totalWt = 0
local areaName = frame.args ~= nil and frame.args[1] or frame
    for i, row in pairs(chest.dropTable) do
local area = Areas.getArea(areaName)
      totalWt = totalWt + row[2]
if area == nil then
    end
return Shared.printError('Could not find a dungeon named ' .. areaName)
    result = result..'\r\n{|class="wikitable sortable"'
end
    result = result..'\r\n!Item!!Qty'
 
    result = result..'!!colspan="2"|Chance!!Price'
--For Dungeons, go through and count how many of each monster are in the dungeon first
local monsterCounts = {}
local monsters = {}
local hasBarrier = false
for i, monsterID in ipairs(area.monsterIDs) do
if monsterCounts[monsterID] == nil then
monsterCounts[monsterID] = 1
else
monsterCounts[monsterID] = monsterCounts[monsterID] + 1
if monsterID ~= 'melvorF:RandomITM' and monsterID ~= 'melvorTotH:RandomSpiderLair' then
monsters[monsterID] = p.getMonsterByID(monsterID)
if not hasBarrier and p._getMonsterBarrier(monsters[monsterID]) > 0 then
hasBarrier = true
end
end
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, monDrReduce, monBarrier
local monData = {}
if specialType ~= nil and Shared.contains({'Afflicted', 'Spider', '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, monBarrier, monHP, monMaxHit, monDrReduce, monStyle, monCount = iconQ, iconQ, iconQ, iconQ, iconQ, iconQ, monsterCount
elseif specialType == 'Spider' then
local iconQ = Icons.Icon({'', notext=true, nolink=true, img='Question'})
local monIconPart = { 'Any of the following:' }
for i, monsterID in ipairs(GameData.rawData.spiderLairMonsters) do
local monster = p.getMonsterByID(monsterID)
if monster ~= nil then
table.insert(monIconPart, Icons.Icon({p.getMonsterName(monster), type='monster'}))
end
end
monIcon = table.concat(monIconPart, '<br/>')
monLevel, monBarrier, monHP, monMaxHit, monDrReduce, monStyle, monCount = iconQ, iconQ, 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.monsterIDs, function(monster) return p._getMonsterCombatLevel(monster) end)}
if hasBarrier then
monBarrier = {p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterBarrier(monster) end)}
end
monHP = {p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterHP(monster) end)}
local lowMaxHit, highMaxHit = p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterMaxHit(monster) end)
local lowDrReduce, highDrReduce = p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterDrReduction(monster) end)
monMaxHit = highMaxHit
monDrReduce = highDrReduce
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({p.getMonsterName(monster), type='monster'})
monLevel = p._getMonsterCombatLevel(monster)
if hasBarrier then
monBarrier = p._getMonsterBarrier(monster)
end
monHP = p._getMonsterHP(monster)
monDrReduce = p._getMonsterDrReduction(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))
if hasBarrier then
table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monBarrier) .. '"| ' .. getValText(monBarrier))
end
table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monHP) .. '"| ' .. getValText(monHP))
if type(monDrReduce) == 'number' and monDrReduce > 0 then
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="'..getValSort(monMaxHit)..'"| -'..monDrReduce..'% DR')
table.insert(resultPart, '\r\n|style="text-align:right"|'..getValText(monMaxHit))
else
table.insert(resultPart, '\r\n|style="text-align:right" colspan="2" data-sort-value="'..getValSort(monMaxHit)..'"|'..getValText(monMaxHit))
end
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 ')
if hasBarrier then
table.insert(returnPart, '!! [[Barrier]] ')
end
table.insert(returnPart, '!! Hitpoints !! colspan="2" | 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
for i, monsterID in ipairs(area.monsterIDs) do
if not Shared.contains(usedMonsters, monsterID) then
if monsterID == 'melvorF:RandomITM' then
--Special handling for Into the Mist
table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], 'Afflicted'))
elseif monsterID == 'melvorTotH:RandomSpiderLair' then
table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], 'Spider'))
else
table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], hasBarrier))
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 Shared.printError('Could not find a dungeon named ' .. areaName)
end
local totalHP = 0
 
for i, monsterID in ipairs(area.monsterIDs) do
        local monster = p.getMonsterByID(monsterID)
        totalHP = totalHP + p._getMonsterHP(monster)
end
return totalHP
end
 
function p._getAreaMonsterList(area)
local monsterList = {}
for i, monsterID in ipairs(area.monsterIDs) do
local monster = p.getMonsterByID(monsterID)
table.insert(monsterList, Icons.Icon({p.getMonsterName(monster), type='monster'}))
end
return table.concat(monsterList, '<br/>')
end
 
function p._getDungeonMonsterList(area)
local monsterList = {}
local lastID = ''
local count = 0
local monsterCounts = {}
for i, monsterID in ipairs(area.monsterIDs) do
if lastID == '' then
lastID = monsterID
count = 1
elseif lastID == monsterID then
count = count + 1
else
table.insert(monsterCounts, { id = lastID, count = count })
lastID = monsterID
count = 1
end
end
table.insert(monsterCounts, { id = lastID, count = count })
 
for i, monster in ipairs(monsterCounts) do
if monster.id == 'melvorF:RandomITM' then
--Special handling for Afflicted Monsters
table.insert(monsterList, Icons.Icon({'Affliction', 'Afflicted Monster', img='Question', qty=monster.count}))
elseif monster.id == 'melvorTotH:RandomSpiderLair' then
local monIconPart = { Shared.formatnum(monster.count) .. ' Spiders:' }
for i, monsterID in ipairs(GameData.rawData.spiderLairMonsters) do
local monster = p.getMonsterByID(monsterID)
if monster ~= nil then
table.insert(monIconPart, '&nbsp;&nbsp;&nbsp;' .. Icons.Icon({p.getMonsterName(monster), type='monster'}))
end
end
table.insert(monsterList, table.concat(monIconPart, '<br/>'))
else
local monsterObj = p.getMonsterByID(monster.id)
table.insert(monsterList, Icons.Icon({p.getMonsterName(monsterObj), type='monster', qty=monster.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 Shared.printError('Could not find an area named ' .. areaName)
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 ipairs(GameData.rawData.monsters) do
if not p._isDungeonOnlyMonster(monster) then
if monster.gpDrops ~= nil and monster.gpDrops.max > 0 then
local avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
result = result .. '<br/>' .. p.getMonsterName(monster) .. ',' .. monster.gpDrops.min .. ',' .. monster.gpDrops.max .. ',' .. avgGp
end
end
end
return result
end
 
function p._getMonsterAverageGP(monster)
local result = ''
local totalGP = 0
 
local bones = p._getMonsterBones(monster)
if bones ~= nil then
totalGP = totalGP + bones.item.sellsFor * bones.quantity
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.gpDrops ~= nil then
avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
end
 
totalGP = totalGP + avgGp
 
local totalWt = 0
for i, row in ipairs(monster.lootTable) do
totalWt = totalWt + row.weight
end
 
for i, row in ipairs(monster.lootTable) do
local thisItem = Items.getItemByID(row.itemID)
 
local itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
 
--Getting the drop chance
local dropChance = (row.weight / totalWt * lootChance)
 
--Adding to the average loot value based on price & dropchance
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 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 Shared.printError('No monster with that name found')
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, monster in ipairs(GameData.rawData.monsters) do
if not p._isDungeonOnlyMonster(monster) then
local monsterGP = p._getMonsterAverageGP(monster)
local combatLevel = p._getMonsterCombatLevel(monster)
result = result..'\r\n|-\r\n|'..Icons.Icon({p.getMonsterName(monster), 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 Shared.printError('No tier specified')
end
 
if tonumber(tier) ~= nil then
slayerTier = Constants.getSlayerTierByID(tonumber(tier))
else
slayerTier = Constants.getSlayerTier(tier)
end
 
if slayerTier == nil then
return Shared.printError('Invalid slayer tier')
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 monsterList = GameData.getEntities('monsters',
        function(monster)
            if monster.canSlayer and not Shared.contains(hiddenMonsterIDs, monster.id) then
                local cmbLevel = p._getMonsterCombatLevel(monster)
                return cmbLevel >= minLevel and (maxLevel == nil or cmbLevel <= maxLevel)
            end
            return false
        end)
 
if Shared.tableIsEmpty(monsterList) then
-- Somehow no monsters are in the tier, return nothing
return ''
else
return p._getMonsterTable(monsterList, true)
end
end
 
function p.getFullMonsterTable(frame)
return p._getMonsterTable(GameData.rawData.monsters, false)
end
 
function p._getMonsterTable(monsters, 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="4" | !! colspan="5" |Offensive Stats !! colspan="3" |Evasion Rating !! colspan="4" |')
-- Second header row
table.insert(tableParts, '\r\n|- class="headerRow-1"\r\n!Monster !!Name !!Combat Level ')
table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Hitpoints', type='skill'}))
table.insert(tableParts, '!!Attack Speed (s) !!colspan="3"|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, '!!DR!!' .. Icons.Icon({'Coins', notext=true, nolink=true}) .. ' Coins !!Bones !!Locations')
 
-- Generate row per monster
for i, monster in ipairs(monsters) do
-- Avoid processing monsters without equipment stats. These aren't actual
-- monsters, but instead are placeholders such as 'melvorF:RandomITM'
-- and 'melvorTotH:RandomSpiderLair' to denote a random selection from
-- a pool of monsters
if monster.equipmentStats ~= nil then
local cmbLevel = p._getMonsterCombatLevel(monster)
local atkSpeed = p._getMonsterAttackSpeed(monster)
local maxHit = p._getMonsterMaxHit(monster)
local dr = p._getMonsterStat(monster, 'damageReduction')
local drReduce = p._getMonsterDrReduction(monster)
local accR = p._getMonsterAR(monster)
local evaR = {p._getMonsterER(monster, "Melee"), p._getMonsterER(monster, "Ranged"), p._getMonsterER(monster, "Magic")}
local gpTxt = nil
if monster.gpDrops.min >= monster.gpDrops.max then
gpTxt = Shared.formatnum(monster.gpDrops.min)
else
gpTxt = Shared.formatnum(monster.gpDrops.min) .. ' - ' .. Shared.formatnum(monster.gpDrops.max)
end
local bones = p._getMonsterBones(monster)
local boneTxt = (bones ~= nil and Icons.Icon({bones.item.name, type='item', notext=true})) or 'None'
table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', size=50, notext=true}))
table.insert(tableParts, '\r\n|style="text-align:left" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', noicon=true}))
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))
if drReduce > 0 then
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. maxHit .. '"| -' .. drReduce..'% DR')
table.insert(tableParts, '\r\n|style="text-align:right;border-right:hidden" |' .. p._getMonsterStyleIcon({monster, notext=true}))
table.insert(tableParts, '\r\n|style="text-align:right" |' .. Shared.formatnum(maxHit))
else
table.insert(tableParts, '\r\n|style="text-align:right;border-right:hidden" colspan="2" data-sort-value="' .. maxHit .. '"|' .. p._getMonsterStyleIcon({monster, notext=true}))
table.insert(tableParts, '\r\n|style="text-align:right"|' .. Shared.formatnum(maxHit))
end
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="' .. dr .. '" |' .. dr..'%')
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. (monster.gpDrops.min + monster.gpDrops.max) / 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
end
 
table.insert(tableParts, '\r\n|}')
return table.concat(tableParts)
end
 
function p.getMattMonsterTable(frame)
--Making a single function for getting a table of monsters given a list of IDs.
local tableParts = {}
table.insert(tableParts, '{| class="wikitable sortable stickyHeader"')
-- 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, '!!' .. Icons.Icon({'Coins', notext=true, nolink=true}) .. ' Coins !!Avg. Kill Value!!Locations')
 
-- Generate row per monster
for i, monster in ipairs(GameData.rawData.monsters) do
if p.getMonsterName(monster) ~= nil then
local cmbLevel = p._getMonsterCombatLevel(monster)
local gpTxt = nil
if monster.gpDrops.min >= monster.gpDrops.max then
gpTxt = Shared.formatnum(monster.gpDrops.min)
else
gpTxt = Shared.formatnum(monster.gpDrops.min) .. ' - ' .. Shared.formatnum(monster.gpDrops.max)
end
local lootVal = p._getMonsterLootValue(monster)
local lootTxt = '0'
if lootVal ~= 0 then
lootTxt = Shared.formatnum(Shared.round(lootVal, 2, 2))
end
table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', size=50, notext=true}))
table.insert(tableParts, '\r\n|style="text-align:left" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', noicon=true}))
table.insert(tableParts, '\r\n|style="text-align:right" |' .. monster.id)
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="' .. (monster.gpDrops.min + monster.gpDrops.max) / 2 .. '" |' .. gpTxt)
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. lootVal .. '" |' .. lootTxt)
table.insert(tableParts, '\r\n|style="text-align:right;width:190px" |' .. p._getMonsterAreas(monster, false))
end
end
 
table.insert(tableParts, '\r\n|}')
return table.concat(tableParts)
end
 
function p.getMattMonsterTableV2(frame)
--Making a single function for getting a table of monsters given a list of IDs.
local tableParts = {}
table.insert(tableParts, '{| class="wikitable sortable stickyHeader"')
-- Second header row
table.insert(tableParts, '\r\n|- class="headerRow-1"\r\n!Monster !!Name !!Combat Level ')
table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Hitpoints', type='skill'}))
table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Defence', type='skill', notext=true}))
table.insert(tableParts, '!!Attack Speed (s) !!colspan="2"|Max Hit !!Accuracy ')
 
-- 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 !!Avg. Kill Value !!Bones')
 
-- Generate row per monster
for i, monster in ipairs(GameData.rawData.monsters) do
if p.getMonsterName(monster) ~= nil then
local cmbLevel = p._getMonsterCombatLevel(monster)
local gpTxt = nil
if monster.gpDrops.min >= monster.gpDrops.max then
gpTxt = Shared.formatnum(monster.gpDrops.min)
else
gpTxt = Shared.formatnum(monster.gpDrops.min) .. ' - ' .. Shared.formatnum(monster.gpDrops.max)
end
local lootVal = p._getMonsterLootValue(monster)
local lootTxt = '0'
if lootVal ~= 0 then
lootTxt = Shared.formatnum(Shared.round(lootVal, 2, 2))
end
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 bones = p._getMonsterBones(monster)
local boneTxt = (bones ~= nil and Icons.Icon({bones.item.name, type='item', notext=true})) or 'None'
table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', size=50, notext=true}))
table.insert(tableParts, '\r\n|style="text-align:left" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', noicon=true}))
-- table.insert(tableParts, '\r\n|style="text-align:right" |' .. monster.id)
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="' .. evaR[1] .. '" |' .. Shared.formatnum(evaR[1]))
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[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="' .. (monster.gpDrops.min + monster.gpDrops.max) / 2 .. '" |' .. gpTxt)
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. lootVal .. '" |' .. lootTxt)
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
end
 
table.insert(tableParts, '\r\n|}')
return table.concat(tableParts)
end
 
function p.getSpecialAttackTable(frame)
local spAttTable = {}


    --Sort the loot table by weight in descending order
for i, monster in ipairs(GameData.rawData.monsters) do
    table.sort(chest.dropTable, function(a, b) return a[2] > b[2] end)
if monster.specialAttacks ~= nil and not Shared.tableIsEmpty(monster.specialAttacks) then
    for i, row in pairs(chest.dropTable) do
local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
      local thisItem = Items.getItemByID(row[1])
for j, spAttID in ipairs(monster.specialAttacks) do
      local qty = 1
                local spAtt = GameData.getEntityByID('attacks', spAttID)
      if chest.dropQty ~= nil then qty = chest.dropQty[i] end
local attChance = (overrideChance and monster.overrideSpecialChances[j] or spAtt.defaultChance)
      result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
if spAttTable[spAtt.id] == nil then
      result = result..'||style="text-align:right" data-sort-value="'..qty..'"|'
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({ p.getMonsterName(monster), type = 'monster' }))
end
end
end


      if qty > 1 then
local resultPart = {}
        result = result.. '1 - '  
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
      end
table.insert(resultPart, '\r\n|- class="headerRow-0"')
      result = result..Shared.formatnum(qty)
table.insert(resultPart, '\r\n!Name!!style="min-width:225px"|Monsters!!Chance!!Effect')


      local dropChance = (row[2] / totalWt) * 100
for i, spAttData in Shared.skpairs(spAttTable) do
      result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
local spAtt = spAttData.defn
      result = result..'|'..Shared.fraction(row[2], totalWt)
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|}')


      result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
return table.concat(resultPart)
end


      result = result..'||style="text-align:right" data-sort-value="'..thisItem.sellsFor..'"'
--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot TablesTemplate:MonsterLootTables
      if qty > 1 then
--It exists because I'm too lazy to manually type up all the new monsters - User:Falterfire
        result = result..'|'..Icons.GP(thisItem.sellsFor, thisItem.sellsFor * qty)
function p.getMonsterLootTableText()
      else
local getAreaText = function(area)
        result = result..'|'..Icons.GP(thisItem.sellsFor)
local outArray = {}
      end
table.insert(outArray, "==={{ZoneIcon|"..area.name.."|size=50}}===")
    end
table.insert(outArray, "")
    result = result..'\r\n|}'
for i, monsterID in ipairs(area.monsterIDs) do
  end
local monster = p.getMonsterByID(monsterID)
table.insert(outArray, "===={{MonsterIcon|"..p.getMonsterName(monster).."|size=40}}====")
table.insert(outArray, "{{MonsterDrops|"..p.getMonsterName(monster).."|size=40}}")
end
return table.concat(outArray, "\r\n")
end
local fullArray = {}
local areaArray = Areas.getAreas(function(a) return a.type == 'combatArea' end)
for i, area in ipairs(areaArray) do
table.insert(fullArray, getAreaText(area))
end
areaArray = Areas.getAreas(function(a) return a.type == 'slayerArea' end)
for i, area in ipairs(areaArray) do
table.insert(fullArray, getAreaText(area))
end
return table.concat(fullArray, "\r\n\r\n----\r\n")
end


  return result
--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot Tables
--It exists because I'm too lazy to manually type up all the new chests - User:Falterfire
function p.getChestLootTables()
local items = Items.getItems(function(item) return item.dropTable ~= nil end)
local outArray = {}
for i, item in ipairs(items) do
table.insert(outArray, "==={{ItemIcon|"..item.name.."|size=30}}===")
table.insert(outArray, "{{ChestDrops|"..item.name.."}}")
end
return table.concat(outArray, "\r\n")
end
end


--Returns the expansion icon for the item if it has one
function p.getExpansionIcon(frame)
local monsterName = frame.args ~= nil and frame.args[1] or frame
local monster = p.getMonster(monsterName)
if monster == nil then
return Shared.printError('No monster with that name found')
end
return Icons.getExpansionIcon(monster.id)
end


return p
return p

Latest revision as of 19:32, 15 March 2024

Data is pulled from Module:GameData/data


local p = {}

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

function p.getMonster(name)
	if name == 'Earth Golem (AoD)' then
		-- Special case for ambiguous monster name
		return p.getMonsterByID('melvorAoD:EarthGolem')
	else
    	return GameData.getEntityByName('monsters', name)
    end
end

function p.getMonsterByID(ID)
    return GameData.getEntityByID('monsters', ID)
end

function p.getMonsterName(monster)
	if monster.id == 'melvorAoD:EarthGolem' then
		-- Special case for ambiguous monster name
		return 'Earth Golem (AoD)'
	else
		return monster.name
	end
end

function p.getPassive(name)
    return GameData.getEntityByName('combatPassives', name)
end

function p.getPassiveByID(ID)
    return GameData.getEntityByID('combatPassives', ID)
end

-- Given a list of monster IDs, calls statFunc with each monster and returns
-- the lowest & highest values
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

function p._getMonsterStat(monster, statName)
	if statName == 'Barrier' then
		return p._getMonsterBarrier(monster)
	elseif statName == 'HP' then
		return p._getMonsterHP(monster)
	elseif statName == 'maxHit' then
		return p._getMonsterMaxHit(monster)
	elseif statName == 'accuracyRating' then
		return p._getMonsterAR(monster)
	elseif statName == 'meleeEvasionRating' then
		return p._getMonsterER(monster, 'Melee')
	elseif statName == 'rangedEvasionRating' then
		return p._getMonsterER(monster, 'Ranged')
	elseif statName == 'magicEvasionRating' then
		return p._getMonsterER(monster, 'Magic')
	elseif statName == 'damageReduction' then
		return p.getEquipmentStat(monster, 'damageReduction')
	elseif statName == 'drReduction' then
		return p._getMonsterDrReduction(monster)
	end

	return monster[statName]
end

function p.getMonsterStat(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
	local StatName = frame.args ~= nil and frame.args[2] or frame[2]
	local monster = p.getMonster(MonsterName)
	if monster == nil then
		return Shared.printError('No monster with that name found')
	end

	return p._getMonsterStat(monster, StatName)
end

function p._getMonsterStyleIcon(frame)
	local args = frame.args ~= nil and frame.args or frame
	local monster = args[1]
	local notext = args.notext
	local nolink = args.nolink

	local iconText = ''
	if monster.attackType == 'melee' then
		iconText = Icons.Icon({'Melee', notext=notext, nolink=nolink})
	elseif monster.attackType == 'ranged' then
		iconText = Icons.Icon({'Ranged', type='skill', notext=notext, nolink=nolink})
	elseif monster.attackType == 'magic' then
		iconText = Icons.Icon({'Magic', type='skill', notext=notext, nolink=nolink})
	elseif monster.attackType == 'random' then
		iconText = Icons.Icon({p.getMonsterName(monster), notext=notext, nolink=nolink, img='Question'})
	end

	return iconText
end

function p.getMonsterStyleIcon(frame)
	local args = frame.args ~= nil and frame.args or frame
	local MonsterName = args[1]
	local monster = p.getMonster(MonsterName)

	if monster == nil then
		return Shared.printError('No monster with that name found')
	end

	args[1] = monster
	return p._getMonsterStyleIcon(args)
end

function p._getMonsterHP(monster)
	return 10 * p._getMonsterLevel(monster, 'Hitpoints')
end

function p._getMonsterBarrier(monster)
	--Monster Barrier is a percentage of its max health
	local barPercent = 0
	if monster.barrierPercent ~= nil then
		barPercent = monster.barrierPercent
	end
	return p._getMonsterHP(monster) * barPercent * 0.01
end

function p.getMonsterEffectiveHP(frame)
	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 Shared.printError('No monster with that name found')
	end
end

function p.getMonsterEffectiveBarrier(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(MonsterName)
	if monster ~= nil then
		return math.floor((p._getMonsterBarrier(monster)/(1 - p._getMonsterStat(monster, 'damageReduction')/100)) + 0.5)
	else
		return Shared.printError('No monster with that name found')
	end
end

function p.getMonsterBarrier(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(MonsterName)
	if monster ~= nil then
		return p._getMonsterBarrier(monster)
	else
		return Shared.printError('No monster with that name found')
	end
end

function p.getMonsterHP(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(MonsterName)
	if monster ~= nil then
		return p._getMonsterHP(monster)
	else
		return Shared.printError('No monster with that name found')
	end
end

function p._getMonsterLevel(monster, skillName)
	local result = 0
	if monster.levels[skillName] ~= nil then
		result = monster.levels[skillName]
	end
	return result
end

function p.getMonsterLevel(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
	local SkillName = frame.args ~= nil and frame.args[2] or frame[2]
	local monster = p.getMonster(MonsterName)

	if monster == nil then
		return Shared.printError('No monster with that name found')
	end

	return p._getMonsterLevel(monster, SkillName)
end

function p.getEquipmentStat(monster, statName)
	if monster.equipmentStats == nil then
		return 0
	else
    	return monster.equipmentStats[statName] or 0
    end
end

function p.calculateStandardStat(effectiveLevel, bonus)
	--Based on calculateStandardStat in Characters.js
	return (effectiveLevel + 9) * (bonus + 64)
end

function p.calculateStandardMaxHit(baseLevel, strengthBonus)
	--Based on calculateStandardMaxHit in Characters.js
	local effectiveLevel = baseLevel + 9
	return math.floor(10 * (1.3 + effectiveLevel / 10 + strengthBonus / 80 + effectiveLevel * strengthBonus / 640))
end

function p._getMonsterAttackSpeed(monster)
	return p.getEquipmentStat(monster, 'attackSpeed') / 1000
end

function p.getMonsterAttackSpeed(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(MonsterName)
	if monster ~= nil then
		return p._getMonsterAttackSpeed(monster)
	else
		return Shared.printError('No monster with that name found')
	end
end

function p._getMonsterCombatLevel(monster)
	local base = 0.25 * (p._getMonsterLevel(monster, 'Defence') + p._getMonsterLevel(monster, 'Hitpoints'))
	local melee = 0.325 * (p._getMonsterLevel(monster, 'Attack') + p._getMonsterLevel(monster, 'Strength'))
	local range = 0.325 * (1.5 * p._getMonsterLevel(monster, 'Ranged'))
	local magic = 0.325 * (1.5 * p._getMonsterLevel(monster, 'Magic'))
	return math.floor(base + math.max(melee, range, magic))
end

function p.getMonsterCombatLevel(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(MonsterName)

	if monster == nil then
		return Shared.printError('No monster with that name found')
	end

	return p._getMonsterCombatLevel(monster)
end

function p._getMonsterAR(monster)
	local baseLevel = 0
	local bonus = 0
	if monster.attackType == 'melee' then
		baseLevel = p._getMonsterLevel(monster, 'Attack')
		bonus = p.getEquipmentStat(monster, 'stabAttackBonus')
	elseif monster.attackType == 'ranged' then
		baseLevel = p._getMonsterLevel(monster, 'Ranged')
		bonus = p.getEquipmentStat(monster, 'rangedAttackBonus')
	elseif monster.attackType == 'magic' then
		baseLevel = p._getMonsterLevel(monster, 'Magic')
		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 Shared.printError('This monster has an invalid attack type somehow')
	end

	return p.calculateStandardStat(baseLevel, bonus)
end

function p.getMonsterAR(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(MonsterName)

	if monster == nil then
		return Shared.printError('No monster with that name found')
	end

	return p._getMonsterAR(monster)
end

function p._getMonsterER(monster, style)
	local baseLevel= 0
	local bonus = 0

	if style == "Melee" then
		baseLevel = p._getMonsterLevel(monster, 'Defence')
		bonus = p.getEquipmentStat(monster, 'meleeDefenceBonus')
	elseif style == "Ranged" then
		baseLevel = p._getMonsterLevel(monster, 'Defence')
		bonus = p.getEquipmentStat(monster, 'rangedDefenceBonus')
	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 Shared.printError('Must choose Melee, Ranged, or Magic')
	end

	return p.calculateStandardStat(baseLevel, bonus)
end

function p.getMonsterER(frame)
	local args = frame.args ~= nil and frame.args or frame
	local MonsterName = args[1]
	local style = args[2]
	local monster = p.getMonster(MonsterName)

	if monster == nil then
		return Shared.printError('No monster with that name found')
	end

	return p._getMonsterER(monster, style)
end

-- Determines if the monster is capable of dropping bones, and returns the bones
-- item if so, or nil otherwise
function p._getMonsterBones(monster)
	if monster.bones ~= nil then
		local boneItem = Items.getItemByID(monster.bones.itemID)
        local boneObj = { ["item"] = boneItem, ["quantity"] = monster.bones.quantity }
		if boneItem.prayerPoints == nil then
			-- Assume bones without prayer points are shards (from God dungeons),
			-- and drop unconditionally
			return boneObj
		elseif not monster.isBoss and not p._isDungeonOnlyMonster(monster) then
			-- Otherwise, bones drop when the monster isn't dungeon exclusive
			return boneObj
		end
	end
end

function p._isDungeonOnlyMonster(monster)
	local areaList = Areas.getMonsterAreas(monster.id)
	local inDungeon = false

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

function p.isDungeonOnlyMonster(frame)
	local monsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(monsterName)

	if monster == nil then
		return Shared.printError('No monster with name ' .. monsterName .. ' found')
	end

	return p._isDungeonOnlyMonster(monster)
end

function p._getMonsterAreas(monster, excludeDungeons, includeEffects)
	if includeEffects == nil then includeEffects = false end
	local resultPart = {}
	local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
	local areaList = Areas.getMonsterAreas(monster.id)
	for i, area in ipairs(areaList) do
		if area.type ~= 'dungeon' or not hideDungeons then
			local imgType = (area.type == 'slayerArea' and 'combatArea') or area.type
			local txt = Icons.Icon({area.name, type = imgType})
			if area.type == 'slayerArea' then
				local areaDescrip = Areas._getAreaStat(area, 'areaEffectDesc')
				if areaDescrip ~= 'None' then
					txt = txt.." - ''"..areaDescrip.."''"
				end
			end
			table.insert(resultPart, txt)
		end
	end
	return table.concat(resultPart, '<br/>')
end

function p.getMonsterAreas(frame)
	local monsterName = frame.args ~= nil and frame.args[1] or frame
	local hideDungeons = frame.args ~= nil and frame.args[2] or nil
	local includeEffects = frame.args ~= nil and frame.args[3] or true
	local monster = p.getMonster(monsterName)

	if monster == nil then
		return Shared.printError('No monster with name ' .. monsterName .. ' found')
	end

	return p._getMonsterAreas(monster, hideDungeons, includeEffects)
end

function p.getSpecAttackMaxHit(specAttack, normalMaxHit, monster)
	local bestHit = 0
	
	for i, dmg in pairs(specAttack.damage) do
		local thisHit = 0
		if dmg.damageType == 'Normal' then
			--Account for special attacks that include a normal attack hit
			thisHit = normalMaxHit
			if dmg.amplitude ~= nil then
				thisHit = thisHit * (dmg.amplitude / 100)
			end
		elseif dmg.maxRoll == 'Fixed' then
			thisHit = dmg.maxPercent * 10
		elseif dmg.maxRoll == 'MaxHit' then
			if dmg.character == 'Target' then
				--Confusion applied damage based on the player's max hit. Gonna just ignore that one
				thisHit = 0
			else
				thisHit = dmg.maxPercent * normalMaxHit * 0.01
			end
		elseif Shared.contains(dmg.maxRoll, "Fixed100") then
			--Handles attacks that are doubled when conditions are met like Trogark's double damage if the player is burning
			thisHit = dmg.maxPercent * 20
		elseif dmg.maxRoll == 'MaxHitScaledByHP2x' then
			thisHit = normalMaxHit * 2
		elseif dmg.maxRoll == 'PoisonMax35' then
			thisHit = normalMaxHit * 1.35
		elseif dmg.maxRoll == "MaxHitDR" then
			local monsterDR = 0
			if monster ~= nil then
				monsterDR = p._getMonsterStat(monster, 'damageReduction')
			end
			thisHit = normalMaxHit * dmg.maxPercent * 0.01 * (1 + monsterDR * 0.01)
		elseif Shared.contains({'Bleeding', 'Poisoned'}, dmg.maxRoll) then
			-- TODO: This is limited in that there is no verification that bleed/poison
			-- can be applied to the target, it is assumed that it can and so this applies
			thisHit = thisHit + dmg.maxPercent * 10
		end
		if thisHit > bestHit then
			bestHit = thisHit
		end
	end
	return bestHit
end

function p.canSpecAttackApplyEffect(specAttack, effectType)
	local effectKeys = { 'prehitEffects', 'onhitEffects' }
	for i, effectKey in ipairs(effectKeys) do
		if type(specAttack[effectKey]) == 'table' then
			for j, effect in pairs(specAttack[effectKey]) do
				if effect.type == effectType or p.canModifiersApplyEffect(effect.modifiers, effectType) then
					return true
				end
			end
		end
	end
	return false
end

function p.canModifiersApplyEffect(modifiers, effectType)
	-- List of modifiers which can result in the application of status effects
	local statusModsAll = {
		["Stun"] = { 'increasedGlobalStunChance', 'increasedMeleeStunChance' },
		["Sleep"] = { 'increasedGlobalSleepChance' },
		["Poison"] = { 'increasedChanceToApplyPoison' },
		["Slow"] = { 'increased15SlowStunChance2Turns', 'increased30Slow5TurnsChance' }
	}

	local statusMods = statusModsAll[effectType]
	if statusMods ~= nil and type(modifiers) == 'table' then
		for modName, modMagnitude in pairs(modifiers) do
			if Shared.contains(statusMods, modName) then
				return true
			end
		end
	end
	return false
end

function p._getMonsterMaxHit(monster, doStuns)
	-- 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
	if doStuns == nil then
		doStuns = true
	elseif type(doStuns) == 'string' then
		doStuns = string.upper(doStuns) == 'TRUE'
	end

	-- Damage adjustments are defined as follows:
	--	multiplier	- Damage from modifier 'increasedDamageTaken' & additional damage while
	--					stunned, asleep, or poisoned. Defined by in-game function
	--					getDamageModifiers(). Applies after other percentage of flat adjustments.
	--	percent		- Percentage adjustments to the max hit. Applies before flat & multiplier
	--					adjustments.
	--	flat		- Flat adjustments to the max hit. Applies after percent adjustments, and
	--					after multiplier adjustments.
	local dmgAdjust = { ["percent"] = 100, ["flat"] = 0, ["multiplier"] = 100 }
	-- Check passives & effects that apply pre or on hit for damage modifiers
	local dmgMods = {
		-- List of modifiers which affect damage dealt, and whether they are percentage or flat adjustments
		["increasedDamageTaken"] = { type = 'multiplier', mult = 1 },
		["increasedMaxHitPercent"] = { type = 'percent', mult = 1 },
		["increasedMeleeMaxHit"] = { type = 'percent', mult = 1 },
		["increasedRangedMaxHit"] = { type = 'percent', mult = 1 },
		["increasedMagicMaxHit"] = { type = 'percent', mult = 1 },
		["increasedMaxHitFlat"] = { type = 'flat', mult = 10 },
		["increasedMeleeMaxHitFlat"] = { type = 'flat', mult = 10 },
		["increasedRangedMaxHitFlat"] = { type = 'flat', mult = 10 },
		["increasedMagicMaxHitFlat"] = { type = 'flat', mult = 10 },
		-- Rage: +2% max hit per stack, maximum of 10 stacks
		["increasedRage"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 10 },
		-- Dark Blade: +1% max hit per successful hit, maximum of 30 stacks
		["increasedChanceDarkBlade"] = { type = 'percent', mult = 1, magnitude = 1, maxStacks = 30 },
		-- Growing Madness/Moment in Time/Reign Over Time: +2% max hit per stack, maximum of 25 stacks
		["growingMadnessPassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 },
		["momentInTimePassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 },
		["reignOverTimePassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 }
	}
	local effectKeys = { 'prehitEffects', 'onhitEffects' }
	local dmgStatuses = {
		-- List of status effects which can affect damage dealt
		["Stun"] = { type = 'multiplier', magnitude = 30 },
		["Sleep"] = { type = 'multiplier', magnitude = 20 }
	}
	local canApplyStatus = {}
	-- Initialize table
	for statusName, def in pairs(dmgStatuses) do
		canApplyStatus[statusName] = false
	end

	local adjustForMod = function(mod, modMagnitude, effect)
		local magnitude = mod.magnitude or modMagnitude
		local maxStacks = mod.maxStacks or (effect ~= nil and effect.maxStacks) or 1
		dmgAdjust[mod.type] = dmgAdjust[mod.type] + magnitude * mod.mult * maxStacks
	end

	local adjustForCurse = function(curseID, effect)
		local curse = Magic.getSpellByID(curseID, 'curse')
		if type(curse) == 'table' and type(curse.targetModifiers) == 'table' then
			for modName, modMagnitude in pairs(curse.targetModifiers) do
				local mod = dmgMods[modName]
				if mod ~= nil then
					-- The modifier is one which affects damage dealt
					adjustForMod(mod, modMagnitude, effect)
				end
			end
		end
	end

	-- Check monster passives for modifiers which affect damage dealt, and alo if any modifiers
	-- present can apply stun or sleep
	if monster ~= nil and type(monster.passives) ~= nil then
		for i, passiveID in ipairs(monster.passives) do
			local passive = p.getPassiveByID(passiveID)
			if passive ~= nil and type(passive.modifiers) == 'table' then
				for modName, modMagnitude in pairs(passive.modifiers) do
					local mod = dmgMods[modName]
					if modName == 'applyRandomCurseOnSpawn' then
						-- Special case in which the enemy can apply a random curse. Currently
						-- Anguish III is the curse with the highest +% damage taken, so use this.
						adjustForCurse('melvorF:AnguishIII')
					elseif mod ~= nil then
						-- The modifier is one which affects damage dealt
						adjustForMod(mod, modMagnitude)
					end
				end
				-- Check for application of relevant status effects
				if doStuns then
					for statusName, statusDef in pairs(dmgStatuses) do
						if not canApplyStatus[statusName] and p.canModifiersApplyEffect(passive.modifiers, statusName) then
							canApplyStatus[statusName] = true
						end
					end
				end
			end
		end
	end

	local normalChance = 100
	local specialMaxHit = 0
	local normalMaxHit = p._getMonsterBaseMaxHit(monster)
	local hasActiveBuffSpec = false
	if monster.specialAttacks ~= nil then
		for i, specAttackID in pairs(monster.specialAttacks) do
            local specAttack = GameData.getEntityByID('attacks', specAttackID)
			for i, effectKey in ipairs(effectKeys) do
				if type(specAttack[effectKey]) == 'table' then
					for j, effect in ipairs(specAttack[effectKey]) do
						local countsOnPlayer = (effect.countsOn == nil or effect.countsOn == 'Attacker')
						if countsOnPlayer then
							-- Check for pre or on hit effects for modifiers which affect damage dealt
							if type(effect.modifiers) == 'table' then
								for modName, modMagnitude in pairs(effect.modifiers) do
									local mod = dmgMods[modName]
									if mod ~= nil then
										-- The modifier is one which affects damage dealt
										adjustForMod(mod, modMagnitude, effect)
									end
								end
							end
							-- Check for curses which may cause the player to incur additional damage
							if effect.effectType == 'Curse' then
								-- If isRandom is true then a random curse is selected. Currently
								-- Anguish III is the curse with the highest +% damage taken, so
								-- use this.
								local curseID = (effect.isRandom and 'melvorF:AnguishIII') or effect.curse
								if curseID ~= nil then
									adjustForCurse(curseID, effect)
								end
							end
						end
					end
				end
			end

			if monster.overrideSpecialChances ~= nil then
				normalChance = normalChance - monster.overrideSpecialChances[i]
			else
				normalChance = normalChance - specAttack.defaultChance
			end
			
			-- Check for application of relevant status effects
			if doStuns then
				for statusName, statusDef in pairs(dmgStatuses) do
					if not canApplyStatus[statusName] and p.canSpecAttackApplyEffect(specAttack, statusName) then
						canApplyStatus[statusName] = true
					end
				end
			end

			local thisMax = p.getSpecAttackMaxHit(specAttack, normalMaxHit, monster)
			if thisMax > specialMaxHit then specialMaxHit = thisMax end
			if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then 
				hasActiveBuffSpec = true 
			end
		end

		if doStuns then
			for statusName, statusDef in pairs(dmgStatuses) do
				if canApplyStatus[statusName] then
					local adjType = statusDef.type
					dmgAdjust[adjType] = dmgAdjust[adjType] + statusDef.magnitude
				end
			end
		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
	local maxHit = math.floor(math.max(specialMaxHit, normalMaxHit) * dmgAdjust.percent / 100) + dmgAdjust.flat
	return math.floor(maxHit * dmgAdjust.multiplier / 100)
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 Shared.printError('No monster with that name found')
	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
        if monster.selectedSpell == nil then
            result = 0
        else
            local mSpell = Magic.getSpellByID(monster.selectedSpell, 'standard')
            if mSpell == nil then
                result = 0
            else
                baseLevel = p._getMonsterLevel(monster, 'Magic')
                bonus = p.getEquipmentStat(monster, 'magicDamageBonus')
                result = math.floor(10 * mSpell.maxHit * (1 + bonus / 100) * (1 + (baseLevel + 1) / 200))
            end
        end
	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 magicDmg = 0
        if monster.selectedSpell ~= nil then
            local mSpell = Magic.getSpellByID(monster.selectedSpell, 'standard')
            if mSpell ~= nil then
                baseLevel = p._getMonsterLevel(monster, 'Magic')
                bonus = p.getEquipmentStat(monster, 'magicDamageBonus')
                magicDmg = math.floor(10 * mSpell.maxHit * (1 + bonus / 100) * (1 + (baseLevel + 1) / 200))
            end
        end
		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 Shared.printError('This monster has an invalid attack type somehow')
	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 Shared.printError('No monster with that name found')
	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 Shared.printError('No monster with that name found')
	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 isNormalAttackRelevant = false

	local normalAttackChance = 100
	if monster.specialAttacks ~= nil then
		for i, specAttackID in pairs(monster.specialAttacks) do
            local specAttack = GameData.getEntityByID('attacks', specAttackID)
			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 this special attack applies a curse, let's actually list what that curse does
			if specAttack.onhitEffects ~= nil then
				for j, hitEffect in ipairs(specAttack.onhitEffects) do
					if hitEffect.effectType == 'Curse' then
						local curse = Magic.getSpellByID(hitEffect.curse, 'curse')
						result = result..'\r\n*** '..Icons.Icon({curse.name, type='curse'})..': '..Magic._getSpellDescription(curse, true)
					end
				end
			end

			if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then
				table.insert(buffAttacks, specAttack.name)
				hasActiveBuffSpec = true
				isNormalAttackRelevant = true
			end
			if not isNormalAttackRelevant and type(specAttack.damage) == 'table' then
				-- Determine if the special attack uses normal damage in some form
				for j, dmgData in ipairs(specAttack.damage) do
					if dmgData.damageType == 'Normal' then
						isNormalAttackRelevant = true
						break
					end
				end
			end
		end
	end
	
	if isNormalAttackRelevant or normalAttackChance > 0 then
		--Reformatting slightly - If there are any special attacks, specifically label the Normal Attack
		
		local normalDmgText = ' 1 - '..Shared.formatnum(p._getMonsterBaseMaxHit(monster))..' '..typeText..' Damage'
		if normalAttackChance > 0 and normalAttackChance < 100 then
			normalDmgText = normalAttackChance .. '% ' ..iconText..' Normal Attack\r\n** '..normalDmgText
		elseif hasActiveBuffSpec and normalAttackChance == 0 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)
			normalDmgText = iconText..' Normal Attack\r\n** '..normalDmgText .. ' (Instead of repeating '..table.concat(buffAttacks, ' or ')..' while the effect is already active)'
		end
		result = '* ' .. normalDmgText .. result
	end

	return result
end

--Function for pulling how much the monster reduces the player DR
--Goes through the passvies to look for the decreasedPlayerDamageReduction modifier
function p._getMonsterDrReduction(monster)
	local totalResult = 0
	
    if type(monster.passives) == 'table' and not Shared.tableIsEmpty(monster.passives) then
		for i, passiveID in ipairs(monster.passives) do
			local passive = p.getPassiveByID(passiveID)
			if passive.modifiers ~= nil then
				if passive.modifiers['decreasedPlayerDamageReduction'] ~= nil then
					totalResult = totalResult + passive.modifiers['decreasedPlayerDamageReduction']
				end
			end
		end
    end
    
    return totalResult
end

function p.getMonsterDrReduction(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(MonsterName)

	if monster == nil then
		return Shared.printError('No monster with that name found')
	end
	
	return p._getMonsterDrReduction(monster)
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 Shared.printError('No monster with that name found')
	end

	local result = ''
    if type(monster.passives) == 'table' and not Shared.tableIsEmpty(monster.passives) then
		result = result .. '===Passives==='
		for i, passiveID in ipairs(monster.passives) do
			local passive = p.getPassiveByID(passiveID)
			result = result .. '\r\n* ' .. passive.name .. '\r\n** ' .. Constants.getDescription(passive.customDescription, passive.modifiers)
		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 Shared.printError('No monster with that name found')
	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 type(monster.passives) == 'table' and not Shared.tableIsEmpty(monster.passives) then
		result = result..'[[Category:Monsters with Special Attacks]]'
	end

	if monster.isBoss then
		result = result..'[[Category:Bosses]]'
	end

	return result
end

function p.getMonsterBoxBarrierText(frame)
	local MonsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(MonsterName)

	if monster == nil then
		return Shared.printError('No monster with that name found')
	end
	
	local barrier = p._getMonsterBarrier(monster)
	if barrier == 0 then
		return ''
	end
	
	local result = {}
	table.insert(result, '|-\r\n| style="font-weight: bold;" | [[Barrier]]:')
	table.insert(result, '\r\n| colspan=15 style="text-align: right" |')
	table.insert(result, Icons.Icon({"Barrier", notext="true"}))
	table.insert(result, ' '..barrier)
	return table.concat(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 Shared.printError('No monster with that name found')
	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 = {combatArea = 0, slayerArea = 0, dungeon = 0}
	for i, area in ipairs(areaList) do
		counts[area.type] = counts[area.type] + 1
	end

	if counts.combatArea > 0 then table.insert(monsterTypes, 'Combat Area') end
	if counts.slayerArea > 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 Shared.printError('No monster with that name found')
	end

	local result = ''

	local bones = p._getMonsterBones(monster)
	local boneVal = 0
	local barrierDust = Items.getItemByID("melvorAoD:Barrier_Dust")
	local dustVal = 0
	--Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
	if bones ~= nil then
		local boneQty = (bones.quantity ~= nil and bones.quantity or 1)
		local barrier = p._getMonsterBarrier(monster)
		result = result.."'''Always Drops:'''"
		result = result..'\r\n{|class="wikitable" id="bonedrops"'
		result = result..'\r\n!Item !! Qty'
		result = result..'\r\n|-\r\n|'..Icons.Icon({bones.item.name, type='item'})
		result = result..'||'..boneQty
		if barrier > 0 then
			local dustQty = math.max(math.floor(barrier / 10 / 20), 1)
			result = result..'\r\n|-\r\n|'..Icons.Icon({barrierDust.name, type='item'})
			result = result..'||'..dustQty
			dustVal = dustQty * barrierDust.sellsFor
		end
		result = result..'\r\n'..'|}'
		boneVal = boneQty * bones.item.sellsFor
	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.gpDrops ~= nil then
			avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
			local gpTxt = Icons.GP(monster.gpDrops.min, monster.gpDrops.max)
			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 ipairs(monster.lootTable) do
			totalWt = totalWt + row.weight
		end
		result = result..'\r\n{|class="wikitable sortable" id="itemdrops"'
		result = result..'\r\n!Item!!Qty'
		result = result..'!!Price!!colspan="2"|Chance'

		--Sort the loot table by weight in descending order
		local lootTable = Shared.shallowClone(monster.lootTable)
		table.sort(lootTable, function(a, b)
			if a.weight == b.weight then
				local aItem, bItem = Items.getItemByID(a.itemID), Items.getItemByID(b.itemID)
				if aItem ~= nil and bItem ~= nil then
					return aItem.name < bItem.name
				else
					return a.itemID < b.itemID
				end
			else
				return a.weight > b.weight
			end
		end)
		for i, row in ipairs(lootTable) do
			local thisItem = Items.getItemByID(row.itemID)
			
			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="'..row.maxQuantity..'"|'

			if row.maxQuantity > row.minQuantity then
				result = result .. Shared.formatnum(row.minQuantity) .. ' - '
			end
			result = result .. Shared.formatnum(row.maxQuantity)

			--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 row.maxQuantity == row.minQuantity then
					result = result..'||'..Icons.GP(itemPrice * row.minQuantity)
				else
					result = result..'||'..Icons.GP(itemPrice * row.minQuantity, itemPrice * row.maxQuantity)
				end
			end

			--Getting the drop chance
			local dropChance = (row.weight / 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.weight..'"'
				result = result..'|'..Shared.fraction(row.weight * lootChance, totalWt * 100)
				result = result..'||'
			else
				result = result..'||colspan="2" data-sort-value="'..row.weight..'"'
			end
			-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
			local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
			result = result..'style="text-align:right"|'..string.format(fmt, dropChance)..'%'

			--Adding to the average loot value based on price & dropchance
			lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 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"
			if boneVal > 0 then
				result = result..' and bones'
			end
			if dustVal > 0 then
				result = result..' and barrier dust'
			end
			result = result..', the average kill is worth '..Icons.GP(Shared.round(avgGp + lootValue + boneVal + dustVal, 2, 0))..'.'
		end
	end

	--If no other drops, make sure to at least say so.
	if result == '' then result = 'None' end
	return result
end

function p._getMonsterLootValue(monster)
	if monster == nil then
		return Shared.printError('No monster with that name found')
	end

	local result = 0
	local boneVal = 0

	local bones = p._getMonsterBones(monster)
	--Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
	if bones ~= nil then
		local boneQty = (bones.quantity ~= nil and bones.quantity) or 1
		boneVal = bones.item.sellsFor * boneQty
		result = result + boneVal
	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.gpDrops ~= nil then
			avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
		end

		local multiDrop = Shared.tableCount(monster.lootTable) > 1
		local totalWt = 0
		for i, row in pairs(monster.lootTable) do
			totalWt = totalWt + row.weight
		end

		for i, row in ipairs(monster.lootTable) do
			local thisItem = Items.getItemByID(row.itemID)

			--Adding price columns
			local itemPrice = 0
			if thisItem ~= nil then
				itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
			end

			--Getting the drop chance
			local dropChance = (row.weight / totalWt * lootChance)
			--Adding to the average loot value based on price & dropchance
			lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 2))
		end
		if avgGp > 0 then
			result = result + avgGp + lootValue
		end
	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 Shared.printError('No monster with that name found')
	end
	if item == nil then
		return Shared.printError('No item with that name found')
	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
		local dropWt = 0
		for i, row in ipairs(monster.lootTable) do
			totalWt = totalWt + row.weight
			if item.id == row.itemID then
				dropWt = row.weight
			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 Shared.printError('No item named ' .. chestName .. ' found')
	end
	local result = ''

	if chest.dropTable == nil then
		return Shared.printError(chestName .. ' does not have a drop table')
	else

		local function formatNumRange(minValue, maxValue)
			if maxValue ~= nil and maxValue > minValue then
				return Shared.formatnum(minValue) .. ' - ' .. Shared.formatnum(maxValue)
			else
				return Shared.formatnum(minValue)
			end
		end

		local lootValue, foodValue = 0, 0
		local totalWt = 0
		local isAllFood = true
		for i, row in ipairs(chest.dropTable) do
			totalWt = totalWt + row.weight
			if isAllFood then
				-- If the container's contents are entirely food then we add additional
				-- information to the output, so we determine this here
				local item = Items.getItemByID(row.itemID)
				if item ~= nil and item.healsFor == nil then
					isAllFood = false
				end
			end
		end
		result = result..'\r\n{|class="wikitable sortable"'
		result = result..'\r\n!Item!!Qty'
		result = result..'!!colspan="2"|Chance!!Price' .. (isAllFood and '!!Healing!!Avg. Healing' or '')

		--Sort the loot table by weight in descending order
		local chestDrops = Shared.shallowClone(chest.dropTable)
		table.sort(chestDrops, function(a, b) return a.weight > b.weight end)
		for i, row in ipairs(chestDrops) do
			local thisItem = Items.getItemByID(row.itemID)
			result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
			result = result..'||style="text-align:right" data-sort-value="'..(row.minQuantity + row.maxQuantity)..'"| ' .. formatNumRange(row.minQuantity, row.maxQuantity)

			local dropChance = (row.weight / totalWt) * 100
			result = result..'||style="text-align:right" data-sort-value="'..row.weight..'"'
			result = result..'|'..Shared.fraction(row.weight, totalWt)

			result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'

			result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
			if thisItem.sellsFor == 0 or row.minQuantity == row.maxQuantity then
				result = result..'|'..Icons.GP(thisItem.sellsFor * row.minQuantity)
			else
				result = result..'|'..Icons.GP(thisItem.sellsFor * row.minQuantity, thisItem.sellsFor * row.maxQuantity)
			end
			lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor * ((row.minQuantity + row.maxQuantity)/ 2))

			if isAllFood then
				local hp = thisItem.healsFor * 10
				local minHeal, maxHeal = hp * row.minQuantity, hp * row.maxQuantity
				local avgHpPerLoot = (dropChance * 0.01 * (minHeal + maxHeal) / 2)
				foodValue = foodValue + avgHpPerLoot
				result = result .. '||data-sort-value="' .. thisItem.healsFor .. '"'
				result = result .. '|' .. Icons.Icon({'Hitpoints', type='skill', notext=true, nolink=true}) .. ' ' .. formatNumRange(minHeal, maxHeal)
				result = result .. '||data-sort-value="' .. avgHpPerLoot .. '"'
				result = result .. '|' .. Icons.Icon({'Hitpoints', type='skill', notext=true, nolink=true}) .. ' ' .. Shared.round(avgHpPerLoot, 2, 0)
			end
		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))..'.'
		if isAllFood then
			result = result..'\r\n\r\nThe average healing of the contents of one chest is ' .. Icons.Icon({'Hitpoints', type='skill', notext=true, nolink=true}) .. ' ' .. Shared.round(foodValue, 2, 0) .. '.'
		end
	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 Shared.printError('Could not find an area named ' .. areaName)
	end

	if area.type == 'dungeon' then
		return p.getDungeonMonsterTable(frame)
	end
	
	
	local monsters = {}
	local hasBarrier = false
	for i, monsterID in ipairs(area.monsterIDs) do
		local monster = p.getMonsterByID(monsterID)
		if not hasBarrier and p._getMonsterBarrier(monster) > 0 then
			hasBarrier = true
		end
		table.insert(monsters, monster)
	end

	local tableBits = {}
	table.insert(tableBits, '{| class="wikitable sortable"')
	table.insert(tableBits, '\r\n! Name !! Combat Level ')
	if hasBarrier then
		table.insert(tableBits, '!! [[Barrier]] ')
	end
	table.insert(tableBits, '!! Hitpoints !! colspan=2| Max Hit !! [[Combat Triangle|Combat Style]]')
	for i, monster in ipairs(monsters) do
		local rowBits = {}
		table.insert(tableBits, '\r\n|-\r\n|'..Icons.Icon({p.getMonsterName(monster), type='monster'}))
		table.insert(tableBits, '||'..p._getMonsterCombatLevel(monster))
		if hasBarrier then
		table.insert(tableBits, '||'..Shared.formatnum(p._getMonsterBarrier(monster)))
		end
		table.insert(tableBits, '||'..Shared.formatnum(p._getMonsterHP(monster)))
		local drReduction = p._getMonsterDrReduction(monster)
		local maxHit = p._getMonsterMaxHit(monster)
		if drReduction > 0 then
			table.insert(tableBits, '||style="text-align:right" data-sort-value="'..maxHit..'"| -'..drReduction..'% DR')
			table.insert(tableBits, '||style="text-align:right"|'..Shared.formatnum(maxHit))
		else
			table.insert(tableBits, '||style="text-align:right" colspan="2" data-sort-value="'..maxHit..'"|'..Shared.formatnum(maxHit))
		end
		table.insert(tableBits, '||'..p._getMonsterStyleIcon({monster, nolink=true}))
		
	end
	table.insert(tableBits, '\r\n|}')
	
	return table.concat(tableBits, '')
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 Shared.printError('Could not find a dungeon named ' .. areaName)
	end

	--For Dungeons, go through and count how many of each monster are in the dungeon first
	local monsterCounts = {}
	local monsters = {}
	local hasBarrier = false
	for i, monsterID in ipairs(area.monsterIDs) do
		if monsterCounts[monsterID] == nil then
			monsterCounts[monsterID] = 1
		else
			monsterCounts[monsterID] = monsterCounts[monsterID] + 1
			if monsterID ~= 'melvorF:RandomITM' and monsterID ~= 'melvorTotH:RandomSpiderLair' then
				monsters[monsterID] = p.getMonsterByID(monsterID)
				if not hasBarrier and p._getMonsterBarrier(monsters[monsterID]) > 0 then
					hasBarrier = true
				end
			end
		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, monDrReduce, monBarrier
		local monData = {}
		if specialType ~= nil and Shared.contains({'Afflicted', 'Spider', '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, monBarrier, monHP, monMaxHit, monDrReduce, monStyle, monCount = iconQ, iconQ, iconQ, iconQ, iconQ, iconQ, monsterCount
			elseif specialType == 'Spider' then
				local iconQ = Icons.Icon({'', notext=true, nolink=true, img='Question'})
				local monIconPart = { 'Any of the following:' }
				for i, monsterID in ipairs(GameData.rawData.spiderLairMonsters) do
					local monster = p.getMonsterByID(monsterID)
					if monster ~= nil then
						table.insert(monIconPart, Icons.Icon({p.getMonsterName(monster), type='monster'}))
					end
				end
				monIcon = table.concat(monIconPart, '<br/>')
				monLevel, monBarrier, monHP, monMaxHit, monDrReduce, monStyle, monCount = iconQ, iconQ, 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.monsterIDs, function(monster) return p._getMonsterCombatLevel(monster) end)}
				if hasBarrier then
					monBarrier = {p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterBarrier(monster) end)}
				end
				monHP = {p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterHP(monster) end)}
				local lowMaxHit, highMaxHit = p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterMaxHit(monster) end)
				local lowDrReduce, highDrReduce = p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterDrReduction(monster) end)
				monMaxHit = highMaxHit
				monDrReduce = highDrReduce
				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({p.getMonsterName(monster), type='monster'})
			monLevel = p._getMonsterCombatLevel(monster)
			if hasBarrier then
				monBarrier = p._getMonsterBarrier(monster)
			end
			monHP = p._getMonsterHP(monster)
			monDrReduce = p._getMonsterDrReduction(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))
		if hasBarrier then
			table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monBarrier) .. '"| ' .. getValText(monBarrier))
		end
		table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monHP) .. '"| ' .. getValText(monHP))
		if type(monDrReduce) == 'number' and monDrReduce > 0 then
			table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="'..getValSort(monMaxHit)..'"| -'..monDrReduce..'% DR')
			table.insert(resultPart, '\r\n|style="text-align:right"|'..getValText(monMaxHit))
		else
			table.insert(resultPart, '\r\n|style="text-align:right" colspan="2" data-sort-value="'..getValSort(monMaxHit)..'"|'..getValText(monMaxHit))
		end
		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 ')
	if hasBarrier then
		table.insert(returnPart, '!! [[Barrier]] ')
	end
	table.insert(returnPart, '!! Hitpoints !! colspan="2" | 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
	for i, monsterID in ipairs(area.monsterIDs) do
		if not Shared.contains(usedMonsters, monsterID) then
			if monsterID == 'melvorF:RandomITM' then
				--Special handling for Into the Mist
				table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], 'Afflicted'))
			elseif monsterID == 'melvorTotH:RandomSpiderLair' then
				table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], 'Spider'))
			else
				table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], hasBarrier))
			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 Shared.printError('Could not find a dungeon named ' .. areaName)
	end
	local totalHP = 0

	for i, monsterID in ipairs(area.monsterIDs) do
        local monster = p.getMonsterByID(monsterID)
        totalHP = totalHP + p._getMonsterHP(monster)
	end
	return totalHP
end

function p._getAreaMonsterList(area)
	local monsterList = {}
	for i, monsterID in ipairs(area.monsterIDs) do
		local monster = p.getMonsterByID(monsterID)
		table.insert(monsterList, Icons.Icon({p.getMonsterName(monster), type='monster'}))
	end
	return table.concat(monsterList, '<br/>')
end

function p._getDungeonMonsterList(area)
	local monsterList = {}
	local lastID = ''
	local count = 0
	
	local monsterCounts = {}
	for i, monsterID in ipairs(area.monsterIDs) do
		if lastID == '' then
			lastID = monsterID
			count = 1
		elseif lastID == monsterID then
			count = count + 1
		else
			table.insert(monsterCounts, { id = lastID, count = count })
			lastID = monsterID
			count = 1
		end
	end
	table.insert(monsterCounts, { id = lastID, count = count })

	for i, monster in ipairs(monsterCounts) do
		if monster.id == 'melvorF:RandomITM' then
			--Special handling for Afflicted Monsters
			table.insert(monsterList, Icons.Icon({'Affliction', 'Afflicted Monster', img='Question', qty=monster.count}))
		elseif monster.id == 'melvorTotH:RandomSpiderLair' then
			local monIconPart = { Shared.formatnum(monster.count) .. ' Spiders:' }
			for i, monsterID in ipairs(GameData.rawData.spiderLairMonsters) do
				local monster = p.getMonsterByID(monsterID)
				if monster ~= nil then
					table.insert(monIconPart, '&nbsp;&nbsp;&nbsp;' .. Icons.Icon({p.getMonsterName(monster), type='monster'}))
				end
			end
			table.insert(monsterList, table.concat(monIconPart, '<br/>'))
		else
			local monsterObj = p.getMonsterByID(monster.id)
			table.insert(monsterList, Icons.Icon({p.getMonsterName(monsterObj), type='monster', qty=monster.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 Shared.printError('Could not find an area named ' .. areaName)
	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 ipairs(GameData.rawData.monsters) do
		if not p._isDungeonOnlyMonster(monster) then
			if monster.gpDrops ~= nil and monster.gpDrops.max > 0 then
				local avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
				result = result .. '<br/>' .. p.getMonsterName(monster) .. ',' .. monster.gpDrops.min .. ',' .. monster.gpDrops.max .. ',' .. avgGp
			end
		end
	end
	return result
end

function p._getMonsterAverageGP(monster)
	local result = ''
	local totalGP = 0

	local bones = p._getMonsterBones(monster)
	if bones ~= nil then
		totalGP = totalGP + bones.item.sellsFor * bones.quantity
	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.gpDrops ~= nil then
			avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
		end

		totalGP = totalGP + avgGp

		local totalWt = 0
		for i, row in ipairs(monster.lootTable) do
			totalWt = totalWt + row.weight
		end

		for i, row in ipairs(monster.lootTable) do
			local thisItem = Items.getItemByID(row.itemID)

			local itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0

			--Getting the drop chance
			local dropChance = (row.weight / totalWt * lootChance)

			--Adding to the average loot value based on price & dropchance
			lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 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 Shared.printError('No monster with that name found')
	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, monster in ipairs(GameData.rawData.monsters) do
		if not p._isDungeonOnlyMonster(monster) then
			local monsterGP = p._getMonsterAverageGP(monster)
			local combatLevel = p._getMonsterCombatLevel(monster)
			result = result..'\r\n|-\r\n|'..Icons.Icon({p.getMonsterName(monster), 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 Shared.printError('No tier specified')
	end

	if tonumber(tier) ~= nil then
		slayerTier = Constants.getSlayerTierByID(tonumber(tier))
	else
		slayerTier = Constants.getSlayerTier(tier)
	end

	if slayerTier == nil then
		return Shared.printError('Invalid slayer tier')
	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 monsterList = GameData.getEntities('monsters',
        function(monster)
            if monster.canSlayer and not Shared.contains(hiddenMonsterIDs, monster.id) then
                local cmbLevel = p._getMonsterCombatLevel(monster)
                return cmbLevel >= minLevel and (maxLevel == nil or cmbLevel <= maxLevel)
            end
            return false
        end)

	if Shared.tableIsEmpty(monsterList) then
		-- Somehow no monsters are in the tier, return nothing
		return ''
	else
		return p._getMonsterTable(monsterList, true)
	end
end

function p.getFullMonsterTable(frame)
	return p._getMonsterTable(GameData.rawData.monsters, false)
end

function p._getMonsterTable(monsters, 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="4" | !! colspan="5" |Offensive Stats !! colspan="3" |Evasion Rating !! colspan="4" |')
	-- Second header row
	table.insert(tableParts, '\r\n|- class="headerRow-1"\r\n!Monster !!Name !!Combat Level ')
	table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Hitpoints', type='skill'}))
	table.insert(tableParts, '!!Attack Speed (s) !!colspan="3"|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, '!!DR!!' .. Icons.Icon({'Coins', notext=true, nolink=true}) .. ' Coins !!Bones !!Locations')

	-- Generate row per monster
	for i, monster in ipairs(monsters) do
		-- Avoid processing monsters without equipment stats. These aren't actual
		-- monsters, but instead are placeholders such as 'melvorF:RandomITM'
		-- and 'melvorTotH:RandomSpiderLair' to denote a random selection from
		-- a pool of monsters
		if monster.equipmentStats ~= nil then
			local cmbLevel = p._getMonsterCombatLevel(monster)
			local atkSpeed = p._getMonsterAttackSpeed(monster)
			local maxHit = p._getMonsterMaxHit(monster)
			local dr = p._getMonsterStat(monster, 'damageReduction')
			local drReduce = p._getMonsterDrReduction(monster)
			local accR = p._getMonsterAR(monster)
			local evaR = {p._getMonsterER(monster, "Melee"), p._getMonsterER(monster, "Ranged"), p._getMonsterER(monster, "Magic")}
	
			local gpTxt = nil
			if monster.gpDrops.min >= monster.gpDrops.max then
				gpTxt = Shared.formatnum(monster.gpDrops.min)
			else
				gpTxt = Shared.formatnum(monster.gpDrops.min) .. ' - ' .. Shared.formatnum(monster.gpDrops.max)
			end
			local bones = p._getMonsterBones(monster)
			local boneTxt = (bones ~= nil and Icons.Icon({bones.item.name, type='item', notext=true})) or 'None'
	
			table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', size=50, notext=true}))
			table.insert(tableParts, '\r\n|style="text-align:left" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', noicon=true}))
			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))
			if drReduce > 0 then
				table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. maxHit .. '"| -' .. drReduce..'% DR')
				table.insert(tableParts, '\r\n|style="text-align:right;border-right:hidden" |' .. p._getMonsterStyleIcon({monster, notext=true}))
				table.insert(tableParts, '\r\n|style="text-align:right" |' .. Shared.formatnum(maxHit))
			else
				table.insert(tableParts, '\r\n|style="text-align:right;border-right:hidden" colspan="2" data-sort-value="' .. maxHit .. '"|' .. p._getMonsterStyleIcon({monster, notext=true}))
				table.insert(tableParts, '\r\n|style="text-align:right"|' .. Shared.formatnum(maxHit))
			end
			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="' .. dr .. '" |' .. dr..'%')
			table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. (monster.gpDrops.min + monster.gpDrops.max) / 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
	end

	table.insert(tableParts, '\r\n|}')
	return table.concat(tableParts)
end

function p.getMattMonsterTable(frame)
	--Making a single function for getting a table of monsters given a list of IDs.
	local tableParts = {}
	table.insert(tableParts, '{| class="wikitable sortable stickyHeader"')
	-- 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, '!!' .. Icons.Icon({'Coins', notext=true, nolink=true}) .. ' Coins !!Avg. Kill Value!!Locations')

	-- Generate row per monster
	for i, monster in ipairs(GameData.rawData.monsters) do
		if p.getMonsterName(monster) ~= nil then
			local cmbLevel = p._getMonsterCombatLevel(monster)
	
			local gpTxt = nil
			if monster.gpDrops.min >= monster.gpDrops.max then
				gpTxt = Shared.formatnum(monster.gpDrops.min)
			else
				gpTxt = Shared.formatnum(monster.gpDrops.min) .. ' - ' .. Shared.formatnum(monster.gpDrops.max)
			end
			
			local lootVal = p._getMonsterLootValue(monster)
			local lootTxt = '0'
			if lootVal ~= 0 then
				lootTxt = Shared.formatnum(Shared.round(lootVal, 2, 2))
			end
			
	
			table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', size=50, notext=true}))
			table.insert(tableParts, '\r\n|style="text-align:left" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', noicon=true}))
			table.insert(tableParts, '\r\n|style="text-align:right" |' .. monster.id)
			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="' .. (monster.gpDrops.min + monster.gpDrops.max) / 2 .. '" |' .. gpTxt)
			table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. lootVal .. '" |' .. lootTxt)
			table.insert(tableParts, '\r\n|style="text-align:right;width:190px" |' .. p._getMonsterAreas(monster, false))
		end
	end

	table.insert(tableParts, '\r\n|}')
	return table.concat(tableParts)
end

function p.getMattMonsterTableV2(frame)
	--Making a single function for getting a table of monsters given a list of IDs.
	local tableParts = {}
	table.insert(tableParts, '{| class="wikitable sortable stickyHeader"')
	-- Second header row
	table.insert(tableParts, '\r\n|- class="headerRow-1"\r\n!Monster !!Name !!Combat Level ')
	table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Hitpoints', type='skill'}))
		table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Defence', type='skill', notext=true}))
		table.insert(tableParts, '!!Attack Speed (s) !!colspan="2"|Max Hit !!Accuracy ')

--	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 !!Avg. Kill Value !!Bones')

	-- Generate row per monster
	for i, monster in ipairs(GameData.rawData.monsters) do
		if p.getMonsterName(monster) ~= nil then
			local cmbLevel = p._getMonsterCombatLevel(monster)
	
			local gpTxt = nil
			if monster.gpDrops.min >= monster.gpDrops.max then
				gpTxt = Shared.formatnum(monster.gpDrops.min)
			else
				gpTxt = Shared.formatnum(monster.gpDrops.min) .. ' - ' .. Shared.formatnum(monster.gpDrops.max)
			end
			
			local lootVal = p._getMonsterLootValue(monster)
			local lootTxt = '0'
			if lootVal ~= 0 then
				lootTxt = Shared.formatnum(Shared.round(lootVal, 2, 2))
			end
			
			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 bones = p._getMonsterBones(monster)
			local boneTxt = (bones ~= nil and Icons.Icon({bones.item.name, type='item', notext=true})) or 'None'
	
			table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', size=50, notext=true}))
			table.insert(tableParts, '\r\n|style="text-align:left" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', noicon=true}))
			-- table.insert(tableParts, '\r\n|style="text-align:right" |' .. monster.id)
			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="' .. evaR[1] .. '" |' .. Shared.formatnum(evaR[1]))
	
			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[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="' .. (monster.gpDrops.min + monster.gpDrops.max) / 2 .. '" |' .. gpTxt)
			table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. lootVal .. '" |' .. lootTxt)
			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
	end

	table.insert(tableParts, '\r\n|}')
	return table.concat(tableParts)
end

function p.getSpecialAttackTable(frame)
	local spAttTable = {}

	for i, monster in ipairs(GameData.rawData.monsters) do
		if monster.specialAttacks ~= nil and not Shared.tableIsEmpty(monster.specialAttacks) then
			local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
			for j, spAttID in ipairs(monster.specialAttacks) do
                local spAtt = GameData.getEntityByID('attacks', spAttID)
				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({ p.getMonsterName(monster), 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

--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot TablesTemplate:MonsterLootTables
--It exists because I'm too lazy to manually type up all the new monsters - User:Falterfire
function p.getMonsterLootTableText()
	local getAreaText = function(area)
			local outArray = {}
			table.insert(outArray, "==={{ZoneIcon|"..area.name.."|size=50}}===")
			table.insert(outArray, "")
			for i, monsterID in ipairs(area.monsterIDs) do
				local monster = p.getMonsterByID(monsterID)
				table.insert(outArray, "===={{MonsterIcon|"..p.getMonsterName(monster).."|size=40}}====")
				table.insert(outArray, "{{MonsterDrops|"..p.getMonsterName(monster).."|size=40}}")
			end
			return table.concat(outArray, "\r\n")
		end
	local fullArray = {}
	local areaArray = Areas.getAreas(function(a) return a.type == 'combatArea' end)
	for i, area in ipairs(areaArray) do
		table.insert(fullArray, getAreaText(area))
	end
	
	areaArray = Areas.getAreas(function(a) return a.type == 'slayerArea' end)
	for i, area in ipairs(areaArray) do
		table.insert(fullArray, getAreaText(area))
	end
	
	
	return table.concat(fullArray, "\r\n\r\n----\r\n")
end

--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot Tables
--It exists because I'm too lazy to manually type up all the new chests - User:Falterfire
function p.getChestLootTables()
	local items = Items.getItems(function(item) return item.dropTable ~= nil end)
	local outArray = {}
	for i, item in ipairs(items) do
		table.insert(outArray, "==={{ItemIcon|"..item.name.."|size=30}}===")
		table.insert(outArray, "{{ChestDrops|"..item.name.."}}")
	end
	return table.concat(outArray, "\r\n")
end

--Returns the expansion icon for the item if it has one
function p.getExpansionIcon(frame)
	local monsterName = frame.args ~= nil and frame.args[1] or frame
	local monster = p.getMonster(monsterName)

	if monster == nil then
		return Shared.printError('No monster with that name found')
	end
	
	return Icons.getExpansionIcon(monster.id)
end

return p