Module:Skills/Artisan: Difference between revisions

From Melvor Idle
m (Amend indentation)
m (Add perfect sell value to cooking table)
 
(82 intermediate revisions by 6 users not shown)
Line 2: Line 2:
--Contains function for skills that consume resources (ie smithing, cooking, herblore, etc.)
--Contains function for skills that consume resources (ie smithing, cooking, herblore, etc.)
local p = {}
local p = {}
local SkillData = mw.loadData('Module:Skills/data')


local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Skills = require('Module:Skills')
local Items = require('Module:Items')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local ItemSourceTables = require('Module:Items/SourceTables')
local Num = require('Module:Number')


function p.getCookedItemsTable(frame)
function p.getCookedItemsTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local args = frame.args ~= nil and frame.args or frame
local itemArray = nil
local category = args[1]
local realmName = args.realm
if category == "Cooking Fire" then
local realm = Skills.getRealmFromName(realmName)
itemArray = Items.getItems(function(item) return item.cookingCategory == 0 end)
if realm == nil then
elseif category == "Furnace" then  
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
itemArray = Items.getItems(function(item) return item.cookingCategory == 1 and item.name ~= 'Lemon Cake' end)
elseif category == "Pot" then
itemArray = Items.getItems(function(item) return item.cookingCategory == 2 end)
else
itemArray = Items.getItems(function(item) return item.cookingCategory ~= nil and item.name ~= 'Lemon Cake' end)
end
end
table.sort(itemArray, function(a, b) return a.cookingLevel < b.cookingLevel end)
local skillID = 'Cooking'
 
-- Logic for generating some cells of the table which are consistent for normal & perfect items
local categoryMap = {
local getHealingCell = function(item, qty)
["Cooking Fire"] = 'melvorD:Fire',
if item ~= nil then
["Furnace"] = 'melvorD:Furnace',
return 'data-sort-value="'..(math.floor(item.healsFor) * qty)..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..math.floor(item.healsFor * 10)..(qty > 1 and ' (x'..qty..')' or '')
["Pot"] = 'melvorD:Pot'
else
}
return ' '
local categoryID = categoryMap[category]
 
-- Find recipes for the relevant categories
-- Note: Excludes Lemon cake
local recipeArray = GameData.getEntities(SkillData.Cooking.recipes,
function(recipe)
return (
(categoryID == nil or recipe.categoryID == categoryID)
and recipe.noMastery == nil
and Skills.getRecipeRealm(recipe) == realm.id
)
end
end
end
)
local getSaleValueCell = function(item, qty)
table.sort(recipeArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
if item ~= nil then
 
return 'data-sort-value="'..(math.floor(item.sellsFor) * qty)..'"|'..Icons.GP(math.floor(item.sellsFor))..(qty > 1 and ' (x'..qty..')' or '')
local cookIcon = Icons._SkillRealmIcon('Cooking', realm.id)
else
local perfectIcon = Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true, size=20})
return ' '
local healIcon = Icons.Icon({"Hitpoints", type="skill", notext=true, nolink=true, size=20})
end
-- Only include time for cooking fire, as the other options have a fixed interval.
end
local includeTime = category == "Cooking Fire"
local result = '{| class="wikitable sortable stickyHeader"'
local html = mw.html.create('table')
result = result..'\r\n|- class="headerRow-0"'
:addClass('wikitable sortable stickyHeader')
result = result..'\r\n!colspan="3" rowspan="2"|Cooked Item!!rowspan="2"|'..Icons.Icon({'Cooking', type='skill', notext=true})..' Level'
result = result..'!!rowspan="2"|Cook Time!!rowspan="2"|XP!!colspan="2"|Healing!!colspan="2"|Value!!rowspan="2"|Ingredients'
result = result..'\r\n|- class="headerRow-1"'
result = result..'\r\n!Normal!!' .. Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true}) .. '!!Normal!!' .. Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true})
for i, item in Shared.skpairs(itemArray) do
local header0 = html:tag('tr'):addClass("headerRow-0")
header0 :tag('th'):wikitext('Cooked Item')
  :attr('colspan', 3)
  :attr('rowspan', 2)
header0 :tag('th'):wikitext(cookIcon .. '<br>Level')
      :attr('rowspan', 2)
header0 :tag('th'):wikitext('[[DLC]]')
  :attr('rowspan', 2)
if includeTime == true then
header0 :tag('th'):wikitext('Cook<br>Time (s)')
  :attr('rowspan', 2)
end  
header0 :tag('th'):wikitext(cookIcon .. '<br>XP')
  :attr('rowspan', 2)
header0 :tag('th'):wikitext('XP/s')
  :attr('rowspan', 2)
header0 :tag('th'):wikitext('Total Healing')
  :attr('colspan', 2)
header0 :tag('th'):wikitext('Ingredients')
  :attr('rowspan', 2)
header0 :tag('th'):wikitext('Value')
  :attr('colspan', 2)
 
html:tag('tr'):addClass("headerRow-1")
:tag('th'):wikitext('Normal')
:tag('th'):wikitext('Perfect')
:tag('th'):wikitext('Normal')
:tag('th'):wikitext('Perfect')
 
for i, recipe in ipairs(recipeArray) do
local level = Skills.getRecipeLevel(skillID, recipe)
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
local baseInt = recipe.baseInterval / 1000
local xpRate = baseXP / baseInt
local item = Items.getItemByID(recipe.productID)
local currency = item.sellsForCurrency or 'melvorD:GP'
local perfectItem = nil
local perfectItem = nil
if item.perfectItem ~= nil then
if recipe.perfectCookID ~= nil then
perfectItem = Items.getItemByID(item.perfectItem)
perfectItem = Items.getItemByID(recipe.perfectCookID)
end
end
local qty = item.cookingQty
local qty = recipe.baseQuantity or 1
if qty == nil then
local normalHeal = math.floor(item.healsFor * 10) * qty
qty = 1
local perfectHeal = math.floor(perfectItem.healsFor * 10) * qty
end
result = result..'\r\n|-'
local row = html:tag('tr')
result = result..'\r\n|style="min-width:25px"|'..Icons.Icon({item.name, type='item', notext=true, size='50'})
row:tag('td'):wikitext(Icons.Icon({item.name, type='item', notext=true}))
result = result..'\r\n|style="min-width:25px"| '
:addClass("table-img")
 
if perfectItem ~= nil then
if perfectItem ~= nil then
result = result..Icons.Icon({perfectItem.name, type='item', notext=true, size='50'})
row:tag('td'):wikitext(Icons.Icon({perfectItem.name, type='item', notext=true}))
:addClass("table-img")
end
end
result = result..'||'
local nameCell = row:tag('td')
nameCell:wikitext('[[' .. item.name .. ']]')
if qty > 1 then
if qty > 1 then
result = result..qty..'x '
nameCell:wikitext(' x' .. qty)
end
row:tag('td'):wikitext(level)
:css('text-align', 'center')
row:tag('td'):wikitext(Icons.getDLCColumnIcon(item.id))
:css('text-align', 'center')
:attr('data-sort-value', Icons.getExpansionID(item.id))
if includeTime == true then
row:tag('td'):wikitext(Num.round(baseInt, 2, 0) .. 's')
:css('text-align', 'right')
:attr('data-sort-value', baseInt)
end
end
result = result..Icons.Icon({item.name, type='item', noicon = true})
row:tag('td'):wikitext(Num.formatnum(baseXP))
result = result..'||style="text-align:right"|'..item.cookingLevel
:css('text-align', 'right')
result = result..'||style="text-align:right" data-sort-value="' .. item.cookingInterval .. '"|'..Shared.timeString(item.cookingInterval / 1000, true)
:attr('data-sort-value', baseXP)
result = result..'||style="text-align:right"|'..item.cookingXP
row:tag('td'):wikitext(Num.formatnum(Num.round(xpRate, 0, 0)))
result = result..'||'..getHealingCell(item, qty)..'||'..getHealingCell(perfectItem, qty)
:css('text-align', 'right')
result = result..'||'..getSaleValueCell(item, qty)..'||'..getSaleValueCell(perfectItem, qty)
:attr('data-sort-value', xpRate)
row:tag('td'):wikitext(healIcon .. ' ')
:wikitext(Num.formatnum(normalHeal))
:attr('data-sort-value', normalHeal)
row:tag('td'):wikitext(perfectIcon .. ' ')
:wikitext(Num.formatnum(perfectHeal))
:attr('data-sort-value', perfectHeal)
local ingrCell = row:tag('td')
local matArray = {}
local matArray = {}
for j, reqSet in pairs(item.recipeRequirements) do
for j, mat in ipairs(recipe.itemCosts) do
for k, mat in pairs(reqSet) do
local matItem = Items.getItemByID(mat.id)
local matItem = Items.getItemByID(mat.id)
if matItem ~= nil then
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.qty}))
local sub = mw.html.create('sub')
:wikitext(mat.quantity .. 'x')
:addClass('item-qty')
:done()
ingrCell:node(sub)
ingrCell:wikitext(Icons.Icon({matItem.name, type='item', notext=true}))
end
end
end
end
result = result..'\r\n|'..table.concat(matArray, ' ')
row:tag('td'):wikitext(Icons._Currency(currency, item.sellsFor))
:css('text-align', 'right')
row:tag('td'):wikitext(Icons._Currency(currency, perfectItem.sellsFor))
:css('text-align', 'right')
end
end


result = result..'\r\n|}'
return tostring(html)
return result
end
end


local tierSuffix = { 'I', 'II', 'III', 'IV' }
local tierSuffix = { 'I', 'II', 'III', 'IV' }
function p._getHerblorePotionTable(category)
function p._getPotionDescription(potion)
if string.upper(category) == 'COMBAT' then
-- TODO: Temporary fix below for incorrect Traps Potion descriptions. To amend
category = 0
-- once corrected within game data
elseif string.upper(category) == 'SKILL' then
if potion.customDescription and not Shared.contains(potion.id, 'melvorTotH:Traps_Potion_') then
category = 1
return potion.customDescription
elseif type(category) == 'string' then
elseif type(potion.modifiers) == 'table' and not Shared.tableIsEmpty(potion.modifiers) then
category = tonumber(category)
return Modifiers.getModifiersText(potion.modifiers, false, true)
else
return ''
end
end
end


local potionArray = {}
function p._getHerblorePotionTable(categoryName)
for i, potion in Shared.skpairs(SkillData.Herblore.ItemData) do
local skillID = 'Herblore'
if potion.category == category then
local category = GameData.getEntityByName(SkillData.Herblore.categories, categoryName)
table.insert(potionArray, potion)
if category == nil then
local catNames = {}
for i, cat in pairs(SkillData.Herblore.categories) do
table.insert(catNames, cat.name)
end
end
return Shared.printError('No such category ' .. categoryName .. ', the following are available: ' .. table.concat(catNames, ', '))
end
end
local categoryID = category.id


local result = '{|class = "wikitable sortable stickyHeader"'
local potionArray = GameData.getEntities(SkillData.Herblore.recipes,
result = result..'\r\n|- class="headerRow-0"'
function(potion)
result = result..'\r\n!Potion!!'..Icons.Icon({'Herblore', type='skill', notext=true})..' Level'
-- Category implies a realm selection, so no ned to check this separately
result = result..'!!XP!!Ingredients!!colspan="2"|Tier!!Value!!Charges!!Effect'
return potion.categoryID == categoryID
end
)


table.sort(potionArray, function(a, b) return a.level < b.level end)
table.sort(potionArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)


for i, potion in Shared.skpairs(potionArray) do
local html = mw.html.create('table')
local tierPots = {}
:addClass('wikitable sortable stickyHeader mw-collapsible')
for j = 1, 4, 1 do
table.insert(tierPots, Items.getItemByID(potion.itemID[j]))
html:tag('tr'):addClass('headerRow-0')
end
:tag('th'):wikitext('Potion')
result = result..'\r\n|-'
    :tag("th"):wikitext(Icons.Icon({'Herblore', type='skill', notext=true}) .. '<br>Level')
if potion.name == 'Bird Nests Potion' then
    :tag('th'):wikitext('XP')
result = result..'\r\n|rowspan="4"|[[Bird Nest Potion]]'
    :tag('th'):wikitext('Ingredients')
else
    :tag('th'):wikitext('[[DLC]]')
result = result..'\r\n|rowspan="4"|[['..potion.name..']]'
    :tag('th'):wikitext('Tier')
end
          :css('width', '30px')
result = result..'||rowspan="4" style="text-align:right"|'..potion.level
    --:tag('th'):wikitext('Value')
result = result..'||rowspan="4" style="text-align:right"|'..potion.herbloreXP
    :tag('th'):wikitext('Charges')
    :tag('th'):wikitext('Effect')


local matArray = {}
for i, potion in ipairs(potionArray) do
for j, mat in Shared.skpairs(tierPots[1].herbloreReq) do
local level = Skills.getRecipeLevel(skillID, potion)
local matItem = Items.getItemByID(mat.id)
local baseXP = potion.baseAbyssalExperience or potion.baseExperience
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.qty}))
local reqText = Skills.getRecipeRequirementText(SkillData.Herblore.name, potion)
local costText = Common.getCostString({
["items"] = potion.itemCosts,
["currencies"] = potion.currencyCosts
}, 'N/A', nil, '<br>')
-- Prefetch potion items, as these are required for the overall row
-- as well as the tier rows
local potionItems, lastPotionIdx = {}, 1
for j, potionID in ipairs(potion.potionIDs) do
potionItems[j] = Items.getItemByID(potionID)
lastPotionIdx = j
end
end
result = result..'||rowspan="4"|'..table.concat(matArray, ', ')..'||'
local tierRows = {}
local row = html:tag('tr')
for j, tierPot in Shared.skpairs(tierPots) do
row:tag('td'):wikitext(Icons.Icon({potionItems[lastPotionIdx].name, type='item', notext=true, nolink=true}))
local rowTxt = Icons.Icon({tierPot.name, type='item', notext=true})
:attr('rowspan', 4):wikitext('[['..potion.name..']]')
rowTxt = rowTxt..'||'..Icons.Icon({tierPot.name, tierSuffix[j], type = 'item', noicon = true})
row:tag('td'):attr('rowspan', 4):wikitext(level)
rowTxt = rowTxt..'||style="text-align:right;" data-sort-value="'..tierPot.sellsFor..'"|'..Icons.GP(tierPot.sellsFor)
:css('text-align', 'center')
rowTxt = rowTxt..'||style="text-align:right;"|'..tierPot.potionCharges..'||'..tierPot.description
row:tag('td'):attr('rowspan', 4):wikitext(Num.formatnum(baseXP))
table.insert(tierRows, rowTxt)
:attr('data-sort-value', baseXP)
:css('text-align', 'right')
row:tag('td'):attr('rowspan', 4):wikitext(costText)
row:tag('td'):attr('rowspan', 4):wikitext(Icons.getDLCColumnIcon(potion.potionIDs[1]))
:css('text-align', 'center')
:attr('data-sort-value', Icons.getExpansionID(potion.potionIDs[1]))
 
for j, potionID in ipairs(potion.potionIDs) do
local tierPot = potionItems[j]
-- First row needs to be added to the main row of the rowspan row.
-- The other 3 need to be added to the main table as new rows.
local tierRow = j == 1 and row or html:tag('tr')
tierRow:tag('td'):wikitext(Icons.Icon({tierPot.name, type='item', notext=true}))
        :wikitext('[[' .. tierPot.name .. '|' .. tierSuffix[j] .. ']]')
--tierRow:tag('td'):wikitext(Items.getValueText(tierPot))
--        :css('text-align', 'right')
--        :attr('data-sort-value', tierPot.sellsFor)
tierRow:tag('td'):wikitext(tierPot.charges)
    :css('text-align', 'right')
tierRow:tag('td'):wikitext(p._getPotionDescription(tierPot))
end
end
result = result..table.concat(tierRows, '\r\n|-\r\n|')
end
end


result = result..'\r\n|}'
return tostring(html)
return result
end
end


function p.getHerblorePotionTable(frame)
function p.getHerblorePotionTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local args = frame.args ~= nil and frame.args or frame
local category = args[1]
return p._getHerblorePotionTable(category)
return p._getHerblorePotionTable(category)
end
end


function p.getRunecraftingTable(frame)
-- Returns the icon of the past potion found in potion.PotiodIDs
local category = frame.args ~= nil and frame.args[1] or frame
-- This is usually the IV potion.
local data = nil
function p._getHerblorePotionIcon(potion)
local lastPot = nil
for k, v in ipairs(potion.potionIDs) do
lastPot = v
end
local pot = Items.getItemByID(lastPot)
return Icons.Icon({pot.name, type='item', notext=true, size='25'})
end


if    category == "Runes"      then data =  
function p._getHerblorePotionSlimTable(realmID)
Items.getItems(function(item) return item.runecraftingCategory == 0 end)
local skillID = 'Herblore'
elseif category == "ComboRunes" then data =
local realmIcon = Icons._SkillRealmIcon('Herblore', realmID)
Items.getItems(function(item) return item.runecraftingCategory == 1 end)
local potions = GameData.getEntities(SkillData.Herblore.recipes,
elseif category == "Weapons"    then data =
function(x)
Items.getItems(function(item) return item.runecraftingCategory == 2 and item.runecraftingID end)
return Skills.getRecipeRealm(x) == realmID
elseif category == "AirGear"    then data =  
end
Items.getItems(function(item) return item.runecraftingCategory == 3 end)
)
elseif category == "WaterGear" then data =
table.sort(potions, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
Items.getItems(function(item) return item.runecraftingCategory == 4 end)
elseif category == "EarthGear"  then data =
local function getIngredients(potion)
Items.getItems(function(item) return item.runecraftingCategory == 5 end)
    local herbs = {}
elseif category == "FireGear"  then data =  
    local secondaries = {}
Items.getItems(function(item) return item.runecraftingCategory == 6 end)
   
    -- Grab secondaries and split herb.
    if type(potion.itemCosts) == 'table' then
    for k, v in pairs(potion.itemCosts) do
    local ingredientID = v.id
    local item = GameData.getEntityByID('items', v.id)
    local icon = Icons.Icon({item.name, type='item', qty=v.quantity})
if ingredientID ~= nil and string.sub(ingredientID, -5) == "_Herb" then
table.insert(herbs, icon)
else
table.insert(secondaries, icon)
end
    end
    end
   
    -- Format currencies as normal
    if type(potion.currencies) == 'table' then
    for k, v in pairs(potion.currencies) do
    local currID = v.id or v.currencyID
    table.insert(secondaries, Icons._Currency(currID, v.quantity))
    end
    end
   
    return {
    herbs = table.concat(herbs, '<br>'),
    secondaries = table.concat(secondaries, '<br>')
    }
end
end


if data == nil then
local tbl = mw.html.create("table")
return "ERROR: Invalid Runecrafting category name.[[Category:Pages with script errors]]"
        :addClass("wikitable sortable stickyHeader mw-collapsible mw-collapsed")
       
    -- Add header
    tbl :tag("tr"):addClass("headerRow-0")
    :tag('th'):wikitext("Potion")
      :attr('colspan', 2)
    :tag('th'):wikitext('[[DLC]]')
    :tag('th'):wikitext(realmIcon .. '<br>Level')
    :tag('th'):wikitext(realmIcon .. '<br>XP')
    :tag('th'):wikitext('Herb')
    :tag('th'):wikitext('Secondary')
    --:tag('th'):wikitext('Effect')
    :done()
 
    for i, potion in ipairs(potions) do
local level = Skills.getRecipeLevel(skillID, potion)
local xp = potion.baseAbyssalExperience or potion.baseExperience
local ingreds = getIngredients(potion)
local row = tbl:tag('tr')
 
row:tag('td'):wikitext(p._getHerblorePotionIcon(potion))
            :css('text-align', 'center')
row:tag('td'):wikitext('[['..potion.name..']]')
row:tag('td'):wikitext(Icons.getDLCColumnIcon(potion.potionIDs[1]))
:css('text-align', 'center')
:attr('data-sort-value', Icons.getExpansionID(potion.potionIDs[1]))
row:tag('td'):wikitext(level)
:css('text-align', 'center')
row:tag('td'):wikitext(Num.formatnum(xp))
:css('text-align', 'right')
row:tag('td'):wikitext(ingreds.herbs)
row:tag('td'):wikitext(ingreds.secondaries)
    end
 
return tostring(tbl)
end
 
function p.getHerblorePotionSlimTable(frame)
local args = frame.args ~= nil and frame.args or frame
local realmName = args.realm or 'Melvor Realm'
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
end


local result = '{| class="wikitable sortable stickyHeader"'
return p._getHerblorePotionSlimTable(realm.id)
result = result..'\r\n|- class="headerRow-0"'
end
result = result..'\r\n!Item\r\n!Name\r\n!'..Icons.Icon({'Runecrafting', type='skill', notext=true})..' Level\r\n!Experience'
 
result = result..'\r\n!Item Price\r\n!Ingredients\r\n!XP/s\r\n!GP/s'
function p._getHerbloreHerbTable(realmID)
local allHerbs = {}


table.sort(data, function(a, b) return (a.runecraftingLevel == b.runecraftingLevel and a.id < b.id)
-- Finds the herb from a potion along with the level required to make the potion.
or a.runecraftingLevel < b.runecraftingLevel end)
local function handlePotion(potion)
local potionCosts = potion.itemCosts
local level = Skills.getRecipeLevel('Herblore', potion)


for i, rune in Shared.skpairs(data) do
if potionCosts == nil or level == nil then
result = result..'\r\n|-'
return
result = result..'\r\n| style="text-align: left;" | '..Icons.Icon({rune.name, type='item', size='50', notext=true})
end
result = result..'\r\n| style ="text-align: left;" |'..Icons.Icon({rune.name, type='item', noicon=true})
result = result..'\r\n| style="text-align:right"|'..rune.runecraftingLevel
result = result..'\r\n| style="text-align:right"|'..rune.runecraftingXP
result = result..'\r\n| style="text-align:right"|'..rune.sellsFor


local matArray = {}
-- Find if this potion uses a herb, and which herb it is.
for j, mat in Shared.skpairs(rune.runecraftReq) do
for _, ingredient in ipairs(potionCosts) do
local matItem = Items.getItemByID(mat.id)
local ingredientID = ingredient.id
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.qty}))
if ingredientID ~= nil and string.sub(ingredientID, -5) == "_Herb" then
-- Set the lowest level of potion this herb is used in.
local currLevel = allHerbs[ingredientID] or 9999999
if level < currLevel then
allHerbs[ingredientID] = level
end
end
end
end
result = result..'\r\n|'..table.concat(matArray, ' ')
end


local rcCraftTime = 2.00
local recipes = GameData.getEntities(SkillData.Herblore.recipes,
local xps = rune.runecraftingXP / rcCraftTime
function(obj)
local gps = rune.sellsFor / rcCraftTime
return Skills.getRecipeRealm(obj) == realmID
result = result..'\r\n| style="text-align:right"|'..string.format("%.2f", xps)
end
result = result..'\r\n| style="text-align:right"|'..string.format("%.2f", gps)
)
for _, potion in ipairs(recipes) do
handlePotion(potion)
end
end
local sortedValues = Shared.sortDictionary(
allHerbs,
function (a, b) return a.value < b.value end)


result = result..'\r\n|}'
local tbl = mw.html.create("table")
return result
        :addClass("wikitable sortable stickyHeader")
end
       
    -- Add header
    tbl :tag("tr"):addClass("headerRow-0")
    :tag("th"):wikitext(Icons.Icon({'Herblore', type='skill', notext=true}) .. '<br>Level')
    :tag("th"):wikitext("Herb")
    :tag("th"):wikitext("[[DLC]]")
    :tag("th"):wikitext("Value")
    :tag("th"):wikitext("Herb Sources")
    :done()
   
    -- Fill wikitable.
    for _, v in pairs(sortedValues) do
    local herbItem = Items.getItemByID(v['key'])
    local herbLevel = v['value']


function p.getFletchingTable(frame)
    -- Add rows
local category = frame.args ~= nil and frame.args[1] or frame
    tbl :tag("tr")
local data = nil
    :tag("td"):wikitext(herbLevel)
      :css('text-align', 'center')
    :tag("td"):wikitext(Icons.Icon({herbItem.name, type='item'}))
    :tag("td"):wikitext(Icons.getDLCColumnIcon(herbItem.id))
      :css('text-align', 'center')
    :tag("td"):wikitext(Items.getValueText(herbItem))
    :tag('td'):wikitext(ItemSourceTables._getItemSources(herbItem, false, nil, ' '))
    :done()
    end


if    category == "Arrows"    then
return tostring(tbl)
data = Items.getItems(function(item) return item.fletchingCategory == 0 end)
end
elseif category == "Shortbows" then
data = Items.getItems(function(item) return item.fletchingCategory == 1 end)
elseif category == "Longbows"  then
data = Items.getItems(function(item) return item.fletchingCategory == 2 end)
elseif category == "Bolts"    then
data = Items.getItems(function(item) return item.fletchingCategory == 3 end)
elseif category == "Crossbows" then
data = Items.getItems(function(item) return item.fletchingCategory == 4 end)
elseif category == "Javelins"  then
data = Items.getItems(function(item) return item.fletchingCategory == 5 end)
end


if data == nil then
function p.getHerbloreHerbTable(frame)
return "ERROR: Invalid Fletching category name.[[Category:Pages with script errors]]"
local args = frame:getParent().args
local realmName = args.realm
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
end
return p._getHerbloreHerbTable(realm.id)
end


local result = '{| class="wikitable sortable stickyHeader"'
function p.getPotionTable(frame)
result = result..'\r\n|- class="headerRow-0"'
local potionName = frame.args ~= nil and frame.args[1] or frame
result = result..'\r\n!Item\r\n!Name\r\n!Fletching Level\r\n!Experience'
result = result..'\r\n!Quantity\r\n!Sells For\r\n!Ingredients'


table.sort(data, function(a, b) return (a.fletchingLevel == b.fletchingLevel and a.id < b.id)  
local recipe = GameData.getEntityByName(SkillData.Herblore.recipes, potionName)
or a.fletchingLevel < b.fletchingLevel end)
if recipe == nil then
return Shared.printError('No potion named "' .. potionName .. '" was found')
end


for i, fletch in Shared.skpairs(data) do
local html = mw.html.create('table')
result = result..'\r\n|-'
:addClass('wikitable')
result = result..'\r\n| style="text-align: left;" | '..Icons.Icon({fletch.name, type='item', size='50', notext=true})
result = result..'\r\n| style ="text-align: left;" |'..Icons.Icon({fletch.name, type='item', noicon=true})
html:tag('tr')
result = result..'\r\n| style="text-align:right"|'..fletch.fletchingLevel
:tag('th'):attr('colspan', 4)
result = result..'\r\n| style="text-align:right"|'..fletch.fletchingXP
  :wikitext('[[' .. potionName ..']]')
result = result..'\r\n| style="text-align:right"|'..fletch.fletchQty
:tag('tr')
result = result..'\r\n| style="text-align:right"|'..fletch.sellsFor
:tag('th'):wikitext('Potion')
:tag('th'):wikitext('Tier')
:tag('th'):wikitext('Charges')
:tag('th'):wikitext('Effect')


local matArray = {}
for i, potionID in ipairs(recipe.potionIDs) do
for j, mat in Shared.skpairs(fletch.fletchReq) do
local tier = tierSuffix[i]
local matItem = Items.getItemByID(mat.id)
local potion = Items.getItemByID(potionID)
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.qty}))
if potion ~= nil then
html:tag('tr')
    :tag('td'):wikitext(Icons.getExpansionIcon(potion.id))
          :wikitext(Icons.Icon({potion.name, type='item', notext=true, size='25'}))
          :css('text-wrap', 'nowrap')
    :tag('td'):wikitext(Icons.Icon({potion.name, tier, type='item', noicon=true}))
:tag('td'):wikitext(potion.charges)
:tag('td'):wikitext(p._getPotionDescription(potion))
end
end
result = result..'\r\n|'..table.concat(matArray, ' ')
end
end


result = result..'\r\n|}'
return tostring(html)
return result
end
 
function p.getRunecraftingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
return p._getRecipeTable('Runecrafting', category, {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'})
end
 
function p.getFletchingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
return p._getRecipeTable('Fletching', category, {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'})
end
end


function p.getCraftingTable(frame)
function p.getCraftingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local category = frame.args ~= nil and frame.args[1] or frame
local data = nil
local columns = {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'}
if category == 'Rings' or category == 'Necklaces' then
table.insert(columns, "Description")
end
return p._getRecipeTable('Crafting', category, columns)
end
 
function p.getSmithingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local columns = {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'}
if category ~= 'Bars' and category ~= 'Abyssal Bars' then
table.insert(columns, 'CurrencyBar')
end
return p._getRecipeTable('Smithing', category, columns)
end
 
-- Given a skill name, category, and column list, produces a table of recipes for that
-- skill & category combination. If categoryName is '', all recipes are returned.
-- Note: This only supports a number of skills with consistent recipe data structures, being:
-- Fletching, Crafting, and Runecrafting
-- Valid column list options are: Item, SkillLevel, SkillXP, Currency, Materials, SkillXPSec, CurrencySec, CurrencyBar
function p._getRecipeTable(skillName, categoryName, columnList)
-- Validation: Parameters
if type(skillName) ~= 'string' then
return Shared.printError('skillName must be a string')
elseif not Shared.contains({'string', 'nil'}, type(categoryName)) then
return Shared.printError('category must be a string or nil')
elseif type(columnList) ~= 'table' or Shared.tableIsEmpty(columnList) then
return Shared.printError('columnList must be a table with 1+ elements')
end


if    category == "Leather"    then data =  
local supportedSkills = {
Items.getItems(function(item) return item.tier == "Leather" or item.tier == "Hard Leather" end)
'Smithing',
elseif category == "Dragonhide" then data =
'Crafting',
Items.getItems(function(item) return item.tier == "Dragonhide" and item.craftingLevel ~= nil end)
'Fletching',
elseif category == "Rings"      then data =
'Runecrafting'
Items.getItems(function(item) return (item.validSlots ~= nil and Shared.contains(item.validSlots, 'Ring')) and item.craftingLevel ~= nil end)
}
elseif category == "Necklaces"  then data =
if not Shared.contains(supportedSkills, skillName) then
Items.getItems(function(item) return (item.validSlots ~= nil and Shared.contains(item.validSlots, 'Amulet')) and item.craftingLevel ~= nil end)
return Shared.printError('The ' .. skillName .. ' skill is not supported by this function')
elseif category == "Bags" then data =
Items.getItems(function(item) return item.type == 'Bag' and item.craftingLevel ~= nil end)
end
end


if data == nil then
-- Validation: Category
return "ERROR: Invalid Crafting category name.[[Category:Pages with script errors]]"
local category = GameData.getEntityByName(SkillData[skillName].categories, categoryName)
if category == nil then
local catNames = {}
for i, cat in pairs(SkillData[skillName].categories) do
table.insert(catNames, cat.name)
end
return Shared.printError('No such category ' .. categoryName .. ' for skill ' .. skillName .. ', the following are available: ' .. table.concat(catNames, ', '))
end
end
local categoryRealm = category.realm or 'melvorD:Melvor'
local actionInterval = SkillData[skillName].baseInterval / 1000


table.sort(data, function(a, b) return (a.craftingLevel == b.craftingLevel and a.id < b.id)  
-- Validation: Skill data
or a.craftingLevel < b.craftingLevel end)
local recipeKey = 'recipes'
if SkillData[skillName] == nil then
return Shared.printError('Could not locate skill data for ' .. skillName)
elseif SkillData[skillName][recipeKey] == nil then
return Shared.printError('Could not locate recipe data for ' .. skillName)
end


local result = '{| class="wikitable sortable stickyHeader"'
-- Validation: Column list
result = result..'\r\n|- class="headerRow-0"'
local columnDef = {
result = result..'\r\n!Item\r\n!Name\r\n!'..Icons.Icon({'Crafting', type='skill', notext=true})..' Level\r\n!Experience'
["Item"] = {["header"] = 'Item', ["altRepeat"] = false},
result = result..'\r\n!Item Price\r\n!Ingredients'
["SkillLevel"] = {["header"] = Icons._SkillRealmIcon(skillName, categoryRealm) .. '<br>Level', ["altRepeat"] = false},
["DLC"] = {["header"] = '[[DLC]]', ["altRepeat"] = true},
["SkillXP"] = {["header"] = 'XP', ["altRepeat"] = false},
["Materials"] = {["header"] = 'Materials', ["altRepeat"] = true},
["Currency"] = {["header"] = 'Value', ["altRepeat"] = true},
["SkillXPSec"] = {["header"] = 'XP/s', ["altRepeat"] = false},
["CurrencySec"] = {["header"] = 'Value/s', ["altRepeat"] = true},
["CurrencyBar"] = {["header"] = 'Value/Bar', ["altRepeat"] = true},
["Description"] = {["header"] = "Description", ["altRepeat"] = true}
}
-- Build the table header while we're here
local html = mw.html.create('table')
:addClass('wikitable sortable stickyHeader')


for i, craft in Shared.skpairs(data) do
local header = html:tag('tr')
result = result..'\r\n|-'
:addClass('headerRow-0')
result = result..'\r\n| style="text-align: left;" | '..Icons.Icon({craft.name, type='item', size='50', notext=true})
result = result..'\r\n| style ="text-align: left;" |'..Icons.Icon({craft.name, type='item', noicon=true})
result = result..'\r\n| style="text-align:right"|'..craft.craftingLevel
result = result..'\r\n| style="text-align:right"|'..craft.craftingXP
result = result..'\r\n| style="text-align:right" data-sort-value="'..craft.sellsFor..'"|'..Icons.GP(craft.sellsFor)


local matArray = {}
local barIDList = {}
if craft.craftGPCost ~= nil and craft.craftGPCost > 0 then
for i, colID in ipairs(columnList) do
table.insert(matArray, Icons.GP(craft.craftGPCost))
if columnDef[colID] == nil then
return Shared.printError('Invalid column ' .. colID .. ' requested')
else
if colID == 'Item' then
header:tag('th'):wikitext('Item')
:attr('colspan', 2)
else
header:tag('th'):wikitext(columnDef[colID].header)
if colID == 'CurrencyBar' then
-- For Smithing, a currency value per bar column is included. If this
-- is requested, then obtain a list of bar item IDs
barIDList = p.getBarItemIDs()
end
end
end
end
for j, mat in Shared.skpairs(craft.craftReq) do
end
local matItem = Items.getItemByID(mat.id)
 
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.qty}))
-- Determine which recipes to include
local recipeList = GameData.getEntities(SkillData[skillName][recipeKey],
function(recipe)
return category.id == nil or recipe.categoryID == category.id
end)
if Shared.tableIsEmpty(recipeList) then
return ''
end
table.sort(recipeList, function(a, b) return Skills.standardRecipeSort(skillName, a, b) end)
 
-- Build rows based on recipes
for i, recipe in ipairs(recipeList) do
local ns, _ = GameData.getLocalID(recipe.id)
local item = Items.getItemByID(recipe.productID)
local level = Skills.getRecipeLevel(skillName, recipe)
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
if item ~= nil then
-- Some recipes have alternative costs, so the recipe may require multiple rows
local hasAltCosts = recipe.alternativeCosts ~= nil and not Shared.tableIsEmpty(recipe.alternativeCosts)
local costList = (hasAltCosts and recipe.alternativeCosts) or {{["itemCosts"] = recipe.itemCosts}}
local costCount = Shared.tableCount(costList)
-- Build one row per element within costList
for recipeRow, costDef in ipairs(costList) do
local rowspanAmt = nil
if recipeRow == 1 and costCount > 1 then
rowspanAmt = costCount
end
local rowspanStr = (recipeRow == 1 and costCount > 1 and 'rowspan="' .. costCount .. '" ') or ''
local qty = (costDef.quantityMultiplier or 1) * (recipe.baseQuantity or 1)
local row = html:tag('tr')
for j, colID in ipairs(columnList) do
local altRepeat = columnDef[colID].altRepeat
-- All columns must be generated if this is the very first row for a recipe,
-- for subsequent rows only columns marked as altRepeat = true are generated
if recipeRow == 1 or altRepeat then
local spanAmt = (not altRepeat and rowspanStr) or ''
local cell = row:tag('td')
if not altRepeat and rowspanAmt ~= nil then
cell:attr('rowspan', tostring(rowspanAmt))
end
if colID == 'Item' then
cell:wikitext(Icons.Icon({item.name, type='item', notext=true}))
:addClass('table-img')
cell = row:tag('td')
if not altRepeat and rowspanAmt ~= nil then
cell:attr('rowspan', tostring(rowspanAmt))
end
cell:wikitext((qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true}))
if qty > 1 then
cell:attr('data-sort-value', item.name)
end
elseif colID == 'SkillLevel' then
cell:wikitext(level)
:css('text-align', 'center')
elseif colID == 'DLC' then
local dlcID = recipe.id
if hasAltCosts then
-- For alt costs, look at the namespace of the required materials,
-- as the recipe could be from base game but using expansion materials
local recNS, recLocalID = Shared.getLocalID(recipe.id)
if Shared.contains({'melvorD', 'melvorF'}, recNS) then
for k, mat in ipairs(costDef.itemCosts) do
local matNS, matLocalID = Shared.getLocalID(mat.id)
if matNS ~= recNS then
dlcID = mat.id
break
end
end
end
end
cell:wikitext(Icons.getDLCColumnIcon(dlcID))
:attr('data-sort-value', Icons.getExpansionID(dlcID))
:css('text-align', 'center')
elseif colID == 'SkillXP' then
cell:wikitext(Num.formatnum(baseXP))
:css('text-align', 'right')
:attr('data-sort-value', tostring(baseXP))
elseif colID == 'Currency' then
local val = math.floor(item.sellsFor)
cell:wikitext(Items.getValueText(item) .. (qty > 1 and ' (x' .. qty .. ')' or ''))
:attr('data-sort-value', tostring(val * qty))
elseif colID == 'Materials' then
local currCost = { ["items"] = {}, ["currencies"] = recipe.currencyCosts }
local currText = Common.getCostString(currCost, nil, nil, ' ')
if currText ~= nil then
cell:wikitext(currText)
end
 
for k, mat in ipairs(costDef.itemCosts) do
local matItem = Items.getItemByID(mat.id)
if matItem ~= nil then
local sub = mw.html.create('sub')
:wikitext(Num.formatnum(mat.quantity) .. 'x')
:addClass('item-qty')
:done()
cell:node(sub)
cell:wikitext(Icons.Icon({matItem.name, type='item', notext=true}))
end
end
elseif colID == 'SkillXPSec' then
local XPRate = baseXP / actionInterval
cell:wikitext(Num.formatnum(string.format('%.2f', XPRate)))
:css('text-align', 'right')
:attr('data-sort-value', tostring(XPRate))
elseif colID == 'CurrencySec' then
local saleCurrency = item.sellsForCurrency or 'melvorD:GP'
local val = math.floor(item.sellsFor) * qty / actionInterval
cell:wikitext(Icons._Currency(saleCurrency, string.format('%.2f', val)))
:attr('data-sort-value', tostring(val))
elseif colID == 'CurrencyBar' then
local barQty = 0
for k, mat in ipairs(costDef.itemCosts) do
if Shared.contains(barIDList, mat.id) then
barQty = barQty + mat.quantity
end
end
if barQty > 0 then
local saleCurrency = item.sellsForCurrency or 'melvorD:GP'
local barVal = Num.round(math.floor(item.sellsFor) * qty / barQty, 1, 1)
cell:wikitext(Icons._Currency(saleCurrency, barVal))
:attr('data-sort-value', tostring(barVal))
else
cell:wikitext('N/A')
:addClass('table-na')
:attr('data-sort-value', '0')
end
elseif colID == 'Description' then
local descrip = Items._getItemStat(item, 'description')
if descrip == 'No Description' and item.modifiers ~= nil and not Shared.tableIsEmpty(item.modifiers) then
descrip = Modifiers.getModifiersText(item.modifiers, false)
end
cell:wikitext(descrip)
end
end
end
end
end
end
result = result..'\r\n|'..table.concat(matArray, ', ')
end
end
return tostring(html)
end


result = result..'\r\n|}'
function p.getBarItemIDs()
return result
local barCategories = {
['melvorD:Bars'] = true,
['melvorItA:AbyssalBars'] = true
}
local barIDList = {}
for i, recipe in ipairs(SkillData.Smithing.recipes) do
if barCategories[recipe.categoryID] then
table.insert(barIDList, recipe.productID)
end
end
return barIDList
end
end


return p
return p

Latest revision as of 20:58, 27 September 2024

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

--Splitting some functions into here to avoid bloating a single file
--Contains function for skills that consume resources (ie smithing, cooking, herblore, etc.)
local p = {}

local Shared = require('Module:Shared')
local Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Skills = require('Module:Skills')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local ItemSourceTables = require('Module:Items/SourceTables')
local Num = require('Module:Number')


function p.getCookedItemsTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local category = args[1]
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	local skillID = 'Cooking'

	local categoryMap = {
		["Cooking Fire"] = 'melvorD:Fire',
		["Furnace"] = 'melvorD:Furnace',
		["Pot"] = 'melvorD:Pot'
	}
	local categoryID = categoryMap[category]

	-- Find recipes for the relevant categories
	-- Note: Excludes Lemon cake
	local recipeArray = GameData.getEntities(SkillData.Cooking.recipes,
		function(recipe)
			return (
				(categoryID == nil or recipe.categoryID == categoryID)
				and recipe.noMastery == nil
				and Skills.getRecipeRealm(recipe) == realm.id
			)
		end
	)
	table.sort(recipeArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)

	local cookIcon = Icons._SkillRealmIcon('Cooking', realm.id)
	local perfectIcon = Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true, size=20})
	local healIcon = Icons.Icon({"Hitpoints", type="skill", notext=true, nolink=true, size=20})
	-- Only include time for cooking fire, as the other options have a fixed interval.
	local includeTime = category == "Cooking Fire"
	
	local html = mw.html.create('table')
		:addClass('wikitable sortable stickyHeader')
	
	local header0 = html:tag('tr'):addClass("headerRow-0")
	header0 :tag('th'):wikitext('Cooked Item')
					  :attr('colspan', 3)
					  :attr('rowspan', 2)
	header0	:tag('th'):wikitext(cookIcon .. '<br>Level')
				      :attr('rowspan', 2)
	header0	:tag('th'):wikitext('[[DLC]]')
					  :attr('rowspan', 2)
	if includeTime == true then
		header0	:tag('th'):wikitext('Cook<br>Time (s)')
						  :attr('rowspan', 2)
	end					  
	header0	:tag('th'):wikitext(cookIcon .. '<br>XP')
					  :attr('rowspan', 2)
	header0	:tag('th'):wikitext('XP/s')
					  :attr('rowspan', 2)
	header0	:tag('th'):wikitext('Total Healing')
					  :attr('colspan', 2)
	header0	:tag('th'):wikitext('Ingredients')
					  :attr('rowspan', 2)
	header0	:tag('th'):wikitext('Value')
					  :attr('colspan', 2)
					  
	html:tag('tr'):addClass("headerRow-1")
			:tag('th'):wikitext('Normal')
			:tag('th'):wikitext('Perfect')
			:tag('th'):wikitext('Normal')
			:tag('th'):wikitext('Perfect')

	for i, recipe in ipairs(recipeArray) do
		local level = Skills.getRecipeLevel(skillID, recipe)
		local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
		local baseInt = recipe.baseInterval / 1000
		local xpRate = baseXP / baseInt
		local item = Items.getItemByID(recipe.productID)
		local currency = item.sellsForCurrency or 'melvorD:GP'
		local perfectItem = nil
		if recipe.perfectCookID ~= nil then
			perfectItem = Items.getItemByID(recipe.perfectCookID)
		end
		local qty = recipe.baseQuantity or 1
		local normalHeal = math.floor(item.healsFor * 10) * qty
		local perfectHeal = math.floor(perfectItem.healsFor * 10) * qty
		
		local row = html:tag('tr')
		row:tag('td'):wikitext(Icons.Icon({item.name, type='item', notext=true}))
					 :addClass("table-img")

		if perfectItem ~= nil then
			row:tag('td'):wikitext(Icons.Icon({perfectItem.name, type='item', notext=true}))
						 :addClass("table-img")
		end
		
		local nameCell = row:tag('td')
		nameCell:wikitext('[[' .. item.name .. ']]')
		if qty > 1 then
			nameCell:wikitext(' x' .. qty)
		end
		row:tag('td'):wikitext(level)
					 :css('text-align', 'center')
		row:tag('td'):wikitext(Icons.getDLCColumnIcon(item.id))
					 :css('text-align', 'center')
					 :attr('data-sort-value', Icons.getExpansionID(item.id))
		if includeTime == true then
			row:tag('td'):wikitext(Num.round(baseInt, 2, 0) .. 's')
						 :css('text-align', 'right')
						 :attr('data-sort-value', baseInt)
		end
		row:tag('td'):wikitext(Num.formatnum(baseXP))
					 :css('text-align', 'right')
					 :attr('data-sort-value', baseXP)
		row:tag('td'):wikitext(Num.formatnum(Num.round(xpRate, 0, 0)))
					 :css('text-align', 'right')
					 :attr('data-sort-value', xpRate)
		row:tag('td'):wikitext(healIcon .. ' ')
					 :wikitext(Num.formatnum(normalHeal))
					 :attr('data-sort-value', normalHeal)
		row:tag('td'):wikitext(perfectIcon .. ' ')
					 :wikitext(Num.formatnum(perfectHeal))
					 :attr('data-sort-value', perfectHeal)			 
		local ingrCell = row:tag('td')
		local matArray = {}
		for j, mat in ipairs(recipe.itemCosts) do
			local matItem = Items.getItemByID(mat.id)
			if matItem ~= nil then
				local sub = mw.html.create('sub')
					:wikitext(mat.quantity .. 'x')
					:addClass('item-qty')
					:done()
				ingrCell:node(sub)
				ingrCell:wikitext(Icons.Icon({matItem.name, type='item', notext=true}))
			end
		end
		row:tag('td'):wikitext(Icons._Currency(currency, item.sellsFor))
					 :css('text-align', 'right')
		row:tag('td'):wikitext(Icons._Currency(currency, perfectItem.sellsFor))
					 :css('text-align', 'right')
	end

	return tostring(html)
end

local tierSuffix = { 'I', 'II', 'III', 'IV' }
function p._getPotionDescription(potion)
	-- TODO: Temporary fix below for incorrect Traps Potion descriptions. To amend
	-- once corrected within game data
	if potion.customDescription and not Shared.contains(potion.id, 'melvorTotH:Traps_Potion_') then
		return potion.customDescription
	elseif type(potion.modifiers) == 'table' and not Shared.tableIsEmpty(potion.modifiers) then
		return Modifiers.getModifiersText(potion.modifiers, false, true)
	else
		return ''
	end
end

function p._getHerblorePotionTable(categoryName)
	local skillID = 'Herblore'
	local category = GameData.getEntityByName(SkillData.Herblore.categories, categoryName)
	if category == nil then
		local catNames = {}
		for i, cat in pairs(SkillData.Herblore.categories) do
			table.insert(catNames, cat.name)
		end
		return Shared.printError('No such category ' .. categoryName .. ', the following are available: ' .. table.concat(catNames, ', '))
	end
	local categoryID = category.id

	local potionArray = GameData.getEntities(SkillData.Herblore.recipes,
		function(potion)
				-- Category implies a realm selection, so no ned to check this separately
				return potion.categoryID == categoryID
			end
		)

	table.sort(potionArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)

	local html = mw.html.create('table')
		:addClass('wikitable sortable stickyHeader mw-collapsible')
	
	html:tag('tr'):addClass('headerRow-0')
			:tag('th'):wikitext('Potion')
	    	:tag("th"):wikitext(Icons.Icon({'Herblore', type='skill', notext=true}) .. '<br>Level')
	    	:tag('th'):wikitext('XP')
	    	:tag('th'):wikitext('Ingredients')
	    	:tag('th'):wikitext('[[DLC]]')
	    	:tag('th'):wikitext('Tier')
	    		      :css('width', '30px')
	    	--:tag('th'):wikitext('Value')
	    	:tag('th'):wikitext('Charges')
	    	:tag('th'):wikitext('Effect')

	for i, potion in ipairs(potionArray) do
		local level = Skills.getRecipeLevel(skillID, potion)
		local baseXP = potion.baseAbyssalExperience or potion.baseExperience
		local reqText = Skills.getRecipeRequirementText(SkillData.Herblore.name, potion)
		local costText = Common.getCostString({
			["items"] = potion.itemCosts,
			["currencies"] = potion.currencyCosts
		}, 'N/A', nil, '<br>')
		
		-- Prefetch potion items, as these are required for the overall row
		-- as well as the tier rows
		local potionItems, lastPotionIdx = {}, 1
		for j, potionID in ipairs(potion.potionIDs) do
			potionItems[j] = Items.getItemByID(potionID)
			lastPotionIdx = j
		end
		
		local row = html:tag('tr')
		row:tag('td'):wikitext(Icons.Icon({potionItems[lastPotionIdx].name, type='item', notext=true, nolink=true}))
					 :attr('rowspan', 4):wikitext('[['..potion.name..']]')
		row:tag('td'):attr('rowspan', 4):wikitext(level)
					 :css('text-align', 'center')
		row:tag('td'):attr('rowspan', 4):wikitext(Num.formatnum(baseXP))
					 :attr('data-sort-value', baseXP)
					 :css('text-align', 'right')
		row:tag('td'):attr('rowspan', 4):wikitext(costText)	
		row:tag('td'):attr('rowspan', 4):wikitext(Icons.getDLCColumnIcon(potion.potionIDs[1]))
					 :css('text-align', 'center')
					 :attr('data-sort-value', Icons.getExpansionID(potion.potionIDs[1]))

		for j, potionID in ipairs(potion.potionIDs) do
			local tierPot = potionItems[j]
			
			-- First row needs to be added to the main row of the rowspan row.
			-- The other 3 need to be added to the main table as new rows.
			local tierRow = j == 1 and row or html:tag('tr')
			tierRow:tag('td'):wikitext(Icons.Icon({tierPot.name, type='item', notext=true}))
				    	     :wikitext('[[' .. tierPot.name .. '|' .. tierSuffix[j] .. ']]')
			--tierRow:tag('td'):wikitext(Items.getValueText(tierPot))
			--      		   :css('text-align', 'right')
			--      		   :attr('data-sort-value', tierPot.sellsFor)
			tierRow:tag('td'):wikitext(tierPot.charges)
						     :css('text-align', 'right')
			tierRow:tag('td'):wikitext(p._getPotionDescription(tierPot))
		end
	end

	return tostring(html)
end

function p.getHerblorePotionTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local category = args[1]
	return p._getHerblorePotionTable(category)
end

-- Returns the icon of the past potion found in potion.PotiodIDs
-- This is usually the IV potion.
function p._getHerblorePotionIcon(potion)
	local lastPot = nil
	for k, v in ipairs(potion.potionIDs) do
		lastPot = v
	end
	
	local pot = Items.getItemByID(lastPot)
	return Icons.Icon({pot.name, type='item', notext=true, size='25'})
end

function p._getHerblorePotionSlimTable(realmID)
	local skillID = 'Herblore'
	local realmIcon = Icons._SkillRealmIcon('Herblore', realmID)
	local potions = GameData.getEntities(SkillData.Herblore.recipes,
		function(x)
			return Skills.getRecipeRealm(x) == realmID
		end
	)
	table.sort(potions, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
	
	local function getIngredients(potion)
    	local herbs = {}
    	local secondaries = {}
    	
    	-- Grab secondaries and split herb.
    	if type(potion.itemCosts) == 'table' then
    		for k, v in pairs(potion.itemCosts) do
    			local ingredientID = v.id
    			local item = GameData.getEntityByID('items', v.id)
    			local icon = Icons.Icon({item.name, type='item', qty=v.quantity})
				if ingredientID ~= nil and string.sub(ingredientID, -5) == "_Herb" then
					table.insert(herbs, icon)
				else
					table.insert(secondaries, icon)
				end
    		end
    	end
    	
    	-- Format currencies as normal
    	if type(potion.currencies) == 'table' then
    		for k, v in pairs(potion.currencies) do
    			local currID = v.id or v.currencyID
    			table.insert(secondaries, Icons._Currency(currID, v.quantity))
    		end
    	end
    	
    	return {
    		herbs = table.concat(herbs, '<br>'),
    		secondaries = table.concat(secondaries, '<br>')
    	}
	end

	local tbl = mw.html.create("table")
        :addClass("wikitable sortable stickyHeader mw-collapsible mw-collapsed")
        
    -- Add header
    tbl :tag("tr"):addClass("headerRow-0")
    		:tag('th'):wikitext("Potion")
    				  :attr('colspan', 2)
    		:tag('th'):wikitext('[[DLC]]')
    		:tag('th'):wikitext(realmIcon .. '<br>Level')
    		:tag('th'):wikitext(realmIcon .. '<br>XP')
    		:tag('th'):wikitext('Herb')
    		:tag('th'):wikitext('Secondary')
    		--:tag('th'):wikitext('Effect')
    	:done()

    for i, potion in ipairs(potions) do
		local level = Skills.getRecipeLevel(skillID, potion)
		local xp = potion.baseAbyssalExperience or potion.baseExperience
		local ingreds = getIngredients(potion)
		local row = tbl:tag('tr')

		row:tag('td'):wikitext(p._getHerblorePotionIcon(potion))
		             :css('text-align', 'center')
		row:tag('td'):wikitext('[['..potion.name..']]')
		row:tag('td'):wikitext(Icons.getDLCColumnIcon(potion.potionIDs[1]))
					 :css('text-align', 'center')
					 :attr('data-sort-value', Icons.getExpansionID(potion.potionIDs[1]))
		row:tag('td'):wikitext(level)
					 :css('text-align', 'center')
		row:tag('td'):wikitext(Num.formatnum(xp))
					 :css('text-align', 'right')
		row:tag('td'):wikitext(ingreds.herbs)
		row:tag('td'):wikitext(ingreds.secondaries)
    end

	return tostring(tbl)
end

function p.getHerblorePotionSlimTable(frame)
	local args = frame.args ~= nil and frame.args or frame
	local realmName = args.realm or 'Melvor Realm'
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end

	return p._getHerblorePotionSlimTable(realm.id)
end

function p._getHerbloreHerbTable(realmID)
	local allHerbs = {}

	-- Finds the herb from a potion along with the level required to make the potion.
	local function handlePotion(potion)
		local potionCosts = potion.itemCosts
		local level = Skills.getRecipeLevel('Herblore', potion)

		if potionCosts == nil or level == nil then
			return
		end

		-- Find if this potion uses a herb, and which herb it is.
		for _, ingredient in ipairs(potionCosts) do
			local ingredientID = ingredient.id
			if ingredientID ~= nil and string.sub(ingredientID, -5) == "_Herb" then
				-- Set the lowest level of potion this herb is used in.
				local currLevel = allHerbs[ingredientID] or 9999999
				if level < currLevel then
					allHerbs[ingredientID] = level
				end
			end
		end
	end

	local recipes = GameData.getEntities(SkillData.Herblore.recipes,
		function(obj)
			return Skills.getRecipeRealm(obj) == realmID
		end
	)
	for _, potion in ipairs(recipes) do
		handlePotion(potion)
	end
	
	local sortedValues = Shared.sortDictionary(
		allHerbs, 
		function (a, b) return a.value < b.value end)

	local tbl = mw.html.create("table")
        :addClass("wikitable sortable stickyHeader")
        
    -- Add header
    tbl :tag("tr"):addClass("headerRow-0")
    	:tag("th"):wikitext(Icons.Icon({'Herblore', type='skill', notext=true}) .. '<br>Level')
    	:tag("th"):wikitext("Herb")
    	:tag("th"):wikitext("[[DLC]]")
    	:tag("th"):wikitext("Value")
    	:tag("th"):wikitext("Herb Sources")
    	:done()
    
    -- Fill wikitable.
    for _, v in pairs(sortedValues) do
    	local herbItem = Items.getItemByID(v['key'])
    	local herbLevel = v['value']

    	-- Add rows
    	tbl :tag("tr")
    		:tag("td"):wikitext(herbLevel)
    				  :css('text-align', 'center')
    		:tag("td"):wikitext(Icons.Icon({herbItem.name, type='item'}))
    		:tag("td"):wikitext(Icons.getDLCColumnIcon(herbItem.id))
    				  :css('text-align', 'center')
    		:tag("td"):wikitext(Items.getValueText(herbItem))
    		:tag('td'):wikitext(ItemSourceTables._getItemSources(herbItem, false, nil, ' '))
    		:done()
    end

	return tostring(tbl)
end

function p.getHerbloreHerbTable(frame)
	local args = frame:getParent().args
	local realmName = args.realm
	local realm = Skills.getRealmFromName(realmName)
	if realm == nil then
		return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
	end
	return p._getHerbloreHerbTable(realm.id)
end

function p.getPotionTable(frame)
	local potionName = frame.args ~= nil and frame.args[1] or frame

	local recipe = GameData.getEntityByName(SkillData.Herblore.recipes, potionName)
	if recipe == nil then
		return Shared.printError('No potion named "' .. potionName .. '" was found')
	end

	local html = mw.html.create('table')
		:addClass('wikitable')
	
	html:tag('tr')
			:tag('th'):attr('colspan', 4)
					  :wikitext('[[' .. potionName ..']]')
		:tag('tr')
			:tag('th'):wikitext('Potion')
			:tag('th'):wikitext('Tier')
			:tag('th'):wikitext('Charges')
			:tag('th'):wikitext('Effect')

	for i, potionID in ipairs(recipe.potionIDs) do
		local tier = tierSuffix[i]
		local potion = Items.getItemByID(potionID)
		if potion ~= nil then
			html:tag('tr')
				    :tag('td'):wikitext(Icons.getExpansionIcon(potion.id))
				    	      :wikitext(Icons.Icon({potion.name, type='item', notext=true, size='25'}))
				    	      :css('text-wrap', 'nowrap')
				    :tag('td'):wikitext(Icons.Icon({potion.name, tier, type='item', noicon=true}))
					:tag('td'):wikitext(potion.charges)
					:tag('td'):wikitext(p._getPotionDescription(potion))
		end
	end

	return tostring(html)
end

function p.getRunecraftingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getRecipeTable('Runecrafting', category, {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'})
end

function p.getFletchingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getRecipeTable('Fletching', category, {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'})
end

function p.getCraftingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	local columns = {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'}
	if category == 'Rings' or category == 'Necklaces' then
		table.insert(columns, "Description")
	end
	return p._getRecipeTable('Crafting', category, columns)
end

function p.getSmithingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	local columns = {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'}
	if category ~= 'Bars' and category ~= 'Abyssal Bars' then
		table.insert(columns, 'CurrencyBar')
	end
	return p._getRecipeTable('Smithing', category, columns)
end

-- Given a skill name, category, and column list, produces a table of recipes for that
-- skill & category combination. If categoryName is '', all recipes are returned.
-- Note: This only supports a number of skills with consistent recipe data structures, being:
-- Fletching, Crafting, and Runecrafting
-- Valid column list options are: Item, SkillLevel, SkillXP, Currency, Materials, SkillXPSec, CurrencySec, CurrencyBar
function p._getRecipeTable(skillName, categoryName, columnList)
	-- Validation: Parameters
	if type(skillName) ~= 'string' then
		return Shared.printError('skillName must be a string')
	elseif not Shared.contains({'string', 'nil'}, type(categoryName)) then
		return Shared.printError('category must be a string or nil')
	elseif type(columnList) ~= 'table' or Shared.tableIsEmpty(columnList) then
		return Shared.printError('columnList must be a table with 1+ elements')
	end

	local supportedSkills = {
		'Smithing',
		'Crafting',
		'Fletching',
		'Runecrafting'
	}
	if not Shared.contains(supportedSkills, skillName) then
		return Shared.printError('The ' .. skillName .. ' skill is not supported by this function')
	end

	-- Validation: Category
	local category = GameData.getEntityByName(SkillData[skillName].categories, categoryName)
	if category == nil then
		local catNames = {}
		for i, cat in pairs(SkillData[skillName].categories) do
			table.insert(catNames, cat.name)
		end
		return Shared.printError('No such category ' .. categoryName .. ' for skill ' .. skillName .. ', the following are available: ' .. table.concat(catNames, ', '))
	end
	local categoryRealm = category.realm or 'melvorD:Melvor'
	local actionInterval = SkillData[skillName].baseInterval / 1000

	-- Validation: Skill data
	local recipeKey = 'recipes'
	if SkillData[skillName] == nil then
		return Shared.printError('Could not locate skill data for ' .. skillName)
	elseif SkillData[skillName][recipeKey] == nil then
		return Shared.printError('Could not locate recipe data for ' .. skillName)
	end

	-- Validation: Column list
	local columnDef = {
		["Item"] = {["header"] = 'Item', ["altRepeat"] = false},
		["SkillLevel"] = {["header"] = Icons._SkillRealmIcon(skillName, categoryRealm) .. '<br>Level', ["altRepeat"] = false},
		["DLC"] = {["header"] = '[[DLC]]', ["altRepeat"] = true},
		["SkillXP"] = {["header"] = 'XP', ["altRepeat"] = false},
		["Materials"] = {["header"] = 'Materials', ["altRepeat"] = true},
		["Currency"] = {["header"] = 'Value', ["altRepeat"] = true},
		["SkillXPSec"] = {["header"] = 'XP/s', ["altRepeat"] = false},
		["CurrencySec"] = {["header"] = 'Value/s', ["altRepeat"] = true},
		["CurrencyBar"] = {["header"] = 'Value/Bar', ["altRepeat"] = true},
		["Description"] = {["header"] = "Description", ["altRepeat"] = true}
	}
	-- Build the table header while we're here
	local html = mw.html.create('table')
		:addClass('wikitable sortable stickyHeader')

		local header = html:tag('tr')
			:addClass('headerRow-0')

	local barIDList = {}
	for i, colID in ipairs(columnList) do
		if columnDef[colID] == nil then
			return Shared.printError('Invalid column ' .. colID .. ' requested')
		else
			if colID == 'Item' then
				header:tag('th'):wikitext('Item')
								:attr('colspan', 2)
			else
				header:tag('th'):wikitext(columnDef[colID].header)
				if colID == 'CurrencyBar' then
					-- For Smithing, a currency value per bar column is included. If this
					-- is requested, then obtain a list of bar item IDs
					barIDList = p.getBarItemIDs()
				end
			end
		end
	end

	-- Determine which recipes to include
	local recipeList = GameData.getEntities(SkillData[skillName][recipeKey],
		function(recipe)
			return category.id == nil or recipe.categoryID == category.id
		end)
	if Shared.tableIsEmpty(recipeList) then
		return ''
	end
	table.sort(recipeList, function(a, b) return Skills.standardRecipeSort(skillName, a, b) end)

	-- Build rows based on recipes
	for i, recipe in ipairs(recipeList) do
		local ns, _ = GameData.getLocalID(recipe.id)
		local item = Items.getItemByID(recipe.productID)
		local level = Skills.getRecipeLevel(skillName, recipe)
		local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
		if item ~= nil then
			-- Some recipes have alternative costs, so the recipe may require multiple rows
			local hasAltCosts = recipe.alternativeCosts ~= nil and not Shared.tableIsEmpty(recipe.alternativeCosts)
			local costList = (hasAltCosts and recipe.alternativeCosts) or {{["itemCosts"] = recipe.itemCosts}}
			local costCount = Shared.tableCount(costList)
			-- Build one row per element within costList
			for recipeRow, costDef in ipairs(costList) do
				local rowspanAmt = nil
				if recipeRow == 1 and costCount > 1 then
					rowspanAmt = costCount
				end
				local rowspanStr = (recipeRow == 1 and costCount > 1 and 'rowspan="' .. costCount .. '" ') or ''
				local qty = (costDef.quantityMultiplier or 1) * (recipe.baseQuantity or 1)
				local row = html:tag('tr')
				for j, colID in ipairs(columnList) do
					local altRepeat = columnDef[colID].altRepeat
					-- All columns must be generated if this is the very first row for a recipe,
					-- for subsequent rows only columns marked as altRepeat = true are generated
					if recipeRow == 1 or altRepeat then
						local spanAmt = (not altRepeat and rowspanStr) or ''
						local cell = row:tag('td')
						if not altRepeat and rowspanAmt ~= nil then
							cell:attr('rowspan', tostring(rowspanAmt))
						end
						if colID == 'Item' then
							cell:wikitext(Icons.Icon({item.name, type='item', notext=true}))
								:addClass('table-img')
							cell = row:tag('td')
							if not altRepeat and rowspanAmt ~= nil then
								cell:attr('rowspan', tostring(rowspanAmt))
							end
							cell:wikitext((qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true}))
							if qty > 1 then
								cell:attr('data-sort-value', item.name)
							end
						elseif colID == 'SkillLevel' then
							cell:wikitext(level)
								:css('text-align', 'center')
						elseif colID == 'DLC' then
							local dlcID = recipe.id
							if hasAltCosts then
								-- For alt costs, look at the namespace of the required materials,
								-- as the recipe could be from base game but using expansion materials
								local recNS, recLocalID = Shared.getLocalID(recipe.id)
								if Shared.contains({'melvorD', 'melvorF'}, recNS) then
									for k, mat in ipairs(costDef.itemCosts) do
										local matNS, matLocalID = Shared.getLocalID(mat.id)
										if matNS ~= recNS then
											dlcID = mat.id
											break
										end
									end
								end
							end
							cell:wikitext(Icons.getDLCColumnIcon(dlcID))
								:attr('data-sort-value', Icons.getExpansionID(dlcID))
								:css('text-align', 'center')
						elseif colID == 'SkillXP' then
							cell:wikitext(Num.formatnum(baseXP))
								:css('text-align', 'right')
								:attr('data-sort-value', tostring(baseXP))
						elseif colID == 'Currency' then
							local val = math.floor(item.sellsFor)
							cell:wikitext(Items.getValueText(item) .. (qty > 1 and ' (x' .. qty .. ')' or ''))
								:attr('data-sort-value', tostring(val * qty))
						elseif colID == 'Materials' then
							local currCost = { ["items"] = {}, ["currencies"] = recipe.currencyCosts }
							local currText = Common.getCostString(currCost, nil, nil, ' ')
							if currText ~= nil then
								cell:wikitext(currText)
							end

							for k, mat in ipairs(costDef.itemCosts) do
								local matItem = Items.getItemByID(mat.id)
								if matItem ~= nil then
									local sub = mw.html.create('sub')
										:wikitext(Num.formatnum(mat.quantity) .. 'x')
										:addClass('item-qty')
										:done()
									cell:node(sub)
									cell:wikitext(Icons.Icon({matItem.name, type='item', notext=true}))
								end
							end
						elseif colID == 'SkillXPSec' then
							local XPRate = baseXP / actionInterval
							cell:wikitext(Num.formatnum(string.format('%.2f', XPRate)))
								:css('text-align', 'right')
								:attr('data-sort-value', tostring(XPRate))
						elseif colID == 'CurrencySec' then
							local saleCurrency = item.sellsForCurrency or 'melvorD:GP'
							local val = math.floor(item.sellsFor) * qty / actionInterval
							cell:wikitext(Icons._Currency(saleCurrency, string.format('%.2f', val)))
								:attr('data-sort-value', tostring(val))
						elseif colID == 'CurrencyBar' then
							local barQty = 0
							for k, mat in ipairs(costDef.itemCosts) do
								if Shared.contains(barIDList, mat.id) then
									barQty = barQty + mat.quantity
								end
							end
							if barQty > 0 then
								local saleCurrency = item.sellsForCurrency or 'melvorD:GP'
								local barVal = Num.round(math.floor(item.sellsFor) * qty / barQty, 1, 1)
								cell:wikitext(Icons._Currency(saleCurrency, barVal))
									:attr('data-sort-value', tostring(barVal))
							else
								cell:wikitext('N/A')
									:addClass('table-na')
									:attr('data-sort-value', '0')
							end
						elseif colID == 'Description' then
							local descrip = Items._getItemStat(item, 'description')
							if descrip == 'No Description' and item.modifiers ~= nil and not Shared.tableIsEmpty(item.modifiers) then
								descrip = Modifiers.getModifiersText(item.modifiers, false)
							end
							cell:wikitext(descrip)
						end
					end
				end
			end
		end
	end
	return tostring(html)
end

function p.getBarItemIDs()
	local barCategories = {
		['melvorD:Bars'] = true,
		['melvorItA:AbyssalBars'] = true
	}
	local barIDList = {}
	for i, recipe in ipairs(SkillData.Smithing.recipes) do
		if barCategories[recipe.categoryID] then
			table.insert(barIDList, recipe.productID)
		end
	end
	return barIDList
end

return p