Module:Sandbox/FalterTest: Difference between revisions

establishing baseline. Copying Monsters and gonna edit from there to get v0.21 compatability
No edit summary
(establishing baseline. Copying Monsters and gonna edit from there to get v0.21 compatability)
Line 1: Line 1:
local p = {}
local p = {}


local Constants = mw.loadData('Module:Constants/data')
local MonsterData = mw.loadData('Module:FalterTest/data')
local MonsterData = mw.loadData('Module:Monsters/data')
 
local Constants = require('Module:Constants')
local Areas = require('Module:CombatAreas')
local Magic = require('Module:Magic')
local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
 
function p.getMonster(name)
  local result = nil
  if name == 'Spider (lv. 51)' or name == 'Spider' then
    return p.getMonsterByID(50)
  elseif name == 'Spider (lv. 52)' or name == 'Spider2' then
    return p.getMonsterByID(51)
  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
  return result
end
 
function p.getMonsterByID(ID)
  local result = Shared.clone(MonsterData.Monsters[ID + 1])
  result.id = ID
  return result
end
 
function p.getSpecialAttack(name)
  local result = nil
 
  for i, attack in pairs(MonsterData.SpecialAttacks) do
    if(attack.name == name) then
      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
 
function p.getSpecialAttackByID(ID)
  return MonsterData.SpecialAttacks[ID + 1]
end
 
function p.getPassive(name)
  local result = nil
 
  for i, passive in pairs(MonsterData.Passives) do
    if passive.name == name then
      result = Shared.clone(passive)
      --Make sure every passive has an ID, and account for the 1-based indexing of Lua
      result.id = i - 1
      break
    end
  end
  return result
end
 
function p.getPassiveByID(ID)
  return MonsterData.Passives[ID + 1]
end


function p.getMonsterStat(frame)
function p.getMonsterStat(frame)
   local MonsterName = frame.args[1]
   local MonsterName = frame.args ~= nil and frame.args[1] or frame[1]
   local StatName = frame.args[2]
   local StatName = frame.args ~= nil and frame.args[2] or frame[2]
   local result = 'No monster found with that name'
  local monster = p.getMonster(MonsterName)
   for key, value in pairs(MonsterData) do
  if monster == nil then
     if(value.name == MonsterName) then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
       --mw.logObject(value)
  end
       mw.log(StatName)
 
       result = value[StatName]
  if StatName == 'HP' then
    return p._getMonsterHP(monster)
  elseif StatName == 'maxHit' then
    return p._getMonsterMaxHit(monster)
  elseif StatName == 'accuracyRating' then
    return p._getMonsterAR(monster)
  elseif StatName == 'meleeEvasionRating' then
    return p._getMonsterER({monster, 'Melee'})
  elseif StatName == 'rangedEvasionRating' then
    return p._getMonsterER({monster, 'Ranged'})
  elseif StatName == 'magicEvasionRating' then
    return p._getMonsterER({monster, 'Magic'})
  end
 
  return monster[StatName]
end
 
function p._getMonsterStyleIcon(frame)
  local args = frame.args ~= nil and frame.args or frame
  local monster = args[1]
  local notext = args.notext
  local nolink = args.nolink
 
  local iconText = ''
  if  Constants.getCombatStyleName(monster.attackType) == 'Melee' then
    iconText = Icons.Icon({'Melee', notext=notext, nolink=nolink})
  elseif Constants.getCombatStyleName(monster.attackType) == 'Ranged' then
    iconText = Icons.Icon({'Ranged', type='skill', notext=notext, nolink=nolink})
  elseif Constants.getCombatStyleName(monster.attackType) == 'Magic' then
    iconText = Icons.Icon({'Magic', type='skill', notext=notext, nolink=nolink})
  end
 
  return iconText
end
 
function p.getMonsterStyleIcon(frame)
  local args = frame.args ~= nil and frame.args or frame
  local MonsterName = args[1]
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  args[1] = monster
  return p._getMonsterStyleIcon(args)
end
 
function p._getMonsterHP(monster)
  return monster.hitpoints * 10
end
 
function p.getMonsterHP(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
  if monster ~= nil then
    return p._getMonsterHP(monster)
  else
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
end
 
function p._getMonsterAttackSpeed(monster)
  return monster.attackSpeed / 1000
end
 
function p.getMonsterAttackSpeed(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
  if monster ~= nil then
    return p._getMonsterAttackSpeed(monster)
  else
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
end
 
function p._getMonsterCombatLevel(monster)
  local base = 0.25 * (monster.defenceLevel + monster.hitpoints)
  local melee = 0.325 * (monster.attackLevel + monster.strengthLevel)
  local range = 0.325 * (1.5 * monster.rangedLevel)
  local magic = 0.325 * (1.5 * monster.magicLevel)
  if melee > range and melee > magic then
    return math.floor(base + melee)
  elseif range > magic then
    return math.floor(base + range)
  else
    return math.floor(base + magic)
  end
end
 
function p.getMonsterCombatLevel(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterCombatLevel(monster)
end
 
function p._getMonsterAR(monster)
  local effAttLvl = 0
   local attBonus = 0
  if Constants.getCombatStyleName(monster.attackType) == 'Melee' then
    effAttLvl = monster.attackLevel + 9
    attBonus = monster.attackBonus + 64
  elseif Constants.getCombatStyleName(monster.attackType) == 'Ranged' then
    effAttLvl = monster.rangedLevel + 9
    attBonus = monster.attackBonusRanged + 64
  elseif Constants.getCombatStyleName(monster.attackType) == 'Magic' then
    effAttLvl = monster.magicLevel + 9
    attBonus = monster.attackBonusMagic + 64
  else
    return "ERROR: This monster has an invalid attack type somehow[[Category:Pages with script errors]]"
  end
 
  return effAttLvl * attBonus
end
 
function p.getMonsterAR(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterAR(monster)
end
 
function p._getMonsterER(frame)
  local args = frame.args ~= nil and frame.args or frame
  local monster = args[1]
  local style = args[2]
 
  local effDefLvl = 0
  local defBonus = 0
  if style == "Melee" then
    effDefLvl = monster.defenceLevel + 9
    defBonus = monster.defenceBonus + 64
  elseif style == "Ranged" then
    effDefLvl = monster.defenceLevel + 9
    defBonus = monster.defenceBonusRanged + 64
  elseif style == "Magic" then
    effDefLvl = math.floor(monster.magicLevel * 0.7) + math.floor(monster.defenceLevel * 0.3) + 9
    defBonus = monster.defenceBonusMagic + 64
  else
    return "ERROR: Must choose Melee, Ranged, or Magic[[Category:Pages with script errors]]"
  end
  return effDefLvl * defBonus
end
 
function p.getMonsterER(frame)
  local args = frame.args ~= nil and frame.args or frame
  local MonsterName = args[1]
  local style = args[2]
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterER({monster, style})
end
 
function p._isDungeonOnlyMonster(monster)
  local areaList = Areas.getMonsterAreas(monster.id)
  local dunCount = 0
  local nonDunCount = 0
 
  for i, area in Shared.skpairs(areaList) do
    if area.type == 'dungeon' then
      dunCount = dunCount + 1
    else
      nonDunCount = nonDunCount + 1
    end
  end
  return dunCount > 0 and nonDunCount == 0
end
 
function p.isDungeonOnlyMonster(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with name "..monsterName.." found[[Category:Pages with script errors]]"
  end
 
  return p._isDungeonOnlyMonster(monster)
end
 
function p._getMonsterAreas(monster, excludeDungeons)
  local result = ''
  local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
  local areaList = Areas.getMonsterAreas(monster.id)
   for i, area in pairs(areaList) do
     if area.type ~= 'dungeon' or not hideDungeons then
      if i > 1 then result = result..'<br/>' end
      result = result..Icons.Icon({area.name, type = area.type})
    end
  end
  return result
end
 
function p.getMonsterAreas(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local hideDungeons = frame.args ~= nil and frame.args[2] or nil
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with name "..monsterName.." found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterAreas(monster, hideDungeons)
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
 
  local normalChance = 100
  local specialMaxHit = 0
  local normalMaxHit = p._getMonsterBaseMaxHit(monster)
  local hasActiveBuffSpec = false
  local damageMultiplier = 1
  if monster.hasSpecialAttack then
    local canStun = false
    local canSleep = false
    for i, specID in pairs(monster.specialAttackID) do
       local specAttack = p.getSpecialAttackByID(specID)
      if monster.overrideSpecialChances ~= nil then
        normalChance = normalChance - monster.overrideSpecialChances[i]
      else
        normalChance = normalChance - specAttack.chance
      end
      local thisMax = 0
      if specAttack.setDamage ~= nil then
        thisMax = specAttack.setDamage * 10
      else
        thisMax = normalMaxHit
      end
      if specAttack.canStun ~= nil and specAttack.canStun then canStun = true end
      if specAttack.canSleep ~= nil and specAttack.canSleep then canSleep = true end
 
      if thisMax > specialMaxHit then specialMaxHit = thisMax end
      if specAttack.activeBuffs and specAttack.activeBuffTurns ~= nil and specAttack.activeBuffTurns > 0 then
        hasActiveBuffSpec = true
      end
    end
 
    if canStun and doStuns then damageMultiplier = damageMultiplier * 1.3 end
    if canSleep and doStuns then damageMultiplier = damageMultiplier * 1.2 end
  end
  --Ensure that if the monster never does a normal attack, the normal max hit is irrelevant
  if normalChance == 0 and not hasActiveBuffSpec then normalMaxHit = 0 end
  return math.floor(math.max(specialMaxHit, normalMaxHit) * damageMultiplier)
end
 
function p.getMonsterMaxHit(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local doStuns = frame.args ~= nil and frame.args[2] or true
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterMaxHit(monster, doStuns)
end
 
function p._getMonsterBaseMaxHit(monster)
  local effStrLvl = 0
  local strBonus = 0
  if Constants.getCombatStyleName(monster.attackType) == 'Melee' then
    effStrLvl = monster.strengthLevel + 9
    strBonus = monster.strengthBonus
  elseif Constants.getCombatStyleName(monster.attackType) == 'Ranged' then
    effStrLvl = monster.rangedLevel + 9
    strBonus = monster.strengthBonusRanged
  elseif Constants.getCombatStyleName(monster.attackType) == 'Magic' then
    local mSpell = nil
    if monster.selectedSpell ~= nil then mSpell = Magic.getSpellByID('Spells', monster.selectedSpell) end
    if mSpell == nil then
      return math.floor(10 * (monster.setMaxHit + (monster.setMaxHit * monster.damageBonusMagic / 100)))
    else
       return math.floor(10 * (mSpell.maxHit + (mSpell.maxHit * monster.damageBonusMagic / 100)))
    end
  else
    return "ERROR: This monster has an invalid attack type somehow[[Category:Pages with script errors]]"
  end
 
  --Should only get here for Melee/Ranged, which use functionally the same damage formula
  return math.floor(10 * (1.3 + (effStrLvl/10) + (strBonus / 80) + ((effStrLvl * strBonus) / 640)))
end
 
function p.getMonsterBaseMaxHit(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterBaseMaxHit(monster)
end
 
function p.getMonsterAttacks(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = ''
 
  local iconText = ''
  local typeText = ''
  if  Constants.getCombatStyleName(monster.attackType) == 'Melee' then
    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 buffAttacks = {}
  local hasActiveBuffSpec = false
 
  local normalAttackChance = 100
  if monster.hasSpecialAttack then
    for i, specID in pairs(monster.specialAttackID) do
      local specAttack = p.getSpecialAttackByID(specID)
      local attChance = 0
      if monster.overrideSpecialChances ~= nil then
        attChance = monster.overrideSpecialChances[i]
      else
        attChance = specAttack.chance
      end
      normalAttackChance = normalAttackChance - attChance
 
      result = result..'\r\n* '..attChance..'% '..iconText..' '..specAttack.name..'\r\n** '..specAttack.description
 
      if specAttack.activeBuffs and specAttack.activeBuffTurns ~= nil and specAttack.activeBuffTurns > 0 then
        table.insert(buffAttacks, specAttack.name)
        hasActiveBuffSpec = true
       end
    end
  end
  if normalAttackChance == 100 then
    result = iconText..'1 - '..p._getMonsterBaseMaxHit(monster)..' '..typeText..' Damage'
  elseif normalAttackChance > 0 then
    result = '* '..normalAttackChance..'% '..iconText..'1 - '..p.getMonsterBaseMaxHit(frame)..' '..typeText..' Damage'..result
  elseif hasActiveBuffSpec then
    --If the monster normally has a 0% chance of doing a normal attack but some special attacks can't be repeated, include it
    --(With a note about when it does it)
    result = '* '..iconText..' 1 - '..p._getMonsterBaseMaxHit(monster)..' '..typeText..' Damage (Instead of repeating '..table.concat(buffAttacks, ' or ')..' while the buff is already active)'..result
  end
 
  return result
end
 
function p.getMonsterPassives(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = ''
 
  if monster.hasPassive then
    result = result .. '===Passives==='
    for i, passiveID in pairs(monster.passiveID) do
      local passive = p.getPassiveByID(passiveID)
      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
     end
     end
   end
   end
Line 18: Line 475:
end
end


function p.testApostrophe(frame)
function p.getMonsterCategories(frame)
   local pageName = frame.args ~= nil and frame.args[1] or frame
   local MonsterName = frame.args ~= nil and frame.args[1] or frame
   local strArray = {}
   local monster = p.getMonster(MonsterName)
   for i = 1, #pageName do
 
     local newStr = string.sub(pageName, i, i)..' ['..pageName:byte(i)..']'
   if monster == nil then
    table.insert(strArray, newStr)
     return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
    mw.log(newStr)
   end
   end
   return table.concat(strArray, '<br/>')
 
  local result = '[[Category:Monsters]]'
 
  if Constants.getCombatStyleName(monster.attackType) == 'Melee' then
    result = result..'[[Category:Melee Monsters]]'
  elseif Constants.getCombatStyleName(monster.attackType) == 'Ranged' then
    result = result..'[[Category:Ranged Monsters]]'
  elseif Constants.getCombatStyleName(monster.attackType) == 'Magic' then
    result = result..'[[Category:Magic Monsters]]'
  end
 
  if monster.hasSpecialAttack then
    result = result..'[[Category:Monsters with Special Attacks]]'
  end
 
  if monster.isBoss then
    result = result..'[[Category:Bosses]]'
  end
 
  return result
end
 
function p.getOtherMonsterBoxText(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = ''
 
  --Going through and finding out which damage bonuses will apply to this monster
  local monsterTypes = {}
  if monster.isBoss then table.insert(monsterTypes, 'Boss') end
 
  local areaList = Areas.getMonsterAreas(monster.id)
  local counts = {combat = 0, slayer = 0, dungeon = 0}
  for i, area in Shared.skpairs(areaList) do
    counts[area.type] = counts[area.type] + 1
  end
 
  if counts.combat > 0 then table.insert(monsterTypes, 'Combat Area') end
  if counts.slayer > 0 then table.insert(monsterTypes, 'Slayer Area') end
  if counts.dungeon > 0 then table.insert(monsterTypes, 'Dungeon') end
 
  result = result.."\r\n|-\r\n|'''Monster Types:''' "..table.concat(monsterTypes, ", ")
 
  local SlayerTier = 'N/A'
  if not p._isDungeonOnlyMonster(monster) then
    SlayerTier = Constants.getSlayerTierNameByLevel(p._getMonsterCombatLevel(monster))
  end
 
  result = result.."\r\n|-\r\n|'''"..Icons.Icon({'Slayer', type='skill'}).." [[Slayer#Slayer Tier Monsters|Tier]]:''' "..SlayerTier
 
  return result
end
 
function p.getMonsterDrops(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  local result = ''
 
  if monster.bones ~= nil then
    local bones = Items.getItemByID(monster.bones)
    --Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
    if not p._isDungeonOnlyMonster(monster) or Shared.contains(bones.name, 'Shard') then
      result = result.."'''Always Drops:'''"
      result = result..'\r\n{|class="wikitable"'
      result = result..'\r\n!Item !! Qty'
      result = result..'\r\n|-\r\n|'..Icons.Icon({bones.name, type='item'})
      result = result..'||'..(monster.boneQty ~= nil and monster.boneQty or 1)..'\r\n'..'|}'
    end
  end
 
  --Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
  if not p._isDungeonOnlyMonster(monster) then
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    local lootValue = 0
 
    result = result.."'''Loot:'''"
    local avgGp = 0
 
    if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
      avgGp = (monster.dropCoins[1] + monster.dropCoins[2] - 1) / 2
      local gpTxt = Icons.GP(monster.dropCoins[1], monster.dropCoins[2] - 1)
      result = result.."\r\nIn addition to loot, the monster will also drop "..gpTxt..'.'
    end
 
    local multiDrop = Shared.tableCount(monster.lootTable) > 1
    local totalWt = 0
    for i, row in pairs(monster.lootTable) do
      totalWt = totalWt + row[2]
    end
    result = result..'\r\n{|class="wikitable sortable"'
    result = result..'\r\n!Item!!Qty'
    result = result..'!!Price!!colspan="2"|Chance'
 
    --Sort the loot table by weight in descending order
    table.sort(monster.lootTable, function(a, b) return a[2] > b[2] end)
    for i, row in Shared.skpairs(monster.lootTable) do
      local thisItem = Items.getItemByID(row[1])
      local maxQty = row[3]
      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
        result = result.. '1 - '
      end
      result = result..Shared.formatnum(row[3])
 
      --Adding price columns
      local itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
      if itemPrice == 0 or maxQty == 1 then
        result = result..'||'..Icons.GP(itemPrice)
      else
        result = result..'||'..Icons.GP(itemPrice, itemPrice * maxQty)
      end
 
      --Getting the drop chance
      local dropChance = (row[2] / totalWt * lootChance)
      if dropChance ~= 100 then
        --Show fraction as long as it isn't going to be 1/1
        result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
        result = result..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
        result = result..'||'
      else
        result = result..'||colspan="2" data-sort-value="'..row[2]..'"'
      end
      result = result..'style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
 
      --Adding to the average loot value based on price & dropchance
      lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
    end
    if multiDrop then
      result = result..'\r\n|-class="sortbottom" \r\n!colspan="3"|Total:'
      if lootChance < 100 then
        result = result..'\r\n|style="text-align:right"|'..Shared.fraction(lootChance, 100)..'||'
      else
        result = result..'\r\n|colspan="2" '
      end
      result = result..'style="text-align:right"|'..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."
    if avgGp > 0 then
      result = result..'<br/>Including GP, the average kill is worth '..Icons.GP(Shared.round(avgGp + lootValue, 2, 0))..'.'
    end
  end
 
  --If no other drops, make sure to at least say so.
  if result == '' then result = 'None' end
  return result
end
 
function p.getChestDrops(frame)
  local ChestName = frame.args ~= nil and frame.args[1] or frame
  local chest = Items.getItem(ChestName)
 
  if chest == nil then
    return "ERROR: No item named "..ChestName..' found[[Category:Pages with script errors]]'
  end
 
  local result = ''
 
  if chest.dropTable == nil then
    return "ERROR: "..ChestName.." does not have a drop table[[Category:Pages with script errors]]"
  else
    local lootChance = 100
    local lootValue = 0
 
    local multiDrop = Shared.tableCount(chest.dropTable) > 1
    local totalWt = 0
    for i, row in pairs(chest.dropTable) do
      totalWt = totalWt + row[2]
    end
    result = result..'\r\n{|class="wikitable sortable"'
    result = result..'\r\n!Item!!Qty'
    result = result..'!!colspan="2"|Chance!!Price'
 
    --Sort the loot table by weight in descending order
    for i, row in pairs(chest.dropTable) do
      if chest.dropQty ~= nil then
        table.insert(row, chest.dropQty[i])
      else
        table.insert(row, 1)
      end
    end
    table.sort(chest.dropTable, function(a, b) return a[2] > b[2] end)
    for i, row in Shared.skpairs(chest.dropTable) do
      local thisItem = Items.getItemByID(row[1])
      local qty = row[3]
      result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
      result = result..'||style="text-align:right" data-sort-value="'..qty..'"|'
 
      if qty > 1 then
        result = result.. '1 - '
      end
      result = result..Shared.formatnum(qty)
 
      local dropChance = (row[2] / totalWt) * 100
      result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
      result = result..'|'..Shared.fraction(row[2], totalWt)
 
      result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
 
      result = result..'||style="text-align:left" data-sort-value="'..thisItem.sellsFor..'"'
      if qty > 1 then
        result = result..'|'..Icons.GP(thisItem.sellsFor, thisItem.sellsFor * qty)
      else
        result = result..'|'..Icons.GP(thisItem.sellsFor)
      end
      lootValue = lootValue + (dropChance * 0.01 * thisItem.sellsFor * ((1 + qty)/ 2))
    end
    result = result..'\r\n|}'
    result = result..'\r\nThe average value of the contents of one chest is '..Icons.GP(Shared.round(lootValue, 2, 0))..'.'
  end
 
  return result
end
 
function p.getAreaMonsterTable(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
  if area == nil then
    return "ERROR: Could not find an area named "..areaName..'[[Category:Pages with script errors]]'
  end
 
  if area.type == 'dungeon' then
    return p.getDungeonMonsterTable(frame)
  end
 
  local tableTxt = '{| class="wikitable sortable"'
  tableTxt = tableTxt..'\r\n! Name !! Combat Level !! Hitpoints !! Max Hit !! [[Combat Triangle|Combat Style]]'
  for i, monsterID in pairs(area.monsters) do
    local monster = p.getMonsterByID(monsterID)
    tableTxt = tableTxt..'\r\n|-\r\n|'..Icons.Icon({monster.name, type='monster'})
    tableTxt = tableTxt..'||'..p._getMonsterCombatLevel(monster)
    tableTxt = tableTxt..'||'..Shared.formatnum(p.getMonsterHP(monster.name))
    tableTxt = tableTxt..'||'..Shared.formatnum(p.getMonsterMaxHit(monster.name))
    tableTxt = tableTxt..'||'..p.getMonsterStyleIcon({monster.name, nolink='true'})
  end
  tableTxt = tableTxt..'\r\n|}'
  return tableTxt
end
 
function p.getDungeonMonsterTable(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
  if area == nil then
    return "ERROR: Could not find a dungeon named "..areaName..'[[Category:Pages with script errors]]'
  end
 
  --For Dungeons, go through and count how many of each monster are in the dungeon first
  local monsterCounts = {}
  for i, monsterID in pairs(area.monsters) do
    if monsterCounts[monsterID] == nil then
      monsterCounts[monsterID] = 1
    else
      monsterCounts[monsterID] = monsterCounts[monsterID] + 1
    end
  end
 
  local usedMonsters = {}
 
  local tableTxt = '{| class="wikitable sortable"'
  tableTxt = tableTxt..'\r\n! Name !! Combat Level !! Hitpoints !! Max Hit !! [[Combat Triangle|Combat Style]] !! Count'
  for i, monsterID in pairs(area.monsters) do
    if not Shared.contains(usedMonsters, monsterID) then
      if monsterID >= 0 then
        local monster = p.getMonsterByID(monsterID)
        local name = monster.name
        if monsterID == 51 then name = 'Spider2' end
        tableTxt = tableTxt..'\r\n|-\r\n|'..Icons.Icon({name, type='monster'})
        tableTxt = tableTxt..'||'..p._getMonsterCombatLevel(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
 
function p._getAreaMonsterList(area)
  local monsterList = {}
  for i, monsterID in pairs(area.monsters) do
    local monster = p.getMonsterByID(monsterID)
    table.insert(monsterList, Icons.Icon({monster.name, type='monster'}))
  end
  return table.concat(monsterList, '<br/>')
end
 
function p._getDungeonMonsterList(area)
  local monsterList = {}
  local lastMonster = nil
  local lastID = -2
  local count = 0
  for i, monsterID in Shared.skpairs(area.monsters) do
    if monsterID ~= lastID then
      local monster = nil
      if monsterID ~= -1 then monster = p.getMonsterByID(monsterID) end
      if lastID ~= -2 then
        if lastID == -1 then
          --Special handling for Afflicted Monsters
          table.insert(monsterList, Icons.Icon({'Affliction', 'Afflicted Monster', img='Question', qty=count}))
        else
          local name = lastMonster.name
          if lastMonster.id == 51 then name = 'Spider2' end
          table.insert(monsterList, Icons.Icon({name, type='monster', qty=count}))
        end
      end
      lastMonster = monster
      lastID = monsterID
      count = 1
    else
      count = count + 1
    end
    --Make sure the final monster in the dungeon gets counted
    if i == Shared.tableCount(area.monsters) then
      local name = lastMonster.name
      table.insert(monsterList, Icons.Icon({lastMonster.name, type='monster', qty=count}))
    end
  end
   return table.concat(monsterList, '<br/>')
end
 
function p.getAreaMonsterList(frame)
  local areaName = frame.args ~= nil and frame.args[1] or frame
  local area = Areas.getArea(areaName)
  if area == nil then
    return "ERROR: Could not find an area named "..areaName..'[[Category:Pages with script errors]]'
  end
 
  if area.type == 'dungeon' then
    return p._getDungeonMonsterList(area)
  else
    return p._getAreaMonsterList(area)
  end
end
 
function p.getFoxyTable(frame)
  local result = 'Monster,Min GP,Max GP,Average GP'
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
    if not p._isDungeonOnlyMonster(monster) then
      if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
      local avgGp = (monster.dropCoins[1] + monster.dropCoins[2] - 1) / 2
      result = result..'<br/>'..monster.name..','..monster.dropCoins[1]..','..(monster.dropCoins[2]-1)..','..avgGp
      end
    end
  end
  return result
end
 
function p._getMonsterAverageGP(monster)
  local result = ''
  local totalGP = 0
 
  if monster.bones ~= nil then
    local bones = Items.getItemByID(monster.bones)
    --Show the bones only if either the monster shows up outside of dungeons _or_ the monster drops shards
    if not p._isDungeonOnlyMonster(monster) or Shared.contains(bones.name, 'Shard') then
      totalGP = totalGP + bones.sellsFor
    end
  end
 
  --Likewise, seeing the loot table is tied to the monster appearing outside of dungeons
  if not p._isDungeonOnlyMonster(monster) then
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    local lootValue = 0
 
    local avgGp = 0
 
    if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
      avgGp = (monster.dropCoins[1] + monster.dropCoins[2] - 1) / 2
    end
 
    totalGP = totalGP + avgGp
 
    local multiDrop = Shared.tableCount(monster.lootTable) > 1
    local totalWt = 0
    for i, row in pairs(monster.lootTable) do
      totalWt = totalWt + row[2]
    end
 
    for i, row in Shared.skpairs(monster.lootTable) do
      local thisItem = Items.getItemByID(row[1])
      local maxQty = row[3]
 
      local itemPrice = thisItem.sellsFor ~= nil and thisItem.sellsFor or 0
 
      --Getting the drop chance
      local dropChance = (row[2] / totalWt * lootChance)
 
      --Adding to the average loot value based on price & dropchance
      lootValue = lootValue + (dropChance * 0.01 * itemPrice * ((1 + maxQty) / 2))
    end
 
    totalGP = totalGP + lootValue
  end
 
  return Shared.round(totalGP, 2, 2)
end
 
function p.getMonsterAverageGP(frame)
  local MonsterName = frame.args ~= nil and frame.args[1] or frame
  local monster = p.getMonster(MonsterName)
 
  if monster == nil then
    return "ERROR: No monster with that name found[[Category:Pages with script errors]]"
  end
 
  return p._getMonsterAverageGP(monster)
end
 
function p.getMonsterEVTable(frame)
  local result = '{| class="wikitable sortable"'
  result = result..'\r\n!Monster!!Combat Level!!Average GP'
  for i, monsterTemp in Shared.skpairs(MonsterData.Monsters) do
    local monster = Shared.clone(monsterTemp)
    monster.id = i - 1
    if not p._isDungeonOnlyMonster(monster) then
      local monsterGP = p._getMonsterAverageGP(monster)
      local combatLevel = p._getMonsterCombatLevel(monster, 'Combat Level')
      result = result..'\r\n|-\r\n|[['..monster.name..']]||'..combatLevel..'||'..monsterGP
    end
  end
  result = result..'\r\n|}'
  return result
end
 
function p.getSlayerTierMonsterTable(frame)
  -- Input validation
  local tier = frame.args ~= nil and frame.args[1] or frame
  local slayerTier = nil
 
  if tier == nil then
    return "ERROR: No tier specified[[Category:Pages with script errors]]"
  end
 
  if tonumber(tier) ~= nil then
    slayerTier = Constants.getSlayerTierByID(tonumber(tier))
  else
    slayerTier = Constants.getSlayerTier(tier)
  end
 
  if slayerTier == nil then
    return "ERROR: Invalid slayer tier[[Category:Pages with script errors]]"
  end
 
  -- Obtain required tier details
  local minLevel, maxLevel = slayerTier.minLevel, slayerTier.maxLevel
 
  -- Build list of monster IDs
  -- Right now hiddenMonsterIDs is empty
  local hiddenMonsterIDs = {}
  local monsterIDs = {}
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
    if monster.canSlayer and not Shared.contains(hiddenMonsterIDs, i - 1) then
      local cmbLevel = p._getMonsterCombatLevel(monster)
      if cmbLevel >= minLevel and (maxLevel == nil or cmbLevel <= maxLevel) then
        table.insert(monsterIDs, i - 1)
      end
    end
  end
 
  if Shared.tableCount(monsterIDs) == 0 then
    -- Somehow no monsters are in the tier, return nothing
    return ''
  else
    return p._getMonsterTable(monsterIDs, true)
  end
end
 
function p.getFullMonsterTable(frame)
  local monsterIDs = {}
  for i = 0, Shared.tableCount(MonsterData.Monsters) - 1, 1 do
    table.insert(monsterIDs, i)
  end
 
  return p._getMonsterTable(monsterIDs, false)
end
 
function p._getMonsterTable(monsterIDs, excludeDungeons)
  --Making a single function for getting a table of monsters given a list of IDs.
  local hideDungeons = excludeDungeons ~= nil and excludeDungeons or false
  local tableParts = {}
  table.insert(tableParts, '{| class="wikitable sortable stickyHeader"')
  -- First header row
  table.insert(tableParts, '\r\n|- class="headerRow-0"\r\n! colspan="5" | !! colspan="4" |Offensive Stats !! colspan="3" |Evasion Rating !! colspan="4" |')
  -- Second header row
  table.insert(tableParts, '\r\n|- class="headerRow-1"\r\n!Monster !!Name !!ID !!Combat Level ')
  table.insert(tableParts, '!!style="padding:0 1em 0 0"|' .. Icons.Icon({'Hitpoints', type='skill'}))
  table.insert(tableParts, '!!Attack 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
  for i, monsterID in Shared.skpairs(monsterIDs) do
    local monster = p.getMonsterByID(monsterID)
    local cmbLevel = p._getMonsterCombatLevel(monster)
    local atkSpeed = p._getMonsterAttackSpeed(monster)
    local maxHit = p._getMonsterMaxHit(monster)
    local accR = p._getMonsterAR(monster)
    local evaR = {p._getMonsterER({monster, "Melee"}), p._getMonsterER({monster, "Ranged"}), p._getMonsterER({monster, "Magic"})}
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    local gpRange = {0, 0}
    if monster.dropCoins ~= nil and monster.dropCoins[2] > 1 then
      gpRange = {monster.dropCoins[1], monster.dropCoins[2] - 1}
    end
    local gpTxt = nil
    if gpRange[1] >= gpRange[2] then
      gpTxt = Icons.GP(gpRange[1])
    else
      gpTxt = Icons.GP(gpRange[1], gpRange[2])
    end
    local boneTxt = 'None'
    if monster.bones ~= nil then
      local bones = Items.getItemByID(monster.bones)
      boneTxt = Icons.Icon({bones.name, type='item', notext=true})
    end
 
    table.insert(tableParts, '\r\n|-\r\n|style="text-align: center;" |' .. Icons.Icon({monster.name, type='monster', size=50, notext=true}))
    table.insert(tableParts, '\r\n|style="text-align:left" |[[' .. monster.name .. ']]')
    table.insert(tableParts, '\r\n|style="text-align:right" |' .. monsterID)
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. cmbLevel .. '" |' .. Shared.formatnum(cmbLevel))
    table.insert(tableParts, '\r\n|style="text-align:right" data-sort-value="' .. 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|}')
  return table.concat(tableParts)
end
end


return p
return p