Module:Monsters: Difference between revisions

From Melvor Idle
(Prevent sorting footer row)
(Added range of prices for chest drop tables, fixed column order)
(4 intermediate revisions by the same user not shown)
Line 377: Line 377:
   if monster.lootTable ~= nil then
   if monster.lootTable ~= nil then
     local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
     local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
    result = result.."'''Loot:'''"
    if monster.dropCoins ~= nil then
      local gpTxt = Icons.GP(monster.dropCoins[1], monster.dropCoins[2])
      if lootChance == 100 then
        result = result.."\r\nIn addition to loot, the monster will also drop "..gpTxt
      else
        result = result.."\r\nIf loot is received, the monster will also drop "..gpTxt
      end
    end
     local multiDrop = Shared.tableCount(monster.lootTable) > 1
     local multiDrop = Shared.tableCount(monster.lootTable) > 1
     local totalWt = 0
     local totalWt = 0
Line 382: Line 394:
       totalWt = totalWt + row[2]
       totalWt = totalWt + row[2]
     end
     end
    result = result.."'''Loot:'''"
     result = result..'\r\n{|class="wikitable sortable"'
     result = result..'\r\n{|class="wikitable sortable"'
     result = result..'\r\n!Item!!Qty'
     result = result..'\r\n!Item!!Qty'
     --if multiDrop then result = result..'!!Weight' end
     --if multiDrop then result = result..'!!Weight' end


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


       local dropChance = (row[2] / totalWt * lootChance)
       local dropChance = (row[2] / totalWt * lootChance)
      result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
      result = result..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
       result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
       result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
     end
     end
     if multiDrop then
     if multiDrop then
       --result = result..'\r\n|-\r\n|colspan="2"|Total:||'..Shared.formatnum(totalWt)..'||'..lootChance..'%'
       result = result..'\r\n|-class="sortbottom" \r\n!colspan="2"|Total:'
       result = result..'\r\n|-class="sortbottom" \r\n!colspan="2"|Total:\r\n|style="text-align:right"|'..lootChance..'.00%'
      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
     end
     result = result..'\r\n|}'
     result = result..'\r\n|}'
  end


  return result
end


     if monster.dropCoins ~= nil then
function p.getChestDrops(frame)
       local gpTxt = Icons.GP(monster.dropCoins[1], monster.dropCoins[2])
  local ChestName = frame.args ~= nil and frame.args[1] or frame
       if lootChance == 100 then
  local chest = Items.getItem(ChestName)
         result = result.."\r\nThe player will also receive "..gpTxt
 
  if chest == nil then
     return "ERROR: No item named "..ChestName..' found'
  end
 
  local result = ''
 
  if chest.dropTable == nil then
    return "ERROR: "..ChestName.." does not have a drop table"
  else
    local lootChance = 100
 
    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
    table.sort(chest.dropTable, function(a, b) return a[2] > b[2] end)
    for i, row in pairs(chest.dropTable) do
      local thisItem = Items.getItemByID(row[1])
      local qty = 1
       if chest.dropQty ~= nil then qty = chest.dropQty[i] end
      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:right" data-sort-value="'..thisItem.sellsFor..'"'
      if qty > 1 then
        result = result..'|'..Icons.GP(thisItem.sellsFor, thisItem.sellsFor * qty)
       else
       else
         result = result.."\r\nIf loot is received, the player will also receive "..gpTxt
         result = result..'|'..Icons.GP(thisItem.sellsFor)
       end
       end
     end
     end
    result = result..'\r\n|}'
   end
   end


   return result
   return result
end
end


return p
return p

Revision as of 20:36, 22 September 2020

Data is pulled from Module:GameData/data


local p = {}

local Constants = mw.loadData('Module:Constants/data')
local MonsterData = mw.loadData('Module:Monsters/data')
local AreaData = mw.loadData('Module:CombatAreas/data')

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
    end
  end
  return result
end

function p.getMonsterByID(ID)
  return MonsterData.Monsters[ID + 1]
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 monster has an ID, and account for the 1-based indexing of Lua
      result.id = i - 1
    end
  end
  return result
end

function p.getSpecialAttackByID(ID)
  return MonsterData.SpecialAttacks[ID + 1]
end

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

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

  return monster[StatName]
end

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

  if monster == nil then
    return "ERROR: No monster with that name found"
  end

  local iconText = ''
  if  monster.attackType == Constants.attackType.Melee then
    iconText = Icons.Icon({'Melee', notext=notext, nolink=nolink})
  elseif monster.attackType == Constants.attackType.Ranged then
    iconText = Icons.Icon({'Ranged', type='skill', notext=notext, nolink=nolink})
  else
    iconText = Icons.Icon({'Magic', type='skill', notext=notext, nolink=nolink})
  end

  return iconText
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 monster.hitpoints * 10
  else
    return "ERROR: No monster with that name found"
  end
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 monster.attackSpeed / 1000
  else
    return "ERROR: No monster with that name found"
  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"
  end
  
  local base = 0.25 * (monster.defenceLevel + monster.hitpoints)
  local melee = 0.325 * (monster.attackLevel + monster.strengthLevel)
  local range = 0.325 * (1.5 * monster.rangedLevel)
  local magic = 0.325 * (1.5 * monster.magicLevel)
  if melee > range and melee > magic then
    return math.floor(base + melee)
  elseif range > magic then
    return math.floor(base + range)
  else
    return math.floor(base + magic)
  end
end

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"
  end
  
  local effAttLvl = 0
  local attBonus = 0
  if monster.attackType == Constants.attackType.Melee then
    effAttLvl = monster.attackLevel + 9
    attBonus = monster.attackBonus + 64
  elseif monster.attackType == Constants.attackType.Ranged then
    effAttLvl = monster.rangedLevel + 9
    attBonus = monster.attackBonusRanged + 64
  elseif monster.attackType == Constants.attackType.Magic then
    effAttLvl = monster.magicLevel + 9
    attBonus = monster.attackBonusMagic + 64
  else
    return "ERROR: This monster has an invalid attack type somehow"
  end

  return effAttLvl * attBonus
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"
  end
  
  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 + monster.defenceLevel * 0.3) + 9
    defBonus = monster.defenceBonusMagic + 64
  else
    return "ERROR: Must choose Melee, Ranged, or Magic"
  end
  return effDefLvl * defBonus
end

function p.getMonsterAreas(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"
  end

  local result = ''
  for i, area in pairs(AreaData.combatAreas) do
    if Shared.contains(area.monsters, monster.id) then
      if string.len(result) > 0 then result = result..'<br/>' end
      result = result..Icons.Icon({area.areaName, type = 'combatArea'})
    end
  end
  for i, area in pairs(AreaData.slayerAreas) do
    if Shared.contains(area.monsters, monster.id) then
      if string.len(result) > 0 then result = result..'<br/>' end
      result = result..Icons.Icon({area.areaName, type = 'combatArea'})..'[[Category:Slayer Monsters]]'
    end
  end
  for i, area in pairs(AreaData.dungeons) do
    if Shared.contains(area.monsters, monster.id) then
      if string.len(result) > 0 then result = result..'<br/>' end
      result = result..Icons.Icon({area.name, type = 'dungeon'})..'[[Category:Dungeon Monsters]]'
    end
  end
  return result
end

function p.getMonsterMaxHit(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"
  end

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

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"
  end
  
  local effStrLvl = 0
  local strBonus = 0
  if monster.attackType == Constants.attackType.Melee then
    effStrLvl = monster.strengthLevel + 9
    strBonus = monster.strengthBonus
  elseif monster.attackType == Constants.attackType.Ranged then
    effStrLvl = monster.rangedLevel + 9
    strBonus = monster.strengthBonusRanged
  elseif monster.attackType == Constants.attackType.Magic then
    local mSpell = nil
    if monster.selectedSpell ~= nil then mSpell = Magic.getSpellByID(monster.selectedSpell) end
    if mSpell == nil then
      return math.floor(10 * (monster.setMaxHit + (monster.setMaxHit * monster.damageBonusMagic / 100)))
    else
      return math.floor(10 * (mSpell.maxHit + (mSpell.maxHit * monster.damageBonusMagic / 100)))
    end
  else
    return "ERROR: This monster has an invalid attack type somehow"
  end

  --Should only get here for Melee/Ranged, which use functionally the same damage formula
  return math.floor(10 * (1.3 + (effStrLvl/10) + (strBonus / 80) + ((effStrLvl * strBonus) / 640)))
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"
  end

  local result = ''

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

  local normalAttackChance = 100
  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
    end
  end
  if normalAttackChance == 100 then
    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

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

  if monster == nil then
    return "ERROR: No monster with that name found"
  end

  local result = '[[Category:Monsters]]'
  
  if monster.attackType == Constants.attackType.Melee then
    result = result..'[[Category:Melee Monsters]]'
  elseif monster.attackType == Constants.attackType.Ranged then
    result = result..'[[Category:Ranged Monsters]]'
  elseif monster.attackType == Constants.attackType.Magic then
    result = result..'[[Category:Magic Monsters]]'
  end

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

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

  if monster == nil then
    return "ERROR: No monster with that name found"
  end

  local result = ''

  if monster.bones ~= nil then
    local bones = Items.getItemByID(monster.bones)
    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

  if monster.lootTable ~= nil then
    local lootChance = monster.lootChance ~= nil and monster.lootChance or 100

    result = result.."'''Loot:'''"

    if monster.dropCoins ~= nil then
      local gpTxt = Icons.GP(monster.dropCoins[1], monster.dropCoins[2])
      if lootChance == 100 then
        result = result.."\r\nIn addition to loot, the monster will also drop "..gpTxt
      else
        result = result.."\r\nIf loot is received, the monster will also drop "..gpTxt
      end
    end

    local multiDrop = Shared.tableCount(monster.lootTable) > 1
    local 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'
    --if multiDrop then result = result..'!!Weight' end

    result = result..'!!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 pairs(monster.lootTable) do
      local thisItem = Items.getItemByID(row[1])
      result = result..'\r\n|-\r\n|'..Icons.Icon({thisItem.name, type='item'})
      result = result..'||style="text-align:right" data-sort-value="'..row[3]..'"|'

      if row[3] > 1 then 
        result = result.. '1 - ' 
      end
      result = result..Shared.formatnum(row[3])

      local dropChance = (row[2] / totalWt * lootChance)
      result = result..'||style="text-align:right" data-sort-value="'..row[2]..'"'
      result = result..'|'..Shared.fraction(row[2] * lootChance, totalWt * 100)
      result = result..'||style="text-align:right"|'..Shared.round(dropChance, 2, 2)..'%'
    end
    if multiDrop then
      result = result..'\r\n|-class="sortbottom" \r\n!colspan="2"|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|}'
  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'
  end

  local result = ''

  if chest.dropTable == nil then
    return "ERROR: "..ChestName.." does not have a drop table"
  else
    local lootChance = 100

    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
    table.sort(chest.dropTable, function(a, b) return a[2] > b[2] end)
    for i, row in pairs(chest.dropTable) do
      local thisItem = Items.getItemByID(row[1])
      local qty = 1
      if chest.dropQty ~= nil then qty = chest.dropQty[i] end
      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:right" 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
    end
    result = result..'\r\n|}'
  end

  return result
end


return p