Module:Monsters: Difference between revisions

Add column name for average healing
(Added manual override for Into the Mist monster list for Afflicted Monsters)
(Add column name for average healing)
(152 intermediate revisions by 9 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)
  local result = Shared.clone(MonsterData.Monsters[ID + 1])
    return GameData.getEntityByID('monsters', ID)
  result.id = ID
end
  return result
 
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
end


function p.getSpecialAttack(name)
function p.getPassive(name)
  local result = nil
    return GameData.getEntityByName('combatPassives', name)
end


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


function p.getSpecialAttackByID(ID)
-- Given a list of monster IDs, calls statFunc with each monster and returns
  return MonsterData.SpecialAttacks[ID + 1]
-- 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.getPassive(name)
function p._getMonsterStat(monster, statName)
  local result = nil
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


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


function p.getPassiveByID(ID)
function p.getMonsterStat(frame)
  return MonsterData.Passives[ID + 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 monster = p.getMonster(MonsterName)
if monster == nil then
return Shared.printError('No monster with that name found')
end
 
return p._getMonsterStat(monster, StatName)
end
end


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


  if StatName == 'HP' then
local iconText = ''
    return p.getMonsterHP(MonsterName)
if monster.attackType == 'melee' then
  elseif StatName == 'maxHit' then
iconText = Icons.Icon({'Melee', notext=notext, nolink=nolink})
    return p.getMonsterMaxHit(MonsterName)
elseif monster.attackType == 'ranged' then
  elseif StatName == 'accuracyRating' then
iconText = Icons.Icon({'Ranged', type='skill', notext=notext, nolink=nolink})
    return p.getMonsterAR(MonsterName)
elseif monster.attackType == 'magic' then
  elseif StatName == 'meleeEvasionRating' then
iconText = Icons.Icon({'Magic', type='skill', notext=notext, nolink=nolink})
    return p.getMonsterER({MonsterName, 'Melee'})
elseif monster.attackType == 'random' then
  elseif StatName == 'rangedEvasionRating' then
iconText = Icons.Icon({p.getMonsterName(monster), notext=notext, nolink=nolink, img='Question'})
    return p.getMonsterER({MonsterName, 'Ranged'})
end
  elseif StatName == 'magicEvasionRating' then
    return p.getMonsterER({MonsterName, 'Magic'})
  end


  return monster[StatName]
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
return Shared.printError('No monster with that name found')
end
 
return p._getMonsterER(monster, style)
end


  if monster == nil then
-- Determines if the monster is capable of dropping bones, and returns the bones
    return "ERROR: No monster with that name found"
-- item if so, or nil otherwise
  end
function p._getMonsterBones(monster)
 
if monster.bones ~= nil then
  local effDefLvl = 0
local boneItem = Items.getItemByID(monster.bones.itemID)
  local defBonus = 0
        local boneObj = { ["item"] = boneItem, ["quantity"] = monster.bones.quantity }
  if style == "Melee" then
if boneItem.prayerPoints == nil then
    effDefLvl = monster.defenceLevel + 9
-- Assume bones without prayer points are shards (from God dungeons),
    defBonus = monster.defenceBonus + 64
-- and drop unconditionally
  elseif style == "Ranged" then
return boneObj
    effDefLvl = monster.defenceLevel + 9
elseif not monster.isBoss and not p._isDungeonOnlyMonster(monster) then
    defBonus = monster.defenceBonusRanged + 64
-- Otherwise, bones drop when the monster isn't dungeon exclusive
  elseif style == "Magic" then
return boneObj
    effDefLvl = math.floor(monster.magicLevel * 0.7 + monster.defenceLevel * 0.3) + 9
end
    defBonus = monster.defenceBonusMagic + 64
end
  else
    return "ERROR: Must choose Melee, Ranged, or Magic"
  end
  return effDefLvl * defBonus
end
end


function p._isDungeonOnlyMonster(monster)
function p._isDungeonOnlyMonster(monster)
  local areaList = Areas.getMonsterAreas(monster.id)
local areaList = Areas.getMonsterAreas(monster.id)
  local dunCount = 0
local inDungeon = false
  local nonDunCount = 0
 
  for i, area in Shared.skpairs(areaList) do
for i, area in ipairs(areaList) do
    if area.type == 'dungeon' then
if area.type == 'dungeon' then
      dunCount = dunCount + 1
inDungeon = true
    else
else
      nonDunCount = nonDunCount + 1
return false
    end
end
  end
end
  return dunCount > 0 and nonDunCount == 0
return inDungeon
end
end


function p.isDungeonOnlyMonster(frame)
function p.isDungeonOnlyMonster(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 name "..monsterName.." found"
return Shared.printError('No monster with name ' .. monsterName .. ' found')
  end
end


  return p._isDungeonOnlyMonster(monster)
return p._isDungeonOnlyMonster(monster)
end
end


function p._getMonsterAreas(monster)
function p._getMonsterAreas(monster, excludeDungeons, includeEffects)
  local result = ''
if includeEffects == nil then includeEffects = false end
  local areaList = Areas.getMonsterAreas(monster.id)
local resultPart = {}
  for i, area in pairs(areaList) do
local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
    if i > 1 then result = result..'<br/>' end
local areaList = Areas.getMonsterAreas(monster.id)
    result = result..Icons.Icon({area.name, type = area.type})
for i, area in ipairs(areaList) do
  end
if area.type ~= 'dungeon' or not hideDungeons then
  return result
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


  return p._getMonsterAreas(monster)
if doStuns then
for statusName, statusDef in pairs(dmgStatuses) do
if canApplyStatus[statusName] then
local adjType = statusDef.type
dmgAdjust[adjType] = dmgAdjust[adjType] + statusDef.magnitude
end
end
end
end
--Ensure that if the monster never does a normal attack, the normal max hit is irrelevant
if normalChance == 0 and not hasActiveBuffSpec then normalMaxHit = 0 end
local maxHit = math.floor(math.max(specialMaxHit, normalMaxHit) * dmgAdjust.percent / 100) + dmgAdjust.flat
return math.floor(maxHit * dmgAdjust.multiplier / 100)
end
end


function p.getMonsterMaxHit(frame)
function p.getMonsterMaxHit(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local doStuns = frame.args ~= nil and frame.args[2] or true
local monster = p.getMonster(MonsterName)
 
if monster == nil then
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 specAttack.stunDamageMultiplier ~= nil and specAttack.stunDamageMultiplier > 1 then
        thisMax = thisMax * specAttack.stunDamageMultiplier
      end
      if specAttack.sleepDamageMultiplier ~= nil and specAttack.sleepDamageMultiplier > 1 then
        thisMax = thisMax * specAttack.sleepDamageMultiplier
      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


  if monster == nil then
local buffAttacks = {}
    return "ERROR: No monster with that name found"
local hasActiveBuffSpec = false
  end
local isNormalAttackRelevant = false


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


  local iconText = ''
result = result..'\r\n* '..attChance..'% '..iconText..' '..specAttack.name..'\r\n** '..specAttack.description
  local typeText = ''
  if monster.attackType == Constants.attackType.Melee then
--If this special attack applies a curse, let's actually list what that curse does
    iconText = Icons.Icon({'Melee', notext=true})
if specAttack.onhitEffects ~= nil then
    typeText = 'Melee'
for j, hitEffect in ipairs(specAttack.onhitEffects) do
  elseif monster.attackType == Constants.attackType.Ranged then
if hitEffect.effectType == 'Curse' then
    iconText = Icons.Icon({'Ranged', type='skill', notext=true})
local curse = Magic.getSpellByID(hitEffect.curse, 'curse')
    typeText = 'Ranged'
result = result..'\r\n*** '..Icons.Icon({curse.name, type='curse'})..': '..Magic._getSpellDescription(curse, true)
  else
end
    iconText = Icons.Icon({'Magic', type='skill', notext=true})
end
    typeText = 'Magic'
end
  end


  local normalAttackChance = 100
if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then
  if monster.hasSpecialAttack then
table.insert(buffAttacks, specAttack.name)
    for i, specID in pairs(monster.specialAttackID) do
hasActiveBuffSpec = true
      local specAttack = p.getSpecialAttackByID(specID)
isNormalAttackRelevant = true
      local attChance = 0
end
      if monster.overrideSpecialChances ~= nil then
if not isNormalAttackRelevant and type(specAttack.damage) == 'table' then
        attChance = monster.overrideSpecialChances[i]
-- Determine if the special attack uses normal damage in some form
      else
for j, dmgData in ipairs(specAttack.damage) do
        attChance = specAttack.chance
if dmgData.damageType == 'Normal' then
      end
isNormalAttackRelevant = true
      normalAttackChance = normalAttackChance - attChance
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


      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
end


function p.getMonsterPassives(frame)
function p.getMonsterPassives(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 = ''
    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 == nil then
if monster.isBoss then
    return "ERROR: No monster with that name found"
result = result..'[[Category:Bosses]]'
  end
end


  local result = ''
return result
end


  if monster.hasPassive then
function p.getMonsterBoxBarrierText(frame)
    result = result .. '===Passives==='
local MonsterName = frame.args ~= nil and frame.args[1] or frame
    for i, passiveID in pairs(monster.passiveID) do
local monster = p.getMonster(MonsterName)
      local passive = p.getPassiveByID(passiveID)
      local passiveChance = 0
      if passive.chance ~= nil then
        passiveChance = passive.chance
      end


      result = result .. '\r\n* ' .. Shared.round(passiveChance, 2, 0) .. '% ' .. passive.name .. '\r\n** ' .. passive.description
if monster == nil then
    end
return Shared.printError('No monster with that name found')
  end
end
  return result
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
end


function p.getMonsterCategories(frame)
function p.getOtherMonsterBoxText(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 = ''
 
--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


  if monster == nil then
result = result.."\r\n|-\r\n|'''Monster Types:''' "..table.concat(monsterTypes, ", ")
    return "ERROR: No monster with that name found"
  end


  local result = '[[Category:Monsters]]'
local SlayerTier = 'N/A'
 
if monster.canSlayer then
  if monster.attackType == Constants.attackType.Melee then
SlayerTier = Constants.getSlayerTierNameByLevel(p._getMonsterCombatLevel(monster))
    result = result..'[[Category:Melee Monsters]]'
end
  elseif monster.attackType == Constants.attackType.Ranged then
    result = result..'[[Category:Ranged Monsters]]'
  elseif monster.attackType == Constants.attackType.Magic then
    result = result..'[[Category:Magic Monsters]]'
  end


  if monster.hasSpecialAttack then
result = result.."\r\n|-\r\n|'''"..Icons.Icon({'Slayer', type='skill'}).." [[Slayer#Slayer Tier Monsters|Tier]]:''' "
    result = result..'[[Category:Monsters with Special Attacks]]'
if monster.canSlayer then
  end
result = result.."[[Slayer#"..SlayerTier.."|"..SlayerTier.."]]"
 
else
  if monster.isBoss then
result = result..SlayerTier
    result = result..'[[Category:Bosses]]'
end
  end


  return result
return result
end
end


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


  if monster == nil then
--Adding to the average loot value based on price & dropchance
    return "ERROR: No monster with that name found"
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 2))
  end
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


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


  if monster.bones ~= nil then
function p._getMonsterLootValue(monster)
    local bones = Items.getItemByID(monster.bones)
if monster == nil then
    --Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
return Shared.printError('No monster with that name found')
    if not p._isDungeonOnlyMonster(monster) or Shared.contains(bones.name, 'Shard') then
end
      result = result.."'''Always Drops:'''"
      result = result..'\r\n{|class="wikitable"'
      result = result..'\r\n!Item !! Qty'
      result = result..'\r\n|-\r\n|'..Icons.Icon({bones.name, type='item'})
      result = result..'||'..(monster.boneQty ~= nil and monster.boneQty or 1)..'\r\n'..'|}'
    end
  end


  --Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
local result = 0
  if not p._isDungeonOnlyMonster(monster) then
local boneVal = 0
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    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 and monster.dropCoins[2] > 1 then
--Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
      avgGp = (monster.dropCoins[1] + monster.dropCoins[2] - 1) / 2
if not p._isDungeonOnlyMonster(monster) then
      local gpTxt = Icons.GP(monster.dropCoins[1], monster.dropCoins[2] - 1)
local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
      result = result.."\r\nIn addition to loot, the monster will also drop "..gpTxt
local lootValue = 0
    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
    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
      if monsterID ~= 1 then
        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]
      else
        --Special handling for Into the Mist
        tableTxt = tableTxt..'\r\n|-\r\n|'..Icons.Icon({'Into the Mist', 'Afflicted Monster', nolink=true, img='Question'})
        tableTxt = tableTxt..'||data-sort-value="0"|'..Icons.Icon({'Into the Mist', notext=true, nolink=true, img='Question'})
        tableTxt = tableTxt..'||data-sort-value="0"|'..Icons.Icon({'Into the Mist', notext=true, nolink=true, img='Question'})
        tableTxt = tableTxt..'||data-sort-value="0"|'..Icons.Icon({'Into the Mist', notext=true, nolink=true, img='Question'})
        tableTxt = tableTxt..'||data-sort-value="0"|'..Icons.Icon({'Into the Mist', notext=true, nolink=true, img='Question'})
        tableTxt = tableTxt..'||'..monsterCounts[monsterID]
      end
      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 Shared.skpairs(area.monsters) do
local monsterCounts = {}
    if monsterID ~= lastID then
for i, monsterID in ipairs(area.monsterIDs) do
      local monster = p.getMonsterByID(monsterID)
if lastID == '' then
      if lastMonster ~= nil then
lastID = monsterID
        if lastID == 1 then
count = 1
          --Special handling for Afflicted Monsters
elseif lastID == monsterID then
          table.insert(monsterList, Icons.Icon({'Affliction', 'Afflicted Monster', img='Question'}))
count = count + 1
        else
else
          local name = lastMonster.name
table.insert(monsterCounts, { id = lastID, count = count })
          if lastMonster.id == 51 then name = 'Spider2' end
lastID = monsterID
          table.insert(monsterList, Icons.Icon({name, type='monster', qty=count}))
count = 1
        end
end
      end
end
      lastMonster = monster
table.insert(monsterCounts, { id = lastID, count = count })
      lastID = monster.id
 
      count = 1
for i, monster in ipairs(monsterCounts) do
    else
if monster.id == 'melvorF:RandomITM' then
      count = count + 1
--Special handling for Afflicted Monsters
    end
table.insert(monsterList, Icons.Icon({'Affliction', 'Afflicted Monster', img='Question', qty=monster.count}))
    --Make sure the final monster in the dungeon gets counted
elseif monster.id == 'melvorTotH:RandomSpiderLair' then
    if i == Shared.tableCount(area.monsters) then
local monIconPart = { Shared.formatnum(monster.count) .. ' Spiders:' }
      local name = lastMonster.name
for i, monsterID in ipairs(GameData.rawData.spiderLairMonsters) do
      table.insert(monsterList, Icons.Icon({lastMonster.name, type='monster', qty=count}))
local monster = p.getMonsterByID(monsterID)
    end
if monster ~= nil then
  end
table.insert(monIconPart, '&nbsp;&nbsp;&nbsp;' .. Icons.Icon({p.getMonsterName(monster), type='monster'}))
  return table.concat(monsterList, '<br/>')
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


  if area.type == 'dungeon' then
table.insert(tableParts, '\r\n|}')
    return p._getDungeonMonsterList(area)
return table.concat(tableParts)
  else
    return p._getAreaMonsterList(area)
  end
end
end


function p._getDungeonRewards(area)
function p.getSpecialAttackTable(frame)
  local bossMonster = p.getMonsterByID(area.monsters[Shared.tableCount(area.monsters)])
local spAttTable = {}
  local gpMin = bossMonster.dropCoins[1]
 
  local gpMax = bossMonster.dropCoins[2]
for i, monster in ipairs(GameData.rawData.monsters) do
  local chestID = bossMonster.lootTable[1][1]
if monster.specialAttacks ~= nil and not Shared.tableIsEmpty(monster.specialAttacks) then
  local chestQty = bossMonster.lootTable[1][3]
local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
  local theChest = Items.getItemByID(chestID)
for j, spAttID in ipairs(monster.specialAttacks) do
  local result = '* '..Icons.GP(gpMin, gpMax)
                local spAtt = GameData.getEntityByID('attacks', spAttID)
  result = result..'\r\n* '..Icons.Icon({theChest.name, type='item', qty=chestQty})
local attChance = (overrideChance and monster.overrideSpecialChances[j] or spAtt.defaultChance)
  if area.name == 'Volcanic Cave' then
if spAttTable[spAtt.id] == nil then
    result = result..'\r\n* '..Icons.Icon({'Fire Cape', type='item', qty=1})
spAttTable[spAtt.id] = { ['defn'] = spAtt, ['icons'] = {} }
  elseif area.name == 'Infernal Stronghold' then
end
    result = result..'\r\n* '..Icons.Icon({'Infernal Cape', type='item', qty=1})
if spAttTable[spAtt.id]['icons'][attChance] == nil then
  end
spAttTable[spAtt.id]['icons'][attChance] = {}
  return result
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
end


function p.getDungeonRewards(frame)
--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot TablesTemplate:MonsterLootTables
  local areaName = frame.args ~= nil and frame.args[1] or frame
--It exists because I'm too lazy to manually type up all the new monsters - User:Falterfire
  local area = Areas.getArea(areaName)
function p.getMonsterLootTableText()
  if area == nil then
local getAreaText = function(area)
    return "ERROR: Could not find an area named "..areaName
local outArray = {}
  end
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


  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 Tables
    return p._getDungeonRewards(area)
--It exists because I'm too lazy to manually type up all the new chests - User:Falterfire
  else
function p.getChestLootTables()
    return "ERROR: "..areaName.." is not a dungeon"
local items = Items.getItems(function(item) return item.dropTable ~= nil end)
  end
local outArray = {}
for i, item in ipairs(items) do
table.insert(outArray, "==={{ItemIcon|"..item.name.."|size=30}}===")
table.insert(outArray, "{{ChestDrops|"..item.name.."}}")
end
return table.concat(outArray, "\r\n")
end
end


function p.getFoxyTable(frame)
--Returns the expansion icon for the item if it has one
  local result = 'Monster,Min GP,Max GP,Average GP'
function p.getExpansionIcon(frame)
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
local monsterName = frame.args ~= nil and frame.args[1] or frame
    if not p._isDungeonOnlyMonster(monster) then
local monster = p.getMonster(monsterName)
      if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
 
      local avgGp = (monster.dropCoins[1] + monster.dropCoins[2] - 1) / 2
if monster == nil then
      result = result..'<br/>'..monster.name..','..monster.dropCoins[1]..','..(monster.dropCoins[2]-1)..','..avgGp
return Shared.printError('No monster with that name found')
      end
end
    end
  end
return Icons.getExpansionIcon(monster.id)
  return result
end
end


return p
return p
3

edits