Module:Navboxes: Difference between revisions

From Melvor Idle
(getRuneNavbox: Order results by level & improve resilience against future rune additions; getSkillcapeNavbox: Include missing max cape & consistency with shop table; getFishingNavbox: Implement to replace manually maintained table)
(getPrayerNavbox/getSpellNavbox: Order prayers/spells by level, in line with in-game appearance)
Line 188: Line 188:


function p.getPrayerNavbox(frame)
function p.getPrayerNavbox(frame)
   local prayers = {}
   local prayerList = {}
   for i, prayer in Shared.skpairs(SkillData.Prayer) do
   for i, prayer in Shared.skpairs(SkillData.Prayer) do
    table.insert(prayers, Icons.Icon({prayer.name, type="prayer"}))
  table.insert(prayerList, { ["name"] = prayer.name, ["order"] = prayer.prayerLevel })
   end
   end
   local result = '{| class="wikitable" style="margin:auto; clear:both; width: 100%"'
  table.sort(prayerList, function(a, b)
   result = result..'\r\n!'..Icons.Icon({'Prayer', 'Prayers', type='skill'})
                        if a.order == b.order then
   result = result..'\r\n|-\r\n|style="text-align:center;"|'..table.concat(prayers, ' • ')
                        return a.name < b.name
   result = result..'\r\n|}'
                        else
   return result
                        return a.order < b.order
                        end
                      end)
 
   local prayerListText = {}
  for i, prayer in ipairs(prayerList) do
  table.insert(prayerListText, Icons.Icon({ prayer.name, type='prayer' }))
  end
 
  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable" style="margin:auto; clear:both; width: 100%"')
   table.insert(resultPart, '\r\n!'..Icons.Icon({'Prayer', 'Prayers', type='skill'}))
   table.insert(resultPart, '\r\n|-\r\n|style="text-align:center;"| ' .. table.concat(prayerListText, ' • '))
   table.insert(resultPart, '\r\n|}')
   return table.concat(resultPart)
end
end


Line 257: Line 271:


function p.getSpellNavbox(frame)
function p.getSpellNavbox(frame)
   local spells = {}
   local spellTable = { ["standard"] = {}, ["curse"] = {}, ["aurora"] = {}, ["ancient"] = {}, ["alt"] = {} }
   local curses = {}
   local catData = {
  local auroras = {}
  { ["name"] = 'standard', ["header"] = '[[Magic#Standard_Magic|Standard Spells]]', ["imgType"] = 'spell' },
  local ancients = {}
  { ["name"] = 'curse', ["header"] = '[[Magic#Curses|Curses]]', ["imgType"] = 'curse' },
   local altmagic = {}
  { ["name"] = 'aurora', ["header"] = '[[Magic#Auroras|Auroras]]', ["imgType"] = 'aurora' },
  { ["name"] = 'ancient', ["header"] = '[[Magic#Ancient_Magicks|Ancient Magicks]]', ["imgType"] = 'spell' },
  { ["name"] = 'alt', ["header"] = '[[Alternative_Magic|Alt Magic]]', ["imgType"] = 'spell' },
   }


   for i, spell in Shared.skpairs(MagicData.Spells) do
   for i, spell in ipairs(MagicData.Spells) do
    table.insert(spells, Icons.Icon({spell.name, type='spell'}))
  table.insert(spellTable['standard'], { ["name"] = spell.name, ["order"] = spell.level })
  end
  for i, spell in ipairs(MagicData.Curses) do
  table.insert(spellTable['curse'], { ["name"] = spell.name, ["order"] = spell.level })
   end
   end
   for i, spell in Shared.skpairs(MagicData.Curses) do
   for i, spell in ipairs(MagicData.Auroras) do
    table.insert(curses, Icons.Icon({spell.name, type='curse'}))
  table.insert(spellTable['aurora'], { ["name"] = spell.name, ["order"] = spell.level })
   end
   end
   for i, spell in Shared.skpairs(MagicData.Auroras) do
   for i, spell in ipairs(MagicData.Ancient) do
    table.insert(auroras, Icons.Icon({spell.name, type='aurora'}))
  table.insert(spellTable['ancient'], { ["name"] = spell.name, ["order"] = spell.level })
   end
   end
   for i, spell in Shared.skpairs(MagicData.Ancient) do
   for i, spell in ipairs(MagicData.AltMagic) do
    table.insert(ancients, Icons.Icon({spell.name, type='spell'}))
  table.insert(spellTable['alt'], { ["name"] = spell.name, ["order"] = spell.level })
   end
   end
   for i, spell in Shared.skpairs(MagicData.AltMagic) do
    
    table.insert(altmagic, Icons.Icon({spell.name, type='spell'}))
  local getSpellList = function(spellTable, imgType)
      local listPart = {}
      for i, obj in ipairs(spellTable) do
        table.insert(listPart, Icons.Icon({obj.name, type=imgType}))
      end
      return table.concat(listPart, ' ')
    end
 
  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable" style="margin:auto; clear:both; width: 100%"')
  table.insert(resultPart, '\r\n!colspan=2|[[File:Magic_(skill).svg|25px|link=Spells]] [[Spells]]')
  for i, catDefn in ipairs(catData) do
  table.sort(spellTable[catDefn.name], function(a, b)
                                if a.order == b.order then
                                return a.name < b.name
                                else
                                return a.order < b.order
                                end
                              end)
  table.insert(resultPart, '\r\n|-\r\n!scope="row"| ' .. catDefn.header)
  table.insert(resultPart, '\r\n|style="text-align:center;| ' .. getSpellList(spellTable[catDefn.name], catDefn.imgType))
   end
   end
  table.insert(resultPart, '\r\n|}')


   local result = '{| class="wikitable" style="margin:auto; clear:both; width: 100%"'
   return table.concat(resultPart)
  result = result..'\r\n!colspan=2|[[File:Magic_(skill).svg|25px|link=Spells]] [[Spells]]'
  result = result..'\r\n|-\r\n!scope="row"|[[Magic#Standard_Magic|Standard Spells]]'
  result = result..'\r\n|style="text-align:center;|'..table.concat(spells, ' • ')
  result = result..'\r\n|-\r\n!scope="row"|[[Magic#Curses|Curses]]'
  result = result..'\r\n|style="text-align:center;|'..table.concat(curses, ' • ')
  result = result..'\r\n|-\r\n!scope="row"|[[Magic#Auroras|Auroras]]'
  result = result..'\r\n|style="text-align:center;|'..table.concat(auroras, ' • ')
  result = result..'\r\n|-\r\n!scope="row"|[[Magic#Ancient_Magicks|Ancient Magicks]]'
  result = result..'\r\n|style="text-align:center;|'..table.concat(ancients, ' • ')
  result = result..'\r\n|-\r\n!scope="row"|[[Alternative_Magic|Alt Magic]]'
  result = result..'\r\n|style="text-align:center;|'..table.concat(altmagic, ' • ')
  result = result..'\r\n|}'
 
  return result
end
end



Revision as of 21:08, 13 November 2021

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

-- New module to stop navbox generators cluttering other modules

local p = {}

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

local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Shop = require('Module:Shop')

function p.getFarmingNavbox(frame)
  local resultPart = {}
  local seedsTable = {}
  local produceTable = {}

  for i, item in ipairs(ItemData.Items) do
    if item.farmingLevel ~= nil then
      local tier = item.tier
      if seedsTable[tier] == nil then
        -- Initialise tier tables
        seedsTable[tier] = {}
        produceTable[tier] = {}
      end

      if item.grownItemID ~= nil then
        local grownItem = Items.getItemByID(item.grownItemID)
        if grownItem ~= nil then
          table.insert(produceTable[tier], { ["name"] = grownItem.name, ["level"] = item.farmingLevel })
        end
      end
      table.insert(seedsTable[tier], { ["name"] = item.name, ["level"] = item.farmingLevel })
    end
  end

  -- Generate output table
  table.insert(resultPart, '{| class="wikitable mw-collapsible" style="margin:auto; clear:both; width: 100%"')
  table.insert(resultPart, '\r\n!colspan="2" style="padding-left:64px;"|' .. Icons.Icon({'Farming', type='skill'}))

  local getItemList = function(itemTable)
      local listPart = {}
      for i, item in ipairs(itemTable) do
        table.insert(listPart, Icons.Icon({item.name, type='item'}))
      end
      return table.concat(listPart, ' • ')
    end
  local sortFunc = function(a, b) return (a.level == b.level and a.name < b.name) or a.level < b.level end

  -- Determine tier list & order in which tiers will be listed in output
  local tierList = {}
  for tier, seeds in pairs(seedsTable) do
    table.insert(tierList, tier)
  end
  table.sort(tierList, function(a, b) return a < b end)

  -- Generate table section for each tier
  for i, tier in pairs(tierList) do
    -- Sort tables by Farming level order
    table.sort(seedsTable[tier], sortFunc)
    table.sort(produceTable[tier], sortFunc)

    table.insert(resultPart, '\r\n|-\r\n!colspan="2"| ' .. tier .. 's')
    table.insert(resultPart, '\r\n|-\r\n!scope="row"| Seeds')
    table.insert(resultPart, '\r\n|style="text-align:center;"| ' .. getItemList(seedsTable[tier]))
    table.insert(resultPart, '\r\n|-\r\n!scope="row"| Produce')
    table.insert(resultPart, '\r\n|style="text-align:center;"| ' .. getItemList(produceTable[tier]))
  end
  table.insert(resultPart, '\r\n|}')

  return table.concat(resultPart)
end

function p.getFoodNavbox(frame)
  local foundIDs, cookedFood, harvestedFood, otherFood = {}, {}, {}, {}

  -- Hide Lemon cake
  foundIDs[1029] = true
  foundIDs[1061] = true

  -- Harvested food first
  for i, item in ipairs(ItemData.Items) do
    if item.grownItemID ~= nil then
      local grownItem = Items.getItemByID(item.grownItemID)
      if grownItem ~= nil and grownItem.canEat then
        table.insert(harvestedFood, { ["name"] = grownItem.name, ["order"] = item.farmingLevel })
        foundIDs[grownItem.id] = true
      end
    end
  end

  -- Any cooked & other food
  for i, item in ipairs(ItemData.Items) do
    -- If an item can be eaten then it must be food
    if foundIDs[i - 1] == nil and item.canEat then
      if item.cookingCategory ~= nil then
        -- Item is cooked, such food items are split by category
        if cookedFood[item.cookingCategory + 1] == nil then
          cookedFood[item.cookingCategory + 1] = {}
        end

        local perfectName = nil
        if item.perfectItem ~= nil then
          local perfectItem = Items.getItemByID(item.perfectItem)
          if perfectItem ~= nil then
            perfectName = perfectItem.name
            foundIDs[item.perfectItem] = true
          end
        end
        table.insert(cookedFood[item.cookingCategory + 1], { ["name"] = item.name, ["order"] = item.cookingLevel, ["perfectName"] = perfectName })
      else
        -- Item cannot be cooked or grown, but can be eaten
        table.insert(otherFood, { ["name"] = item.name, ["order"] = item.id })
      end
      foundIDs[i - 1] = true
    end
  end

  -- Sort food lists
  local sortFunc = function(a, b) return (a.order == b.order and a.name < b.name) or a.order < b.order end
  for i, items in pairs(cookedFood) do
    table.sort(cookedFood[i], sortFunc)
  end
  table.sort(harvestedFood, sortFunc)
  table.sort(otherFood, sortFunc)

  -- Generate food lists for final output
  local cookingCatHeader = {
    Icons.Icon({'Normal Cooking Fire', 'Cooking Fire', type='upgrade', nolink=true}),
    Icons.Icon({'Basic Furnace', 'Furnace', type='upgrade', nolink=true}),
    Icons.Icon({'Basic Pot', 'Pot', type='upgrade', nolink=true})
  }
  local getFoodList = function(foodTable)
      local listPart = {}
      for i, food in ipairs(foodTable) do
        local foodText = Icons.Icon({food.name, type='item'})
        if food.perfectName ~= nil then
          foodText = Icons.Icon({food.perfectName, type='item', notext=true}) .. ' ' .. foodText
        end
        table.insert(listPart, foodText)
      end
      return table.concat(listPart, ' • ')
    end

  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable mw-collapsible" style="margin:0 auto 10px; clear:both; width: 100%"')
  table.insert(resultPart, '\r\n|-\r\n!style="background-color:#275C87;color:#FFFFFF;padding-left:64px;" colspan="2"| [[File:Crab_(item).svg|25px|link=Food]] [[Food]]')
  table.insert(resultPart, '\r\n|-\r\n!colspan="2"| Cooked')
  for catID, foodTable in ipairs(cookedFood) do
    table.insert(resultPart, '\r\n|-\r\n!scope="row"| ' .. cookingCatHeader[catID])
    table.insert(resultPart, '\r\n|style="text-align:center;"| ' .. getFoodList(foodTable))
  end
  table.insert(resultPart, '\r\n|-\r\n!colspan="2"| Harvested')
  table.insert(resultPart, '\r\n|-\r\n|colspan="2" style="text-align:center;"| ' .. getFoodList(harvestedFood))
  table.insert(resultPart, '\r\n|-\r\n!colspan="2"| Other')
  table.insert(resultPart, '\r\n|-\r\n|colspan="2" style="text-align:center;"| ' .. getFoodList(otherFood))
  table.insert(resultPart, '\r\n|}')

  return table.concat(resultPart)
end

function p.getPotionNavbox(frame)
  local result = '{| class="wikitable" style="margin:auto; clear:both; width: 100%"'
  result = result..'\r\n!colspan=2|'..Icons.Icon({'Herblore', 'Potions', type='skill'})

  local CombatPots = {}
  local SkillPots = {}
  for i, potData in Shared.skpairs(SkillData.Herblore.ItemData) do
    if potData.category == 0 then
      table.insert(CombatPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
    else
      if potData.name == 'Bird Nests Potion' then
        table.insert(SkillPots, Icons.Icon({"Bird Nest Potion", type='item', img="Bird Nest Potion I"}))
      else
        table.insert(SkillPots, Icons.Icon({potData.name, type='item', img=(potData.name..' I')}))
      end
    end
  end

  result = result..'\r\n|-\r\n!Combat Potions\r\n|class="center" style="vertical-align:middle;"'
  result = result..'|'..table.concat(CombatPots, ' • ')
  result = result..'\r\n|-\r\n!Skill Potions\r\n|class="center" style="vertical-align:middle;"'
  result = result..'|'..table.concat(SkillPots, ' • ')
  result = result..'\r\n|}'
  return result
end

function p.getPrayerNavbox(frame)
  local prayerList = {}
  for i, prayer in Shared.skpairs(SkillData.Prayer) do
  	table.insert(prayerList, { ["name"] = prayer.name, ["order"] = prayer.prayerLevel })
  end
  table.sort(prayerList, function(a, b)
  	                       if a.order == b.order then
  	                       	 return a.name < b.name
  	                       else
  	                       	 return a.order < b.order
  	                       end
  	                     end)

  local prayerListText = {}
  for i, prayer in ipairs(prayerList) do
  	table.insert(prayerListText, Icons.Icon({ prayer.name, type='prayer' }))
  end
  
  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable" style="margin:auto; clear:both; width: 100%"')
  table.insert(resultPart, '\r\n!'..Icons.Icon({'Prayer', 'Prayers', type='skill'}))
  table.insert(resultPart, '\r\n|-\r\n|style="text-align:center;"| ' .. table.concat(prayerListText, ' • '))
  table.insert(resultPart, '\r\n|}')
  return table.concat(resultPart)
end

function p.getRuneNavbox(frame)
  -- Assumes all runes are from Runecrafting, which may need revising in future updates
  local runeList = { ["Standard"] = {}, ["Combination"] = {} }
  for i, item in ipairs(ItemData.Items) do
    if item.category == 'Runecrafting' and item.type ~= nil and item.type == 'Rune' and item.runecraftingLevel ~= nil then
      local runeType = (type(item.providesRune) == 'table' and Shared.tableCount(item.providesRune) > 1 and 'Combination') or 'Standard'
      table.insert(runeList[runeType], { ["name"] = item.name, ["order"] = item.runecraftingLevel })
    end
  end

  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable" style="margin:auto; clear:both; width: 100%"')
  table.insert(resultPart, '\r\n!colspan="2"|[[File:Air_Rune_(item).svg|25px|link=Runes]] [[Runes]]')
  for i, cat in ipairs({'Standard', 'Combination'}) do
    table.sort(runeList[cat], function(a, b) return (a.order == b.order and a.name < b.name) or a.order < b.order end)
    table.insert(resultPart, '\r\n|-\r\n!scope="row"|' .. cat .. ' Runes')

    local listPart = {}
    for j, rune in ipairs(runeList[cat]) do
      table.insert(listPart, Icons.Icon({rune.name, type='item'}))
    end
    table.insert(resultPart, '\r\n|style="text-align:center;"|'..table.concat(listPart, ' • '))
  end
  table.insert(resultPart, '\r\n|}')

  return table.concat(resultPart)
end

function p.getSkillcapeNavbox(frame)
  local capeList = Shop.getPurchases(function(cat, purch) return cat == 'Skillcapes' end)
  table.sort(capeList, function(a, b)
                         if a.cost.gp == b.cost.gp then
                           return a.name < b.name
                         else
                           return a.cost.gp < b.cost.gp
                         end
                       end)

  local capeText = {}
  for i, purch in ipairs(capeList) do
    if purch.contains ~= nil and purch.contains.items ~= nil then
      local item = Items.getItemByID(purch.contains.items[1][1])
      if item ~= nil then
        table.insert(capeText, Icons.Icon({item.name, type='item'}))
      end
    end
  end

  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable" style="margin:auto; clear:both; width: 100%"')
  table.insert(resultPart, '\r\n![[File:Cape_of_Completion_(item).svg|25px|link=Skillcapes]] [[Skillcapes]]')
  table.insert(resultPart, '\r\n|-\r\n|style="text-align:center;"|'..table.concat(capeText, ' • '))
  table.insert(resultPart, '\r\n|}')

  return table.concat(resultPart)
end

function p.getSpellNavbox(frame)
  local spellTable = { ["standard"] = {}, ["curse"] = {}, ["aurora"] = {}, ["ancient"] = {}, ["alt"] = {} }
  local catData = {
  	{ ["name"] = 'standard', ["header"] = '[[Magic#Standard_Magic|Standard Spells]]', ["imgType"] = 'spell' },
  	{ ["name"] = 'curse', ["header"] = '[[Magic#Curses|Curses]]', ["imgType"] = 'curse' },
  	{ ["name"] = 'aurora', ["header"] = '[[Magic#Auroras|Auroras]]', ["imgType"] = 'aurora' },
  	{ ["name"] = 'ancient', ["header"] = '[[Magic#Ancient_Magicks|Ancient Magicks]]', ["imgType"] = 'spell' },
  	{ ["name"] = 'alt', ["header"] = '[[Alternative_Magic|Alt Magic]]', ["imgType"] = 'spell' },
  }

  for i, spell in ipairs(MagicData.Spells) do
  	table.insert(spellTable['standard'], { ["name"] = spell.name, ["order"] = spell.level })
  end
  for i, spell in ipairs(MagicData.Curses) do
  	table.insert(spellTable['curse'], { ["name"] = spell.name, ["order"] = spell.level })
  end
  for i, spell in ipairs(MagicData.Auroras) do
  	table.insert(spellTable['aurora'], { ["name"] = spell.name, ["order"] = spell.level })
  end
  for i, spell in ipairs(MagicData.Ancient) do
  	table.insert(spellTable['ancient'], { ["name"] = spell.name, ["order"] = spell.level })
  end
  for i, spell in ipairs(MagicData.AltMagic) do
  	table.insert(spellTable['alt'], { ["name"] = spell.name, ["order"] = spell.level })
  end
  
  local getSpellList = function(spellTable, imgType)
      local listPart = {}
      for i, obj in ipairs(spellTable) do
        table.insert(listPart, Icons.Icon({obj.name, type=imgType}))
      end
      return table.concat(listPart, ' • ')
    end
  
  local resultPart = {}
  table.insert(resultPart, '{| class="wikitable" style="margin:auto; clear:both; width: 100%"')
  table.insert(resultPart, '\r\n!colspan=2|[[File:Magic_(skill).svg|25px|link=Spells]] [[Spells]]')
  for i, catDefn in ipairs(catData) do
  	table.sort(spellTable[catDefn.name], function(a, b)
  		                              if a.order == b.order then
  		                              	return a.name < b.name
  		                              else
  		                              	return a.order < b.order
  		                              end
  		                            end)
  	table.insert(resultPart, '\r\n|-\r\n!scope="row"| ' .. catDefn.header)
  	table.insert(resultPart, '\r\n|style="text-align:center;| ' .. getSpellList(spellTable[catDefn.name], catDefn.imgType))
  end
  table.insert(resultPart, '\r\n|}')

  return table.concat(resultPart)
end

function p.getFamiliarNavbox(frame)
  local familiars = Items.getItems(function(item) return item.type == 'Familiar' end)
  table.sort(familiars, function(a, b) return a.summoningLevel < b.summoningLevel end)

  local result = '{| class="wikitable" style="margin:auto; clear:both; width: 100%"'
  result = result..'\r\n!colspan=2|[[File:Summoning_(skill).svg|25px|link=Summoning]] [[Summoning|Summoning Familiars]]'
  local iconArray = {}
  for i, fam in Shared.skpairs(familiars) do
    table.insert(iconArray, Icons.Icon({fam.name, type='item'}))
  end
  result = result..'\r\n|-\r\n|style="text-align:center;"|'..table.concat(iconArray, ' • ')
  result = result..'\r\n|}'
  return result
end

function p.getThievingNavbox()
  local returnPart = {}

  -- Create table header
  table.insert(returnPart, '{| class="wikitable" style="text-align:center; clear:both; margin:auto; margin-bottom:1em;"')
  table.insert(returnPart, '|-\r\n!' .. Icons.Icon({'Thieving', type='skill', notext=true}) .. '[[Thieving|Thieving Targets]]')
  table.insert(returnPart, '|-\r\n|')

  local npcData = {}
  for i, npc in ipairs(SkillData.Thieving.NPCs) do
    table.insert(npcData, {["level"] = npc.level, ["name"] = npc.name})
  end
  table.sort(npcData, function(a, b) return a.level < b.level end)

  local npcList = {}
  -- Create row for each NPC
  for i, npc in ipairs(npcData) do
    table.insert(npcList, Icons.Icon({npc.name, type='thieving'}))
  end
  table.insert(returnPart, table.concat(npcList, ' • '))
  table.insert(returnPart, '|}')

  return table.concat(returnPart, '\r\n')
end

function p.getFishingNavbox()
  local categoryHeader = {}
  local categoryItems = {}
  local addCatData = function(cat, catLink, itemName, itemOrder)
                       if categoryItems[cat] == nil then
                         -- Initialise category
                         table.insert(categoryHeader, { ["name"] = cat, ["link"] = catLink })
                         categoryItems[cat] = {}
                       end
                       table.insert(categoryItems[cat], { ["name"] = itemName, ["order"] = itemOrder })
                     end

   -- Identify fishing catchable items
   local fishingToItemID = {}
   local junkItems = {}
   local specialItems = {}
   for i, item in ipairs(ItemData.Items) do
     if item.fishingID ~= nil then
       -- Create FishingID to item map
       fishingToItemID[item.fishingID] = item
     elseif item.category == 'Fishing' and item.type == 'Junk' then
       table.insert(junkItems, item)
     elseif item.fishingCatchWeight ~= nil then
       table.insert(specialItems, item)
     end
   end
  -- Fishing areas
  -- Iterate through all fishing areas, identifying fish within each
  for i, area in ipairs(SkillData.Fishing.Areas) do
    for j, fishID in ipairs(area.fish) do
      local fishItem = fishingToItemID[fishID]
      if fishItem ~= nil then
        addCatData(area.name, 'Fishing#Fishing Areas', fishItem.name, fishItem.fishingLevel)
      end
    end
  end
  -- Junk items
  for i, item in ipairs(junkItems) do
    addCatData('Junk', 'Fishing#Junk', item.name, 1)
  end
  -- Special items
  for i, item in ipairs(specialItems) do
    addCatData('Special Items', 'Fishing#Special', item.name, 1 / (item.fishingCatchWeight or 1))
  end

  local resultPart = {}
  -- Generate navbox header
  table.insert(resultPart, '{| class="wikitable" style="margin:auto; clear:both; width: 100%"')
  table.insert(resultPart, '\r\n|-\r\n!colspan="2" | ' .. Icons.Icon({'Fishing', type='skill'}))
  -- Generate section for each fishing area, junk, and special
  for i, cat in ipairs(categoryHeader) do
    local itemList = {}
    if categoryItems[cat.name] ~= nil then
      table.sort(categoryItems[cat.name], function(a, b) return (a.order == b.order and a.name < b.name) or a.order < b.order end)
      for j, item in ipairs(categoryItems[cat.name]) do
        table.insert(itemList, Icons.Icon({item.name, type='item'}))
      end
    end

    table.insert(resultPart, '\r\n|-\r\n!class="center" style="min-width:140px" | [[' .. (cat.link or cat.name) .. '|' .. cat.name .. ']]')
    table.insert(resultPart, '\r\n| class="center" style="vertical-align:middle;" | ' .. table.concat(itemList, ' • '))
  end
  table.insert(resultPart, '\r\n|}')

  return table.concat(resultPart)
end

return p