Module:Items/SourceTables

From Melvor Idle
< Module:Items
Revision as of 08:04, 11 September 2021 by ByteFoolish (talk | contribs) (Fix Alt Magic source for ItemBox)
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Documentation for this module may be created at Module:Items/SourceTables/doc

local p = {}

local MonsterData = mw.loadData('Module:Monsters/data')
local ItemData = mw.loadData('Module:Items/data')
local SkillData = mw.loadData('Module:Skills/data')

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Magic = require('Module:Magic')
local Areas = require('Module:CombatAreas')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Shop = require('Module:Shop')
local Monsters = require('Module:Monsters')

-- Implements overrides for sources which cannot be obtained from game data
-- Currently only overrides for dungeon sources are implemented here
local sourceOverrides = {
  ['Dungeon'] = {
    [361] = 'Volcanic Cave', -- Fire Cape
    [941] = 'Infernal Stronghold', -- Infernal Cape
    [950] = 'Volcanic Cave', -- A Tale of the Past, a future's prophecy
    [951] = 'Fire God Dungeon' -- The First Hero and an Unknown Evil
  }
}

function p._getCreationTable(item)
  local skill = ''
  local specialReq = nil
  local time = 0
  local maxTime = nil
  local lvl = 0
  local xp = 0
  local qty = nil
  local req = nil

  local tables = {}
  --First figure out what skill is used to make this...
  if item.smithingLevel ~= nil then
    skill = 'Smithing'
    lvl = item.smithingLevel
    xp = item.smithingXP
    req = item.smithReq
    qty = item.smithingQty
    time = 2
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
  end
  if item.craftingLevel ~= nil then
    skill = 'Crafting'
    lvl = item.craftingLevel
    xp = item.craftingXP
    req = item.craftReq
    qty = item.craftQty
    time = 3
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
  end
  if item.runecraftingLevel ~= nil then
    skill = 'Runecrafting'
    lvl = item.runecraftingLevel
    xp = item.runecraftingXP
    req = item.runecraftReq
    qty = item.runecraftQty
    time = 2
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
  end
  if item.fletchingLevel ~= nil then
    skill = 'Fletching'
    lvl = item.fletchingLevel
    xp = item.fletchingXP
    req = item.fletchReq
    qty = item.fletchQty
    time = 2
    if item.name == 'Arrow Shafts' then
      --Arrow Shafts get special (weird) treatment
      req = '1 of any [[Log]]'
      qty = '15 - 135'
    end
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
  end
  if item.herbloreReq ~= nil then
    skill = 'Herblore'
    req = item.herbloreReq
    --Currently using 'masteryID' as shorthand to find details, could be a better method
    local potionID = item.masteryID[2]
    local potionData = SkillData.Herblore.ItemData[potionID + 1]
    lvl = potionData.herbloreLevel
    xp = potionData.herbloreXP
    time = 2
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
  end
  if item.masteryID ~= nil and item.masteryID[1] == 4 then
    skill = 'Mining'
    lvl = SkillData.Mining.Rocks[item.masteryID[2] + 1].level
    time = 3
    xp = item.miningXP
    --Rune Essence has double quantity, but that's a hard-coded thing in the game so it's hard-coded here
    if item.name == 'Rune Essence' then qty = 2 else qty = 1 end

    if item.name == 'Dragonite Ore' then
      specialReq = Icons.Icon({"Mastery", notext='true'})..' 271 total [[Mining]] [[Mastery]]'
    end
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, specialReq))
  end
  if item.type == "Logs" then
    --Well this feels like cheating, but for as long as logs are the first items by ID it works
    local treeData = SkillData.Woodcutting.Trees[item.id + 1]
    skill = 'Woodcutting'
    lvl = treeData.level
    time = treeData.interval / 1000
    xp = treeData.xp
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
  end
  if item.fishingLevel ~= nil then
    skill = 'Fishing'
    lvl = item.fishingLevel
    xp = item.fishingXP
    time = item.minFishingInterval/1000
    maxTime = item.maxFishingInterval/1000
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime))
  end
  if item.type == "Harvest" or item.type == "Herb" or item.type == "Logs" then
    --Harvest/Herb means farming
    --Logs might mean farming or might not. Depends on the logs
    for i, item2 in pairs(ItemData.Items) do
      if item2.grownItemID == item.id then
        skill = 'Farming'
        lvl = item2.farmingLevel
        xp = item2.farmingXP
        time = item2.timeToGrow
        if item.type == 'Logs' then
          qty = 35
        else
          qty = 15
        end
        req = {{id = i - 1, qty = (item2.seedsRequired ~= nil and item2.seedsRequired or 1)}}
        table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
        break
      end
    end
  end
  if item.type == "Food" or item.type == "Cooked Fish" then
    --Food/Cooked Fish is Fishing, need to figure out source item
    for i, item2 in pairs(ItemData.Items) do
      if item2.burntItemID == item.id or item2.cookedItemID == item.id then
        skill = 'Cooking'
        lvl = item2.cookingLevel
        if item2.burntItemID == item.id then
          xp = 1
        else
          xp = item2.cookingXP
        end
        time = 3
        req = {{id = i - 1, qty = 1}}
        break
      end
    end
    if skill ~= '' then
      table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
    end
  end
  if item.summoningLevel ~= nil then
    skill = 'Summoning'
    lvl = item.summoningLevel
    --Summoning uses a formula to calculate XP for creation instead of referring to the item's XP data directly
    xp = (5 + 2 * math.floor(item.summoningLevel / 5))
    local ShardCostArray = {}
    for j, cost in Shared.skpairs(item.summoningReq[1]) do
      if cost.id >= 0 then
        local item = Items.getItemByID(cost.id)
        if item.type == 'Shard' then
          table.insert(ShardCostArray, Icons.Icon({item.name,  type='item', notext=true, qty=cost.qty}))
        end
      end
    end
    req = table.concat(ShardCostArray, ', ')
    req = req..'<br/>\r\nand one of the following<br/>\r\n'
    local OtherCostArray = {}
    local recipeGPCost = SkillData.Summoning.Settings.recipeGPCost
    for j, altCost in Shared.skpairs(item.summoningReq) do
      local nonShardArray = {}
      for k, cost in Shared.skpairs(altCost) do
        if cost.id >= 0 then
          local item = Items.getItemByID(cost.id)
          if item.type ~= 'Shard' then
            local sellPrice = math.max(item.sellsFor, 20)
            table.insert(nonShardArray, Icons.Icon({item.name, type='item', notext=true, qty=math.max(1, math.floor(recipeGPCost / sellPrice))}))
          end
        else
          if cost.id == -4 then
            table.insert(nonShardArray, Icons.GP(recipeGPCost))
          elseif cost.id == -5 then
            table.insert(nonShardArray, Icons.SC(recipeGPCost))
          end
        end
      end
      table.insert(OtherCostArray, table.concat(nonShardArray, ', '))
    end
    req = req..table.concat(OtherCostArray, "<br/>'''OR''' ")
    qty = item.summoningQty
    time = 5
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
  end
  --A couple special exceptions for Alt Magic
  --Not Gems or Bars though since those have their own separate thing
  if item.name == 'Rune Essence' then
    table.insert(tables, p.buildAltMagicTable('Just Learning'))
  elseif item.name == 'Bones' then
    table.insert(tables, p.buildAltMagicTable('Bone Offering'))
  elseif item.name == 'Holy Dust' then
    table.insert(tables, p.buildAltMagicTable('Blessed Offering'))
  end

  if Shared.tableCount(tables) == 0 then
    return ""
  else
    return table.concat(tables, '\r\n')
  end
end

function p.buildAltMagicTable(spellName)
  local spell = Magic.getSpell(spellName, 'AltMagic')
  local resultPart = {}
  table.insert(resultPart, '{|class="wikitable"\r\n|-')
  table.insert(resultPart, '\r\n!colspan="2"|'..Icons.Icon({spell.name, type='spell'}))
  table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
  table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.magicLevelRequired))
  -- 1 means select any item. 0 would mean Superheat, but that's handled elsewhere
  -- -1 means no item is needed, so hide this section
  if spell.selectItem == 1 then
    table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
    table.insert(resultPart, '\r\n|1 of any item')
  end
  --Add runes
  table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Runes\r\n|')
  for i, req in pairs(spell.runesRequired) do
    local rune = Items.getItemByID(req.id)
    if i > 1 then table.insert(resultPart, ', ') end
    table.insert(resultPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
  end
  if spell.runesRequiredAlt ~= nil and Shared.tableCount(spell.runesRequired) ~= Shared.tableCount(spell.runesRequiredAlt) then
    table.insert(resultPart, "<br/>'''OR'''<br/>")
    for i, req in pairs(spell.runesRequiredAlt) do
      local rune = Items.getItemByID(req.id)
      if i > 1 then table.insert(resultPart, ', ') end
      table.insert(resultPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
    end
  end

  --Now just need the output quantity, xp, and casting time (which is always 2)
  table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity\r\n|'..spell.convertToQty)
  table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base XP\r\n|'..spell.magicXP)
  table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Cast Time\r\n|2s')
  table.insert(resultPart, '\r\n|}')
  return table.concat(resultPart)
end

function p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq)
  if qty == nil then qty = 1 end
  local resultPart = {}
  table.insert(resultPart, '{|class="wikitable"')
  if req ~= nil then
    table.insert(resultPart, '\r\n!colspan="2"|Item Creation')
  else
    table.insert(resultPart, '\r\n!colspan="2"|Item Production')
  end
  table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Requirements')
  table.insert(resultPart, '\r\n|'..Icons._SkillReq(skill, lvl))
  if specialReq ~= nil then table.insert(resultPart, '<br/>'..specialReq) end

  if req ~= nil then
    table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Materials\r\n|')
    if type(req) == 'table' then
      for i, mat in pairs(req) do
        if i > 1 then table.insert(resultPart, '<br/>') end
        local matItem = Items.getItemByID(mat.id)
        if matItem == nil then
          table.insert(resultPart, mat.qty..'x ?????')
        else
          table.insert(resultPart, Icons.Icon({matItem.name, type='item', qty=mat.qty}))
        end
      end
    else
     table.insert(resultPart, req)
    end
  end
  table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity')
  table.insert(resultPart, '\r\n|'..qty)
  table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Experience')
  table.insert(resultPart, '\r\n|'..Shared.formatnum(xp)..' XP')
  table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Creation Time')
  table.insert(resultPart, '\r\n|'..Shared.formatnum(Shared.round(time, 2, 0))..'s')
  if maxTime ~= nil then table.insert(resultPart, ' - '..Shared.formatnum(Shared.round(maxTime, 2, 0))..'s') end
  table.insert(resultPart, '\r\n|}')

  return table.concat(resultPart)
end

function p.getCreationTable(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = Items.getItem(itemName)
  if item == nil then
    return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  end

  return p._getCreationTable(item)
end

function p._getItemSources(item, asList, addCategories)
  local lineArray = {}
  local categoryArray = {}

  --Alright, time to go through all the ways you can get an item...
  --First up: Can we kill somebody and take theirs?
  local killStr = ''
  local dungeonStr = ''
  local count1 = 0
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
    local isDrop = false
    if monster.bones == item.id and ((monster.lootTable ~= nil and not monster.isBoss) or Shared.contains(item.name, "Shard")) then
      isDrop = true
    elseif monster.lootTable ~= nil then
      for j, loot in pairs(monster.lootTable) do
        if loot[1] == item.id then
          isDrop = true
          break
        end
      end
      if isDrop and Monsters.isDungeonOnlyMonster({args={monster.name}}) then
        -- 2021-05-24 Additional checks for dungeon exclusive monsters: Loot is not rolled on
        -- dungeon exclusive monsters unless they are the boss/last enemy of that dungeon
        local rollForLoot = false
        for k, area in pairs(Areas.getMonsterAreas(i - 1)) do
          if not (area.type == 'dungeon') or (area.type == 'dungeon' and area.monsters[Shared.tableCount(area.monsters)] == i - 1) then
            -- Either monster isn't dungeon exclusive, or is the boss/last enemy of a dungeon
            rollForLoot = true
            break
          end
        end
        isDrop = rollForLoot
      end
    end
    if isDrop then
      if monster.isBoss and not Shared.contains(item.name, "Shard") then
        local areaList = Areas.getMonsterAreas(i - 1)
        --If this is a boss then we actually are completing dungeons for this and need to figure out which one
        for j, dung in pairs(areaList) do
          if string.len(dungeonStr) > 0 then
            dungeonStr = dungeonStr..','
          else
            dungeonStr = 'Completing: '
          end
          dungeonStr = dungeonStr..Icons.Icon({dung.name, type="dungeon", notext=true})
        end
      else
        count1 = count1 + 1
        if string.len(killStr) > 0 then
          killStr = killStr..','
          --if count1 % 3 == 1 and count1 > 1 then killStr = killStr..'<br/>' end
          killStr = killStr..Icons.Icon({monster.name, type="monster", notext="true"})
        else
          killStr = killStr..'Killing: '..Icons.Icon({monster.name, type="monster", notext="true"})
        end
      end
    end
  end
  -- Special exceptions for Fire/Infernal Cape and first two lore books
  if sourceOverrides['Dungeon'][item.id] ~= nil then
    if string.len(dungeonStr) > 0 then
      dungeonStr = dungeonStr .. ','
    else
      dungeonStr = 'Completing: '
    end
    dungeonStr = dungeonStr .. Icons.Icon({sourceOverrides['Dungeon'][item.id], type='dungeon', notext=true})
  end

  if string.len(dungeonStr) > 0 then table.insert(lineArray, dungeonStr) end
  if string.len(killStr) > 0 then table.insert(lineArray, killStr) end

  --Next: Can we find it in a box?
  --While we're here, check for upgrades, cooking, and growing
  local lootStr = ''
  local upgradeStr = ''
  local cookStr = ''
  local burnStr = ''
  local growStr = ''
  local count2 = 0
  count1 = 0
  for i, item2 in pairs(ItemData.Items) do
    if item2.dropTable ~= nil then
      for j, loot in pairs(item2.dropTable) do
        if loot[1] == item.id then
          count1 = count1 + 1
          if string.len(lootStr) > 0 then
            lootStr = lootStr..','
            --if count1 % 3 == 1 and count1 > 1 then lootStr = lootStr..'<br/>' end
            lootStr = lootStr..Icons.Icon({item2.name, type="item", notext="true"})
          else
            lootStr = lootStr..'Opening: '..Icons.Icon({item2.name, type="item", notext="true"})
          end
        end
      end
    end
    if item2.trimmedItemID == item.id then
          count2 = count2 + 1
        if string.len(upgradeStr) > 0 then
          upgradeStr = upgradeStr..','
          --if count2 % 3 == 1 and count2 > 1 then upgradeStr = upgradeStr..'<br/>' end
          upgradeStr = upgradeStr..Icons.Icon({item2.name, type="item", notext="true"})
        else
          table.insert(categoryArray, '[[Category:Upgraded Items]]')
          upgradeStr = upgradeStr..'Upgrading: '..Icons.Icon({item2.name, type="item", notext="true"})
        end
    end
    if item2.cookedItemID == item.id then
        if string.len(cookStr) > 0 then
          cookStr = cookStr..','..Icons.Icon({item2.name, type="item", notext="true"})
        else
          table.insert(categoryArray, '[[Category:Cooked Items]]')
          cookStr = cookStr..'Cooking: '..Icons.Icon({item2.name, type="item", notext="true"})
        end
    end
    if item2.burntItemID == item.id then
        if string.len(burnStr) > 0 then
          burnStr = burnStr..','..Icons.Icon({item2.name, type="item", notext="true"})
        else
          table.insert(categoryArray, '[[Category:Burnt Items]]')
          burnStr = burnStr..'Burning: '..Icons.Icon({item2.name, type="item", notext="true"})
        end
    end
    if item2.grownItemID == item.id then
        if string.len(growStr) > 0 then
          growStr = growStr..','..Icons.Icon({item2.name, type="item", notext="true"})
        else
          table.insert(categoryArray, '[[Category:Harvestable Items]]')
          growStr = growStr..'Growing: '..Icons.Icon({item2.name, type="item", notext="true"})
        end
    end
  end
  if string.len(lootStr) > 0 then table.insert(lineArray, lootStr) end
  if string.len(upgradeStr) > 0 then table.insert(lineArray, upgradeStr) end
  if string.len(cookStr) > 0 then table.insert(lineArray, cookStr) end
  if string.len(burnStr) > 0 then table.insert(lineArray, burnStr) end
  if string.len(growStr) > 0 then table.insert(lineArray, growStr) end

  --Next: Can we take it from somebody else -without- killing them?
  local thiefStr = ''
  for i, npc in pairs(SkillData.Thieving) do
    if npc.lootTable ~= nil then
      for j, loot in pairs(npc.lootTable) do
        if loot[1] == item.id then
          if string.len(thiefStr) > 0 then
            thiefStr = thiefStr..','..Icons.Icon({npc.name, type="thieving", notext="true"})
          else
            thiefStr = thiefStr..'Pickpocketing: '..Icons.Icon({npc.name, type="thieving", notext="true"})
          end
        end
      end
    end
  end
  if string.len(thiefStr) > 0 then table.insert(lineArray, thiefStr) end

  --If all else fails, I guess we should check if we can make it ourselves
  --SmithCheck:
  if item.smithingLevel ~= nil then
    table.insert(lineArray, Icons._SkillReq("Smithing", item.smithingLevel))
  end

  --CraftCheck:
  if item.craftingLevel ~= nil then
    table.insert(lineArray, Icons._SkillReq("Crafting", item.craftingLevel))
  end

  --FletchCheck:
  if item.fletchingLevel ~= nil then
    table.insert(lineArray, Icons._SkillReq("Fletching", item.fletchingLevel))
  end

  --RunecraftCheck:
  if item.runecraftingLevel ~= nil then
    table.insert(lineArray, Icons._SkillReq("Runecrafting", item.runecraftingLevel))
  end

  --MineCheck:
  if item.masteryID ~= nil and item.masteryID[1] == 4 then
    table.insert(lineArray, Icons._SkillReq("Mining", SkillData.Mining.Rocks[item.masteryID[2] + 1].level))
  end

  --FishCheck:
  if (item.category == "Fishing" and (item.type == "Junk" or item.type == "Special")) then
    table.insert(lineArray, Icons.Icon({"Fishing", type='skill', notext=true})..' [[Fishing#'..item.type..'|'..item.type..']]')
  elseif item.fishingLevel ~= nil then
    table.insert(lineArray, Icons._SkillReq("Fishing", item.fishingLevel))
  end

  --HerbCheck:
  if item.masteryID ~= nil and item.masteryID[1] == 19 then
    local potionData = SkillData.Herblore.ItemData[item.masteryID[2] + 1].herbloreLevel
    table.insert(lineArray, Icons._SkillReq("Herblore", potionData))
  end

  --WoodcuttingCheck:
  if item.type == 'Logs' then
    local treeData = SkillData.Woodcutting.Trees[item.id + 1]
    local lvl = treeData.level
    table.insert(lineArray, Icons._SkillReq("Woodcutting", lvl))
  end

  --SummoningCheck:
  if item.summoningLevel ~= nil then
    table.insert(lineArray, Icons._SkillReq("Summoning", item.summoningLevel))
  end

  --Finally there are some weird exceptions:
  --Coal can be acquired via firemaking
  if item.name == "Coal Ore" then
    table.insert(lineArray, Icons._SkillReq("Firemaking", 1))
  end

  --Gems can be acquired from mining, fishing, and alt. magic
  if item.type == 'Gem' then
    table.insert(lineArray, Icons.Icon({"Fishing", type='skill', notext=true})..' [[Fishing#Special|Special]]')
    table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
    table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
  end

  --Bars and some other stuff can also be acquired via Alt. Magic
  if item.type == 'Bar' or Shared.contains(Items.AltMagicProducts, item.name) then
    table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
  end

  --Chapeau Noir & Bobby's Pocket are special Thieving items
  if item.name == "Chapeau Noir" or item.name == "Bobby&apos;s Pocket" then
    table.insert(lineArray, Icons._SkillReq("Thieving", 1))
  end

  --Rhaelyx pieces are also special
  if Shared.contains({'Circlet of Rhaelyx', 'Jewel of Rhaelyx', 'Mysterious Stone'}, item.name) then
    local rhaSkills = {
      Circlet = {'Woodcutting', 'Fishing', 'Mining', 'Thieving', 'Farming', 'Agility'},
      Jewel = {'Firemaking', 'Cooking', 'Smithing', 'Fletching', 'Crafting', 'Runecrafting', 'Herblore', 'Summoning'}
    }
    local rhaSkList = {}
    if item.name == 'Circlet of Rhaelyx' then
      rhaSkList = rhaSkills.Circlet
    elseif item.name == 'Jewel of Rhaelyx' then
      rhaSkList = rhaSkills.Jewel
    elseif item.name == 'Mysterious Stone' then
      rhaSkList = rhaSkills.Jewel
      for i, v in ipairs(rhaSkills.Circlet) do
        table.insert(rhaSkList, v)
      end
    end

    local rhaStrPart = {}
    for i, skillName in ipairs(rhaSkList) do
      table.insert(rhaStrPart, Icons.Icon({skillName, type='skill', notext=true}))
    end
    local rhaStr = 'Any action in: ' .. table.concat(rhaStrPart, ', ')
    if item.name == 'Mysterious Stone' then rhaStr = rhaStr .. '<br/>after finding ' .. Icons.Icon({'Crown of Rhaelyx', type='item'}) end
    table.insert(lineArray, rhaStr)
  end

  --Tokens are from the appropriate skill
  if item.isToken and item.skill ~= nil then
    table.insert(lineArray, Icons._SkillReq(Constants.getSkillName(item.skill), 1))
  end

  --Shop items (including special items like gloves that aren't otherwise listed)
  local shopSources = Shop.getItemSourceArray(item.id)
  if Shared.tableCount(shopSources) > 0 then
    table.insert(lineArray, Icons.Icon({'Shop'}))
  end

  --Easter Eggs (manual list 'cause don't have a better way to do that)
  if Shared.contains(Items.EasterEggs, item.name) then
    table.insert(lineArray, '[[Easter Eggs]]')
  end

  local resultPart = {}
  if asList then
    table.insert(resultPart, '* '..table.concat(lineArray, "\r\n* "))
  else
    table.insert(resultPart, '<div style="max-width:180px;text-align:right">' .. table.concat(lineArray, "<br/>") .. '</div>')
  end
  if addCategories then table.insert(resultPart, table.concat(categoryArray, '')) end
  return table.concat(resultPart)
end

function p.getItemSources(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = Items.getItem(itemName)
  local asList = false
  local addCategories = false
  if frame.args ~= nil then
    asList = frame.args.asList ~= nil and frame.args.asList ~= '' and frame.args.asList ~= 'false'
    addCategories = frame.args.addCategories ~= nil and frame.args.addCategories ~= '' and frame.args.addCategories ~= 'false'
  end
  if item == nil then
    return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  end

  return p._getItemSources(item, asList, addCategories)
end

function p._getItemLootSourceTable(item)
  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
  table.insert(resultPart, '\r\n|- class="headerRow-0"')
  table.insert(resultPart, '\r\n!Source!!Source Type!!Quantity!!colspan="2"|Chance')

  --Set up function for adding rows
  local buildRow = function(source, type, minqty, qty, weight, totalWeight)
    if minqty == nil then minqty = 1 end
    local rowPart = {}
    table.insert(rowPart, '\r\n|-')
    table.insert(rowPart, '\r\n|style="text-align: left;"|'..source)
    table.insert(rowPart, '\r\n|style="text-align: left;"|'..type)

    table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="'..qty..'"|'..minqty)
    if qty ~= minqty then table.insert(rowPart, ' - '..qty) end
    local chance = Shared.round(weight / totalWeight * 100, 2, 2)
    if weight >= totalWeight then
      -- Fraction would be 1/1, so only show the percentage
      chance = 100
      table.insert(rowPart, '\r\n|colspan="2" ')
    else
      table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="' .. chance .. '"| ' .. Shared.fraction(weight, totalWeight) .. '\r\n|')
    end
    table.insert(rowPart, 'style="text-align: right;" data-sort-value="'.. chance .. '"|'..chance..'%')
    return table.concat(rowPart)
  end
  local dropRows = {}

  --Alright, time to go through a few ways to get the item
  --First up: Can we kill somebody and take theirs?
  for i, monster in pairs(MonsterData.Monsters) do
    local minqty = 1
    local qty = 1
    local wt = 0
    local totalWt = 0
    --Only add bones if this monster has loot (ie appears outside a dungeon) and isn't a boss
    --... unless we're looking for Shards of course, at which point we'll take any monster with the right bones
    if ((monster.lootTable ~= nil and not monster.isBoss) or Shared.contains(item.name, 'Shard')) and monster.bones == item.id then
      qty = monster.boneQty ~= nil and monster.boneQty or 1
      minqty = qty
      wt = 1
      totalWt = 1
    elseif monster.lootTable ~= nil then
      for j, loot in pairs(monster.lootTable) do
        totalWt = totalWt + loot[2]
        if loot[1] == item.id then
          wt = loot[2]
          qty = loot[3]
        end
      end
    end
    local lootChance = monster.lootChance ~= nil and monster.bones ~= item.id and monster.lootChance or 100

    if wt > 0 and lootChance > 0 and not Shared.contains(item.name, 'Shard') and Monsters.isDungeonOnlyMonster({args={monster.name}}) then
      -- 2021-05-24 Additional checks for dungeon exclusive monsters: Loot is not rolled on
      -- dungeon exclusive monsters unless they are the boss/last enemy of that dungeon
      local rollForLoot = false
      for k, area in pairs(Areas.getMonsterAreas(i - 1)) do
        if area.type ~= 'dungeon' or (area.type == 'dungeon' and area.monsters[Shared.tableCount(area.monsters)] == i - 1) then
          -- Either monster isn't dungeon exclusive, or is the boss/last enemy of a dungeon
          rollForLoot = true
          break
        end
      end
      if not rollForLoot then
        wt = 0
      end
    end
    if wt > 0 and lootChance > 0 then
      local sourceTxt = nil
      local typeTxt = nil
      --If we're dealing with a boss, this is a Dungeon row instead
      if monster.isBoss and not Shared.contains(item.name, 'Shard') then
        local dung = Areas.getMonsterAreas(i - 1)[1]
        sourceTxt = Icons.Icon({dung.name, type='dungeon', notext=true})
        typeTxt = '[[Dungeon]]'
      else
        sourceTxt = Icons.Icon({monster.name, type='monster'})
        typeTxt = '[[Monster]]'
      end
      table.insert(dropRows, {source = sourceTxt, type = typeTxt, minqty = minqty, qty = qty, weight = wt * lootChance, totalWeight = totalWt * 100})
    end
  end

  --Special exception for the Fire/Infernal Cape and first two lore books as bonus dungeon drops
  if sourceOverrides['Dungeon'][item.id] ~= nil then
    local sourceTxt = Icons.Icon({sourceOverrides['Dungeon'][item.id], type='dungeon', notext=true})
    table.insert(dropRows, {source=sourceTxt, type='[[Dungeon]]', minqty=1, qty=1, weight = 1, totalWeight = 1})
  end

  --Next: Can we find it by rummaging around in another item?
  for i, item2 in pairs(ItemData.Items) do
    if item2.dropTable ~= nil then
      local qty = 1
      local wt = 0
      local totalWt = 0
      for j, loot in pairs(item2.dropTable) do
        totalWt = totalWt + loot[2]
        if loot[1] == item.id then
          wt = loot[2]
          if item2.dropQty ~= nil then qty = item2.dropQty[j] end
        end
      end

      if wt > 0 then
        local sourceTxt = Icons.Icon({item2.name, type='item'})
        table.insert(dropRows, {source = sourceTxt, type = '[[Chest]]', minqty = 1, qty = qty, weight = wt, totalWeight = totalWt})
      end
    end
  end

  --Finally, let's try just stealing it
  local thiefType = Icons.Icon({"Thieving", type='skill'})
  for i, npc in pairs(SkillData.Thieving) do
    local qty = 1
    local wt = 0
    local totalWt = 0
    if npc.lootTable ~= nil then
      for j, loot in pairs(npc.lootTable) do
        totalWt = totalWt + loot[2]
        if loot[1] == item.id then
          wt = loot[2]
        end
      end
      if wt > 0 then
        -- There is a constant 75% chance of gaining an item per successful steal attempt
        local sourceTxt = Icons.Icon({npc.name, type='thieving'})
        table.insert(dropRows, {source = sourceTxt, type = thiefType, minqty = 1, qty = qty, weight = wt * 75, totalWeight = totalWt * 100})
      end
    end
  end

  --Bonus overtime: Special Fishing table & mining gem table. Also Rags to Riches
  if item.type == 'Gem' then
    local mineType = Icons.Icon({'Mining', type='skill'})
    local thisGemChance = Items.GemTable[item.name].chance
    local totalGemChance = 0
    for i, gem in pairs(Items.GemTable) do
      totalGemChance = totalGemChance + gem.chance
    end
    table.insert(dropRows, {source = '[[Mining#Gems|Gem]]', type = mineType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
    local magicType = Icons.Icon({'Magic', type = 'skill'})
    table.insert(dropRows, {source = Icons.Icon({"Rags to Riches I", type="spell"}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
    table.insert(dropRows, {source = Icons.Icon({"Rags to Riches II", type="spell"}), type = magicType, minqty = 1, qty = 1, weight = thisGemChance, totalWeight = totalGemChance})
  end

  if item.fishingCatchWeight ~= nil then
    local fishSource = '[[Fishing#Special|Special]]'
    local fishType = Icons.Icon({'Fishing', type='skill'})
    table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = item.fishingCatchWeight, totalWeight = Items.specialFishWt})
  end

  if item.type == 'Junk' then
    local fishSource = '[[Fishing#Junk|Junk]]'
    local fishType = Icons.Icon({'Fishing', type='skill'})
    table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, weight = 1, totalWeight = Items.junkCount})
  end

  --Make sure to return nothing if there are no drop sources
  if Shared.tableCount(dropRows) == 0 then return '' end

  table.sort(dropRows, function(a, b)
                         if a.weight / a.totalWeight == b.weight / b.totalWeight then
                           return a.minqty + a.qty > b.minqty + b.qty
                         else
                           return a.weight / a.totalWeight > b.weight / b.totalWeight
                         end
                       end)
  for i, data in pairs(dropRows) do
    table.insert(resultPart, buildRow(data.source, data.type, data.minqty, data.qty, data.weight, data.totalWeight))
  end

  table.insert(resultPart, '\r\n|}')
  return table.concat(resultPart)
end

function p.getItemLootSourceTable(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = Items.getItem(itemName)
  if item == nil then
    return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  end

  return p._getItemLootSourceTable(item)
end

function p._getItemUpgradeTable(item)
  local resultPart = {}
  if item.itemsRequired ~= nil then
    --First, get details on all the required materials
    local upgradeFrom = {}
    local materials = {}
    for i, row in pairs(item.itemsRequired) do
      local mat = Items.getItemByID(row[1])
      --Check to see if the source item can trigger the upgrade
      if mat.canUpgrade or (mat.type == 'Armour' and mat.canUpgrade == nil) then
        table.insert(upgradeFrom, Icons.Icon({mat.name, type='item'}))
      end
      table.insert(materials, Icons.Icon({mat.name, type='item', qty=row[2]}))
    end
    if item.trimmedGPCost ~= nil then
      table.insert(materials, Icons.GP(item.trimmedGPCost))
    end
    table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]')
    --[[result = result..'\r\n|-\r\n!style="text-align:right;"|Upgrades From\r\n|'
    result = result..table.concat(upgradeFrom, '<br/>')--]]
    table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials\r\n|')
    table.insert(resultPart, table.concat(materials, '<br/>'))
    table.insert(resultPart, '\r\n|}')
  end
  return table.concat(resultPart)
end

function p.getItemUpgradeTable(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = Items.getItem(itemName)
  if item == nil then
    return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  end

  return p._getItemUpgradeTable(item)
end

function p._getItemSuperheatTable(item)
  --Manually build the Superheat Item table
  local oreString = ''
  local coalString = ''
  for i, mat in pairs(item.smithReq) do
    local thisMat = Items.getItemByID(mat.id)
    if thisMat.name == 'Coal Ore' then
      coalString = ', '..Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
    else
      if string.len(oreString) > 0 then oreString = oreString..', ' end
      oreString =  oreString..Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
    end
  end
  --Set up the header
  local superheatTable = {}
  table.insert(superheatTable, '{|class="wikitable"\r\n!colspan="2"|Spell')
  table.insert(superheatTable, '!!'..Icons.Icon({'Smithing', type='skill', notext='true'})..' Level')
  table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' Level')
  table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' XP')
  table.insert(superheatTable, '!!'..Icons.Icon({item.name, type='item', notext='true'})..' Bars')
  table.insert(superheatTable, '!!Ore!!Runes')
   --Loop through all the variants
  local spellNames = {'Superheat I', 'Superheat II', 'Superheat III', 'Superheat IV'}
  for i, sName in pairs(spellNames) do
    local spell = Magic.getSpell(sName, 'AltMagic')
    local rowPart = {}
    table.insert(rowPart, '\r\n|-\r\n|'..Icons.Icon({spell.name, type='spell', notext=true, size=50}))
    table.insert(rowPart, '||[['..spell.name..']]||'..item.smithingLevel)
    table.insert(rowPart, '||'..spell.magicLevelRequired..'||'..spell.magicXP..'||'..spell.convertToQty)
    table.insert(rowPart, '||'..oreString)
    if spell.ignoreCoal ~= nil and not spell.ignoreCoal then table.insert(rowPart, coalString) end
    table.insert(rowPart, '||style="text-align:center"|')
    for i, req in pairs(spell.runesRequired) do
      local rune = Items.getItemByID(req.id)
      if i > 1 then table.insert(rowPart, ', ') end
      table.insert(rowPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
    end
    table.insert(rowPart, "<br/>'''OR'''<br/>")
    for i, req in pairs(spell.runesRequiredAlt) do
      local rune = Items.getItemByID(req.id)
      if i > 1 then table.insert(rowPart, ', ') end
      table.insert(rowPart, Icons.Icon({rune.name, type='item', notext=true, qty=req.qty}))
    end
    table.insert(superheatTable, table.concat(rowPart))
  end
   --Add the table end and add the table to the result string
  table.insert(superheatTable, '\r\n|}')
  return table.concat(superheatTable)
end

function p.getItemSuperheatTable(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = Items.getItem(itemName)
  if item == nil then
    return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  end

  return p._getItemSuperheatTable(item)
end

function p._getItemSourceTables(item)
  local resultPart = {}
  local shopTable = Shop._getItemShopTable(item)
  if string.len(shopTable) > 0 then
    table.insert(resultPart, '===Shop===\r\n'..shopTable)
  end

  local creationTable = p._getCreationTable(item)
  if string.len(creationTable) > 0 then
    if #resultPart > 0 then table.insert(resultPart, '\r\n') end
    table.insert(resultPart, '===Creation===\r\n'..creationTable)
  end

  local upgradeTable = p._getItemUpgradeTable(item)
  if string.len(upgradeTable) > 0 then
    if #resultPart > 0 then table.insert(resultPart, '\r\n') end
    if string.len(creationTable) == 0 then table.insert(resultPart, '===Creation===\r\n') end
    table.insert(resultPart, upgradeTable)
  end

  if item.type == 'Bar' then
    table.insert(resultPart, '\r\n==='..Icons.Icon({'Alt Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item))
  end

  local lootTable = p._getItemLootSourceTable(item)
  if string.len(lootTable) > 0 then
    if #resultPart > 0 then table.insert(resultPart, '\r\n') end
    table.insert(resultPart, '===Loot===\r\n'..lootTable)
  end
  return table.concat(resultPart)
end

function p.getItemSourceTables(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = Items.getItem(itemName)
  if item == nil then
    return "ERROR: No item named "..itemName.." exists in the data module[[Category:Pages with script errors]]"
  end

  return p._getItemSourceTables(item)
end

function p.getCombatPassiveSlotItems(frame)
  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable"\r\n')
  table.insert(resultPart, '|-\r\n')
  table.insert(resultPart, '!colspan="2"|Item\r\n! Passive\r\n')

  local itemArray = Items.getItems(function(item) return item.validSlots ~= nil and Shared.contains(item.validSlots, 'Passive') end)

  table.sort(itemArray, function(a, b) return a.id < b.id end)

  for i, item in Shared.skpairs(itemArray) do
    table.insert(resultPart, '|-\r\n')
    table.insert(resultPart, '! '..Icons.Icon({item.name, type='item', notext='true'})..'\r\n! [['..item.name..']]\r\n')
    table.insert(resultPart, '| '..item.description..'\r\n')
  end

  table.insert(resultPart, '|}')

  return table.concat(resultPart)
end

return p