Module:Skills/Artisan: Difference between revisions

From Melvor Idle
(Update for v1.0.3)
m (Add space for separator for herb itemsources)
 
(29 intermediate revisions by 4 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 Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
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')


function p.getCookedItemsTable(frame)
function p.getCookedItemsTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local category = frame.args ~= nil and frame.args[1] or frame
local categoryMap = {
local categoryMap = {
["Cooking Fire"] = 0,
["Cooking Fire"] = 'melvorD:Fire',
["Furnace"] = 1,
["Furnace"] = 'melvorD:Furnace',
["Pot"] = 2
["Pot"] = 'melvorD:Pot'
}
}
local categoryID = categoryMap[category]
local categoryID = categoryMap[category]
local recipeArray = {}


-- Find recipes for the relevant categories
-- Find recipes for the relevant categories
for i, recipe in ipairs(SkillData.Cooking.Recipes) do
-- Note: Excludes Lemon cake
-- Note: Excludes Lemon cake
local recipeArray = GameData.getEntities(SkillData.Cooking.recipes,
if (categoryID == nil or recipe.category == categoryID) and recipe.masteryID >= 0 then
function(recipe)
table.insert(recipeArray, recipe)
return (categoryID == nil or recipe.categoryID == categoryID) and recipe.noMastery == nil
end
end)
end
table.sort(recipeArray, function(a, b) return a.level < b.level end)
table.sort(recipeArray, function(a, b) return (a.level == b.level and a.masteryID < b.masteryID) or a.level < b.level end)


-- Logic for generating some cells of the table which are consistent for normal & perfect items
-- Logic for generating some cells of the table which are consistent for normal & perfect items
Line 38: Line 38:
local getSaleValueCell = function(item, qty)
local getSaleValueCell = function(item, qty)
if item ~= nil then
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 '')
return 'data-sort-value="'..math.floor(item.sellsFor * qty)..'"|'..Icons.GP(math.floor(item.sellsFor))..(qty > 1 and ' (x'..qty..')' or '')
else
else
return ' '
return ' '
Line 48: Line 48:
table.insert(resultPart, '\r\n|- class="headerRow-0"')
table.insert(resultPart, '\r\n|- class="headerRow-0"')
table.insert(resultPart, '\r\n!colspan="3" rowspan="2"|Cooked Item!!rowspan="2"|'..Icons.Icon({'Cooking', type='skill', notext=true})..' Level')
table.insert(resultPart, '\r\n!colspan="3" rowspan="2"|Cooked Item!!rowspan="2"|'..Icons.Icon({'Cooking', type='skill', notext=true})..' Level')
table.insert(resultPart, '!!rowspan="2"|Cook Time!!rowspan="2"|XP!!colspan="2"|Healing!!colspan="2"|Value!!rowspan="2"|Ingredients')
table.insert(resultPart, '!!rowspan="2"|Cook Time (s)!!rowspan="2"|XP!!rowspan="2"|XP/s!!colspan="2"|Healing!!colspan="2"|Value!!rowspan="2"|Ingredients')
table.insert(resultPart, '\r\n|- class="headerRow-1"')
table.insert(resultPart, '\r\n|- class="headerRow-1"')
table.insert(resultPart, '\r\n!Normal!!' .. Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true}))
table.insert(resultPart, '\r\n!Normal!!' .. Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true}))
Line 54: Line 54:


for i, recipe in ipairs(recipeArray) do
for i, recipe in ipairs(recipeArray) do
local item = Items.getItemByID(recipe.itemID)
local item = Items.getItemByID(recipe.productID)
local perfectItem = nil
local perfectItem = nil
if recipe.perfectCookID ~= nil then
if recipe.perfectCookID ~= nil then
Line 62: Line 62:


table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\r\n|style="min-width:25px"|'..Icons.Icon({item.name, type='item', notext=true, size='50'}))
table.insert(resultPart, '\r\n|class="table-img"|'..Icons.Icon({item.name, type='item', notext=true, size='50'}))
table.insert(resultPart, '\r\n|style="min-width:25px"| ')
table.insert(resultPart, '\r\n|class="table-img"| ')
if perfectItem ~= nil then
if perfectItem ~= nil then
table.insert(resultPart, Icons.Icon({perfectItem.name, type='item', notext=true, size='50'}))
table.insert(resultPart, Icons.Icon({perfectItem.name, type='item', notext=true, size='50'}))
Line 71: Line 71:
table.insert(resultPart, qty..'x ')
table.insert(resultPart, qty..'x ')
end
end
table.insert(resultPart, Icons.getExpansionIcon(item.id))
table.insert(resultPart, Icons.Icon({item.name, type='item', noicon = true}))
table.insert(resultPart, Icons.Icon({item.name, type='item', noicon = true}))
table.insert(resultPart, '||style="text-align:right"|' .. recipe.level)
table.insert(resultPart, '||style="text-align:right"|' .. recipe.level)
table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseInterval .. '"|' .. Shared.timeString(recipe.baseInterval / 1000, true))
table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseInterval .. '"|' .. Shared.round(recipe.baseInterval / 1000, 2, 0))
table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseXP .. '"|' .. Shared.formatnum(recipe.baseXP))
table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseExperience .. '"|' .. Shared.formatnum(recipe.baseExperience))
local xpRate = recipe.baseExperience / (recipe.baseInterval / 1000)
table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. xpRate .. '"|' .. Shared.round(xpRate, 2, 0))
table.insert(resultPart, '||'..getHealingCell(item, qty)..'||'..getHealingCell(perfectItem, qty))
table.insert(resultPart, '||'..getHealingCell(item, qty)..'||'..getHealingCell(perfectItem, qty))
table.insert(resultPart, '||'..getSaleValueCell(item, qty)..'||'..getSaleValueCell(perfectItem, qty))
table.insert(resultPart, '||'..getSaleValueCell(item, qty)..'||'..getSaleValueCell(perfectItem, qty))
Line 81: Line 84:
local matItem = Items.getItemByID(mat.id)
local matItem = Items.getItemByID(mat.id)
if matItem ~= nil then
if matItem ~= nil then
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.qty}))
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
end
end
end
end
Line 92: Line 95:


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 Constants.getModifiersText(potion.modifiers, false, true)
else
return ''
end
end
end


local potionArray = {}
function p._getHerblorePotionTable(categoryName)
for i, potion in Shared.skpairs(SkillData.Herblore.Potions) do
local categoryID = nil
if potion.category == category then
if string.upper(categoryName) == 'COMBAT' then
table.insert(potionArray, potion)
categoryID = 'melvorF:CombatPotions'
end
elseif string.upper(categoryName) == 'SKILL' then
categoryID = 'melvorF:SkillPotions'
else
return Shared.printError('No such potion category ' .. (categoryName or 'nil'))
end
end
local potionArray = GameData.getEntities(SkillData.Herblore.recipes, function(potion) return potion.categoryID == categoryID end)
table.sort(potionArray, function(a, b) return a.level < b.level end)


local resultPart = {}
local resultPart = {}
Line 113: Line 125:
table.insert(resultPart, '\r\n!Potion!!'..Icons.Icon({'Herblore', type='skill', notext=true})..' Level')
table.insert(resultPart, '\r\n!Potion!!'..Icons.Icon({'Herblore', type='skill', notext=true})..' Level')
table.insert(resultPart, '!!XP!!Ingredients!!style="width:30px;"|Tier!!Value!!Charges!!Effect')
table.insert(resultPart, '!!XP!!Ingredients!!style="width:30px;"|Tier!!Value!!Charges!!Effect')
table.sort(potionArray, function(a, b) return a.level < b.level end)


for i, potion in ipairs(potionArray) do
for i, potion in ipairs(potionArray) do
table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\r\n|-')
if potion.name == 'Bird Nests Potion' then
local expIcon = Icons.getExpansionIcon(potion.potionIDs[1])
table.insert(resultPart, '\r\n|rowspan="4"|[[Bird Nest Potion]]')
table.insert(resultPart, '\r\n|rowspan="4"|'..expIcon..'[['..potion.name..']]')
else
table.insert(resultPart, '\r\n|rowspan="4"|[['..potion.name..']]')
end
table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.level)
table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.level)
table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.baseXP)
table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.baseExperience)


local matArray = {}
local matArray = {}
for j, mat in ipairs(potion.itemCosts) do
for j, mat in ipairs(potion.itemCosts) do
local matItem = Items.getItemByID(mat.id)
local matItem = Items.getItemByID(mat.id)
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.qty}))
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
end
end
table.insert(resultPart, '||rowspan="4"|'..table.concat(matArray, ', ')..'||')
table.insert(resultPart, '||rowspan="4"|'..table.concat(matArray, ', ')..'||')
Line 138: Line 145:
local tierPot = Items.getItemByID(potionID)
local tierPot = Items.getItemByID(potionID)
table.insert(rowTxt, Icons.Icon({tierPot.name, type='item', notext=true}))
table.insert(rowTxt, Icons.Icon({tierPot.name, type='item', notext=true}))
table.insert(rowTxt, Icons.Icon({tierPot.name, tierSuffix[j], type = 'item', noicon = true}))
table.insert(rowTxt, Icons.Icon({tierPot.name, tierSuffix[j], type = 'item', noicon=true}))
table.insert(rowTxt, '||style="text-align:right;" data-sort-value="'..tierPot.sellsFor..'"|'..Icons.GP(tierPot.sellsFor))
table.insert(rowTxt, '||style="text-align:right;" data-sort-value="'..tierPot.sellsFor..'"|'..Icons.GP(tierPot.sellsFor))
table.insert(rowTxt, '||style="text-align:right;"|'..tierPot.potionCharges..'||'..tierPot.description)
table.insert(rowTxt, '||style="text-align:right;"|'..tierPot.charges..'|| '..p._getPotionDescription(tierPot))
table.insert(tierRows, table.concat(rowTxt))
table.insert(tierRows, table.concat(rowTxt))
end
end
Line 153: Line 160:
local category = frame.args ~= nil and frame.args[1] or frame
local category = frame.args ~= nil and frame.args[1] or frame
return p._getHerblorePotionTable(category)
return p._getHerblorePotionTable(category)
end
function p._getHerbloreHerbTable(args)
local allHerbs = {}
local allPotions = GameData.getEntities(SkillData.Herblore.recipes, function() return true end)
-- 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 = potion.level
if potionCosts == nil or level == nil then
return
end
-- Find if this potion uses a herb, and which herb it is.
for _, ingredient in pairs(potionCosts) do
local ingredientID = ingredient.id
if ingredientID == nil or string.sub(ingredientID, -5) ~= "_Herb" then
return
end
-- 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
for _, potion in pairs(allPotions) 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})..' Level')
    :tag("th"):wikitext("Herb")
    :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']
    local dlcIcon = Icons.getExpansionIcon(herbItem.id)
    -- Add rows
    tbl :tag("tr")
    :tag("td"):wikitext(herbLevel)
    :tag("td"):wikitext(dlcIcon .. Icons.Icon({herbItem.name, type='item'}))
    :tag("td"):wikitext(Icons.GP(herbItem.sellsFor))
    :tag('td'):wikitext(ItemSourceTables._getItemSources(herbItem, false, nil, ' '))
    :done()
    end
return tostring(tbl)
end
function p.getHerbloreHerbTable(frame)
local args = frame:getParent().args
return p._getHerbloreHerbTable(args)
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 resultPart = {}
table.insert(resultPart, '{| class="wikitable"')
table.insert(resultPart, '\r\n!colspan=4|[['..potionName..']]')
table.insert(resultPart, '\r\n|-\r\n!Potion!!Tier!!Charges!!Effect')
for i, potionID in ipairs(recipe.potionIDs) do
local tier = tierSuffix[i]
local potion = Items.getItemByID(potionID)
if potion ~= nil then
table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\r\n| ' .. Icons.Icon({potion.name, type='item', notext=true, size='60'}))
table.insert(resultPart, '|| ' .. Icons.getExpansionIcon(potion.id) .. Icons.Icon({potion.name, tier, type='item', noicon=true}))
table.insert(resultPart, '|| ' .. potion.charges .. '|| ' .. p._getPotionDescription(potion))
end
end
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
end


function p.getRunecraftingTable(frame)
function p.getRunecraftingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local category = frame.args ~= nil and frame.args[1] or frame
return p._getRecipeTable('Runecrafting', category, {'Item', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients', 'SkillXPSec', 'GPSec'})
return p._getRecipeTable('Runecrafting', category, {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients', 'SkillXPSec', 'GPSec'})
end
end


function p.getFletchingTable(frame)
function p.getFletchingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local category = frame.args ~= nil and frame.args[1] or frame
return p._getRecipeTable('Fletching', category, {'Item', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'})
return p._getRecipeTable('Fletching', category, {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'})
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
return p._getRecipeTable('Crafting', category, {'Item', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'})
local columns = {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'}
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 = {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'}
if category ~= 'Bars' then
table.insert(columns, 'GPBar')
end
return p._getRecipeTable('Smithing', category, columns)
end
end


Line 178: Line 296:
-- Validation: Parameters
-- Validation: Parameters
if type(skillName) ~= 'string' then
if type(skillName) ~= 'string' then
return 'ERROR: skillName must be a string'
return Shared.printError('skillName must be a string')
elseif not Shared.contains({'string', 'nil'}, type(categoryName)) then
elseif not Shared.contains({'string', 'nil'}, type(categoryName)) then
return 'ERROR: category must be a string or nil'
return Shared.printError('category must be a string or nil')
elseif type(columnList) ~= 'table' or Shared.tableCount(columnList) == 0 then
elseif type(columnList) ~= 'table' or Shared.tableIsEmpty(columnList) then
return 'ERROR: columnList must be a table with 1+ elements'
return Shared.printError('columnList must be a table with 1+ elements')
end
end


local categoryMap = {
local supportedSkills = {
["Runecrafting"] = {
'Smithing',
["Runes"] = 0,
'Crafting',
["ComboRunes"] = 1,
'Fletching',
["Weapons"] = 2,
'Runecrafting'
["AirGear"] = 3,
["WaterGear"] = 4,
["EarthGear"] = 5,
["FireGear"] = 6
},
["Fletching"] = {
["Arrows"] = 0,
["Shortbows"] = 1,
["Longbows"] = 2,
["Bolts"] = 3,
["Crossbows"] = 4,
["Javelins"] = 5
},
["Crafting"] = {
["Leather"] = 0,
["Dragonhide"] = 1,
["Rings"] = 2,
["Necklaces"] = 3,
["Bags"] = 4
}
}
local actionIntervals = {
["Runecrafting"] = 2,
["Fletching"]  = 2,
["Crafting"] = 2
}
}
if not Shared.contains(supportedSkills, skillName) then
return Shared.printError('The ' .. skillName .. ' skill is not supported by this function')
end
-- Validation: Category
-- Validation: Category
if categoryMap[skillName] == nil then
local category = GameData.getEntityByName(SkillData[skillName].categories, categoryName)
return 'ERROR: The ' .. skillName .. ' skill is not supported by this function'
if category == nil then
elseif categoryName ~= '' and categoryMap[skillName][categoryName] == nil then
local catNames = {}
local catNames = {}
for catName, catID in pairs(categoryMap[skillName]) do
for i, cat in pairs(SkillData[skillName].categories) do
table.insert(catNames, catName)
table.insert(catNames, cat.name)
end
end
return 'ERROR: No such category ' .. categoryName .. ' for skill ' .. skillName .. ', the following are available: ' .. table.concat(catNames, ', ')
return Shared.printError('No such category ' .. categoryName .. ' for skill ' .. skillName .. ', the following are available: ' .. table.concat(catNames, ', '))
end
end
local categoryID = categoryMap[skillName][categoryName]
local actionInterval = SkillData[skillName].baseInterval / 1000
local actionInterval = actionIntervals[skillName]


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


-- Validation: Column list
-- Validation: Column list
local columnDef = {
local columnDef = {
["Item"] = {["header"] = 'Item\r\n!Name', ["altRepeat"] = true},
["ItemImage"] = {["header"] = 'Item', ["altRepeat"] = false},
["ItemName"] = {["header"] = 'Name', ["altRepeat"] = true},
["SkillLevel"] = {["header"] = Icons.Icon({skillName, type='skill', notext=true}) .. ' Level', ["altRepeat"] = false},
["SkillLevel"] = {["header"] = Icons.Icon({skillName, type='skill', notext=true}) .. ' Level', ["altRepeat"] = false},
["SkillXP"] = {["header"] = 'XP', altRepeat = false},
["SkillXP"] = {["header"] = 'XP', ["altRepeat"] = false},
["GP"] = {["header"] = 'Value', ["altRepeat"] = true},
["GP"] = {["header"] = 'Value', ["altRepeat"] = true},
["Ingredients"] = {["header"] = 'Ingredients', ["altRepeat"] = true},
["Ingredients"] = {["header"] = 'Ingredients', ["altRepeat"] = true},
["SkillXPSec"] = {["header"] = 'XP/s', ["altRepeat"] = false},
["SkillXPSec"] = {["header"] = 'XP/s', ["altRepeat"] = false},
["GPSec"] = {["header"] = 'GP/s', ["altRepeat"] = true}
["GPSec"] = {["header"] = 'GP/s', ["altRepeat"] = true},
["GPBar"] = {["header"] = 'Value/Bar', ["altRepeat"] = true },
["Description"] = {["header"] = "Description", ["altRepeat"] = true}
}
}
-- Build the table header while we're here
-- Build the table header while we're here
local resultPart = {}
local resultPart, barIDList = {}, {}
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"\r\n|- class="headerRow-0"')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"\r\n|- class="headerRow-0"')
for i, colID in ipairs(columnList) do
for i, colID in ipairs(columnList) do
if columnDef[colID] == nil then
if columnDef[colID] == nil then
return 'ERROR: Invalid column ' .. colID .. ' requested'
return Shared.printError('Invalid column ' .. colID .. ' requested')
else
else
table.insert(resultPart, '\r\n! ' .. columnDef[colID].header)
table.insert(resultPart, '\r\n! ' .. columnDef[colID].header)
if colID == 'GPBar' then
-- For Smithing, a GP 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
end


-- Determine which recipes to include
-- Determine which recipes to include
local recipeList = {}
local recipeList = GameData.getEntities(SkillData[skillName][recipeKey],
for i, recipe in ipairs(SkillData[skillName][recipeKey]) do
function(recipe)
if categoryID == nil or recipe.category == categoryID then
return category.id == nil or recipe.categoryID == category.id
table.insert(recipeList, recipe)
end)
end
if Shared.tableIsEmpty(recipeList) then
end
if Shared.tableCount(recipeList) == 0 then
return ''
return ''
end
end
table.sort(recipeList, function(a, b) return (a.level == b.level and a.masteryID < b.masteryID) or a.level < b.level end)
table.sort(recipeList, function(a, b) return a.level < b.level end)


-- Build rows based on recipes
-- Build rows based on recipes
for i, recipe in ipairs(recipeList) do
for i, recipe in ipairs(recipeList) do
local item = Items.getItemByID(recipe.itemID)
local ns, _ = GameData.getLocalID(recipe.id)
local item = Items.getItemByID(recipe.productID)
if item ~= nil then
if item ~= nil then
-- Some recipes have alternative costs, so the recipe may require multiple rows
-- Some recipes have alternative costs, so the recipe may require multiple rows
local costList = nil
local costList = nil
if recipe.alternativeCosts ~= nil and Shared.tableCount(recipe.alternativeCosts) > 0 then
if recipe.alternativeCosts ~= nil and not Shared.tableIsEmpty(recipe.alternativeCosts) then
costList = recipe.alternativeCosts
costList = recipe.alternativeCosts
else
else
Line 286: Line 388:
local rowspanStr = (recipeRow == 1 and costCount > 1 and 'rowspan="' .. costCount .. '" ') or ''
local rowspanStr = (recipeRow == 1 and costCount > 1 and 'rowspan="' .. costCount .. '" ') or ''
local qty = (costDef.quantityMultiplier or 1) * (recipe.baseQuantity or 1)
local qty = (costDef.quantityMultiplier or 1) * (recipe.baseQuantity or 1)
table.insert(resultPart, '\r\n|-')
table.insert(resultPart, '\n|-')
for j, colID in ipairs(columnList) do
for j, colID in ipairs(columnList) do
local altRepeat = columnDef[colID].altRepeat
local altRepeat = columnDef[colID].altRepeat
Line 293: Line 395:
if recipeRow == 1 or altRepeat then
if recipeRow == 1 or altRepeat then
local spanStr = (not altRepeat and rowspanStr) or ''
local spanStr = (not altRepeat and rowspanStr) or ''
if colID == 'Item' then
if colID == 'ItemImage' then
local namePrefix = spanStr .. (qty > 1 and 'data-sort-value="' .. item.name .. '"')
table.insert(resultPart, '\n|' .. spanStr .. 'class="table-img"| ' .. Icons.Icon({item.name, type='item', size='50', notext=true}))
table.insert(resultPart, '\r\n|' .. spanStr .. 'style="text-align:center;min-width:25px"| ' .. Icons.Icon({item.name, type='item', size='50', notext=true}))
elseif colID == "ItemName" then
table.insert(resultPart, '\r\n|'.. (namePrefix ~= '' and namePrefix .. '| ' or ' ') .. (qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true}))
local namePrefix = spanStr
if qty > 1 then
namePrefix = namePrefix .. 'data-sort-value="' .. item.name .. '"'
end
table.insert(resultPart, '\n|'.. (namePrefix ~= '' and namePrefix .. '| ' or ' ') .. Icons.getExpansionIcon(item.id) .. (qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true}))
elseif colID == 'SkillLevel' then
elseif colID == 'SkillLevel' then
table.insert(resultPart, '\r\n|' .. spanStr .. 'style="text-align:right"| ' .. recipe.level)
table.insert(resultPart, '\n|' .. spanStr .. 'style="text-align:right"| ' .. recipe.level)
elseif colID == 'SkillXP' then
elseif colID == 'SkillXP' then
table.insert(resultPart, '\r\n|' .. spanStr .. 'style="text-align:right"| ' .. recipe.baseXP)
table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. recipe.baseExperience ..'" style="text-align:right"| ' .. Shared.formatnum(recipe.baseExperience))
elseif colID == 'GP' then
elseif colID == 'GP' then
local val = math.floor(item.sellsFor)
local val = math.floor(item.sellsFor)
table.insert(resultPart, '\r\n|' .. spanStr .. 'data-sort-value="' .. (val * qty) .. '"| ' .. Icons.GP(val) .. (qty > 1 and ' (x' .. qty .. ')' or ''))
table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. (val * qty) .. '"| ' .. Icons.GP(val) .. (qty > 1 and ' (x' .. qty .. ')' or ''))
elseif colID == 'Ingredients' then
elseif colID == 'Ingredients' then
local matArray = {}
local matArray = {}
Line 309: Line 415:
local matItem = Items.getItemByID(mat.id)
local matItem = Items.getItemByID(mat.id)
if matItem ~= nil then
if matItem ~= nil then
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.qty}))
table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
end
end
end
end
table.insert(resultPart, '\r\n|' .. (spanStr ~= '' and spanStr .. '| ' or ' ') .. table.concat(matArray, ', '))
if recipe.gpCost ~= nil and recipe.gpCost > 0 then
table.insert(matArray, Icons.GP(recipe.gpCost))
end
if recipe.scCost ~= nil and recipe.scCost > 0 then
table.insert(matArray, Icons.SC(recipe.scCost))
end
table.insert(resultPart, '\n|' .. (spanStr ~= '' and spanStr .. '| ' or ' ') .. table.concat(matArray, ', '))
elseif colID == 'SkillXPSec' then
elseif colID == 'SkillXPSec' then
table.insert(resultPart, '\r\n|' .. spanStr .. 'style="text-align:right"| ' .. string.format('%.2f', recipe.baseXP / actionInterval))
table.insert(resultPart, '\n|' .. spanStr .. 'style="text-align:right"| ' .. string.format('%.2f', recipe.baseExperience / actionInterval))
elseif colID == 'GPSec' then
elseif colID == 'GPSec' then
local val = math.floor(item.sellsFor) * qty / actionInterval
local val = math.floor(item.sellsFor) * qty / actionInterval
table.insert(resultPart, '\r\n|' .. spanStr .. 'data-sort-value="' .. val .. '"| ' .. Icons.GP(string.format('%.2f', val)))
table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. val .. '"| ' .. Icons.GP(string.format('%.2f', val)))
elseif colID == 'GPBar' 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 barVal = Shared.round(math.floor(item.sellsFor) * qty / barQty, 1, 1)
table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. barVal .. '"| ' .. Icons.GP(barVal))
else
table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="0" class="table-na"| N/A')
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 = Constants.getModifiersText(item.modifiers, false)
end
table.insert(resultPart, '\n| '..spanStr..'|'..descrip)
else
else
table.insert(resultPart, '\r\n| ')
table.insert(resultPart, '\n| ')
end
end
end
end
Line 326: Line 457:
end
end
end
end
table.insert(resultPart, '\r\n|}')
table.insert(resultPart, '\n|}')
return table.concat(resultPart)
return table.concat(resultPart)
end
function p.getBarItemIDs()
local barIDList = {}
for i, recipe in ipairs(SkillData.Smithing.recipes) do
if recipe.categoryID == 'melvorD:Bars' then
table.insert(barIDList, recipe.productID)
end
end
return barIDList
end
end


return p
return p

Latest revision as of 22:27, 26 March 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 Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local ItemSourceTables = require('Module:Items/SourceTables')

function p.getCookedItemsTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	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
		end)
	table.sort(recipeArray, function(a, b) return a.level < b.level end)

	-- Logic for generating some cells of the table which are consistent for normal & perfect items
	local getHealingCell = function(item, qty)
		if item ~= nil then
			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 '')
		else
			return ' '
		end
	end
	local getSaleValueCell = function(item, qty)
		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 '')
		else
			return ' '
		end
	end

	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!colspan="3" rowspan="2"|Cooked Item!!rowspan="2"|'..Icons.Icon({'Cooking', type='skill', notext=true})..' Level')
	table.insert(resultPart, '!!rowspan="2"|Cook Time (s)!!rowspan="2"|XP!!rowspan="2"|XP/s!!colspan="2"|Healing!!colspan="2"|Value!!rowspan="2"|Ingredients')
	table.insert(resultPart, '\r\n|- class="headerRow-1"')
	table.insert(resultPart, '\r\n!Normal!!' .. Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true}))
	table.insert(resultPart, '!!Normal!!' .. Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true}))

	for i, recipe in ipairs(recipeArray) do
		local item = Items.getItemByID(recipe.productID)
		local perfectItem = nil
		if recipe.perfectCookID ~= nil then
			perfectItem = Items.getItemByID(recipe.perfectCookID)
		end
		local qty = recipe.baseQuantity or 1

		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|class="table-img"|'..Icons.Icon({item.name, type='item', notext=true, size='50'}))
		table.insert(resultPart, '\r\n|class="table-img"| ')
		if perfectItem ~= nil then
			table.insert(resultPart, Icons.Icon({perfectItem.name, type='item', notext=true, size='50'}))
		end
		table.insert(resultPart, '||')
		if qty > 1 then
			table.insert(resultPart, qty..'x ')
		end
		table.insert(resultPart, Icons.getExpansionIcon(item.id))
		table.insert(resultPart, Icons.Icon({item.name, type='item', noicon = true}))
		table.insert(resultPart, '||style="text-align:right"|' .. recipe.level)
		table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseInterval .. '"|' .. Shared.round(recipe.baseInterval / 1000, 2, 0))
		table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseExperience .. '"|' .. Shared.formatnum(recipe.baseExperience))
		local xpRate = recipe.baseExperience / (recipe.baseInterval / 1000)
		table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. xpRate .. '"|' .. Shared.round(xpRate, 2, 0))
		table.insert(resultPart, '||'..getHealingCell(item, qty)..'||'..getHealingCell(perfectItem, qty))
		table.insert(resultPart, '||'..getSaleValueCell(item, qty)..'||'..getSaleValueCell(perfectItem, qty))
		local matArray = {}
		for j, mat in ipairs(recipe.itemCosts) do
			local matItem = Items.getItemByID(mat.id)
			if matItem ~= nil then
				table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
			end
		end
		table.insert(resultPart, '\r\n|'..table.concat(matArray, ' '))
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
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 Constants.getModifiersText(potion.modifiers, false, true)
	else
		return ''
	end
end

function p._getHerblorePotionTable(categoryName)
	local categoryID = nil
	if string.upper(categoryName) == 'COMBAT' then
		categoryID = 'melvorF:CombatPotions'
	elseif string.upper(categoryName) == 'SKILL' then
		categoryID = 'melvorF:SkillPotions'
	else
		return Shared.printError('No such potion category ' .. (categoryName or 'nil'))
	end

	local potionArray = GameData.getEntities(SkillData.Herblore.recipes, function(potion) return potion.categoryID == categoryID end)
	table.sort(potionArray, function(a, b) return a.level < b.level end)

	local resultPart = {}
	table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Potion!!'..Icons.Icon({'Herblore', type='skill', notext=true})..' Level')
	table.insert(resultPart, '!!XP!!Ingredients!!style="width:30px;"|Tier!!Value!!Charges!!Effect')

	for i, potion in ipairs(potionArray) do
		table.insert(resultPart, '\r\n|-')
		local expIcon = Icons.getExpansionIcon(potion.potionIDs[1])
		table.insert(resultPart, '\r\n|rowspan="4"|'..expIcon..'[['..potion.name..']]')
		table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.level)
		table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.baseExperience)

		local matArray = {}
		for j, mat in ipairs(potion.itemCosts) do
			local matItem = Items.getItemByID(mat.id)
			table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
		end
		table.insert(resultPart, '||rowspan="4"|'..table.concat(matArray, ', ')..'||')

		local tierRows = {}
		for j, potionID in ipairs(potion.potionIDs) do
			local rowTxt = {}
			local tierPot = Items.getItemByID(potionID)
			table.insert(rowTxt, Icons.Icon({tierPot.name, type='item', notext=true}))
			table.insert(rowTxt, Icons.Icon({tierPot.name, tierSuffix[j], type = 'item', noicon=true}))
			table.insert(rowTxt, '||style="text-align:right;" data-sort-value="'..tierPot.sellsFor..'"|'..Icons.GP(tierPot.sellsFor))
			table.insert(rowTxt, '||style="text-align:right;"|'..tierPot.charges..'|| '..p._getPotionDescription(tierPot))
			table.insert(tierRows, table.concat(rowTxt))
		end
		table.insert(resultPart, table.concat(tierRows, '\r\n|-\r\n|'))
	end

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

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

function p._getHerbloreHerbTable(args)
	local allHerbs = {}
	local allPotions = GameData.getEntities(SkillData.Herblore.recipes, function() return true end)
	
	-- 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 = potion.level
		
		if potionCosts == nil or level == nil then
			return
		end
		
		-- Find if this potion uses a herb, and which herb it is.
		for _, ingredient in pairs(potionCosts) do
			local ingredientID = ingredient.id
			if ingredientID == nil or string.sub(ingredientID, -5) ~= "_Herb" then
				return
			end
			
			-- 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

	for _, potion in pairs(allPotions) 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})..' Level')
    	:tag("th"):wikitext("Herb")
    	: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']
    	local dlcIcon = Icons.getExpansionIcon(herbItem.id)

    	-- Add rows
    	tbl :tag("tr")
    		:tag("td"):wikitext(herbLevel)
    		:tag("td"):wikitext(dlcIcon .. Icons.Icon({herbItem.name, type='item'}))
    		:tag("td"):wikitext(Icons.GP(herbItem.sellsFor))
    		:tag('td'):wikitext(ItemSourceTables._getItemSources(herbItem, false, nil, ' '))
    		:done()
    end

	return tostring(tbl)
end

function p.getHerbloreHerbTable(frame)
	local args = frame:getParent().args
	return p._getHerbloreHerbTable(args)
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 resultPart = {}
	table.insert(resultPart, '{| class="wikitable"')
	table.insert(resultPart, '\r\n!colspan=4|[['..potionName..']]')
	table.insert(resultPart, '\r\n|-\r\n!Potion!!Tier!!Charges!!Effect')

	for i, potionID in ipairs(recipe.potionIDs) do
		local tier = tierSuffix[i]
		local potion = Items.getItemByID(potionID)
		if potion ~= nil then
			table.insert(resultPart, '\r\n|-')
			table.insert(resultPart, '\r\n| ' .. Icons.Icon({potion.name, type='item', notext=true, size='60'}))
			table.insert(resultPart, '|| ' .. Icons.getExpansionIcon(potion.id) .. Icons.Icon({potion.name, tier, type='item', noicon=true}))
			table.insert(resultPart, '|| ' .. potion.charges .. '|| ' .. p._getPotionDescription(potion))
		end
	end

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

function p.getRunecraftingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getRecipeTable('Runecrafting', category, {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients', 'SkillXPSec', 'GPSec'})
end

function p.getFletchingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getRecipeTable('Fletching', category, {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'})
end

function p.getCraftingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	local columns = {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'}
	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 = {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'}
	if category ~= 'Bars' then
		table.insert(columns, 'GPBar')
	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, GP, Ingredients, SkillXPSec, GPSec
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 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 = {
		["ItemImage"] = {["header"] = 'Item', ["altRepeat"] = false},
		["ItemName"] = {["header"] = 'Name', ["altRepeat"] = true},
		["SkillLevel"] = {["header"] = Icons.Icon({skillName, type='skill', notext=true}) .. ' Level', ["altRepeat"] = false},
		["SkillXP"] = {["header"] = 'XP', ["altRepeat"] = false},
		["GP"] = {["header"] = 'Value', ["altRepeat"] = true},
		["Ingredients"] = {["header"] = 'Ingredients', ["altRepeat"] = true},
		["SkillXPSec"] = {["header"] = 'XP/s', ["altRepeat"] = false},
		["GPSec"] = {["header"] = 'GP/s', ["altRepeat"] = true},
		["GPBar"] = {["header"] = 'Value/Bar', ["altRepeat"] = true },
		["Description"] = {["header"] = "Description", ["altRepeat"] = true}
	}
	-- Build the table header while we're here
	local resultPart, barIDList = {}, {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"\r\n|- class="headerRow-0"')
	for i, colID in ipairs(columnList) do
		if columnDef[colID] == nil then
			return Shared.printError('Invalid column ' .. colID .. ' requested')
		else
			table.insert(resultPart, '\r\n! ' .. columnDef[colID].header)
			if colID == 'GPBar' then
				-- For Smithing, a GP value per bar column is included. If this
				-- is requested, then obtain a list of bar item IDs
				barIDList = p.getBarItemIDs()
			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 a.level < b.level 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)
		if item ~= nil then
			-- Some recipes have alternative costs, so the recipe may require multiple rows
			local costList = nil
			if recipe.alternativeCosts ~= nil and not Shared.tableIsEmpty(recipe.alternativeCosts) then
				costList = recipe.alternativeCosts
			else
				costList = {{["itemCosts"] = recipe.itemCosts}}
			end
			local costCount = Shared.tableCount(costList)
			-- Build one row per element within costList
			for recipeRow, costDef in ipairs(costList) do
				local rowspanStr = (recipeRow == 1 and costCount > 1 and 'rowspan="' .. costCount .. '" ') or ''
				local qty = (costDef.quantityMultiplier or 1) * (recipe.baseQuantity or 1)
				table.insert(resultPart, '\n|-')
				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 spanStr = (not altRepeat and rowspanStr) or ''
						if colID == 'ItemImage' then
							table.insert(resultPart, '\n|' .. spanStr .. 'class="table-img"| ' .. Icons.Icon({item.name, type='item', size='50', notext=true}))
						elseif colID == "ItemName" then
							local namePrefix = spanStr
							if qty > 1 then
								namePrefix = namePrefix .. 'data-sort-value="' .. item.name .. '"'
							end
							table.insert(resultPart, '\n|'.. (namePrefix ~= '' and namePrefix .. '| ' or ' ') .. Icons.getExpansionIcon(item.id) .. (qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true}))
						elseif colID == 'SkillLevel' then
							table.insert(resultPart, '\n|' .. spanStr .. 'style="text-align:right"| ' .. recipe.level)
						elseif colID == 'SkillXP' then
							table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. recipe.baseExperience ..'" style="text-align:right"| ' .. Shared.formatnum(recipe.baseExperience))
						elseif colID == 'GP' then
							local val = math.floor(item.sellsFor)
							table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. (val * qty) .. '"| ' .. Icons.GP(val) .. (qty > 1 and ' (x' .. qty .. ')' or ''))
						elseif colID == 'Ingredients' then
							local matArray = {}
							for k, mat in ipairs(costDef.itemCosts) do
								local matItem = Items.getItemByID(mat.id)
								if matItem ~= nil then
									table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
								end
							end
							if recipe.gpCost ~= nil and recipe.gpCost > 0 then
								table.insert(matArray, Icons.GP(recipe.gpCost))
							end
							if recipe.scCost ~= nil and recipe.scCost > 0 then
								table.insert(matArray, Icons.SC(recipe.scCost))
							end
							table.insert(resultPart, '\n|' .. (spanStr ~= '' and spanStr .. '| ' or ' ') .. table.concat(matArray, ', '))
						elseif colID == 'SkillXPSec' then
							table.insert(resultPart, '\n|' .. spanStr .. 'style="text-align:right"| ' .. string.format('%.2f', recipe.baseExperience / actionInterval))
						elseif colID == 'GPSec' then
							local val = math.floor(item.sellsFor) * qty / actionInterval
							table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. val .. '"| ' .. Icons.GP(string.format('%.2f', val)))
						elseif colID == 'GPBar' 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 barVal = Shared.round(math.floor(item.sellsFor) * qty / barQty, 1, 1)
								table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. barVal .. '"| ' .. Icons.GP(barVal))
							else
								table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="0" class="table-na"| N/A')
							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 = Constants.getModifiersText(item.modifiers, false)
							end
							table.insert(resultPart, '\n| '..spanStr..'|'..descrip)
						else
							table.insert(resultPart, '\n| ')
						end
					end
				end
			end
		end
	end
	table.insert(resultPart, '\n|}')
	return table.concat(resultPart)
end

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

return p