Module:Monsters: Difference between revisions

Add column name for average healing
(Updated some references to now call Constants instead of directly hitting Constants.data, added Slayer Tier to the otherMonsterBoxText)
(Add column name for average healing)
(136 intermediate revisions by 9 users not shown)
Line 1: Line 1:
local p = {}
local p = {}
local MonsterData = mw.loadData('Module:Monsters/data')


local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
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
      break
     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
  return result
end
end


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


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


function p.getSpecialAttackByID(ID)
function p.getPassiveByID(ID)
  return MonsterData.SpecialAttacks[ID + 1]
    return GameData.getEntityByID('combatPassives', ID)
end
end


function p.getPassive(name)
-- Given a list of monster IDs, calls statFunc with each monster and returns
  local result = nil
-- 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


  for i, passive in pairs(MonsterData.Passives) do
function p._getMonsterStat(monster, statName)
    if passive.name == name then
if statName == 'Barrier' then
      result = Shared.clone(passive)
return p._getMonsterBarrier(monster)
      --Make sure every passive has an ID, and account for the 1-based indexing of Lua
elseif statName == 'HP' then
      result.id = i - 1
return p._getMonsterHP(monster)
      break
elseif statName == 'maxHit' then
    end
return p._getMonsterMaxHit(monster)
  end
elseif statName == 'accuracyRating' then
  return result
return p._getMonsterAR(monster)
end
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


function p.getPassiveByID(ID)
return monster[statName]
  return MonsterData.Passives[ID + 1]
end
end


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


  if StatName == 'HP' then
return p._getMonsterStat(monster, StatName)
    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'})
  end
 
  return monster[StatName]
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 monster = args[1]
local monster = args[1]
  local notext = args.notext
local notext = args.notext
  local nolink = args.nolink
local nolink = args.nolink


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


  return iconText
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 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[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


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


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


function p._getMonsterAttackSpeed(monster)
function p._getMonsterAttackSpeed(monster)
  return monster.attackSpeed / 1000
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 p._getMonsterAttackSpeed(monster)
return p._getMonsterAttackSpeed(monster)
  else
else
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end
end
end


function p._getMonsterCombatLevel(monster)
function p._getMonsterCombatLevel(monster)
  local base = 0.25 * (monster.defenceLevel + monster.hitpoints)
local base = 0.25 * (p._getMonsterLevel(monster, 'Defence') + p._getMonsterLevel(monster, 'Hitpoints'))
  local melee = 0.325 * (monster.attackLevel + monster.strengthLevel)
local melee = 0.325 * (p._getMonsterLevel(monster, 'Attack') + p._getMonsterLevel(monster, 'Strength'))
  local range = 0.325 * (1.5 * monster.rangedLevel)
local range = 0.325 * (1.5 * p._getMonsterLevel(monster, 'Ranged'))
  local magic = 0.325 * (1.5 * monster.magicLevel)
local magic = 0.325 * (1.5 * p._getMonsterLevel(monster, 'Magic'))
  if melee > range and melee > magic then
return math.floor(base + math.max(melee, range, magic))
    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.getMonsterCombatLevel(frame)
function p.getMonsterCombatLevel(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


  return p._getMonsterCombatLevel(monster)
return p._getMonsterCombatLevel(monster)
end
end


function p._getMonsterAR(monster)
function p._getMonsterAR(monster)
  local effAttLvl = 0
local baseLevel = 0
  local attBonus = 0
local bonus = 0
  if Constants.getCombatStyleName(monster.attackType) == 'Melee' then
if monster.attackType == 'melee' then
    effAttLvl = monster.attackLevel + 9
baseLevel = p._getMonsterLevel(monster, 'Attack')
    attBonus = monster.attackBonus + 64
bonus = p.getEquipmentStat(monster, 'stabAttackBonus')
  elseif Constants.getCombatStyleName(monster.attackType) == 'Ranged' then
elseif monster.attackType == 'ranged' then
    effAttLvl = monster.rangedLevel + 9
baseLevel = p._getMonsterLevel(monster, 'Ranged')
    attBonus = monster.attackBonusRanged + 64
bonus = p.getEquipmentStat(monster, 'rangedAttackBonus')
  elseif Constants.getCombatStyleName(monster.attackType) == 'Magic' then
elseif monster.attackType == 'magic' then
    effAttLvl = monster.magicLevel + 9
baseLevel = p._getMonsterLevel(monster, 'Magic')
    attBonus = monster.attackBonusMagic + 64
bonus = p.getEquipmentStat(monster, 'magicAttackBonus')
  else
elseif monster.attackType == 'random' then
    return "ERROR: This monster has an invalid attack type somehow[[Category:Pages with script errors]]"
--Bane has the same AR with every attack type so being lazy and just showing the one.
  end
baseLevel = p._getMonsterLevel(monster, 'Attack')
bonus = p.getEquipmentStat(monster, 'stabAttackBonus')
else
return Shared.printError('This monster has an invalid attack type somehow')
end


  return effAttLvl * attBonus
return p.calculateStandardStat(baseLevel, bonus)
end
end


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


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


  return p._getMonsterAR(monster)
return p._getMonsterAR(monster)
end
end


function p._getMonsterER(frame)
function p._getMonsterER(monster, style)
  local args = frame.args ~= nil and frame.args or frame
local baseLevel= 0
  local monster = args[1]
local bonus = 0
  local style = args[2]


  local effDefLvl = 0
if style == "Melee" then
  local defBonus = 0
baseLevel = p._getMonsterLevel(monster, 'Defence')
  if style == "Melee" then
bonus = p.getEquipmentStat(monster, 'meleeDefenceBonus')
    effDefLvl = monster.defenceLevel + 9
elseif style == "Ranged" then
    defBonus = monster.defenceBonus + 64
baseLevel = p._getMonsterLevel(monster, 'Defence')
  elseif style == "Ranged" then
bonus = p.getEquipmentStat(monster, 'rangedDefenceBonus')
    effDefLvl = monster.defenceLevel + 9
elseif style == "Magic" then
    defBonus = monster.defenceBonusRanged + 64
baseLevel = math.floor(p._getMonsterLevel(monster, 'Magic') * 0.7 + p._getMonsterLevel(monster, 'Defence') * 0.3)
  elseif style == "Magic" then
bonus = p.getEquipmentStat(monster, 'magicDefenceBonus')
    effDefLvl = math.floor(monster.magicLevel * 0.7 + monster.defenceLevel * 0.3) + 9
else
    defBonus = monster.defenceBonusMagic + 64
return Shared.printError('Must choose Melee, Ranged, or Magic')
  else
end
    return "ERROR: Must choose Melee, Ranged, or Magic[[Category:Pages with script errors]]"
 
  end
return p.calculateStandardStat(baseLevel, bonus)
  return effDefLvl * defBonus
end
end


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


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


  return p._getMonsterER({monster, style})
return p._getMonsterER(monster, style)
end
 
-- Determines if the monster is capable of dropping bones, and returns the bones
-- item if so, or nil otherwise
function p._getMonsterBones(monster)
if monster.bones ~= nil then
local boneItem = Items.getItemByID(monster.bones.itemID)
        local boneObj = { ["item"] = boneItem, ["quantity"] = monster.bones.quantity }
if boneItem.prayerPoints == nil then
-- Assume bones without prayer points are shards (from God dungeons),
-- and drop unconditionally
return boneObj
elseif not monster.isBoss and not p._isDungeonOnlyMonster(monster) then
-- Otherwise, bones drop when the monster isn't dungeon exclusive
return boneObj
end
end
end
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[[Category:Pages with script errors]]"
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, excludeDungeons)
function p._getMonsterAreas(monster, excludeDungeons, includeEffects)
  local result = ''
if includeEffects == nil then includeEffects = false end
  local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
local resultPart = {}
  local areaList = Areas.getMonsterAreas(monster.id)
local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
  for i, area in pairs(areaList) do
local areaList = Areas.getMonsterAreas(monster.id)
    if area.type ~= 'dungeon' or not hideDungeons then
for i, area in ipairs(areaList) do
      if i > 1 then result = result..'<br/>' end
if area.type ~= 'dungeon' or not hideDungeons then
      result = result..Icons.Icon({area.name, type = area.type})
local imgType = (area.type == 'slayerArea' and 'combatArea') or area.type
    end
local txt = Icons.Icon({area.name, type = imgType})
  end
if area.type == 'slayerArea' then
  return result
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 hideDungeons = frame.args ~= nil and frame.args[2] or nil
local hideDungeons = frame.args ~= nil and frame.args[2] or nil
  local monster = p.getMonster(MonsterName)
local includeEffects = frame.args ~= nil and frame.args[3] or true
local monster = p.getMonster(monsterName)


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with name "..monsterName.." found[[Category:Pages with script errors]]"
return Shared.printError('No monster with name ' .. monsterName .. ' found')
  end
end


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


function p._getMonsterMaxHit(monster)
function p.getSpecAttackMaxHit(specAttack, normalMaxHit, monster)
  local normalChance = 100
local bestHit = 0
  local specialMaxHit = 0
  local normalMaxHit = p._getMonsterBaseMaxHit(monster)
for i, dmg in pairs(specAttack.damage) do
  if monster.hasSpecialAttack then
local thisHit = 0
    for i, specID in pairs(monster.specialAttackID) do
if dmg.damageType == 'Normal' then
      local specAttack = p.getSpecialAttackByID(specID)
--Account for special attacks that include a normal attack hit
      if monster.overrideSpecialChances ~= nil then
thisHit = normalMaxHit
        normalChance = normalChance - monster.overrideSpecialChances[i]
if dmg.amplitude ~= nil then
      else
thisHit = thisHit * (dmg.amplitude / 100)
        normalChance = normalChance - specAttack.chance
end
      end
elseif dmg.maxRoll == 'Fixed' then
      local thisMax = 0
thisHit = dmg.maxPercent * 10
      if specAttack.setDamage ~= nil then
elseif dmg.maxRoll == 'MaxHit' then
        thisMax = specAttack.setDamage * 10
if dmg.character == 'Target' then
      else
--Confusion applied damage based on the player's max hit. Gonna just ignore that one
        thisMax = normalMaxHit
thisHit = 0
      end
else
      if specAttack.stunDamageMultiplier ~= nil and specAttack.stunDamageMultiplier > 1 then
thisHit = dmg.maxPercent * normalMaxHit * 0.01
        thisMax = thisMax * specAttack.stunDamageMultiplier
end
      end
elseif Shared.contains(dmg.maxRoll, "Fixed100") then
      if specAttack.sleepDamageMultiplier ~= nil and specAttack.sleepDamageMultiplier > 1 then
--Handles attacks that are doubled when conditions are met like Trogark's double damage if the player is burning
        thisMax = thisMax * specAttack.sleepDamageMultiplier
thisHit = dmg.maxPercent * 20
      end
elseif dmg.maxRoll == 'MaxHitScaledByHP2x' then
      if thisMax > specialMaxHit then specialMaxHit = thisMax end
thisHit = normalMaxHit * 2
    end
elseif dmg.maxRoll == 'PoisonMax35' then
  end
thisHit = normalMaxHit * 1.35
  --Ensure that if the monster never does a normal attack, the normal max hit is irrelevant
elseif dmg.maxRoll == "MaxHitDR" then
  if normalChance == 0 then normalMaxHit = 0 end
local monsterDR = 0
  return math.max(specialMaxHit, normalMaxHit)
if monster ~= nil then
monsterDR = p._getMonsterStat(monster, 'damageReduction')
end
thisHit = normalMaxHit * dmg.maxPercent * 0.01 * (1 + monsterDR * 0.01)
elseif Shared.contains({'Bleeding', 'Poisoned'}, dmg.maxRoll) then
-- TODO: This is limited in that there is no verification that bleed/poison
-- can be applied to the target, it is assumed that it can and so this applies
thisHit = thisHit + dmg.maxPercent * 10
end
if thisHit > bestHit then
bestHit = thisHit
end
end
return bestHit
end
 
function p.canSpecAttackApplyEffect(specAttack, effectType)
local effectKeys = { 'prehitEffects', 'onhitEffects' }
for i, effectKey in ipairs(effectKeys) do
if type(specAttack[effectKey]) == 'table' then
for j, effect in pairs(specAttack[effectKey]) do
if effect.type == effectType or p.canModifiersApplyEffect(effect.modifiers, effectType) then
return true
end
end
end
end
return false
end
 
function p.canModifiersApplyEffect(modifiers, effectType)
-- List of modifiers which can result in the application of status effects
local statusModsAll = {
["Stun"] = { 'increasedGlobalStunChance', 'increasedMeleeStunChance' },
["Sleep"] = { 'increasedGlobalSleepChance' },
["Poison"] = { 'increasedChanceToApplyPoison' },
["Slow"] = { 'increased15SlowStunChance2Turns', 'increased30Slow5TurnsChance' }
}
 
local statusMods = statusModsAll[effectType]
if statusMods ~= nil and type(modifiers) == 'table' then
for modName, modMagnitude in pairs(modifiers) do
if Shared.contains(statusMods, modName) then
return true
end
end
end
return false
end
 
function p._getMonsterMaxHit(monster, doStuns)
-- 2021-06-11 Adjusted for v0.20 stun/sleep changes, where damage multiplier now applies
-- to all enemy attacks if stun/sleep is present on at least one special attack
if doStuns == nil then
doStuns = true
elseif type(doStuns) == 'string' then
doStuns = string.upper(doStuns) == 'TRUE'
end
 
-- Damage adjustments are defined as follows:
-- multiplier - Damage from modifier 'increasedDamageTaken' & additional damage while
-- stunned, asleep, or poisoned. Defined by in-game function
-- getDamageModifiers(). Applies after other percentage of flat adjustments.
-- percent - Percentage adjustments to the max hit. Applies before flat & multiplier
-- adjustments.
-- flat - Flat adjustments to the max hit. Applies after percent adjustments, and
-- after multiplier adjustments.
local dmgAdjust = { ["percent"] = 100, ["flat"] = 0, ["multiplier"] = 100 }
-- Check passives & effects that apply pre or on hit for damage modifiers
local dmgMods = {
-- List of modifiers which affect damage dealt, and whether they are percentage or flat adjustments
["increasedDamageTaken"] = { type = 'multiplier', mult = 1 },
["increasedMaxHitPercent"] = { type = 'percent', mult = 1 },
["increasedMeleeMaxHit"] = { type = 'percent', mult = 1 },
["increasedRangedMaxHit"] = { type = 'percent', mult = 1 },
["increasedMagicMaxHit"] = { type = 'percent', mult = 1 },
["increasedMaxHitFlat"] = { type = 'flat', mult = 10 },
["increasedMeleeMaxHitFlat"] = { type = 'flat', mult = 10 },
["increasedRangedMaxHitFlat"] = { type = 'flat', mult = 10 },
["increasedMagicMaxHitFlat"] = { type = 'flat', mult = 10 },
-- Rage: +2% max hit per stack, maximum of 10 stacks
["increasedRage"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 10 },
-- Dark Blade: +1% max hit per successful hit, maximum of 30 stacks
["increasedChanceDarkBlade"] = { type = 'percent', mult = 1, magnitude = 1, maxStacks = 30 },
-- Growing Madness/Moment in Time/Reign Over Time: +2% max hit per stack, maximum of 25 stacks
["growingMadnessPassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 },
["momentInTimePassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 },
["reignOverTimePassive"] = { type = 'percent', mult = 1, magnitude = 2, maxStacks = 25 }
}
local effectKeys = { 'prehitEffects', 'onhitEffects' }
local dmgStatuses = {
-- List of status effects which can affect damage dealt
["Stun"] = { type = 'multiplier', magnitude = 30 },
["Sleep"] = { type = 'multiplier', magnitude = 20 }
}
local canApplyStatus = {}
-- Initialize table
for statusName, def in pairs(dmgStatuses) do
canApplyStatus[statusName] = false
end
 
local adjustForMod = function(mod, modMagnitude, effect)
local magnitude = mod.magnitude or modMagnitude
local maxStacks = mod.maxStacks or (effect ~= nil and effect.maxStacks) or 1
dmgAdjust[mod.type] = dmgAdjust[mod.type] + magnitude * mod.mult * maxStacks
end
 
local adjustForCurse = function(curseID, effect)
local curse = Magic.getSpellByID(curseID, 'curse')
if type(curse) == 'table' and type(curse.targetModifiers) == 'table' then
for modName, modMagnitude in pairs(curse.targetModifiers) do
local mod = dmgMods[modName]
if mod ~= nil then
-- The modifier is one which affects damage dealt
adjustForMod(mod, modMagnitude, effect)
end
end
end
end
 
-- Check monster passives for modifiers which affect damage dealt, and alo if any modifiers
-- present can apply stun or sleep
if monster ~= nil and type(monster.passives) ~= nil then
for i, passiveID in ipairs(monster.passives) do
local passive = p.getPassiveByID(passiveID)
if passive ~= nil and type(passive.modifiers) == 'table' then
for modName, modMagnitude in pairs(passive.modifiers) do
local mod = dmgMods[modName]
if modName == 'applyRandomCurseOnSpawn' then
-- Special case in which the enemy can apply a random curse. Currently
-- Anguish III is the curse with the highest +% damage taken, so use this.
adjustForCurse('melvorF:AnguishIII')
elseif mod ~= nil then
-- The modifier is one which affects damage dealt
adjustForMod(mod, modMagnitude)
end
end
-- Check for application of relevant status effects
if doStuns then
for statusName, statusDef in pairs(dmgStatuses) do
if not canApplyStatus[statusName] and p.canModifiersApplyEffect(passive.modifiers, statusName) then
canApplyStatus[statusName] = true
end
end
end
end
end
end
 
local normalChance = 100
local specialMaxHit = 0
local normalMaxHit = p._getMonsterBaseMaxHit(monster)
local hasActiveBuffSpec = false
if monster.specialAttacks ~= nil then
for i, specAttackID in pairs(monster.specialAttacks) do
            local specAttack = GameData.getEntityByID('attacks', specAttackID)
for i, effectKey in ipairs(effectKeys) do
if type(specAttack[effectKey]) == 'table' then
for j, effect in ipairs(specAttack[effectKey]) do
local countsOnPlayer = (effect.countsOn == nil or effect.countsOn == 'Attacker')
if countsOnPlayer then
-- Check for pre or on hit effects for modifiers which affect damage dealt
if type(effect.modifiers) == 'table' then
for modName, modMagnitude in pairs(effect.modifiers) do
local mod = dmgMods[modName]
if mod ~= nil then
-- The modifier is one which affects damage dealt
adjustForMod(mod, modMagnitude, effect)
end
end
end
-- Check for curses which may cause the player to incur additional damage
if effect.effectType == 'Curse' then
-- If isRandom is true then a random curse is selected. Currently
-- Anguish III is the curse with the highest +% damage taken, so
-- use this.
local curseID = (effect.isRandom and 'melvorF:AnguishIII') or effect.curse
if curseID ~= nil then
adjustForCurse(curseID, effect)
end
end
end
end
end
end
 
if monster.overrideSpecialChances ~= nil then
normalChance = normalChance - monster.overrideSpecialChances[i]
else
normalChance = normalChance - specAttack.defaultChance
end
-- Check for application of relevant status effects
if doStuns then
for statusName, statusDef in pairs(dmgStatuses) do
if not canApplyStatus[statusName] and p.canSpecAttackApplyEffect(specAttack, statusName) then
canApplyStatus[statusName] = true
end
end
end
 
local thisMax = p.getSpecAttackMaxHit(specAttack, normalMaxHit, monster)
if thisMax > specialMaxHit then specialMaxHit = thisMax end
if Shared.contains(string.upper(specAttack.description), 'NORMAL ATTACK INSTEAD') then  
hasActiveBuffSpec = true
end
end
 
if doStuns then
for statusName, statusDef in pairs(dmgStatuses) do
if canApplyStatus[statusName] then
local adjType = statusDef.type
dmgAdjust[adjType] = dmgAdjust[adjType] + statusDef.magnitude
end
end
end
end
--Ensure that if the monster never does a normal attack, the normal max hit is irrelevant
if normalChance == 0 and not hasActiveBuffSpec then normalMaxHit = 0 end
local maxHit = math.floor(math.max(specialMaxHit, normalMaxHit) * dmgAdjust.percent / 100) + dmgAdjust.flat
return math.floor(maxHit * dmgAdjust.multiplier / 100)
end
end


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


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


  return p._getMonsterMaxHit(monster)
return p._getMonsterMaxHit(monster, doStuns)
end
end


function p._getMonsterBaseMaxHit(monster)
function p._getMonsterBaseMaxHit(monster)
  local effStrLvl = 0
--8/27/21 - Now references p.calculateStandardMaxHit for Melee & Ranged
  local strBonus = 0
local result = 0
  if Constants.getCombatStyleName(monster.attackType) == 'Melee' then
local baseLevel = 0
    effStrLvl = monster.strengthLevel + 9
local bonus = 0
    strBonus = monster.strengthBonus
if monster.attackType == 'melee' then
  elseif Constants.getCombatStyleName(monster.attackType) == 'Ranged' then
baseLevel = p._getMonsterLevel(monster, 'Strength')
    effStrLvl = monster.rangedLevel + 9
bonus = p.getEquipmentStat(monster, 'meleeStrengthBonus')
    strBonus = monster.strengthBonusRanged
result = p.calculateStandardMaxHit(baseLevel, bonus)
  elseif Constants.getCombatStyleName(monster.attackType) == 'Magic' then
elseif monster.attackType == 'ranged' then
    local mSpell = nil
baseLevel = p._getMonsterLevel(monster, 'Ranged')
    if monster.selectedSpell ~= nil then mSpell = Magic.getSpellByID('Spells', monster.selectedSpell) end
bonus = p.getEquipmentStat(monster, 'rangedStrengthBonus')
    if mSpell == nil then
result = p.calculateStandardMaxHit(baseLevel, bonus)
      return math.floor(10 * (monster.setMaxHit + (monster.setMaxHit * monster.damageBonusMagic / 100)))
elseif monster.attackType == 'magic' then
    else
        if monster.selectedSpell == nil then
      return math.floor(10 * (mSpell.maxHit + (mSpell.maxHit * monster.damageBonusMagic / 100)))
            result = 0
    end
        else
  else
            local mSpell = Magic.getSpellByID(monster.selectedSpell, 'standard')
    error('blah')
            if mSpell == nil then
    return "ERROR: This monster has an invalid attack type somehow[[Category:Pages with script errors]]"
                result = 0
  end
            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


  --Should only get here for Melee/Ranged, which use functionally the same damage formula
return result
  return math.floor(10 * (1.3 + (effStrLvl/10) + (strBonus / 80) + ((effStrLvl * strBonus) / 640)))
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[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


  return p._getMonsterBaseMaxHit(monster)
return p._getMonsterBaseMaxHit(monster)
end
end


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


  if monster == nil then
if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


  local result = ''
local result = ''
local iconText = p._getMonsterStyleIcon({monster, notext=true})
local typeText = ''
if monster.attackType == 'melee' then
typeText = 'Melee'
elseif monster.attackType == 'ranged' then
typeText = 'Ranged'
elseif monster.attackType == 'magic' then
typeText = 'Magic'
elseif monster.attackType == 'random' then
typeText = "Random"
end


  local iconText = ''
local buffAttacks = {}
  local typeText = ''
local hasActiveBuffSpec = false
  if  Constants.getCombatStyleName(monster.attackType) == 'Melee' then
local isNormalAttackRelevant = false
    iconText = Icons.Icon({'Melee', notext=true})
    typeText = 'Melee'
  elseif Constants.getCombatStyleName(monster.attackType) == 'Ranged' then
    iconText = Icons.Icon({'Ranged', type='skill', notext=true})
    typeText = 'Ranged'
  elseif Constants.getCombatStyleName(monster.attackType) == 'Magic' then
    iconText = Icons.Icon({'Magic', type='skill', notext=true})
    typeText = 'Magic'
  end


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


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


function p.getMonsterPassives(frame)
function p.getMonsterDrReduction(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[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end
return p._getMonsterDrReduction(monster)
end


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


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


function p.getMonsterCategories(frame)
function p.getMonsterCategories(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
local monster = p.getMonster(MonsterName)
 
if monster == nil then
return Shared.printError('No monster with that name found')
end
 
local result = '[[Category:Monsters]]'


  if monster == nil then
if monster.attackType == 'melee' then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
result = result..'[[Category:Melee Monsters]]'
  end
elseif monster.attackType == 'ranged' then
result = result..'[[Category:Ranged Monsters]]'
elseif monster.attackType == 'magic' then
result = result..'[[Category:Magic Monsters]]'
end


  local result = '[[Category:Monsters]]'
if type(monster.passives) == 'table' and not Shared.tableIsEmpty(monster.passives) then
result = result..'[[Category:Monsters with Special Attacks]]'
end


  if Constants.getCombatStyleName(monster.attackType) == 'Melee' then
if monster.isBoss then
    result = result..'[[Category:Melee Monsters]]'
result = result..'[[Category:Bosses]]'
  elseif Constants.getCombatStyleName(monster.attackType) == 'Ranged' then
end
    result = result..'[[Category:Ranged Monsters]]'
  elseif Constants.getCombatStyleName(monster.attackType) == 'Magic' then
    result = result..'[[Category:Magic Monsters]]'
  end


  if monster.hasSpecialAttack then
return result
    result = result..'[[Category:Monsters with Special Attacks]]'
end
  end


  if monster.isBoss then
function p.getMonsterBoxBarrierText(frame)
    result = result..'[[Category:Bosses]]'
local MonsterName = frame.args ~= nil and frame.args[1] or frame
  end
local monster = p.getMonster(MonsterName)


  return result
if monster == nil then
return Shared.printError('No monster with that name found')
end
local barrier = p._getMonsterBarrier(monster)
if barrier == 0 then
return ''
end
local result = {}
table.insert(result, '|-\r\n| style="font-weight: bold;" | [[Barrier]]:')
table.insert(result, '\r\n| colspan=15 style="text-align: right" |')
table.insert(result, Icons.Icon({"Barrier", notext="true"}))
table.insert(result, ' '..barrier)
return table.concat(result, '')
end
end


function p.getOtherMonsterBoxText(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
if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


  local result = ''
local result = ''


  --Going through and finding out which damage bonuses will apply to this monster
--Going through and finding out which damage bonuses will apply to this monster
  local monsterTypes = {}
local monsterTypes = {}
  if monster.isBoss then table.insert(monsterTypes, 'Boss') end
if monster.isBoss then table.insert(monsterTypes, 'Boss') end


  local areaList = Areas.getMonsterAreas(monster.id)
local areaList = Areas.getMonsterAreas(monster.id)
  local counts = {combat = 0, slayer = 0, dungeon = 0}
local counts = {combatArea = 0, slayerArea = 0, dungeon = 0}
  for i, area in Shared.skpairs(areaList) do
for i, area in ipairs(areaList) do
    counts[area.type] = counts[area.type] + 1
counts[area.type] = counts[area.type] + 1
  end
end


  if counts.combat > 0 then table.insert(monsterTypes, 'Combat Area') end
if counts.combatArea > 0 then table.insert(monsterTypes, 'Combat Area') end
  if counts.slayer > 0 then table.insert(monsterTypes, 'Slayer Area') end
if counts.slayerArea > 0 then table.insert(monsterTypes, 'Slayer Area') end
  if counts.dungeon > 0 then table.insert(monsterTypes, 'Dungeon') end
if counts.dungeon > 0 then table.insert(monsterTypes, 'Dungeon') end


  result = result.."\r\n|-\r\n|'''Monster Types:''' "..table.concat(monsterTypes, ", ")
result = result.."\r\n|-\r\n|'''Monster Types:''' "..table.concat(monsterTypes, ", ")


  local SlayerTier = 'N/A'
local SlayerTier = 'N/A'
  if not p._isDungeonOnlyMonster(monster) then
if monster.canSlayer then
    SlayerTier = Constants.getSlayerTierNameByLevel(p._getMonsterCombatLevel(monster))
SlayerTier = Constants.getSlayerTierNameByLevel(p._getMonsterCombatLevel(monster))
  end
end


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


  return result
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 monster == nil then
if row.maxQuantity > row.minQuantity then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
result = result .. Shared.formatnum(row.minQuantity) .. ' - '
  end
end
result = result .. Shared.formatnum(row.maxQuantity)


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


  if monster.bones ~= nil then
--Getting the drop chance
    local bones = Items.getItemByID(monster.bones)
local dropChance = (row.weight / totalWt * lootChance)
    --Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
if dropChance < 100 then
    if not p._isDungeonOnlyMonster(monster) or Shared.contains(bones.name, 'Shard') then
--Show fraction as long as it isn't going to be 1/1
      result = result.."'''Always Drops:'''"
result = result..'||style="text-align:right" data-sort-value="'..row.weight..'"'
      result = result..'\r\n{|class="wikitable"'
result = result..'|'..Shared.fraction(row.weight * lootChance, totalWt * 100)
      result = result..'\r\n!Item !! Qty'
result = result..'||'
      result = result..'\r\n|-\r\n|'..Icons.Icon({bones.name, type='item'})
else
      result = result..'||'..(monster.boneQty ~= nil and monster.boneQty or 1)..'\r\n'..'|}'
result = result..'||colspan="2" data-sort-value="'..row.weight..'"'
    end
end
  end
-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
local fmt = (dropChance < 0.10 and '%.2g') or '%.2f'
result = result..'style="text-align:right"|'..string.format(fmt, dropChance)..'%'
 
--Adding to the average loot value based on price & dropchance
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 2))
end
if multiDrop then
result = result..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
if lootChance < 100 then
result = result..'\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||'
else
result = result..'\r\n|colspan="2" '
end
result = result..'style="text-align:right"|'..Shared.round(lootChance, 2, 2)..'%'
end
result = result..'\r\n|}'
result = result..'\r\nThe loot dropped by the average kill is worth '..Icons.GP(Shared.round(lootValue, 2, 0)).." if sold."
if avgGp > 0 then
result = result.."<br/>Including GP"
if boneVal > 0 then
result = result..' and bones'
end
if dustVal > 0 then
result = result..' and barrier dust'
end
result = result..', the average kill is worth '..Icons.GP(Shared.round(avgGp + lootValue + boneVal + dustVal, 2, 0))..'.'
end
end
 
--If no other drops, make sure to at least say so.
if result == '' then result = 'None' end
return result
end
 
function p._getMonsterLootValue(monster)
if monster == nil then
return Shared.printError('No monster with that name found')
end


  --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[[Category:Pages with script errors]]'
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[[Category:Pages with script errors]]"
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..'[[Category:Pages with script errors]]'
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)
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..'[[Category:Pages with script errors]]'
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 = {}


  --For Dungeons, go through and count how many of each monster are in the dungeon first
-- Declare function for building table rows to avoid repeating code
  local monsterCounts = {}
local buildRow = function(entityID, monsterCount, specialType)
  for i, monsterID in pairs(area.monsters) do
local monIcon, monLevel, monHP, monMaxHit, monStyle, monCount, monDrReduce, monBarrier
    if monsterCounts[monsterID] == nil then
local monData = {}
      monsterCounts[monsterID] = 1
if specialType ~= nil and Shared.contains({'Afflicted', 'Spider', 'SlayerArea'}, specialType) then
    else
-- Special handling for Into the Mist
      monsterCounts[monsterID] = monsterCounts[monsterID] + 1
if specialType == 'Afflicted' then
    end
local iconQ = Icons.Icon({'Into the Mist', notext=true, nolink=true, img='Question'})
  end
monIcon = Icons.Icon({'Into the Mist', 'Afflicted Monster', nolink=true, img='Question'})
monLevel, monBarrier, monHP, monMaxHit, monDrReduce, monStyle, monCount = iconQ, iconQ, iconQ, iconQ, iconQ, iconQ, monsterCount
elseif specialType == 'Spider' then
local iconQ = Icons.Icon({'', notext=true, nolink=true, img='Question'})
local monIconPart = { 'Any of the following:' }
for i, monsterID in ipairs(GameData.rawData.spiderLairMonsters) do
local monster = p.getMonsterByID(monsterID)
if monster ~= nil then
table.insert(monIconPart, Icons.Icon({p.getMonsterName(monster), type='monster'}))
end
end
monIcon = table.concat(monIconPart, '<br/>')
monLevel, monBarrier, monHP, monMaxHit, monDrReduce, monStyle, monCount = iconQ, iconQ, iconQ, iconQ, iconQ, iconQ, monsterCount
elseif specialType == 'SlayerArea' then
-- entityID corresponds to a slayer area
local area = Areas.getAreaByID('slayer', entityID)
monIcon = Icons.Icon({area.name, type='combatArea'}) .. ' Monsters'
monLevel = {p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterCombatLevel(monster) end)}
if hasBarrier then
monBarrier = {p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterBarrier(monster) end)}
end
monHP = {p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterHP(monster) end)}
local lowMaxHit, highMaxHit = p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterMaxHit(monster) end)
local lowDrReduce, highDrReduce = p.getLowHighStat(area.monsterIDs, function(monster) return p._getMonsterDrReduction(monster) end)
monMaxHit = highMaxHit
monDrReduce = highDrReduce
monStyle = Icons.Icon({area.name, area.name, notext=true, nolink=true, img='Question'})
monCount = monsterCount
end
else
-- entityID corresponds to a monster
local monster = p.getMonsterByID(entityID)
monIcon = Icons.Icon({p.getMonsterName(monster), type='monster'})
monLevel = p._getMonsterCombatLevel(monster)
if hasBarrier then
monBarrier = p._getMonsterBarrier(monster)
end
monHP = p._getMonsterHP(monster)
monDrReduce = p._getMonsterDrReduction(monster)
monMaxHit = p._getMonsterMaxHit(monster)
monStyle = p._getMonsterStyleIcon({monster})
monCount = monsterCount
end
local getValSort = function(val)
if type(val) == 'table' then
if type(val[1]) == 'number' and type(val[2]) == 'number' then
return (val[1] + val[2]) / 2
else
return (type(val[1]) == 'number' and val[1]) or 0
end
else
return (type(val) == 'number' and val) or 0
end
end
local getValText = function(val)
if type(val) == 'table' and Shared.tableCount(val) == 2 then
if type(val[1]) == 'number' and type(val[2]) == 'number' then
return Shared.formatnum(val[1]) .. ' - ' .. Shared.formatnum(val[2])
else
return val[1] .. ' - ' .. val[2]
end
elseif type(val) == 'number' then
return Shared.formatnum(val)
else
return val
end
end
local resultPart = {}
table.insert(resultPart, '\r\n|-\r\n| ' .. monIcon)
table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monLevel) .. '"| ' .. getValText(monLevel))
if hasBarrier then
table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monBarrier) .. '"| ' .. getValText(monBarrier))
end
table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monHP) .. '"| ' .. getValText(monHP))
if type(monDrReduce) == 'number' and monDrReduce > 0 then
table.insert(resultPart, '\r\n|style="text-align:right" data-sort-value="'..getValSort(monMaxHit)..'"| -'..monDrReduce..'% DR')
table.insert(resultPart, '\r\n|style="text-align:right"|'..getValText(monMaxHit))
else
table.insert(resultPart, '\r\n|style="text-align:right" colspan="2" data-sort-value="'..getValSort(monMaxHit)..'"|'..getValText(monMaxHit))
end
table.insert(resultPart, '\r\n| ' .. monStyle)
table.insert(resultPart, '\r\n|style="text-align:right;" data-sort-value="' .. getValSort(monCount) .. '"| ' .. getValText(monCount))
return table.concat(resultPart)
end
 
local returnPart = {}
table.insert(returnPart, '{| class="wikitable sortable"')
table.insert(returnPart, '\r\n! Name !! Combat Level ')
if hasBarrier then
table.insert(returnPart, '!! [[Barrier]] ')
end
table.insert(returnPart, '!! Hitpoints !! colspan="2" | Max Hit !! [[Combat Triangle|Combat Style]] !! Count')
-- Special handing for Impending Darkness event
-- TODO needs to be revised once there is a better understanding of how the event works
for i, monsterID in ipairs(area.monsterIDs) do
if not Shared.contains(usedMonsters, monsterID) then
if monsterID == 'melvorF:RandomITM' then
--Special handling for Into the Mist
table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], 'Afflicted'))
elseif monsterID == 'melvorTotH:RandomSpiderLair' then
table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], 'Spider'))
else
table.insert(returnPart, buildRow(monsterID, monsterCounts[monsterID], hasBarrier))
end
table.insert(usedMonsters, monsterID)
end
end
table.insert(returnPart, '\r\n|}')
return table.concat(returnPart)
end


  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(monster)
        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', qty=count}))
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
    else
      count = count + 1
    end
    --Make sure the final monster in the dungeon gets counted
    if i == Shared.tableCount(area.monsters) then
      local name = lastMonster.name
      table.insert(monsterList, Icons.Icon({lastMonster.name, type='monster', qty=count}))
    end
  end
  return table.concat(monsterList, '<br/>')
end


function p.getAreaMonsterList(frame)
for i, monster in ipairs(monsterCounts) do
  local areaName = frame.args ~= nil and frame.args[1] or frame
if monster.id == 'melvorF:RandomITM' then
  local area = Areas.getArea(areaName)
--Special handling for Afflicted Monsters
  if area == nil then
table.insert(monsterList, Icons.Icon({'Affliction', 'Afflicted Monster', img='Question', qty=monster.count}))
    return "ERROR: Could not find an area named "..areaName..'[[Category:Pages with script errors]]'
elseif monster.id == 'melvorTotH:RandomSpiderLair' then
  end
local monIconPart = { Shared.formatnum(monster.count) .. ' Spiders:' }
for i, monsterID in ipairs(GameData.rawData.spiderLairMonsters) do
local monster = p.getMonsterByID(monsterID)
if monster ~= nil then
table.insert(monIconPart, '&nbsp;&nbsp;&nbsp;' .. Icons.Icon({p.getMonsterName(monster), type='monster'}))
end
end
table.insert(monsterList, table.concat(monIconPart, '<br/>'))
else
local monsterObj = p.getMonsterByID(monster.id)
table.insert(monsterList, Icons.Icon({p.getMonsterName(monsterObj), type='monster', qty=monster.count}))
end
end


  if area.type == 'dungeon' then
return table.concat(monsterList, '<br/>')
    return p._getDungeonMonsterList(area)
  else
    return p._getAreaMonsterList(area)
  end
end
end


function p._getDungeonRewards(area)
function p.getAreaMonsterList(frame)
  local bossMonster = p.getMonsterByID(area.monsters[Shared.tableCount(area.monsters)])
local areaName = frame.args ~= nil and frame.args[1] or frame
  local gpMin = bossMonster.dropCoins[1]
local area = Areas.getArea(areaName)
  local gpMax = bossMonster.dropCoins[2] - 1
if area == nil then
  local chestID = bossMonster.lootTable[1][1]
return Shared.printError('Could not find an area named ' .. areaName)
  local chestQty = bossMonster.lootTable[1][3]
end
  local theChest = Items.getItemByID(chestID)
  local result = ''
  if gpMin > 0 and gpMax > 0 then
    result = result..'* '..Icons.GP(gpMin, gpMax)..'\r\n'
  end
  result = result..'* '..Icons.Icon({theChest.name, type='item', qty=chestQty})
  if area.name == 'Volcanic Cave' then
    result = result..'\r\n* '..Icons.Icon({'Fire Cape', type='item', qty=1})
  elseif area.name == 'Infernal Stronghold' then
    result = result..'\r\n* '..Icons.Icon({'Infernal Cape', type='item', qty=1})
  end
  return result
end


function p.getDungeonRewards(frame)
if area.type == 'dungeon' then
  local areaName = frame.args ~= nil and frame.args[1] or frame
return p._getDungeonMonsterList(area)
  local area = Areas.getArea(areaName)
else
  if area == nil then
return p._getAreaMonsterList(area)
    return "ERROR: Could not find an area named "..areaName..'[[Category:Pages with script errors]]'
end
  end
 
  if area.type == 'dungeon' then
    return p._getDungeonRewards(area)
  else
    return "ERROR: "..areaName.." is not a dungeon[[Category:Pages with script errors]]"
  end
end
end


function p.getFoxyTable(frame)
function p.getFoxyTable(frame)
  local result = 'Monster,Min GP,Max GP,Average GP'
local result = 'Monster,Min GP,Max GP,Average GP'
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
for i, monster in ipairs(GameData.rawData.monsters) do
    if not p._isDungeonOnlyMonster(monster) then
if not p._isDungeonOnlyMonster(monster) then
      if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
if monster.gpDrops ~= nil and monster.gpDrops.max > 0 then
      local avgGp = (monster.dropCoins[1] + monster.dropCoins[2] - 1) / 2
local avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
      result = result..'<br/>'..monster.name..','..monster.dropCoins[1]..','..(monster.dropCoins[2]-1)..','..avgGp
result = result .. '<br/>' .. p.getMonsterName(monster) .. ',' .. monster.gpDrops.min .. ',' .. monster.gpDrops.max .. ',' .. avgGp
      end
end
    end
end
  end
end
  return result
return result
end
end


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


  if monster.bones ~= nil then
local bones = p._getMonsterBones(monster)
    local bones = Items.getItemByID(monster.bones)
if bones ~= nil then
    --Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
totalGP = totalGP + bones.item.sellsFor * bones.quantity
    if not p._isDungeonOnlyMonster(monster) or Shared.contains(bones.name, 'Shard') then
end
      totalGP = totalGP + bones.sellsFor
    end
  end


  --Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
--Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
  if not p._isDungeonOnlyMonster(monster) then
if not p._isDungeonOnlyMonster(monster) then
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    local lootValue = 0
local lootValue = 0


    local avgGp = 0
local avgGp = 0


    if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
if monster.gpDrops ~= nil then
      avgGp = (monster.dropCoins[1] + monster.dropCoins[2] - 1) / 2
avgGp = (monster.gpDrops.min + monster.gpDrops.max) / 2
    end
end


    totalGP = totalGP + avgGp
totalGP = totalGP + avgGp


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


    for i, row in Shared.skpairs(monster.lootTable) do
for i, row in ipairs(monster.lootTable) do
      local thisItem = Items.getItemByID(row[1])
local thisItem = Items.getItemByID(row.itemID)
      local maxQty = row[3]


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


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


      --Adding to the average loot value based on price & dropchance
--Adding to the average loot value based on price & dropchance
      lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((row.minQuantity + row.maxQuantity) / 2))
    end
end


    totalGP = totalGP + lootValue
totalGP = totalGP + lootValue
  end
end


  return Shared.round(totalGP, 2, 2)
return Shared.round(totalGP, 2, 2)
end
end


function p.getMonsterAverageGP(frame)
function p.getMonsterAverageGP(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[[Category:Pages with script errors]]"
return Shared.printError('No monster with that name found')
  end
end


  return p._getMonsterAverageGP(monster)
return p._getMonsterAverageGP(monster)
end
end


function p.getMonsterEVTable(frame)
function p.getMonsterEVTable(frame)
  local result = '{| class="wikitable sortable"'
local result = '{| class="wikitable sortable"'
  result = result..'\r\n!Monster!!Combat Level!!Average GP'
result = result..'\r\n!Monster!!Combat Level!!Average GP'
  for i, monsterTemp in Shared.skpairs(MonsterData.Monsters) do
for i, monster in ipairs(GameData.rawData.monsters) do
    local monster = Shared.clone(monsterTemp)
if not p._isDungeonOnlyMonster(monster) then
    monster.id = i - 1
local monsterGP = p._getMonsterAverageGP(monster)
    if not p._isDungeonOnlyMonster(monster) then
local combatLevel = p._getMonsterCombatLevel(monster)
      local monsterGP = p._getMonsterAverageGP(monster)
result = result..'\r\n|-\r\n|'..Icons.Icon({p.getMonsterName(monster), type='monster', noicon=true})..'||'..combatLevel..'||'..monsterGP
      local combatLevel = p._getMonsterCombatLevel(monster, 'Combat Level')
end
      result = result..'\r\n|-\r\n|[['..monster.name..']]||'..combatLevel..'||'..monsterGP
end
    end
result = result..'\r\n|}'
  end
return result
  result = result..'\r\n|}'
  return result
end
end


function p.getSlayerTierMonsterTable(frame)
function p.getSlayerTierMonsterTable(frame)
  -- Input validation
-- Input validation
  local tier = frame.args ~= nil and frame.args[1] or frame
local tier = frame.args ~= nil and frame.args[1] or frame
  local slayerTier = nil
local slayerTier = nil


  if tier == nil then
if tier == nil then
    return "ERROR: No tier specified[[Category:Pages with script errors]]"
return Shared.printError('No tier specified')
  end
end


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


  if slayerTier == nil then
if slayerTier == nil then
    return "ERROR: Invalid slayer tier[[Category:Pages with script errors]]"
return Shared.printError('Invalid slayer tier')
  end
end


  -- Obtain required tier details
-- Obtain required tier details
  local minLevel, maxLevel = slayerTier.minLevel, slayerTier.maxLevel
local minLevel, maxLevel = slayerTier.minLevel, slayerTier.maxLevel
  if maxLevel < 0 then
    maxLevel = nil
  end


  -- Build list of monster IDs
-- Build list of monster IDs
  -- Right now hiddenMonsterIDs is empty
-- Right now hiddenMonsterIDs is empty
  local hiddenMonsterIDs = {}
local hiddenMonsterIDs = {}
  local monsterIDs = {}
local monsterList = GameData.getEntities('monsters',
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
        function(monster)
    if monster.canSlayer and not Shared.contains(hiddenMonsterIDs, i - 1) then
            if monster.canSlayer and not Shared.contains(hiddenMonsterIDs, monster.id) then
      local cmbLevel = p._getMonsterCombatLevel(monster)
                local cmbLevel = p._getMonsterCombatLevel(monster)
      if cmbLevel >= minLevel and (maxLevel == nil or cmbLevel <= maxLevel) then
                return cmbLevel >= minLevel and (maxLevel == nil or cmbLevel <= maxLevel)
        table.insert(monsterIDs, i - 1)
            end
      end
            return false
    end
        end)
  end


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


function p.getFullMonsterTable(frame)
function p.getFullMonsterTable(frame)
  local monsterIDs = {}
return p._getMonsterTable(GameData.rawData.monsters, false)
  for i = 0, Shared.tableCount(MonsterData.Monsters) - 1, 1 do
end
    table.insert(monsterIDs, i)
 
  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


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


function p._getMonsterTable(monsterIDs, excludeDungeons)
function p.getMattMonsterTableV2(frame)
  --Making a single function for getting a table of monsters given a list of IDs.
--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 = {}
  local tableParts = {}
table.insert(tableParts, '{| class="wikitable sortable stickyHeader"')
  table.insert(tableParts, '{| class="wikitable sortable stickyHeader"')
-- Second header row
  -- First header row
table.insert(tableParts, '\r\n|- class="headerRow-1"\r\n!Monster !!Name !!Combat Level ')
  table.insert(tableParts, '\r\n|- class="headerRow-0"\r\n! colspan="5" | !! colspan="4" |Offensive Stats !! colspan="3" |Evasion Rating !! colspan="4" |')
table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Hitpoints', type='skill'}))
  -- Second header row
table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Defence', type='skill', notext=true}))
  table.insert(tableParts, '\r\n|- class="headerRow-1"\r\n!Monster !!Name !!ID !!Combat Level ')
table.insert(tableParts, '!!Attack Speed (s) !!colspan="2"|Max Hit !!Accuracy ')
  table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Hitpoints', type='skill'}))
  table.insert(tableParts, '!!Attack Type !!Attack Speed (s) !!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, '!!Drop Chance !!Coins !!Bones !!Locations')


  -- Generate row per monster
-- table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Ranged', type='skill', notext=true}))
  for i, monsterID in Shared.skpairs(monsterIDs) do
-- table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Magic', type='skill', notext=true}))
    local monster = p.getMonsterByID(monsterID)
table.insert(tableParts, '!!' .. Icons.Icon({'Coins', notext=true, nolink=true}) .. ' Coins !!Avg. Kill Value !!Bones')
    local cmbLevel = p._getMonsterCombatLevel(monster)
 
    local atkSpeed = p._getMonsterAttackSpeed(monster)
-- Generate row per monster
    local maxHit = p._getMonsterMaxHit(monster)
for i, monster in ipairs(GameData.rawData.monsters) do
    local accR = p._getMonsterAR(monster)
if p.getMonsterName(monster) ~= nil then
    local evaR = {p._getMonsterER({monster, "Melee"}), p._getMonsterER({monster, "Ranged"}), p._getMonsterER({monster, "Magic"})}
local cmbLevel = p._getMonsterCombatLevel(monster)
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    local gpRange = {0, 0}
local gpTxt = nil
    if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
if monster.gpDrops.min >= monster.gpDrops.max then
      gpRange = {monster.dropCoins[1], monster.dropCoins[2] - 1}
gpTxt = Shared.formatnum(monster.gpDrops.min)
    end
else
    local gpTxt = nil
gpTxt = Shared.formatnum(monster.gpDrops.min) .. ' - ' .. Shared.formatnum(monster.gpDrops.max)
    if gpRange[1] >= gpRange[2] then
end
      gpTxt = Icons.GP(gpRange[1])
    else
local lootVal = p._getMonsterLootValue(monster)
      gpTxt = Icons.GP(gpRange[1], gpRange[2])
local lootTxt = '0'
    end
if lootVal ~= 0 then
    local boneTxt = 'None'
lootTxt = Shared.formatnum(Shared.round(lootVal, 2, 2))
    if monster.bones ~= nil then
end
      local bones = Items.getItemByID(monster.bones)
      boneTxt = Icons.Icon({bones.name, type='item', notext=true})
local atkSpeed = p._getMonsterAttackSpeed(monster)
    end
local maxHit = p._getMonsterMaxHit(monster)
local accR = p._getMonsterAR(monster)
local evaR = {p._getMonsterER(monster, "Melee"), p._getMonsterER(monster, "Ranged"), p._getMonsterER(monster, "Magic")}
local bones = p._getMonsterBones(monster)
local boneTxt = (bones ~= nil and Icons.Icon({bones.item.name, type='item', notext=true})) or 'None'
table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', size=50, notext=true}))
table.insert(tableParts, '\r\n|style="text-align:left" |' .. Icons.Icon({p.getMonsterName(monster), type='monster', noicon=true}))
-- table.insert(tableParts, '\r\n|style="text-align:right" |' .. monster.id)
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. cmbLevel .. '" |' .. Shared.formatnum(cmbLevel))
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. p._getMonsterHP(monster) .. '" |' .. Shared.formatnum(p._getMonsterHP(monster)))
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[1] .. '" |' .. Shared.formatnum(evaR[1]))
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. atkSpeed .. '" |' .. Shared.round(atkSpeed, 1, 1))
table.insert(tableParts, '\r\n|style="text-align:center;border-right:hidden" |' .. p._getMonsterStyleIcon({monster, notext=true}))
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. maxHit .. '" |' .. Shared.formatnum(maxHit))
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. accR .. '" |' .. Shared.formatnum(accR))
--table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[2] .. '" |' .. Shared.formatnum(evaR[2]))
--table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[3] .. '" |' .. Shared.formatnum(evaR[3]))
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. (monster.gpDrops.min + monster.gpDrops.max) / 2 .. '" |' .. gpTxt)
table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. lootVal .. '" |' .. lootTxt)
table.insert(tableParts, '\r\n|style="text-align:center" |' .. boneTxt)
-- table.insert(tableParts, '\r\n|style="text-align:right;width:190px" |' .. p._getMonsterAreas(monster, hideDungeons))
end
end
 
table.insert(tableParts, '\r\n|}')
return table.concat(tableParts)
end
 
function p.getSpecialAttackTable(frame)
local spAttTable = {}
 
for i, monster in ipairs(GameData.rawData.monsters) do
if monster.specialAttacks ~= nil and not Shared.tableIsEmpty(monster.specialAttacks) then
local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
for j, spAttID in ipairs(monster.specialAttacks) do
                local spAtt = GameData.getEntityByID('attacks', spAttID)
local attChance = (overrideChance and monster.overrideSpecialChances[j] or spAtt.defaultChance)
if spAttTable[spAtt.id] == nil then
spAttTable[spAtt.id] = { ['defn'] = spAtt, ['icons'] = {} }
end
if spAttTable[spAtt.id]['icons'][attChance] == nil then
spAttTable[spAtt.id]['icons'][attChance] = {}
end
table.insert(spAttTable[spAtt.id]['icons'][attChance], Icons.Icon({ p.getMonsterName(monster), type = 'monster' }))
end
end
end
 
local resultPart = {}
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\r\n|- class="headerRow-0"')
table.insert(resultPart, '\r\n!Name!!style="min-width:225px"|Monsters!!Chance!!Effect')
 
for i, spAttData in Shared.skpairs(spAttTable) do
local spAtt = spAttData.defn
local firstRow = true
local rowsSpanned = Shared.tableCount(spAttData.icons)
local rowSuffix = ''
if rowsSpanned > 1 then
rowSuffix = '|rowspan="' .. rowsSpanned .. '"'
end
for chance, iconList in Shared.skpairs(spAttData.icons) do
table.insert(resultPart, '\r\n|-')
if firstRow then
table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.name)
end
table.insert(resultPart, '\r\n|data-sort-value="' .. spAtt.name .. '"| ' .. table.concat(iconList, '<br/>'))
table.insert(resultPart, '\r\n|data-sort-value="' .. chance .. '"| ' .. Shared.round(chance, 2, 0) .. '%')
if firstRow then
table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.description)
firstRow = false
end
end
end
table.insert(resultPart, '\r\n|}')
 
return table.concat(resultPart)
end
 
--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot TablesTemplate:MonsterLootTables
--It exists because I'm too lazy to manually type up all the new monsters - User:Falterfire
function p.getMonsterLootTableText()
local getAreaText = function(area)
local outArray = {}
table.insert(outArray, "==={{ZoneIcon|"..area.name.."|size=50}}===")
table.insert(outArray, "")
for i, monsterID in ipairs(area.monsterIDs) do
local monster = p.getMonsterByID(monsterID)
table.insert(outArray, "===={{MonsterIcon|"..p.getMonsterName(monster).."|size=40}}====")
table.insert(outArray, "{{MonsterDrops|"..p.getMonsterName(monster).."|size=40}}")
end
return table.concat(outArray, "\r\n")
end
local fullArray = {}
local areaArray = Areas.getAreas(function(a) return a.type == 'combatArea' end)
for i, area in ipairs(areaArray) do
table.insert(fullArray, getAreaText(area))
end
areaArray = Areas.getAreas(function(a) return a.type == 'slayerArea' end)
for i, area in ipairs(areaArray) do
table.insert(fullArray, getAreaText(area))
end
return table.concat(fullArray, "\r\n\r\n----\r\n")
end
 
--NOTE: This is not a function that should be called directly. It generates text to be pasted into Chest Loot Tables
--It exists because I'm too lazy to manually type up all the new chests - User:Falterfire
function p.getChestLootTables()
local items = Items.getItems(function(item) return item.dropTable ~= nil end)
local outArray = {}
for i, item in ipairs(items) do
table.insert(outArray, "==={{ItemIcon|"..item.name.."|size=30}}===")
table.insert(outArray, "{{ChestDrops|"..item.name.."}}")
end
return table.concat(outArray, "\r\n")
end


    table.insert(tableParts, '\r\n|-\r\n|style="text-align: left;" |' .. Icons.Icon({monster.name, type='monster', size=50, notext=true}))
--Returns the expansion icon for the item if it has one
    table.insert(tableParts, '\r\n|style="text-align:left" |[[' .. monster.name .. ']]')
function p.getExpansionIcon(frame)
    table.insert(tableParts, '\r\n|style="text-align:right" |' .. monsterID)
local monsterName = frame.args ~= nil and frame.args[1] or frame
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. cmbLevel .. '" |' .. Shared.formatnum(cmbLevel))
local monster = p.getMonster(monsterName)
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. monster.hitpoints .. '" |' .. Shared.formatnum(p._getMonsterHP(monster)))
    table.insert(tableParts, '\r\n|style="text-align:right;white-space:nowrap" |' .. p._getMonsterStyleIcon({monster, nolink='true'}))
    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:right" data-sort-value="' .. maxHit .. '" |' .. Shared.formatnum(maxHit))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. accR .. '" |' .. Shared.formatnum(accR))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[1] .. '" |' .. Shared.formatnum(evaR[1]))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[2] .. '" |' .. Shared.formatnum(evaR[2]))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. evaR[3] .. '" |' .. Shared.formatnum(evaR[3]))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. lootChance .. '" |' .. lootChance .. '%')
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. (gpRange[1] + gpRange[2]) / 2 .. '" |' .. gpTxt)
    table.insert(tableParts, '\r\n|style="text-align:center" |' .. boneTxt)
    table.insert(tableParts, '\r\n|style="text-align:right;white-space:nowrap" |' .. p._getMonsterAreas(monster, hideDungeons))
  end


  table.insert(tableParts, '\r\n|}')
if monster == nil then
  return table.concat(tableParts)
return Shared.printError('No monster with that name found')
end
return Icons.getExpansionIcon(monster.id)
end
end


return p
return p
3

edits