Module:Monsters: Difference between revisions

From Melvor Idle
(Reorganizing to hopefully fix issue with boss monsters getting left out)
(Add column name for average healing)
 
(177 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 GameData = require('Module:GameData')
local Areas = require('Module:CombatAreas')
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
end


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


  for i, attack in pairs(MonsterData.SpecialAttacks) do
function p.getPassive(name)
     if(attack.name == name) then
     return GameData.getEntityByName('combatPassives', name)
      result = Shared.clone(attack)
end
      --Make sure every monster has an ID, and account for the 1-based indexing of Lua
 
      result.id = i - 1
function p.getPassiveByID(ID)
    end
    return GameData.getEntityByID('combatPassives', ID)
  end
end
  return result
 
-- 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
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
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


  if monster == nil then
function p.getMonsterEffectiveHP(frame)
    return "ERROR: No monster with that name found"
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  end
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


  local iconText = ''
function p.getMonsterEffectiveBarrier(frame)
  if  monster.attackType == Constants.attackType.Melee then
local MonsterName = frame.args ~= nil and frame.args[1] or frame
    iconText = Icons.Icon({'Melee', notext=notext, nolink=nolink})
local monster = p.getMonster(MonsterName)
  elseif monster.attackType == Constants.attackType.Ranged then
if monster ~= nil then
    iconText = Icons.Icon({'Ranged', type='skill', notext=notext, nolink=nolink})
return math.floor((p._getMonsterBarrier(monster)/(1 - p._getMonsterStat(monster, 'damageReduction')/100)) + 0.5)
  else
else
    iconText = Icons.Icon({'Magic', type='skill', notext=notext, nolink=nolink})
return Shared.printError('No monster with that name found')
  end
end
end


  return iconText
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
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


  if monster == nil then
return p.calculateStandardStat(baseLevel, bonus)
    return "ERROR: No monster with that name found"
  end
 
  local base = 0.25 * (monster.defenceLevel + monster.hitpoints)
  local melee = 0.325 * (monster.attackLevel + monster.strengthLevel)
  local range = 0.325 * (1.5 * monster.rangedLevel)
  local magic = 0.325 * (1.5 * monster.magicLevel)
  if melee > range and melee > magic then
    return math.floor(base + melee)
  elseif range > magic then
    return math.floor(base + range)
  else
    return math.floor(base + magic)
  end
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
return Shared.printError('No monster with that name found')
end
 
return p._getMonsterAR(monster)
end


  if monster == nil then
function p._getMonsterER(monster, style)
    return "ERROR: No monster with that name found"
local baseLevel= 0
  end
local bonus = 0
 
  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
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
 
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


  if monster == nil then
local thisMax = p.getSpecAttackMaxHit(specAttack, normalMaxHit, monster)
    return "ERROR: No monster with name "..monsterName.." found"
if thisMax > specialMaxHit then specialMaxHit = thisMax end
  end
if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then
hasActiveBuffSpec = true
end
end


  local result = ''
if doStuns then
  local areaList = Areas.getMonsterAreas(monster.id)
for statusName, statusDef in pairs(dmgStatuses) do
  for i, area in pairs(areaList) do
if canApplyStatus[statusName] then
    if i > 1 then result = result..'<br/>' end
local adjType = statusDef.type
    result = result..Icons.Icon({area.name, type = area.type})
dmgAdjust[adjType] = dmgAdjust[adjType] + statusDef.magnitude
  end
end
  return result
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
return Shared.printError('No monster with that name found')
end
 
return p._getMonsterMaxHit(monster, doStuns)
end


  if monster == nil then
function p._getMonsterBaseMaxHit(monster)
    return "ERROR: No monster with that name found"
--8/27/21 - Now references p.calculateStandardMaxHit for Melee & Ranged
  end
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


  local normalChance = 100
return result
  local specialMaxHit = 0
  local normalMaxHit = p.getMonsterBaseMaxHit(frame)
  if monster.hasSpecialAttack then
    for i, specID in pairs(monster.specialAttackID) do
      local specAttack = p.getSpecialAttackByID(specID)
      if monster.overrideSpecialChances ~= nil then
        normalChance = normalChance - monster.overrideSpecialChances[i]
      else
        normalChance = normalChance - specAttack.chance
      end
      local thisMax = 0
      if specAttack.setDamage ~= nil then
        thisMax = specAttack.setDamage * 10
      else
        thisMax = normalMaxHit
      end
      if thisMax > specialMaxHit then specialMaxHit = thisMax end
    end
  end
  --Ensure that if the monster never does a normal attack, the normal max hit is irrelevant
  if normalChance == 0 then normalMaxHit = 0 end
  return math.max(specialMaxHit, normalMaxHit)
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('Spells', 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
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


  if monster == nil then
local normalAttackChance = 100
    return "ERROR: No monster with that name found"
if monster.specialAttacks ~= nil then
  end
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


  local result = ''
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


  local iconText = ''
if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then
  local typeText = ''
table.insert(buffAttacks, specAttack.name)
  if monster.attackType == Constants.attackType.Melee then
hasActiveBuffSpec = true
    iconText = Icons.Icon({'Melee', notext=true})
isNormalAttackRelevant = true
    typeText = 'Melee'
end
  elseif monster.attackType == Constants.attackType.Ranged then
if not isNormalAttackRelevant and type(specAttack.damage) == 'table' then
    iconText = Icons.Icon({'Ranged', type='skill', notext=true})
-- Determine if the special attack uses normal damage in some form
    typeText = 'Ranged'
for j, dmgData in ipairs(specAttack.damage) do
  else
if dmgData.damageType == 'Normal' then
    iconText = Icons.Icon({'Magic', type='skill', notext=true})
isNormalAttackRelevant = true
    typeText = 'Magic'
break
  end
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


  local normalAttackChance = 100
return result
  if monster.hasSpecialAttack then
end
    for i, specID in pairs(monster.specialAttackID) do
      local specAttack = p.getSpecialAttackByID(specID)
      local attChance = 0
      if monster.overrideSpecialChances ~= nil then
        attChance = monster.overrideSpecialChances[i]
      else
        attChance = specAttack.chance
      end
      normalAttackChance = normalAttackChance - attChance


      result = result..'\r\n* '..attChance..'% '..iconText..' '..specAttack.name..'\r\n** '..specAttack.description
--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 == nil then
if monster.isBoss then
    return "ERROR: No monster with that name found"
result = result..'[[Category:Bosses]]'
  end
end


  local result = '[[Category:Monsters]]'
return result
 
end
  if monster.attackType == Constants.attackType.Melee then
    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
function p.getMonsterBoxBarrierText(frame)
    result = result..'[[Category:Monsters with Special Attacks]]'
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  end
local monster = p.getMonster(MonsterName)
 
  if monster.isBoss then
    result = result..'[[Category:Bosses]]'
  end


  return result
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
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
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 = ''


  if monster.bones ~= nil then
local bones = p._getMonsterBones(monster)
    local bones = Items.getItemByID(monster.bones)
local boneVal = 0
    --Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
local barrierDust = Items.getItemByID("melvorAoD:Barrier_Dust")
    if (monster.lootTable ~= nil and not monster.isBoss) or Shared.contains(bones.name, 'Shard') then
local dustVal = 0
      result = result.."'''Always Drops:'''"
--Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
      result = result..'\r\n{|class="wikitable"'
if bones ~= nil then
      result = result..'\r\n!Item !! Qty'
local boneQty = (bones.quantity ~= nil and bones.quantity or 1)
      result = result..'\r\n|-\r\n|'..Icons.Icon({bones.name, type='item'})
local barrier = p._getMonsterBarrier(monster)
      result = result..'||'..(monster.boneQty ~= nil and monster.boneQty or 1)..'\r\n'..'|}'
result = result.."'''Always Drops:'''"
    end
result = result..'\r\n{|class="wikitable" id="bonedrops"'
  end
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


  if monster.lootTable ~= nil and not monster.isBoss then
local result = 0
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
local boneVal = 0
    local lootValue = 0


    result = result.."'''Loot:'''"
local bones = p._getMonsterBones(monster)
    local avgGp = 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
boneVal = bones.item.sellsFor * boneQty
result = result + boneVal
end


    if monster.dropCoins ~= nil then
--Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
      avgGp = (monster.dropCoins[1] + monster.dropCoins[2]) / 2
if not p._isDungeonOnlyMonster(monster) then
      local gpTxt = Icons.GP(monster.dropCoins[1], monster.dropCoins[2])
local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
      if lootChance == 100 then
local lootValue = 0
        result = result.."\r\nIn addition to loot, the monster will also drop "..gpTxt
      else
        result = result.."\r\nIf loot is received, the monster will also drop "..gpTxt
      end
    end


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


    --Sort the loot table by weight in descending order
if monster.gpDrops ~= nil then
    table.sort(monster.lootTable, function(a, b) return a[2] > b[2] end)
avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
    for i, row in Shared.skpairs(monster.lootTable) do
end
      local thisItem = Items.getItemByID(row[1])
      local maxQty = row[3]
      result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
      result = result..'||style="text-align:right" data-sort-value="'..maxQty..'"|'


      if maxQty > 1 then
local multiDrop = Shared.tableCount(monster.lootTable) > 1
        result = result.. '1 - '
local totalWt = 0
      end
for i, row in pairs(monster.lootTable) do
      result = result..Shared.formatnum(row[3])
totalWt = totalWt + row.weight
end


      --Adding price columns
for i, row in ipairs(monster.lootTable) do
      local itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
local thisItem = Items.getItemByID(row.itemID)
      if itemPrice == 0 or maxQty == 1 then
        result = result..'||'..Icons.GP(itemPrice)
      else
        result = result..'||'..Icons.GP(itemPrice, itemPrice * maxQty)
      end


      --Getting the drop chance
--Adding price columns
      local dropChance = (row[2] / totalWt * lootChance)
local itemPrice = 0
      if dropChance ~= 100 then
if thisItem ~= nil then
        --Show fraction as long as it isn't going to be 1/1
itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
        result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
end
        result = result..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
        result = result..'||'
      else
        result = result..'||colspan="2" data-sort-value="'..row[2]..'"'
      end
      result = result..'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'


      --Adding to the average loot value based on price & dropchance
--Getting the drop chance
      lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
local dropChance = (row.weight / totalWt * lootChance)
    end
--Adding to the average loot value based on price & dropchance
    if multiDrop then
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 2))
      result = result..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
end
      if lootChance < 100 then
if avgGp > 0 then
        result = result..'\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||'
result = result + avgGp + lootValue
      else
end
        result = result..'\r\n|colspan="2" '
end
      end
      result = result..'style="text-align:right"|'..lootChance..'.00%'
    end
    --Account for GP only dropping when other loot is dropped
    avgGp = avgGp * (lootChance * .01)
    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"
    result = result..'<br/>Including GP, the average kill is worth '..Icons.GP(Shared.round(avgGp + lootValue, 2, 0))
  end


  --If no other drops, make sure to at least say so.
return result
  if result == '' then result = 'None' end
  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
if chest == nil then
    return "ERROR: No item named "..ChestName..' found'
return Shared.printError('No item named ' .. chestName .. ' found')
  end
end
local result = ''


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


  if chest.dropTable == nil then
local function formatNumRange(minValue, maxValue)
    return "ERROR: "..ChestName.." does not have a drop table"
if maxValue ~= nil and maxValue > minValue then
  else
return Shared.formatnum(minValue) .. ' - ' .. Shared.formatnum(maxValue)
    local lootChance = 100
else
    local lootValue = 0
return Shared.formatnum(minValue)
end
end


    local multiDrop = Shared.tableCount(chest.dropTable) > 1
local lootValue, foodValue = 0, 0
    local totalWt = 0
local totalWt = 0
    for i, row in pairs(chest.dropTable) do
local isAllFood = true
      totalWt = totalWt + row[2]
for i, row in ipairs(chest.dropTable) do
    end
totalWt = totalWt + row.weight
    result = result..'\r\n{|class="wikitable sortable"'
if isAllFood then
    result = result..'\r\n!Item!!Qty'
-- If the container's contents are entirely food then we add additional
    result = result..'!!colspan="2"|Chance!!Price'
-- 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
--Sort the loot table by weight in descending order
    for i, row in pairs(chest.dropTable) do
local chestDrops = Shared.shallowClone(chest.dropTable)
      if chest.dropQty ~= nil then
table.sort(chestDrops, function(a, b) return a.weight > b.weight end)
        table.insert(row, chest.dropQty[i])
for i, row in ipairs(chestDrops) do
      else
local thisItem = Items.getItemByID(row.itemID)
        table.insert(row, 1)
result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
      end
result = result..'||style="text-align:right" data-sort-value="'..(row.minQuantity + row.maxQuantity)..'"| ' .. formatNumRange(row.minQuantity, row.maxQuantity)
    end
    table.sort(chest.dropTable, function(a, b) return a[2] > b[2] end)
    for i, row in Shared.skpairs(chest.dropTable) do
      local thisItem = Items.getItemByID(row[1])
      local qty = row[3]
      result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
      result = result..'||style="text-align:right" data-sort-value="'..qty..'"|'


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


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


      result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
if 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))


      result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
if isAllFood then
      if qty > 1 then
local hp = thisItem.healsFor * 10
        result = result..'|'..Icons.GP(thisItem.sellsFor, thisItem.sellsFor * qty)
local minHeal, maxHeal = hp * row.minQuantity, hp * row.maxQuantity
      else
local avgHpPerLoot = (dropChance * 0.01 * (minHeal + maxHeal) / 2)
        result = result..'|'..Icons.GP(thisItem.sellsFor)
foodValue = foodValue + avgHpPerLoot
      end
result = result .. '||data-sort-value="' .. thisItem.healsFor .. '"'
      lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor * ((1 + qty)/ 2))
result = result .. '|' .. Icons.Icon({'Hitpoints', type='skill', notext=true, nolink=true}) .. ' ' .. formatNumRange(minHeal, maxHeal)
    end
result = result .. '||data-sort-value="' .. avgHpPerLoot .. '"'
    result = result..'\r\n|}'
result = result .. '|' .. Icons.Icon({'Hitpoints', type='skill', notext=true, nolink=true}) .. ' ' .. Shared.round(avgHpPerLoot, 2, 0)
    result = result..'\r\nThe average value of the contents of one chest is '..Icons.GP(Shared.round(lootValue, 2, 0))
end
  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
return result
end
end


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


  if area.type == 'dungeon' then
if area.type == 'dungeon' then
    return p.getDungeonMonsterTable(frame)
return p.getDungeonMonsterTable(frame)
  end
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 tableTxt = '{| class="wikitable sortable"'
local tableBits = {}
  tableTxt = tableTxt..'\r\n! Name !! Combat Level !! Hitpoints !! Max Hit !! [[Combat Triangle|Combat Style]]'
table.insert(tableBits, '{| class="wikitable sortable"')
  for i, monsterID in pairs(area.monsters) do
table.insert(tableBits, '\r\n! Name !! Combat Level ')
    local monster = p.getMonsterByID(monsterID)
if hasBarrier then
    tableTxt = tableTxt..'\r\n|-\r\n|'..Icons.Icon({monster.name, type='monster'})
table.insert(tableBits, '!! [[Barrier]] ')
    tableTxt = tableTxt..'||'..p.getMonsterCombatLevel(monster.name)
end
    tableTxt = tableTxt..'||'..Shared.formatnum(p.getMonsterHP(monster.name))
table.insert(tableBits, '!! Hitpoints !! colspan=2| Max Hit !! [[Combat Triangle|Combat Style]]')
    tableTxt = tableTxt..'||'..Shared.formatnum(p.getMonsterMaxHit(monster.name))
for i, monster in ipairs(monsters) do
    tableTxt = tableTxt..'||'..p.getMonsterStyleIcon({monster.name, nolink='true'})
local rowBits = {}
  end
table.insert(tableBits, '\r\n|-\r\n|'..Icons.Icon({p.getMonsterName(monster), type='monster'}))
  tableTxt = tableTxt..'\r\n|}'
table.insert(tableBits, '||'..p._getMonsterCombatLevel(monster))
  return tableTxt
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
end


function p.getDungeonMonsterTable(frame)
function p.getDungeonMonsterTable(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
local area = Areas.getArea(areaName)
  if area == nil then
if area == nil then
    return "ERROR: Could not find a dungeon named "..areaName
return Shared.printError('Could not find a dungeon named ' .. areaName)
  end
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


  --For Dungeons, go through and count how many of each monster are in the dungeon first
local returnPart = {}
  local monsterCounts = {}
table.insert(returnPart, '{| class="wikitable sortable"')
  for i, monsterID in pairs(area.monsters) do
table.insert(returnPart, '\r\n! Name !! Combat Level ')
    if monsterCounts[monsterID] == nil then  
if hasBarrier then
      monsterCounts[monsterID] = 1
table.insert(returnPart, '!! [[Barrier]] ')
    else
end
      monsterCounts[monsterID] = monsterCounts[monsterID] + 1
table.insert(returnPart, '!! Hitpoints !! colspan="2" | Max Hit !! [[Combat Triangle|Combat Style]] !! Count')
    end
-- Special handing for Impending Darkness event
  end
-- 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


  local usedMonsters = {}
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


  local tableTxt = '{| class="wikitable sortable"'
for i, monsterID in ipairs(area.monsterIDs) do
  tableTxt = tableTxt..'\r\n! Name !! Combat Level !! Hitpoints !! Max Hit !! [[Combat Triangle|Combat Style]] !! Count'
        local monster = p.getMonsterByID(monsterID)
  for i, monsterID in pairs(area.monsters) do
        totalHP = totalHP + p._getMonsterHP(monster)
    if not Shared.contains(usedMonsters, monsterID) then
end
      local monster = p.getMonsterByID(monsterID)
return totalHP
      local name = monster.name
      if monsterID == 51 then name = 'Spider2' end
      tableTxt = tableTxt..'\r\n|-\r\n|'..Icons.Icon({name, type='monster'})
      tableTxt = tableTxt..'||'..p.getMonsterCombatLevel(name)
      tableTxt = tableTxt..'||'..Shared.formatnum(p.getMonsterHP(name))
      tableTxt = tableTxt..'||'..Shared.formatnum(p.getMonsterMaxHit(name))
      tableTxt = tableTxt..'||'..p.getMonsterStyleIcon({name, nolink='true'})
      tableTxt = tableTxt..'||'..monsterCounts[monsterID]
      table.insert(usedMonsters, monsterID)
    end
  end
  tableTxt = tableTxt..'\r\n|}'
  return tableTxt
end
end


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


function p._getDungeonMonsterList(area)
function p._getDungeonMonsterList(area)
  local monsterList = {}
local monsterList = {}
  local lastMonster = nil
local lastID = ''
  local lastID = -1
local count = 0
  local count = 0
  for i, monsterID in pairs(area.monsters) do
local monsterCounts = {}
    local monster = p.getMonsterByID(monsterID)
for i, monsterID in ipairs(area.monsterIDs) do
    if monster.id ~= lastID then
if lastID == '' then
      if lastMonster ~= nil then
lastID = monsterID
        local name = lastMonster.name
count = 1
        if lastMonster.id == 51 then name = 'Spider2' end
elseif lastID == monsterID then
        table.insert(monsterList, Icons.Icon({name, type='monster', qty=count}))
count = count + 1
      end
else
      lastMonster = monster
table.insert(monsterCounts, { id = lastID, count = count })
      lastID = monster.id
lastID = monsterID
      count = 1
count = 1
    else
end
      count = count + 1
end
    end
table.insert(monsterCounts, { id = lastID, count = count })
    --Make sure the final monster in the dungeon gets counted
 
    if i == Shared.tableCount(monsterList) then
for i, monster in ipairs(monsterCounts) do
      local name = lastMonster.name
if monster.id == 'melvorF:RandomITM' then
      table.insert(monsterList, Icons.Icon({lastMonster.name, type='monster', qty=count}))
--Special handling for Afflicted Monsters
    end
table.insert(monsterList, Icons.Icon({'Affliction', 'Afflicted Monster', img='Question', qty=monster.count}))
  end
elseif monster.id == 'melvorTotH:RandomSpiderLair' then
  return table.concat(monsterList, '<br/>')
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
end


function p.getAreaMonsterList(frame)
function p.getAreaMonsterList(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
local area = Areas.getArea(areaName)
  if area == nil then
if area == nil then
    return "ERROR: Could not find an area named "..areaName
return Shared.printError('Could not find an area named ' .. areaName)
  end
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


  if area.type == 'dungeon' then
--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot TablesTemplate:MonsterLootTables
    return p._getDungeonMonsterList(area)
--It exists because I'm too lazy to manually type up all the new monsters - User:Falterfire
  else
function p.getMonsterLootTableText()
    return p._getAreaMonsterList(area)
local getAreaText = function(area)
  end
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
end


function p._getDungeonRewards(area)
--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot Tables
  local bossMonster = p.getMonsterByID(area.monsters[Shared.tableCount(area.monsters)])
--It exists because I'm too lazy to manually type up all the new chests - User:Falterfire
  local gpMin = bossMonster.dropCoins[1]
function p.getChestLootTables()
  local gpMax = bossMonster.dropCoins[2]
local items = Items.getItems(function(item) return item.dropTable ~= nil end)
  local chestID = bossMonster.lootTable[1][1]
local outArray = {}
  local chestQty = bossMonster.lootTable[1][3]
for i, item in ipairs(items) do
  local theChest = Items.getItemByID(chestID)
table.insert(outArray, "==={{ItemIcon|"..item.name.."|size=30}}===")
  local result = '* '..Icons.GP(gpMin, gpMax)
table.insert(outArray, "{{ChestDrops|"..item.name.."}}")
  result = result..'\r\n* '..Icons.Icon({theChest.name, type='item', qty=chestQty})
end
  if area.name == 'Volcanic Cave' then
return table.concat(outArray, "\r\n")
    result = result..'\r\n* '..Icons.Icon({'Fire Cape', type='item', qty=1})
  elseif area.name == 'Infernal Stronghold' then
    result = result..'\r\n* '..Icons.Icon({'Infernal Cape', type='item', qty=1})
  end
  return result
end
end


function p.getDungeonRewards(frame)
--Returns the expansion icon for the item if it has one
  local areaName = frame.args ~= nil and frame.args[1] or frame
function p.getExpansionIcon(frame)
  local area = Areas.getArea(areaName)
local monsterName = frame.args ~= nil and frame.args[1] or frame
  if area == nil then
local monster = p.getMonster(monsterName)
    return "ERROR: Could not find an area named "..areaName
  end


  if area.type == 'dungeon' then
if monster == nil then
    return p._getDungeonRewards(area)
return Shared.printError('No monster with that name found')
  else
end
    return "ERROR: "..areaName.." is not a dungeon"
  end
return Icons.getExpansionIcon(monster.id)
end
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